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:
1 parent
d4d67295c6
commit
0994fd7d5a
18 files changed
+800
-23
No files matched your search
@@ -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())
|
||||
}
|
||||
Reference in new issue
Block a user