cross server notifications
This commit is contained in:
+1
-1
@@ -82,7 +82,7 @@ Focus lives in `Store`. In-app `#/` links go through `Store.route`: `#/task/<id>
|
|||||||
|
|
||||||
## Window and layout
|
## Window and layout
|
||||||
|
|
||||||
One window (a `Window` scene, id `main`) shows one saved server at a time: `Servers.current`, the origin it shows, or nil for the connection screen. The title bar holds no title, only the server picker (`ServerPicker`, a toolbar item in the principal placement): the current server's colour dot and short name (its host up to the first dot, `serverName`), or Add Server with none. Its menu lists every saved server in saved order (`ServerRecord.saved`, the order Previous and Next Server follow), then Add Server…, which shows the connection screen. Switching servers swaps the window's content; every connection keeps running in the background. Each server has its own `Overlays`, held by its `Connection`, so its switcher, dialogs and left-pane tab survive switching away and back, and its `MainView` is rebuilt (`.id(origin)`) so no view state carries from one server to another. The content sits inside a 3-point border in the server's colour (`Servers.color`, one of the task colours, picked from a row of swatches in Settings under Window colour and kept in the server's `ServerRecord`, so every Mac shows the same colour). The connection screen has no border. The panes follow the "UI layout" rules in `AGENTS.md`: an `HStack` of two panes of exactly equal width at every size, no sidebar, no navbar, nothing above them but the identity banner when the user has no git identity.
|
One window (a `Window` scene, id `main`) shows one saved server at a time: `Servers.current`, the origin it shows, or nil for the connection screen. The title bar holds no title, only the server picker (`ServerPicker`, a toolbar item in the principal placement): a row of every saved server in saved order (`ServerRecord.saved`, the order Previous and Next Server follow), each its colour dot (hollow until its connection is running) and short name (its host up to the first dot, `serverName`), then **+**, which shows the connection screen. Clicking a server shows it. Since every connection runs in the background, each server carries what needs the user there: its count of open inbox items as a badge, with the server's name in the warning colour and tint, and its count of active tasks with an unread turn summary (`Store.unreadSummaries`) as "N new". The current server's name is bold on the surface colour. Switching servers swaps the window's content; every connection keeps running in the background. Each server has its own `Overlays`, held by its `Connection`, so its switcher, dialogs and left-pane tab survive switching away and back, and its `MainView` is rebuilt (`.id(origin)`) so no view state carries from one server to another. The content sits inside a 3-point border in the server's colour (`Servers.color`, one of the task colours, picked from a row of swatches in Settings under Window colour and kept in the server's `ServerRecord`, so every Mac shows the same colour). The connection screen has no border. The panes follow the "UI layout" rules in `AGENTS.md`: an `HStack` of two panes of exactly equal width at every size, no sidebar, no navbar, nothing above them but the identity banner when the user has no git identity.
|
||||||
|
|
||||||
At launch the window shows the oldest saved server. A CloudKit import that brings a server saved on another Mac starts its connection, and the window takes it while it shows an empty connection screen. A CloudKit import that removes the current server switches to the first saved one. SwiftUI's own window restoration is off (`restorationBehavior(.disabled)`); the scene keeps the id `main` that earlier builds used, because AppKit restores their saved window by that id and SwiftUI then opens no launch window at all when the id names no scene.
|
At launch the window shows the oldest saved server. A CloudKit import that brings a server saved on another Mac starts its connection, and the window takes it while it shows an empty connection screen. A CloudKit import that removes the current server switches to the first saved one. SwiftUI's own window restoration is off (`restorationBehavior(.disabled)`); the scene keeps the id `main` that earlier builds used, because AppKit restores their saved window by that id and SwiftUI then opens no launch window at all when the id names no scene.
|
||||||
|
|
||||||
|
|||||||
@@ -458,28 +458,62 @@ struct ServerWindow: View {
|
|||||||
struct ServerPicker: View {
|
struct ServerPicker: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let servers = Servers.shared
|
let servers = Servers.shared
|
||||||
Menu {
|
HStack(spacing: 4) {
|
||||||
ForEach(servers.origins, id: \.self) { origin in
|
ForEach(servers.origins, id: \.self) { origin in
|
||||||
Button(serverName(origin)) {
|
ServerChip(origin: origin)
|
||||||
servers.current = origin
|
|
||||||
}
|
}
|
||||||
}
|
Button("+") {
|
||||||
Divider()
|
|
||||||
Button("Add Server…") {
|
|
||||||
servers.current = nil
|
servers.current = nil
|
||||||
}
|
}
|
||||||
} label: {
|
.buttonStyle(.plain)
|
||||||
HStack(spacing: 6) {
|
.foregroundStyle(servers.current == nil ? Palette.text : Palette.muted)
|
||||||
if let current = servers.current {
|
.padding(.horizontal, 8)
|
||||||
Circle()
|
.padding(.vertical, 3)
|
||||||
.fill(servers.color(current))
|
.help("Add Server")
|
||||||
.frame(width: 8, height: 8)
|
|
||||||
Text(serverName(current))
|
|
||||||
} else {
|
|
||||||
Text("Add Server")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.font(Fonts.font(13))
|
.font(Fonts.font(13))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ServerChip: View {
|
||||||
|
let origin: String
|
||||||
|
@ViewState private var hover = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let servers = Servers.shared
|
||||||
|
let store = servers.connections[origin]?.store
|
||||||
|
let questions = store?.inbox.count ?? 0
|
||||||
|
let summaries = store?.unreadSummaries ?? 0
|
||||||
|
let current = servers.current == origin
|
||||||
|
Button {
|
||||||
|
servers.current = origin
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Circle()
|
||||||
|
.fill(store == nil ? .clear : servers.color(origin))
|
||||||
|
.stroke(servers.color(origin))
|
||||||
|
.frame(width: 8, height: 8)
|
||||||
|
Text(serverName(origin))
|
||||||
|
.fontWeight(current || questions > 0 ? .bold : .regular)
|
||||||
|
if questions > 0 {
|
||||||
|
Badge(count: questions)
|
||||||
|
.help("open questions")
|
||||||
|
}
|
||||||
|
if summaries > 0 {
|
||||||
|
Text("\(summaries) new")
|
||||||
|
.font(Fonts.font(11, weight: 700))
|
||||||
|
.foregroundStyle(Palette.accent)
|
||||||
|
.help("Turn summaries you haven't read")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundStyle(hover || current ? Palette.text : questions > 0 ? Palette.warn : Palette.muted)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 3)
|
||||||
|
.background(questions > 0 ? Palette.warnTint : current ? Palette.surface : .clear, in: RoundedRectangle(cornerRadius: 6))
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.help(store == nil ? "\(serverName(origin)): not connected" : serverName(origin))
|
||||||
|
.onHover { hover = $0 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -482,6 +482,10 @@ final class Store {
|
|||||||
task.workflow == nil && task.summarySeq > (summaryReads[task.id] ?? 0)
|
task.workflow == nil && task.summarySeq > (summaryReads[task.id] ?? 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var unreadSummaries: Int {
|
||||||
|
tasks.values.joined().filter { $0.status == .active && unreadSummary($0) }.count
|
||||||
|
}
|
||||||
|
|
||||||
private func noteSummaryRead(_ taskId: String, _ seq: Int64) -> Bool {
|
private func noteSummaryRead(_ taskId: String, _ seq: Int64) -> Bool {
|
||||||
if (summaryReads[taskId] ?? 0) >= seq {
|
if (summaryReads[taskId] ?? 0) >= seq {
|
||||||
return false
|
return false
|
||||||
|
|||||||
Reference in New Issue
Block a user