Watch: waiting-count complication and multi-select questions

A WidgetKit extension (accessoryCircular, Rectangular, Inline, Corner) shows
GET /v1/summary. The device token moves to the App Group's Keychain access
group on launch so the extension can read it without re-pairing; the app
reloads the timeline when its feed counts change and on notifications.
Multi-select questions toggle options and send an array of values.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
gibandClaude Opus 5.5 committed 2026-11-01 11:06:38 -05:00
1 parent d4d67295c6
commit 0994fd7d5a
18 files changed
+800 -23

No files matched your search

+66 -9
View File
@@ -13,7 +13,9 @@ struct ThreadView: View {
@State private var error: String?
@State private var sending = false
@State private var visible = false
@State private var answers: [String: String] = [:]
@State private var answers: [String: RespondRequest.Answer] = [:]
/// Options ticked so far on multi-select questions, by question id.
@State private var selections: [String: Set<String>] = [:]
@State private var scrolledToLatest = false
var body: some View {
@@ -152,19 +154,29 @@ struct ThreadView: View {
Text(question.question)
.font(.system(size: 14, weight: .semibold))
.padding(.top, 1)
if question.multiSelect {
Text("Choose any")
.font(.system(size: 11))
.foregroundStyle(Theme.secondaryText)
.padding(.top, 1)
}
VStack(spacing: 6) {
ForEach(question.options, id: \.value) { choice in
let chosen = answers[question.id] == choice.value
Button(choice.label) { answer(request, question.id, choice.value) }
.buttonStyle(ChoiceButtonStyle(
fill: chosen ? Theme.you : Theme.control,
foreground: chosen ? .black : .white))
if question.multiSelect {
multiSelectOptions(request, question)
} else {
ForEach(question.options, id: \.value) { choice in
let chosen = answers[question.id] == .text(choice.value)
Button(choice.label) { answer(request, question.id, .text(choice.value)) }
.buttonStyle(ChoiceButtonStyle(
fill: chosen ? Theme.you : Theme.control,
foreground: chosen ? .black : .white))
}
}
if question.allowCustomAnswer {
TextFieldLink(prompt: Text(question.question)) {
Text("Other…")
} onSubmit: { text in
answer(request, question.id, text)
answer(request, question.id, .text(text))
}
.buttonStyle(ChoiceButtonStyle())
}
@@ -175,6 +187,37 @@ struct ThreadView: View {
}
}
/// Checkmark toggles, then Send. With more questions still open, the button says Done and the
/// answers go out together once the last one is in.
@ViewBuilder
private func multiSelectOptions(_ request: PendingRequest, _ question: PendingRequest.Question) -> some View {
let picked = selections[question.id, default: []]
ForEach(question.options, id: \.value) { choice in
let checked = picked.contains(choice.value)
Button { toggle(question.id, choice.value) } label: {
HStack(spacing: 6) {
Image(systemName: checked ? "checkmark.circle.fill" : "circle")
Text(choice.label)
.multilineTextAlignment(.leading)
Spacer(minLength: 0)
}
}
.buttonStyle(ChoiceButtonStyle(
fill: checked ? Theme.you : Theme.control,
foreground: checked ? .black : .white))
.accessibilityAddTraits(checked ? .isSelected : [])
}
let last = request.questions.allSatisfy { $0.id == question.id || answers[$0.id] != nil }
Button(last ? "Send" : "Done") {
let values = question.options.map(\.value).filter(picked.contains)
answer(request, question.id, .values(values))
}
.buttonStyle(PillButtonStyle(
fill: picked.isEmpty ? Theme.control : Theme.you,
foreground: picked.isEmpty ? Theme.done : .black))
.disabled(picked.isEmpty)
}
private func statusLine(_ item: FeedItem) -> String {
let place = "\(item.hostLabel) · \(item.projectTitle)"
return switch item.phase {
@@ -224,13 +267,20 @@ struct ThreadView: View {
perform { try await model.api.respond(hostId: hostId, threadId: threadId, request: request) }
}
private func toggle(_ questionId: String, _ value: String) {
if selections[questionId, default: []].remove(value) == nil {
selections[questionId, default: []].insert(value)
}
}
/// Records one answer. Once every question has one, sends them together.
private func answer(_ request: PendingRequest, _ questionId: String, _ value: String) {
private func answer(_ request: PendingRequest, _ questionId: String, _ value: RespondRequest.Answer) {
answers[questionId] = value
let all = request.questions.allSatisfy { answers[$0.id] != nil }
guard all else { return }
let payload = answers.filter { key, _ in request.questions.contains { $0.id == key } }
answers = [:]
selections = [:]
respond(.answers(requestId: request.id, answers: payload))
}
@@ -307,3 +357,10 @@ private struct QuickReplies: View {
}
.environment(AppModel.preview())
}
#Preview("Multi-select") {
NavigationStack {
ThreadView(hostId: "mac-mini", threadId: "t-release")
}
.environment(AppModel.preview())
}