crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

commit 49922995fe4e7d368a03b5f66d9a67d8cf8f66b7
parent 347d9f44d6b36f38766b16a0ecee15baa618d2b7
Author: Michael Camilleri <[email protected]>
Date:   Thu, 21 May 2026 11:51:28 +0900

Prevent solved puzzle opens from sending win pings

This commit separates local completion events from observed synced completion
state in the Puzzle View. A solve reported by PlayerSession can still call
onComplete and fan out the .win ping, but loading or observing an
already-solved shared puzzle only updates the solved UI state. This prevents a
collaborator who merely opens a solved puzzle from being reported as the
solver.

Co-Authored-By: Codex GPT 5.5 <[email protected]>

Diffstat:
MCrossmate/Views/PuzzleView.swift | 53+++++++++++++++++++++++++++++++++--------------------
1 file changed, 33 insertions(+), 20 deletions(-)

diff --git a/Crossmate/Views/PuzzleView.swift b/Crossmate/Views/PuzzleView.swift @@ -95,7 +95,8 @@ struct PuzzleView: View { session: session, roster: roster, hasSolved: $hasSolved, - onCompletionStateChanged: handleCompletionState + onLocalCompletionStateChanged: handleLocalCompletionState, + onObservedCompletionStateChanged: handleObservedCompletionState )) .modifier(PuzzlePresentationModifier( session: session, @@ -211,23 +212,34 @@ struct PuzzleView: View { } } - private func handleCompletionState(_ newValue: Game.CompletionState) { - switch newValue { - case .incomplete: - break - case .filledWithErrors: - showErrorsAlert = true - case .solved: - guard !hasSolved else { return } - hasSolved = true - if session.isPencilMode { - session.togglePencil() - } - Task { @MainActor in - onComplete?() - } + private func handleLocalCompletionState(_ newValue: Game.CompletionState) { + switch newValue { + case .incomplete: + break + case .filledWithErrors: + showErrorsAlert = true + case .solved: + guard !hasSolved else { return } + hasSolved = true + if session.isPencilMode { + session.togglePencil() + } + Task { @MainActor in + onComplete?() } } + } + + private func handleObservedCompletionState(_ newValue: Game.CompletionState) { + switch newValue { + case .incomplete: + break + case .filledWithErrors: + showErrorsAlert = true + case .solved: + hasSolved = true + } + } private var puzzleArea: some View { ZStack { @@ -910,7 +922,8 @@ private struct PuzzleLifecycleModifier: ViewModifier { let session: PlayerSession let roster: PlayerRoster @Binding var hasSolved: Bool - let onCompletionStateChanged: (Game.CompletionState) -> Void + let onLocalCompletionStateChanged: (Game.CompletionState) -> Void + let onObservedCompletionStateChanged: (Game.CompletionState) -> Void func body(content: Content) -> some View { content @@ -919,15 +932,15 @@ private struct PuzzleLifecycleModifier: ViewModifier { } .onAppear { if session.game.completionState == .solved { - onCompletionStateChanged(.solved) + hasSolved = true } } .onChange(of: session.game.completionState) { _, newValue in - onCompletionStateChanged(newValue) + onObservedCompletionStateChanged(newValue) } .onAppear { session.onCompletionStateChanged = { newValue in - onCompletionStateChanged(newValue) + onLocalCompletionStateChanged(newValue) } } .onDisappear {