summaryrefslogtreecommitdiff
path: root/Hutch/Views/Inbox
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-13 13:09:57 -0500
committerChristian Cleberg <[email protected]>2026-04-13 13:09:57 -0500
commitf5f5757d0e1b78470429ae7cb9f742c95662849b (patch)
tree758f67965a985f4f1f52a456d9b672f8256851de /Hutch/Views/Inbox
parent0e461a382257979037e8927e5f6b468635b0a7fe (diff)
downloadhutch-3.0.0.tar.gz
hutch-3.0.0.tar.bz2
hutch-3.0.0.zip
release: v3.0.0 — navigation overhaul, Work view, and multi-pin supportv3.0.0
Reworks the app's core navigation and home screen for v3.0.0: - Replace Inbox with Work view: unified dashboard showing unread threads and assigned tickets, with All/Unread/Assigned scope picker - Overhaul Home screen: redesigned with pinned items grid (trackers, repos, mailing lists, users), recent activity section, and system status banner; backed by HomePinStore supporting all resource types - Simplify navigation bars across list screens: default toolbar shows only the primary action (+) and an overflow menu (…); pin, share, and select moved into the overflow menu; selection mode gets its own nav bar with Cancel / "N Selected" / All - Consolidate repo detail toolbars: pin and share moved into the existing actions menu for both Git and Mercurial detail views - Add recent activity tracking via RecentActivityStore - Add work and inbox deep link aliases (hutch://work, hutch://inbox) Implements: https://todo.sr.ht/~ccleberg/hutch/55
Diffstat (limited to 'Hutch/Views/Inbox')
-rw-r--r--Hutch/Views/Inbox/InboxView.swift192
1 files changed, 3 insertions, 189 deletions
diff --git a/Hutch/Views/Inbox/InboxView.swift b/Hutch/Views/Inbox/InboxView.swift
index c5299ec..a4870db 100644
--- a/Hutch/Views/Inbox/InboxView.swift
+++ b/Hutch/Views/Inbox/InboxView.swift
@@ -1,196 +1,10 @@
import SwiftUI
+// Legacy wrapper kept temporarily so stale references continue to compile while
+// the app transitions from Inbox to Work.
struct InboxView: View {
- @Environment(AppState.self) private var appState
- @Environment(\.scenePhase) private var scenePhase
- @State private var viewModel: InboxViewModel?
- @State private var selectedThreadID: InboxThreadSummary.ID?
- @State private var selectedThreadSnapshot: InboxThreadSummary?
- @State private var isShowingThreadDetail = false
-
var body: some View {
- Group {
- if let viewModel {
- listContent(viewModel)
- } else {
- SRHTLoadingStateView(message: "Loading inbox…")
- }
- }
- .navigationTitle("Inbox")
- .task {
- let vm: InboxViewModel
- if let viewModel {
- vm = viewModel
- } else {
- let newViewModel = InboxViewModel(
- client: appState.client,
- defaults: appState.accountDefaults,
- accountID: appState.activeAccountID
- )
- viewModel = newViewModel
- vm = newViewModel
- }
-
- await vm.loadThreads()
- }
- .onChange(of: scenePhase) { _, newPhase in
- guard newPhase == .active, let viewModel, !isShowingThreadDetail else { return }
- Task {
- await viewModel.loadThreads()
- }
- }
- }
-
- @ViewBuilder
- private func listContent(_ viewModel: InboxViewModel) -> some View {
- @Bindable var vm = viewModel
-
- List {
- Section {
- Picker("Filter", selection: $vm.filter) {
- ForEach(InboxThreadFilter.allCases, id: \.self) { filter in
- Text(filter.rawValue).tag(filter)
- }
- }
- .pickerStyle(.segmented)
- .listRowBackground(Color.clear)
- .listRowInsets(EdgeInsets())
- }
-
- ForEach(viewModel.filteredThreads) { thread in
- Button {
- selectThread(thread)
- } label: {
- InboxThreadRow(thread: thread)
- }
- .buttonStyle(.plain)
- .swipeActions(edge: .leading, allowsFullSwipe: true) {
- readStateAction(for: thread, in: viewModel)
- }
- .swipeActions(edge: .trailing, allowsFullSwipe: true) {
- readStateAction(for: thread, in: viewModel)
- }
- }
- }
- .themedList()
- .searchable(
- text: $vm.searchText,
- placement: .navigationBarDrawer(displayMode: .always),
- prompt: "Search inbox"
- )
- .listStyle(.plain)
- .toolbar {
- ToolbarItem(placement: .topBarTrailing) {
- if viewModel.hasUnreadThreads {
- Button("Mark All Read") {
- withAnimation(.easeInOut(duration: 0.2)) {
- viewModel.markAllThreadsRead()
- }
- }
- }
- }
- }
- .overlay {
- if viewModel.isLoading, viewModel.threads.isEmpty {
- SRHTLoadingStateView(message: "Loading inbox…")
- } else if let error = viewModel.error, viewModel.threads.isEmpty {
- SRHTErrorStateView(
- title: "Failed to load inbox",
- message: error,
- retryAction: { await viewModel.loadThreads() }
- )
- } else if !viewModel.threads.isEmpty, viewModel.filteredThreads.isEmpty {
- ContentUnavailableView.search(text: viewModel.searchText)
- } else if viewModel.threads.isEmpty, viewModel.error == nil {
- ContentUnavailableView(
- "Inbox Zero",
- systemImage: "tray",
- description: Text("You're up to date.")
- )
- }
- }
- .connectivityOverlay(hasContent: !viewModel.threads.isEmpty) {
- await viewModel.loadThreads()
- }
- .srhtErrorBanner(error: $vm.error)
- .refreshable {
- await viewModel.loadThreads()
- }
- .onChange(of: viewModel.threads) { _, threads in
- syncSelectedThreadSnapshot(with: threads)
- }
- .navigationDestination(isPresented: Binding(
- get: { isShowingThreadDetail && selectedThread(for: viewModel) != nil },
- set: { isPresented in
- if !isPresented {
- clearSelection()
- }
- isShowingThreadDetail = isPresented
- }
- )) {
- if let thread = selectedThread(for: viewModel) {
- ThreadDetailView(thread: thread) {
- viewModel.markThreadRead(thread)
- }
- .onAppear {
- cacheSelectedThread(thread)
- }
- .onDisappear {
- handleThreadDetailDisappear(for: thread.id)
- }
- } else {
- ContentUnavailableView(
- "Thread Unavailable",
- systemImage: "tray",
- description: Text("This thread could not be restored.")
- )
- }
- }
- }
-
- @ViewBuilder
- private func readStateAction(for thread: InboxThreadSummary, in viewModel: InboxViewModel) -> some View {
- Button {
- withAnimation(.easeInOut(duration: 0.2)) {
- viewModel.markThreadRead(thread)
- }
- } label: {
- Label("Mark as Read", systemImage: "envelope.open")
- }
- .tint(.blue)
- }
-
- private func selectThread(_ thread: InboxThreadSummary) {
- cacheSelectedThread(thread)
- isShowingThreadDetail = true
- }
-
- private func cacheSelectedThread(_ thread: InboxThreadSummary) {
- selectedThreadID = thread.id
- selectedThreadSnapshot = thread
- }
-
- private func selectedThread(for viewModel: InboxViewModel) -> InboxThreadSummary? {
- guard let selectedThreadID else { return selectedThreadSnapshot }
- return viewModel.thread(withID: selectedThreadID) ?? (selectedThreadSnapshot?.id == selectedThreadID ? selectedThreadSnapshot : nil)
- }
-
- private func handleThreadDetailDisappear(for threadID: String) {
- let isActiveSelection = selectedThreadID == threadID
- guard isActiveSelection else { return }
- clearSelection()
- }
-
- private func syncSelectedThreadSnapshot(with threads: [InboxThreadSummary]) {
- guard let selectedThreadID else { return }
- guard let updatedThread = threads.first(where: { $0.id == selectedThreadID }) else { return }
- selectedThreadSnapshot = updatedThread
- }
-
- private func clearSelection() {
- selectedThreadID = nil
- selectedThreadSnapshot = nil
- isShowingThreadDetail = false
+ WorkView()
}
}