crossmate

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

commit 63e3eeea4d388dc75141d45cb66e11bbd6e5970c
parent e0bee2c7973d378c75d3331d8dabbcee33d05029
Author: Michael Camilleri <[email protected]>
Date:   Fri, 12 Jun 2026 16:59:13 +0900

Use nicknames in announcement banners

Diffstat:
MCrossmate/Persistence/GameStore.swift | 17+++++++++++++++++
MCrossmate/Sync/SessionMonitor.swift | 5++++-
MTests/Unit/Sync/SessionMonitorTests.swift | 24++++++++++++++++++++++++
3 files changed, 45 insertions(+), 1 deletion(-)

diff --git a/Crossmate/Persistence/GameStore.swift b/Crossmate/Persistence/GameStore.swift @@ -1560,6 +1560,23 @@ final class GameStore { return fetchPlayerEntity(gameID: gameID, authorID: authorID)?.name ?? "" } + /// The user's private nickname for `authorID` (`FriendEntity.nickname`), + /// or `nil` when none is set. The same override `resolvedDisplayName` + /// applies, exposed here for surfaces hydrated through `GameStore` + /// rather than from a `FriendEntity` row — currently the catch-up + /// banner's `SessionMonitor.movesSummaries`. + func friendNickname(for authorID: String) -> String? { + guard !authorID.isEmpty else { return nil } + let request = NSFetchRequest<FriendEntity>(entityName: "FriendEntity") + request.predicate = NSPredicate(format: "authorID == %@", authorID) + request.fetchLimit = 1 + guard let nickname = (try? context.fetch(request).first)?.nickname? + .trimmingCharacters(in: .whitespacesAndNewlines), + !nickname.isEmpty + else { return nil } + return nickname + } + /// The read cursor persisted for `(gameID, authorID)`, or `nil` when no row /// exists or none has been stamped. Used by the local pause-diagnostics /// mirror to compare this device's actual cursor against the value a peer's diff --git a/Crossmate/Sync/SessionMonitor.swift b/Crossmate/Sync/SessionMonitor.swift @@ -103,7 +103,10 @@ final class SessionMonitor { summaries.append(SessionSummary( gameID: gameID, authorID: authorID, - playerName: store.playerName(for: gameID, by: authorID), + // A nickname the user assigned via Rename wins over the + // peer's own published name, matching every other surface. + playerName: store.friendNickname(for: authorID) + ?? store.playerName(for: gameID, by: authorID), puzzleTitle: puzzleTitle, added: added, cleared: cleared, diff --git a/Tests/Unit/Sync/SessionMonitorTests.swift b/Tests/Unit/Sync/SessionMonitorTests.swift @@ -147,6 +147,30 @@ struct SessionMonitorTests { #expect(summary.isFirstObservation) } + @Test("A friend nickname overrides the peer's published name in summaries") + func nicknameOverridesPlayerName() throws { + let fixture = try makeFixture() + try addPlayer(in: fixture, authorID: Self.alice, name: "Alice") + let ctx = fixture.persistence.viewContext + let friend = FriendEntity(context: ctx) + friend.authorID = Self.alice + friend.pairKey = "pair-alice" + friend.friendZoneName = "friend-pair-alice" + friend.friendZoneOwnerName = "_owner" + friend.databaseScope = 0 + friend.createdAt = Date() + friend.nickname = "Mum" + try ctx.save() + try writeMoves( + in: fixture, + authorID: Self.alice, + cells: [position(0, 0): ("A", Date())] + ) + + let summary = try #require(fixture.monitor.movesSummaries(for: fixture.gameID).first) + #expect(summary.playerName == "Mum") + } + @Test("First observation skips a peer whose filled count is zero") func firstObservationSkipsEmptyPeer() throws { let fixture = try makeFixture()