diff --git a/Flipcash/Core/Screens/Conversation/ConversationGate.swift b/Flipcash/Core/Screens/Conversation/ConversationGate.swift index 063f84c41..07aa4f4c6 100644 --- a/Flipcash/Core/Screens/Conversation/ConversationGate.swift +++ b/Flipcash/Core/Screens/Conversation/ConversationGate.swift @@ -101,9 +101,10 @@ struct ConversationGate: Equatable { /// unless it equals the viewer, so an unknown creator fails closed. Staff get no bypass: only an /// explicit `.staff` rule looks at staff standing. /// -/// `rules` is nil for every non-group chat and for a group whose metadata hasn't -/// hydrated yet; both are open, as is an empty rule list — the contract -/// documents empty as "anyone can". `rates` is `RatesController.cachedRates`, +/// `rules` is nil for most DMs and for a group whose metadata hasn't hydrated +/// yet; both are open, as is an empty rule list — the contract documents empty +/// as "anyone can". Some DMs do carry rules (the Flipcash account's DM has a +/// `never` speaker rule), and they are evaluated the same way. `rates` is `RatesController.cachedRates`, /// needed only to restate a requirement the server denominated in something /// other than USD. /// diff --git a/Flipcash/Core/Screens/Conversation/ConversationScreen.swift b/Flipcash/Core/Screens/Conversation/ConversationScreen.swift index 6246c4c5d..16787f3a0 100644 --- a/Flipcash/Core/Screens/Conversation/ConversationScreen.swift +++ b/Flipcash/Core/Screens/Conversation/ConversationScreen.swift @@ -267,13 +267,18 @@ struct ConversationScreen: View { } /// The chat's participation rules weighed against the signed-in user: what the bottom of the - /// screen draws, and whether the transcript is readable at all. `.open` for every DM and for a - /// group with no rules. + /// screen draws, and whether the transcript is readable at all. `.open` for a chat with no rules. + /// + /// Not limited to groups: the server sends rules on some DMs too — the Flipcash account's DM + /// carries a `never` speaker rule — and a DM participant is always a member, so its speaker + /// rules close the composer the same way a group's do. /// /// Recomputed on each observation tick rather than cached, so a balance that crosses the /// requirement — or a rate that finally loads — opens the chat without a reopen. private var gate: ConversationGatePresentation { - guard let conversation = groupConversation else { + guard let conversationID, + let conversation = conversationController.conversation(withID: conversationID) + else { return awaitingMetadata ? .undetermined : .open } return conversationGatePresentation(gateVerdicts, isMember: conversationController.isMember(of: conversation)) @@ -301,7 +306,9 @@ struct ConversationScreen: View { /// advertised listener rule (``ConversationGate/headline``) whether or not the viewer is short /// of it, and the presentation only ever carries something unmet. private var gateVerdicts: ConversationGate { - guard let conversation = groupConversation else { return .open } + guard let conversationID, + let conversation = conversationController.conversation(withID: conversationID) + else { return .open } return conversationGate( session: session, rules: conversation.rules,