crossmate

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

commit 338bc54aaa463dddafa63206a878c382bd28ad51
parent 335c5c1f3579132a7c407f66fe14be2e073f4883
Author: Michael Camilleri <[email protected]>
Date:   Tue, 30 Jun 2026 11:16:58 +0900

Add a Settings unblock surface

Blocked friends were hidden from the Crossmates list, and the
unblockFriend action was already wired, but the user had no in-app
surface for reversing a block. This commit adds a 'Sharing' section to
Settings with a 'Blocked Users' screen that lists blocked friends and
confirms before restoring them.

The screen reuses unblockFriend, so unblocking keeps the existing
CloudKit permission restore and hidden-game reprojection path. The Game
List's blocked-friend fetch is also documented as the guard that keeps
pending invites from blocked crossmates out of the 'Invited' section
while cleanup catches up.

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

Diffstat:
MCrossmate/Views/GameList/GameListView.swift | 3+++
MCrossmate/Views/Settings/SettingsView.swift | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 75 insertions(+), 0 deletions(-)

diff --git a/Crossmate/Views/GameList/GameListView.swift b/Crossmate/Views/GameList/GameListView.swift @@ -33,6 +33,9 @@ struct GameListView: View { sortDescriptors: [], predicate: NSPredicate(format: "isBlocked == YES") ) + // Drives the Invited section filter below. Pending invites from blocked + // crossmates can exist briefly until the ping cleanup path consumes them, + // but the library and badge state should never surface them. private var blockedFriends: FetchedResults<FriendEntity> @Environment(\.acceptInvite) private var acceptInvite diff --git a/Crossmate/Views/Settings/SettingsView.swift b/Crossmate/Views/Settings/SettingsView.swift @@ -78,6 +78,12 @@ struct SettingsView: View { Text("Receive notifications when friends nudge you, pause playing after making changes, invite you to play, join one of your puzzles or finish a puzzle. These settings apply only to this device.") } + Section("Sharing") { + NavigationLink("Blocked Users") { + BlockedUsersView() + } + } + if debugMode { Section("Debugging") { Toggle("Enable iCloud Sync", isOn: $preferences.isICloudSyncEnabled) @@ -212,6 +218,72 @@ struct SettingsView: View { } } +private struct BlockedUsersView: View { + @Environment(\.unblockFriend) private var unblockFriend + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \FriendEntity.createdAt, ascending: true)], + predicate: NSPredicate(format: "isBlocked == YES"), + animation: .default + ) + private var blockedFriends: FetchedResults<FriendEntity> + + @State private var unblockTarget: FriendEntity? + + var body: some View { + List { + ForEach(blockedFriends, id: \.authorID) { friend in + blockedFriendRow(for: friend) + } + } + .overlay { + if blockedFriends.isEmpty { + ContentUnavailableView { + Label("No Blocked Users", systemImage: "person.crop.circle.badge.xmark") + } description: { + Text("Blocked crossmates will appear here.") + } + } + } + .navigationTitle("Blocked Users") + .navigationBarTitleDisplayMode(.inline) + .alert("Unblock This Player?", isPresented: .init( + get: { unblockTarget != nil }, + set: { if !$0 { unblockTarget = nil } } + )) { + Button("Unblock") { + guard let authorID = unblockTarget?.authorID, !authorID.isEmpty else { return } + Task { await unblockFriend?(authorID) } + } + Button("Cancel", role: .cancel) {} + } message: { + let name = unblockTarget?.resolvedDisplayName ?? "this player" + Text("\(name) will be able to invite you again, and any puzzles hidden when you blocked them will return to your list.") + } + } + + @ViewBuilder + private func blockedFriendRow(for friend: FriendEntity) -> some View { + let authorID = friend.authorID ?? "" + + HStack { + FriendAvatarView(authorID: authorID) + .padding(.trailing, 8) + Text(friend.resolvedDisplayName) + .foregroundStyle(.primary) + Spacer() + Button("Unblock") { + unblockTarget = friend + } + .disabled(authorID.isEmpty) + } + .swipeActions(edge: .trailing) { + Button("Unblock") { unblockTarget = friend } + .tint(.blue) + } + } +} + private struct ProfileColorEditView: View { @Environment(PlayerPreferences.self) private var preferences @Environment(\.dismiss) private var dismiss