crossmate

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

FriendsView.swift (4456B)


      1 import SwiftUI
      2 
      3 /// Sheet listing the user's crossmates (friends), presented from the game
      4 /// list. Friends are accumulated automatically the first time you collaborate
      5 /// with someone (see `FriendController`); the actions are renaming (a private
      6 /// nickname via `\.renameFriend`) and blocking, which tears down the pairwise
      7 /// channel via `\.blockFriend`.
      8 struct FriendsView: View {
      9     @Environment(\.blockFriend) private var blockFriend
     10     @Environment(\.renameFriend) private var renameFriend
     11     @Environment(\.dismiss) private var dismiss
     12 
     13     @FetchRequest(
     14         sortDescriptors: [NSSortDescriptor(keyPath: \FriendEntity.createdAt, ascending: true)],
     15         predicate: NSPredicate(format: "isBlocked == NO")
     16     )
     17     private var friends: FetchedResults<FriendEntity>
     18 
     19     @State private var blockTarget: FriendEntity?
     20     @State private var renameTarget: FriendEntity?
     21     @State private var renameText = ""
     22 
     23     var body: some View {
     24         NavigationStack {
     25             List {
     26                 ForEach(friends, id: \.authorID) { friend in
     27                     friendRow(for: friend)
     28                 }
     29             }
     30             .overlay {
     31                 if friends.isEmpty {
     32                     ContentUnavailableView {
     33                         Label("No Crossmates", systemImage: "person.2")
     34                     } description: {
     35                         Text("Crossmates are added automatically when you start a shared puzzle with someone.")
     36                     }
     37                 }
     38             }
     39             .navigationTitle("Crossmates")
     40             .navigationBarTitleDisplayMode(.inline)
     41             .toolbar {
     42                 ToolbarItem(placement: .cancellationAction) {
     43                     Button {
     44                         dismiss()
     45                     } label: {
     46                         Image(systemName: "xmark")
     47                     }
     48                     .accessibilityLabel("Cancel")
     49                 }
     50             }
     51             .alert("Rename This Player?", isPresented: .init(
     52                 get: { renameTarget != nil },
     53                 set: { if !$0 { renameTarget = nil } }
     54             )) {
     55                 TextField("Name", text: $renameText)
     56                 Button("Rename") {
     57                     if let authorID = renameTarget?.authorID {
     58                         let nickname = renameText
     59                         Task { await renameFriend?(authorID, nickname) }
     60                     }
     61                 }
     62                 Button("Cancel", role: .cancel) {}
     63             } message: {
     64                 Text("Only you will see this name. Leave it blank to use the name they chose.")
     65             }
     66             .alert("Block This Player?", isPresented: .init(
     67                 get: { blockTarget != nil },
     68                 set: { if !$0 { blockTarget = nil } }
     69             )) {
     70                 Button("Block", role: .destructive) {
     71                     if let authorID = blockTarget?.authorID {
     72                         Task { await blockFriend?(authorID) }
     73                     }
     74                 }
     75                 Button("Cancel", role: .cancel) {}
     76             } message: {
     77                 let name = blockTarget?.resolvedDisplayName ?? "this player"
     78                 Text("You won't receive further invites from \(name), and any games they currently share with you will be removed from this device.")
     79             }
     80         }
     81     }
     82 
     83     @ViewBuilder
     84     private func friendRow(for friend: FriendEntity) -> some View {
     85         HStack {
     86             FriendAvatarView(authorID: friend.authorID ?? "")
     87                 .padding(.trailing, 8)
     88             Text(friend.resolvedDisplayName)
     89             Spacer()
     90             Menu {
     91                 Button {
     92                     renameText = friend.nickname ?? ""
     93                     renameTarget = friend
     94                 } label: {
     95                     Label("Rename", systemImage: "character.cursor.ibeam")
     96                 }
     97                 Button(role: .destructive) { blockTarget = friend } label: {
     98                     Label("Block", systemImage: "hand.raised")
     99                 }
    100             } label: {
    101                 Image(systemName: "ellipsis")
    102                     .font(.body)
    103                     .frame(width: 32, height: 32)
    104                     .contentShape(Rectangle())
    105             }
    106             .tint(.secondary)
    107             .compositingGroup()
    108         }
    109         .swipeActions(edge: .trailing) {
    110             Button("Block", role: .destructive) { blockTarget = friend }
    111         }
    112     }
    113 }