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:
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))