diff options
| author | Christian Cleberg <[email protected]> | 2026-04-13 13:09:57 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-13 13:09:57 -0500 |
| commit | f5f5757d0e1b78470429ae7cb9f742c95662849b (patch) | |
| tree | 758f67965a985f4f1f52a456d9b672f8256851de /Hutch/Views/Inbox/InboxView.swift | |
| parent | 0e461a382257979037e8927e5f6b468635b0a7fe (diff) | |
| download | hutch-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/InboxView.swift')
| -rw-r--r-- | Hutch/Views/Inbox/InboxView.swift | 192 |
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() } } |
