crossmate

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

commit 508f13c9f2abe9f1df9652001538a6f0204af52b
parent 8db7b6f12971d01cb72cb2c87efc784acf27d098
Author: Michael Camilleri <[email protected]>
Date:   Tue, 21 Jul 2026 16:08:22 +0900

Replace the scrolling invite row with a friend grid

The horizontal row did not make its additional friends discoverable.
This commit shows the 12 most recent friends in a three-column grid and
presents 'See All Crossmates' when the full list contains more entries.

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

Diffstat:
MCrossmate/Views/GameList/GameShareItem.swift | 58+++++++++++++++++-----------------------------------------
1 file changed, 17 insertions(+), 41 deletions(-)

diff --git a/Crossmate/Views/GameList/GameShareItem.swift b/Crossmate/Views/GameList/GameShareItem.swift @@ -13,7 +13,7 @@ struct GameShareSheet: View { @Environment(SyncMonitor.self) private var syncMonitor @Environment(EventLog.self) private var eventLog @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \FriendEntity.createdAt, ascending: true)], + sortDescriptors: [NSSortDescriptor(keyPath: \FriendEntity.createdAt, ascending: false)], predicate: NSPredicate(format: "isBlocked == NO"), animation: .default ) @@ -32,6 +32,11 @@ struct GameShareSheet: View { @State private var reportFallbackNote: String? private static let supportEmail = "[email protected]" + private static let visibleFriendLimit = 12 + private static let friendGridColumns = Array( + repeating: GridItem(.flexible(), spacing: 8), + count: 3 + ) init(gameID: UUID, title: String, shareController: ShareController) { self.gameID = gameID @@ -46,9 +51,11 @@ struct GameShareSheet: View { } private var visibleFriends: Array<FetchedResults<FriendEntity>.Element> { - Array(friends.prefix(4)) + Array(friends.prefix(Self.visibleFriendLimit)) } + private var hasMoreFriends: Bool { friends.count > Self.visibleFriendLimit } + /// A live public link is the definitive signal the owner took the link /// route, so it wins over any participants the share carries — those are /// public joiners, not directly invited friends. CloudKit forbids mixing @@ -63,11 +70,6 @@ struct GameShareSheet: View { /// this mode while the link is still on offer. private var isDirectInviteMode: Bool { shareURL == nil && !invitedAuthorIDs.isEmpty } - /// Width of the trailing scroll fade. The scroll content gets a matching - /// trailing margin so the last item can scroll clear of the fade instead of - /// stopping flush beneath it. - private static let trailingFadeWidth: CGFloat = 72 - var body: some View { NavigationStack { List { @@ -169,12 +171,14 @@ struct GameShareSheet: View { .foregroundStyle(.secondary) .frame(maxWidth: .infinity, minHeight: 72, alignment: .center) } else { - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 12) { + VStack(spacing: 12) { + LazyVGrid(columns: Self.friendGridColumns, spacing: 8) { ForEach(visibleFriends, id: \.authorID) { friend in friendInviteButton(for: friend) } + } + if hasMoreFriends { NavigationLink { FriendPickerView( gameID: gameID, @@ -182,40 +186,12 @@ struct GameShareSheet: View { isInviteLimitReached: isInviteLimitReached ) } label: { - VStack(spacing: 6) { - Image(systemName: "ellipsis") - .font(.system(size: 40, weight: .regular)) - .frame(width: 40, height: 40) - Text("All") - .font(.callout.weight(.medium)) - .lineLimit(1) - } - .frame(width: 96, height: 88) + Text("See All Crossmates") + .font(.callout.weight(.medium)) } - .buttonStyle(.plain) - .disabled(isInviteLimitReached || isLoadingExistingLink) + .disabled(isLoadingExistingLink) } - .frame(maxWidth: .infinity) - .padding(.vertical, 2) } - // Fade a fixed strip at the trailing edge so the row - // always reads as scrollable, even when a whole number - // of friends lines up flush with the edge. - .mask( - HStack(spacing: 0) { - Rectangle() - LinearGradient( - colors: [.black, .clear], - startPoint: .leading, - endPoint: .trailing - ) - .frame(width: Self.trailingFadeWidth) - } - ) - // Be proportional to the trailing fade width so the last item can - // scroll clear of the fade instead of stopping flush - // beneath it. - .contentMargins(.trailing, (Self.trailingFadeWidth / 2), for: .scrollContent) .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) } } @@ -312,7 +288,7 @@ struct GameShareSheet: View { .lineLimit(1) .minimumScaleFactor(0.8) } - .frame(width: 108, height: 88) + .frame(maxWidth: .infinity, minHeight: 88) } .buttonStyle(.plain) .disabled(authorID.isEmpty || invitingAuthorID != nil || wasInvited || isLoadingExistingLink || (isInviteLimitReached && !wasInvited))