FriendEntity+DisplayName.swift (3425B)
1 import CoreData 2 import Foundation 3 4 extension FriendEntity { 5 /// The name the invite surfaces should show for this friend. 6 /// 7 /// A `nickname` the user assigned via Rename wins outright — it's their 8 /// private label for the friend and never follows the friend's own 9 /// renames. Otherwise the friend's own name (`givenDisplayName`), then 10 /// the "Player" placeholder. 11 var resolvedDisplayName: String { 12 if let nickname = nickname?.trimmingCharacters(in: .whitespacesAndNewlines), 13 !nickname.isEmpty { 14 return nickname 15 } 16 return givenDisplayName ?? "Player" 17 } 18 19 /// The friend's own, self-chosen name — `resolvedDisplayName` without the 20 /// local nickname override, and the substring the nickname directory 21 /// rewrites in sender-composed notification text. 22 /// 23 /// `displayName` is fed exclusively by the friend's `name` Decision in the 24 /// pairwise friend zone (`RecordSerializer.applyDecisionRecord`), so it is 25 /// the live, rename-following value. Until the first Decision syncs the 26 /// fallback is the freshest per-game `Player` snapshot the friend wrote at 27 /// game open; `nil` when neither has arrived. 28 var givenDisplayName: String? { 29 if let name = displayName?.trimmingCharacters(in: .whitespacesAndNewlines), 30 !name.isEmpty { 31 return name 32 } 33 if let authorID, !authorID.isEmpty, let ctx = managedObjectContext { 34 let req = NSFetchRequest<PlayerEntity>(entityName: "PlayerEntity") 35 req.predicate = NSPredicate( 36 format: "authorID == %@ AND name != nil AND name != %@", 37 authorID, "" 38 ) 39 req.sortDescriptors = [NSSortDescriptor(key: "updatedAt", ascending: false)] 40 req.fetchLimit = 1 41 if let name = (try? ctx.fetch(req).first)?.name? 42 .trimmingCharacters(in: .whitespacesAndNewlines), 43 !name.isEmpty { 44 return name 45 } 46 } 47 return nil 48 } 49 50 /// Rewrites the App Group nickname directory from Core Data ground truth: 51 /// one `authorID → nickname` entry per unblocked friend with a nickname. 52 /// The Notification Service Extension pairs these with the live 53 /// `PushPayload.senderName` to swap the friend's own name for the nickname 54 /// in sender-composed alert text. Called after a local rename, after sync 55 /// applies a `nickname` Decision, and once at launch as a heal. Must run 56 /// inside the context's queue (`performAndWait`) when `ctx` is a background 57 /// context. 58 static func rebuildNicknameDirectory(in ctx: NSManagedObjectContext) { 59 let req = NSFetchRequest<FriendEntity>(entityName: "FriendEntity") 60 req.predicate = NSPredicate( 61 format: "isBlocked == NO AND nickname != nil AND nickname != %@", "" 62 ) 63 var directory: [String: NicknameDirectory.Entry] = [:] 64 for friend in (try? ctx.fetch(req)) ?? [] { 65 guard let authorID = friend.authorID, !authorID.isEmpty, 66 let nickname = friend.nickname? 67 .trimmingCharacters(in: .whitespacesAndNewlines), 68 !nickname.isEmpty 69 else { continue } 70 directory[authorID] = NicknameDirectory.Entry(nickname: nickname) 71 } 72 NicknameDirectory.save(directory) 73 } 74 }