diff --git a/graphcode/Sources/Features/App/AppSidebarView.swift b/graphcode/Sources/Features/App/AppSidebarView.swift index b2739ae..1f4c584 100644 --- a/graphcode/Sources/Features/App/AppSidebarView.swift +++ b/graphcode/Sources/Features/App/AppSidebarView.swift @@ -282,6 +282,12 @@ struct AppSidebarView: View { .frame(width: 3, height: 14) Text(node.title).font(.system(size: 12.5)).lineLimit(1) Spacer(minLength: 4) + if let watching = MailroomWatchPresentation.tooltip(for: node) { + Image(systemName: MailroomWatchPresentation.symbolName) + .font(.system(size: 10)) + .foregroundStyle(.white.opacity(0.45)) + .help(watching) + } Text(LoopCardPresentation.duration(sidebarNow.timeIntervalSince(node.createdAt))) .font(.system(size: 10.5, design: .monospaced)) .foregroundStyle(.white.opacity(0.5)) diff --git a/graphcode/Sources/Features/Canvas/LoopCardView.swift b/graphcode/Sources/Features/Canvas/LoopCardView.swift index 36b3d51..c5ea21d 100644 --- a/graphcode/Sources/Features/Canvas/LoopCardView.swift +++ b/graphcode/Sources/Features/Canvas/LoopCardView.swift @@ -241,6 +241,12 @@ struct LoopCardView: View { if isRemote { Image(systemName: "network").font(.system(size: 9)).foregroundStyle(.white.opacity(0.4)) } + if let watching = MailroomWatchPresentation.tooltip(for: node) { + Image(systemName: MailroomWatchPresentation.symbolName) + .font(.system(size: 9)) + .foregroundStyle(.white.opacity(0.4)) + .help(watching) + } switch entryRole { case .entry: EntryChip() case .cycleOnly: CycleChip() diff --git a/graphcode/Sources/Features/Canvas/MailroomWatchPresentation.swift b/graphcode/Sources/Features/Canvas/MailroomWatchPresentation.swift new file mode 100644 index 0000000..18e7e43 --- /dev/null +++ b/graphcode/Sources/Features/Canvas/MailroomWatchPresentation.swift @@ -0,0 +1,18 @@ +import GraphcodeKit +import MailroomKit + +/// The envelope that marks a loop watching its project's Mailroom, and its tooltip. +/// +/// Hidden on a resolved loop: `mailroomWatch` outlives the session that set it, but a +/// loop that has finished for good cannot be rung. +enum MailroomWatchPresentation { + static let symbolName = "envelope" + + static func showsGlyph(for node: LoopNode) -> Bool { tooltip(for: node) != nil } + + static func tooltip(for node: LoopNode) -> String? { + guard let watch = node.mailroomWatch, !node.isResolved else { return nil } + guard let topic = watch.topic else { return "Watching the Mailroom" } + return "Watching the Mailroom · topic \(topic)" + } +} diff --git a/graphcode/Tests/MailroomWatchPresentationTests.swift b/graphcode/Tests/MailroomWatchPresentationTests.swift new file mode 100644 index 0000000..62cfc2a --- /dev/null +++ b/graphcode/Tests/MailroomWatchPresentationTests.swift @@ -0,0 +1,46 @@ +import Foundation +import GraphcodeKit +import MailroomKit +import Testing + +@testable import graphcode + +@Suite +struct MailroomWatchPresentationTests { + private func node(watch: MailroomWatch?, state: LoopState = .running) -> LoopNode { + LoopNode(title: "Triage", loopType: .goalBased, mailroomWatch: watch, state: state) + } + + @Test + func aLoopWithoutAWatchShowsNothing() { + let plain = node(watch: nil) + + #expect(!MailroomWatchPresentation.showsGlyph(for: plain)) + #expect(MailroomWatchPresentation.tooltip(for: plain) == nil) + } + + @Test + func aWatchOnEveryPostSaysSoWithoutATopic() { + let hearingAll = node(watch: MailroomWatch()) + + #expect(MailroomWatchPresentation.showsGlyph(for: hearingAll)) + #expect(MailroomWatchPresentation.tooltip(for: hearingAll) == "Watching the Mailroom") + } + + @Test + func aWatchOnOneTopicNamesIt() { + let narrowed = node(watch: MailroomWatch(topic: "design")) + + #expect(MailroomWatchPresentation.showsGlyph(for: narrowed)) + #expect( + MailroomWatchPresentation.tooltip(for: narrowed) == "Watching the Mailroom · topic design") + } + + @Test(arguments: [LoopState.succeeded, .failed, .stalled, .stopped]) + func aResolvedLoopHidesItsWatch(state: LoopState) { + let finished = node(watch: MailroomWatch(topic: "design"), state: state) + + #expect(!MailroomWatchPresentation.showsGlyph(for: finished)) + #expect(MailroomWatchPresentation.tooltip(for: finished) == nil) + } +}