diff --git a/Flipcash/Core/Screens/Main/Home/ActivityAvatar.swift b/Flipcash/Core/Screens/Main/Home/ActivityAvatar.swift index 595c8fa12..e84a85066 100644 --- a/Flipcash/Core/Screens/Main/Home/ActivityAvatar.swift +++ b/Flipcash/Core/Screens/Main/Home/ActivityAvatar.swift @@ -11,7 +11,7 @@ import FlipcashCore /// activity (tips/sends), the token image for token activity (deposits, buys), /// two overlapping coins for a conversion, or a monogram fallback. A peer avatar /// shows a face rather than a token, so it carries the transacted token as a coin -/// badge (Figma 8966:1910, 9717:14215). +/// badge (Figma 8966:1910, 9717:14215) unless the caller turns it off. /// /// Shared by the activity row and the transaction details header at different /// sizes, so the details screen opens on exactly the avatar that was tapped. Every @@ -22,6 +22,11 @@ struct ActivityAvatar: View { let resolution: ActivityResolution var size: CGFloat = 40 + /// Whether a peer avatar carries its token badge. The details header names the + /// token in full under the amount, so it turns the badge off rather than + /// saying the same thing twice at two sizes. + var showsTokenBadge: Bool = true + @Environment(SessionContainer.self) private var sessionContainer private var session: Session { sessionContainer.session } @@ -41,25 +46,31 @@ struct ActivityAvatar: View { .frame(width: size, height: size) .clipShape(Circle()) .overlay(alignment: .bottomTrailing) { - if Self.showsTokenBadge(for: activity) { + if drawsTokenBadge { tokenBadge.offset(x: badgeOverhang, y: badgeOverhang) } } // Reserves the badge's overhang so it doesn't eat into the gap // before whatever sits beside the avatar. - .padding(.trailing, Self.showsTokenBadge(for: activity) ? badgeOverhang : 0) + .padding(.trailing, drawsTokenBadge ? badgeOverhang : 0) } } - /// Whether the avatar carries a token badge: only a peer activity, whose - /// avatar is the counterparty rather than the token itself. + /// Whether this avatar draws a badge: one the activity calls for, that the + /// caller hasn't turned off. + private var drawsTokenBadge: Bool { + showsTokenBadge && Self.showsTokenBadge(for: activity) + } + + /// Whether the activity's avatar calls for a token badge: only a peer + /// activity, whose avatar is the counterparty rather than the token itself. static func showsTokenBadge(for activity: Activity) -> Bool { activity.swapMetadata == nil && activity.counterparty != nil } /// The token the payment moved in, as a coin badge over the counterparty's - /// avatar (Figma 9717:14140) — a peer activity shows *who*, so this badge is - /// the only place the token reads. + /// avatar (Figma 9717:14140) — a peer row shows *who*, so in a row this badge + /// is the only place the token reads. @ViewBuilder private var tokenBadge: some View { tokenCoin( url: resolution.imageURL(for: activity.exchangedFiat.mint, fallback: resolution.entryMint, session: session), diff --git a/Flipcash/Core/Screens/Main/Home/TransactionDetailsScreen.swift b/Flipcash/Core/Screens/Main/Home/TransactionDetailsScreen.swift index 1f20daeb5..ffd45b6ef 100644 --- a/Flipcash/Core/Screens/Main/Home/TransactionDetailsScreen.swift +++ b/Flipcash/Core/Screens/Main/Home/TransactionDetailsScreen.swift @@ -43,6 +43,10 @@ struct TransactionDetailsScreen: View { /// gives the header. private static let avatarSize: CGFloat = 80 + /// The gap between the cards, per Figma node 9708:118142 — tighter than the + /// gap that separates the header from them. + private static let cardSpacing: CGFloat = 8 + private var details: TransactionDetails { TransactionDetails( activity: activity, @@ -57,21 +61,27 @@ struct TransactionDetailsScreen: View { ScrollView(.vertical, showsIndicators: false) { VStack(spacing: 16) { header - receiptCard - idCard - - if let userID = activity.counterparty?.userID, details.canViewInChat { - Button("View in Chat") { - // Pushed onto the stack this screen is already on, - // not routed to the Chat tab: a cross-stack jump - // swaps the tab out from under the transition, so - // the bar and the conversation list both show - // before the chat lands. Pushed, the chat arrives - // from the entry it belongs to and back returns - // here. - router.push(.tipConversationForUser(userID)) + + // The cards are one block, at the tighter gap Figma sets + // between them; the header sits apart from that block, so + // its gap is the outer stack's rather than this one's. + VStack(spacing: Self.cardSpacing) { + receiptCard + idCard + + if let userID = activity.counterparty?.userID, details.canViewInChat { + Button("View in Chat") { + // Pushed onto the stack this screen is already + // on, not routed to the Chat tab: a cross-stack + // jump swaps the tab out from under the + // transition, so the bar and the conversation + // list both show before the chat lands. Pushed, + // the chat arrives from the entry it belongs to + // and back returns here. + router.push(.tipConversationForUser(userID)) + } + .buttonStyle(.filled05) } - .buttonStyle(.filled05) } } .padding(.horizontal, 20) @@ -103,7 +113,15 @@ struct TransactionDetailsScreen: View { private var header: some View { VStack(spacing: 24) { VStack(spacing: 16) { - ActivityAvatar(activity: activity, resolution: resolution, size: Self.avatarSize) + // No token badge here, unlike the row: the line under the + // amount already names the token, and Figma draws the header + // avatar plain. + ActivityAvatar( + activity: activity, + resolution: resolution, + size: Self.avatarSize, + showsTokenBadge: false + ) Text(details.title) .font(.appTextLarge)