Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .claude/plans/2026-10-06-group-profile-refresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Group profile refresh (iOS)

Spec: `docs/specs/group-profile-refresh.md` in the orchestrator root, sections "Approved new pieces"
and "Group profile screen". Figma file Jxf2wD9QyfKaONbhHSdU0K: node 10913:358 (profile), 10918:203
(host view), 10935:202/10935:263 (leave), 11001:1674 (share), 10913:246/282/318 (gate states).

## Where things live

- `ChatProfileScreen.swift` — the screen. Same shape as `UserProfileScreen`: `ProfileHeaderView`
with `.group(conversationID, picture: coverPicture)`, ⋯ menu in the system toolbar, pinned
actions in `scrollEdgeBar(.bottom)`.
- `GroupProfileState.swift` — the pure logic, tested in `GroupProfileStateTests`:
- `GroupProfileCTA.resolve(gate:isMember:)` reads the `ConversationGate` verdicts directly.
`conversationGatePresentation` ignores membership, so it would tell a member who fell under the
join minimum to "Buy … to Join".
- `GroupBalanceRequirements` — Join is the first listener minimum; Chat is the first speaker
minimum, falling back to Join (backend: speaker rules equal listener rules when none are set).
- `balanceShortfall(of:mint:holdings:rates:)` — the Buy button names the shortfall, the line
above it the full requirement. Held balance comes from `heldBalance(in:session:)`, extracted
from `unmetBalance` in `ConversationGate.swift` so the two can't disagree.
- `GroupChattingGrid.swift` — Chatting grid and host wave badge. Fed by `sampleChatters` only, no
per-chatter GetProfile. Hidden for private groups (the RPC is DENIED) and for an empty sample.

## Decisions

- Join and Open Chat pop back to the conversation: the only entry is `ConversationScreen.swift:154`,
and that screen loads its transcript when the gate stops obscuring it.
- Overflow: Invite and Mute for members, Report and Encryption for everyone. Edit moved to an
"Edit Group" capsule in the header row.
- Share sheet "Share on Flipcash" opens `GroupInviteSheet`; Copy Link copies `groupChatInvite`.
- `ChatMuteRow` and `ReportRow` had no other callers and were removed.
10 changes: 8 additions & 2 deletions Flipcash/Core/Screens/Conversation/ConversationGate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -232,13 +232,19 @@ private func unmetBalance(
}
// The contract allows at most one mint today; empty means every mint counts.
let mint = requirement.mints.first
let held = mint.map { session.balance(for: $0)?.usdf ?? .usd(0) } ?? session.totalBalance.usdfValue
guard held.roundedToSmallestUnit() >= required else {
guard heldBalance(in: mint, session: session).roundedToSmallestUnit() >= required else {
return .minimumBalance(amount: requirement.amount, mint: mint)
}
return nil
}

/// What the user holds toward a balance requirement, in USD: the one mint's holding when the
/// requirement names one, else every holding together.
@MainActor
func heldBalance(in mint: PublicKey?, session: some ConversationGateReading) -> FiatAmount {
mint.map { session.balance(for: $0)?.usdf ?? .usd(0) } ?? session.totalBalance.usdfValue
}

/// Picks the requirement to name: the first unmet minimum balance, since it is
/// the only one a user can act on, falling back to the first unmet rule.
private func verdict(for unmet: [ConversationGateRequirement]) -> ConversationGateVerdict {
Expand Down
47 changes: 0 additions & 47 deletions Flipcash/Core/Screens/Profile/ChatMuteRow.swift

This file was deleted.

4 changes: 2 additions & 2 deletions Flipcash/Core/Screens/Profile/ChatMuteStatusLabel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import SwiftUI
import FlipcashCore
import FlipcashUI

/// What the chat's mute currently is, drawn under the title on both chat settings surfaces —
/// What the chat's mute currently is, drawn in the action row of both profile headers —
/// "Muted until 5:56 PM" for a timed mute, plain "Muted" for an indefinite one, and nothing at all
/// while the chat is audible.
///
/// Stating the deadline is what makes a timed mute trustworthy: without it a timed mute can't be
/// told apart from an indefinite one, and the user has no way to know when the chat comes back.
/// It sits here rather than on ``ChatMuteRow`` because the row has no room for it.
/// The Mute action itself lives in the ⋯ menu, which has no room to say it.
///
/// Drawn as a chip rather than as another line of secondary text: the lines above it are the chat's
/// own identity — its name, its size, when someone joined — and this is the viewer's setting. Given
Expand Down
Loading
Loading