From 89ce45ac67ada37c5c3e4fd95f4479465dca089d Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 9 Nov 2026 15:23:52 -0500 Subject: [PATCH] fix(chat): no placeholder shapes behind the group-ready card --- .../Conversation/ConversationScreen.swift | 10 ++++++++-- .../Screens/Conversation/GroupReadyCard.swift | 14 ++++++++++++++ FlipcashTests/Chat/GroupReadyCardTests.swift | 17 +++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/Flipcash/Core/Screens/Conversation/ConversationScreen.swift b/Flipcash/Core/Screens/Conversation/ConversationScreen.swift index a7baefc99..1506f97b4 100644 --- a/Flipcash/Core/Screens/Conversation/ConversationScreen.swift +++ b/Flipcash/Core/Screens/Conversation/ConversationScreen.swift @@ -534,8 +534,14 @@ struct ConversationScreen: View { // which is why this reads `withholdsTranscript` and not `obscuresTranscript`: a chat // whose rules haven't landed yet is blurred without yet refusing anything. // Also stands in while an empty transcript's first load is out, so a chat with nothing - // cached opens on the placeholder and paints once with its history. - showsGatePlaceholder: (gate.withholdsTranscript || !didInitialRead) && (coordinator?.items.isEmpty ?? true), + // cached opens on the placeholder and paints once with its history, except under the + // group-ready card. + showsGatePlaceholder: GroupReadyCard.showsGatePlaceholder( + withholdsTranscript: gate.withholdsTranscript, + didInitialRead: didInitialRead, + transcriptIsEmpty: coordinator?.items.isEmpty ?? true, + cardShown: showsInviteCard + ), gateMintName: gateMintName, gateShortfall: gateShortfall, gateDisplayCurrency: ratesController.displayCurrency, diff --git a/Flipcash/Core/Screens/Conversation/GroupReadyCard.swift b/Flipcash/Core/Screens/Conversation/GroupReadyCard.swift index 8506be819..46311e0fb 100644 --- a/Flipcash/Core/Screens/Conversation/GroupReadyCard.swift +++ b/Flipcash/Core/Screens/Conversation/GroupReadyCard.swift @@ -33,6 +33,20 @@ enum GroupReadyCard { ) } + /// Whether the transcript draws the gate's placeholder shapes behind an empty chat. + /// + /// A gated chat the viewer is refused always gets them. Otherwise they only bridge an empty + /// transcript's first load, and not while the group-ready card shows: that chat is new and has + /// nothing to wait for, so the shapes would sit behind the card. + static func showsGatePlaceholder( + withholdsTranscript: Bool, + didInitialRead: Bool, + transcriptIsEmpty: Bool, + cardShown: Bool + ) -> Bool { + (withholdsTranscript || (!didInitialRead && !cardShown)) && transcriptIsEmpty + } + /// The chat-list line for a conversation that has no message at all: an invitation while the /// group-ready card would show, "Nothing yet" otherwise. Client-only, so it follows the card /// leaving once someone joins. diff --git a/FlipcashTests/Chat/GroupReadyCardTests.swift b/FlipcashTests/Chat/GroupReadyCardTests.swift index d3479e362..b125045b5 100644 --- a/FlipcashTests/Chat/GroupReadyCardTests.swift +++ b/FlipcashTests/Chat/GroupReadyCardTests.swift @@ -89,4 +89,21 @@ struct GroupReadyCardTests { // A DM never gets the invitation. #expect(GroupReadyCard.emptyPreview(for: chat(type: .contactDm, count: 1, canInvite: true, isPrivate: false)) == nothing) } + + @Test("Placeholder shapes: none under the card, kept for a gated chat and a plain first load") + func gatePlaceholder() { + func shows(withholds: Bool = false, read: Bool, empty: Bool = true, card: Bool) -> Bool { + GroupReadyCard.showsGatePlaceholder(withholdsTranscript: withholds, didInitialRead: read, transcriptIsEmpty: empty, cardShown: card) + } + // A new group before its first read: the card, no shapes. + #expect(!shows(read: false, card: true)) + // An empty chat with no card still bridges its first load. + #expect(shows(read: false, card: false)) + // A gated chat keeps them, card or not. + #expect(shows(withholds: true, read: true, card: false)) + #expect(shows(withholds: true, read: false, card: true)) + // Rows loaded, or read finished: none. + #expect(!shows(read: false, empty: false, card: false)) + #expect(!shows(read: true, card: false)) + } }