crossmate

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

commit 008ff511015134a2ac84de5d8cedb4a83381d276
parent 5474938b1f03bb7dbc52c77ed1996927b54324da
Author: Michael Camilleri <[email protected]>
Date:   Fri, 29 May 2026 21:22:14 +0900

Group revoked games into their own list section

This commit pulls revoked games out of both groups into a new 'Revoked'
section rendered after 'In progress' in both the list and grid layouts.
the section autohides when empty, like the others.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

Diffstat:
MCrossmate/Sync/CloudQuery.swift | 13+++++++++++++
MCrossmate/Views/GameListView.swift | 34++++++++++++++++++++++++++++++++--
2 files changed, 45 insertions(+), 2 deletions(-)

diff --git a/Crossmate/Sync/CloudQuery.swift b/Crossmate/Sync/CloudQuery.swift @@ -508,6 +508,19 @@ extension SyncEngine { info.scope == scopeValue else { return false } + // The zone has already been confirmed missing server-side (see + // `applyZoneOrphaning`). Re-querying it just fails with `.zoneNotFound` + // (CKError 26) every time the revoked puzzle appears, leaving the + // diagnostics `Last Error` stuck on "Zone does not exist". Report the + // freshen as handled so the caller skips the full-DB `fetchChanges` + // fallback too — there is nothing left to converge. + guard !info.isAccessRevoked else { + await trace( + "\(label) game catch-up: \(gameID.uuidString.prefix(8)) skipped (access revoked)" + ) + return true + } + let checkpointKey = "\(scopeValue):\(gameID.uuidString)" let since = liveQueryCheckpoints[checkpointKey]? .addingTimeInterval(-liveQueryCheckpointOverlap) diff --git a/Crossmate/Views/GameListView.swift b/Crossmate/Views/GameListView.swift @@ -190,10 +190,13 @@ struct GameListView: View { private func content(usesRoomierType: Bool) -> some View { let summaries = games.compactMap { summaryCache.summary(for: $0) } let inProgress = summaries - .filter { $0.completedAt == nil } + .filter { $0.completedAt == nil && !$0.isAccessRevoked } + .sorted { ($0.updatedAt ?? .distantPast) > ($1.updatedAt ?? .distantPast) } + let revoked = summaries + .filter { $0.isAccessRevoked } .sorted { ($0.updatedAt ?? .distantPast) > ($1.updatedAt ?? .distantPast) } let completed = summaries - .filter { $0.completedAt != nil } + .filter { $0.completedAt != nil && !$0.isAccessRevoked } .sorted { ($0.completedAt ?? .distantPast) > ($1.completedAt ?? .distantPast) } let visibleCount = min(completedVisibleCount, completed.count) let visibleCompleted = Array(completed.prefix(visibleCount)) @@ -210,6 +213,7 @@ struct GameListView: View { gridLayout( invites: visibleInvites, inProgress: inProgress, + revoked: revoked, completed: visibleCompleted, hasMore: hasMore, usesRoomierType: usesRoomierType @@ -218,6 +222,7 @@ struct GameListView: View { listLayout( invites: visibleInvites, inProgress: inProgress, + revoked: revoked, completed: visibleCompleted, hasMore: hasMore, usesRoomierType: usesRoomierType @@ -246,6 +251,7 @@ struct GameListView: View { private func listLayout( invites: [InviteEntity], inProgress: [GameSummary], + revoked: [GameSummary], completed: [GameSummary], hasMore: Bool, usesRoomierType: Bool @@ -271,6 +277,16 @@ struct GameListView: View { } } + if !revoked.isEmpty { + Section { + ForEach(revoked) { game in + rowView(for: game, usesRoomierType: usesRoomierType) + } + } header: { + Text("Revoked") + } + } + if !completed.isEmpty { Section { ForEach(completed) { game in @@ -300,6 +316,7 @@ struct GameListView: View { private func gridLayout( invites: [InviteEntity], inProgress: [GameSummary], + revoked: [GameSummary], completed: [GameSummary], hasMore: Bool, usesRoomierType: Bool @@ -332,6 +349,19 @@ struct GameListView: View { } } + if !revoked.isEmpty { + Section { + LazyVGrid(columns: gridColumns, spacing: 12) { + ForEach(revoked) { game in + gameCard(for: game, usesRoomierType: usesRoomierType) + } + } + .padding(.horizontal) + } header: { + gridSectionHeader("Revoked") + } + } + if !completed.isEmpty { Section { LazyVGrid(columns: gridColumns, spacing: 12) {