Merge nucleic/warm-breezy-raven into dev
This commit is contained in:
@@ -1,17 +1,18 @@
|
||||
import SwiftUI
|
||||
import NucleicProtocol
|
||||
|
||||
/// Attention-first home (UX_IOS §3): NEEDS YOU pinned at top, then RUNNING, then DONE. Archived
|
||||
/// chats are hidden (matching the Mac sidebar); disposition splits "needs you" from "done".
|
||||
/// Attention-first home (UX_IOS §3): NEEDS YOU pinned at top, then RUNNING, then DONE, then a
|
||||
/// collapsible ARCHIVED group at the bottom (mirrors the Mac sidebar); disposition splits "needs
|
||||
/// you" from "done".
|
||||
struct SessionsView: View {
|
||||
@EnvironmentObject var store: RemoteStore
|
||||
@State private var showArchived = false
|
||||
@State private var archivedExpanded = false
|
||||
// Typed (not `NavigationPath`) so `consumeRoute` can see what's already pushed — a deep link
|
||||
// to the session on top must land on it, not stack a duplicate detail over it.
|
||||
@State private var path: [SessionID] = []
|
||||
|
||||
private var grouped: [(title: String, rows: [WireSessionSummary])] {
|
||||
let pool = (showArchived ? store.sessions : store.liveSessions)
|
||||
let pool = store.liveSessions
|
||||
.sorted(by: StatusStyle.attentionThenRecency)
|
||||
// One pass, first bucket wins — the old `done = pool.filter { !needs.contains($0) … }`
|
||||
// ran O(rows²) full-summary equality scans on every body evaluation.
|
||||
@@ -25,10 +26,17 @@ struct SessionsView: View {
|
||||
return [("Needs you", needs), ("Running", running), ("Done", done)].filter { !$0.rows.isEmpty }
|
||||
}
|
||||
|
||||
/// Archived chats, shown in their own collapsible section below "Done". These are excluded
|
||||
/// from `liveSessions` (and therefore from `grouped`), so they can only appear here.
|
||||
private var archivedSessions: [WireSessionSummary] {
|
||||
store.sessions.filter { $0.archived }
|
||||
.sorted(by: StatusStyle.attentionThenRecency)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $path) {
|
||||
Group {
|
||||
if store.liveSessions.isEmpty && !showArchived {
|
||||
if store.liveSessions.isEmpty && archivedSessions.isEmpty {
|
||||
ContentUnavailableView(
|
||||
"No sessions", systemImage: "square.stack.3d.up",
|
||||
description: Text(store.connectivity.isLive
|
||||
@@ -56,29 +64,33 @@ struct SessionsView: View {
|
||||
ForEach(grouped, id: \.title) { group in
|
||||
Section(group.title.uppercased()) {
|
||||
ForEach(group.rows, id: \.sessionID) { summary in
|
||||
NavigationLink(value: summary.sessionID) {
|
||||
SessionRow(summary: summary)
|
||||
}
|
||||
.listRowBackground(SessionRowWash(summary: summary))
|
||||
.swipeActions(edge: .leading) {
|
||||
Button {
|
||||
store.setFavorite(summary.sessionID, !summary.favorite)
|
||||
} label: {
|
||||
Label("Favorite", systemImage: summary.favorite ? "star.slash" : "star.fill")
|
||||
}.tint(.yellow)
|
||||
}
|
||||
.swipeActions(edge: .trailing) {
|
||||
Button(role: .destructive) {
|
||||
store.deleteSession(summary.sessionID)
|
||||
} label: { Label("Delete", systemImage: "trash") }
|
||||
Button {
|
||||
store.setArchived(summary.sessionID, !summary.archived)
|
||||
} label: {
|
||||
Label(summary.archived ? "Unarchive" : "Archive",
|
||||
systemImage: summary.archived ? "tray.and.arrow.up" : "archivebox")
|
||||
}.tint(.gray)
|
||||
sessionRow(summary)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Archived chats live in their own collapsible section, pinned below
|
||||
// "Done" (mirrors the Mac sidebar's "Archived Projects" group).
|
||||
if !archivedSessions.isEmpty {
|
||||
Section {
|
||||
if archivedExpanded {
|
||||
ForEach(archivedSessions, id: \.sessionID) { summary in
|
||||
sessionRow(summary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.22)) { archivedExpanded.toggle() }
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
Text("Archived (\(archivedSessions.count))")
|
||||
Spacer(minLength: 0)
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption2.weight(.semibold))
|
||||
.rotationEffect(.degrees(archivedExpanded ? 90 : 0))
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,6 +127,34 @@ struct SessionsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// One tappable session row with its favorite / delete / archive swipe actions. Shared by
|
||||
/// the live sections (Needs you / Running / Done) and the collapsible Archived section.
|
||||
@ViewBuilder
|
||||
private func sessionRow(_ summary: WireSessionSummary) -> some View {
|
||||
NavigationLink(value: summary.sessionID) {
|
||||
SessionRow(summary: summary)
|
||||
}
|
||||
.listRowBackground(SessionRowWash(summary: summary))
|
||||
.swipeActions(edge: .leading) {
|
||||
Button {
|
||||
store.setFavorite(summary.sessionID, !summary.favorite)
|
||||
} label: {
|
||||
Label("Favorite", systemImage: summary.favorite ? "star.slash" : "star.fill")
|
||||
}.tint(.yellow)
|
||||
}
|
||||
.swipeActions(edge: .trailing) {
|
||||
Button(role: .destructive) {
|
||||
store.deleteSession(summary.sessionID)
|
||||
} label: { Label("Delete", systemImage: "trash") }
|
||||
Button {
|
||||
store.setArchived(summary.sessionID, !summary.archived)
|
||||
} label: {
|
||||
Label(summary.archived ? "Unarchive" : "Archive",
|
||||
systemImage: summary.archived ? "tray.and.arrow.up" : "archivebox")
|
||||
}.tint(.gray)
|
||||
}
|
||||
}
|
||||
|
||||
/// Push the session a notification tap asked for, then clear the request.
|
||||
private func consumeRoute() {
|
||||
guard let route = store.pendingRoute else { return }
|
||||
|
||||
Reference in New Issue
Block a user