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
1 change: 1 addition & 0 deletions Flipcash/Core/Screens/Main/You/YouScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ struct YouScreen: View {
)
},
bannerControls: { EmptyView() },
coverAccessory: { EmptyView() },
rowActions: {
ProfileEditCapsule {
router.push(.editProfile)
Expand Down
30 changes: 17 additions & 13 deletions Flipcash/Core/Screens/Profile/ChatMuteStatusLabel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import SwiftUI
import FlipcashCore
import FlipcashUI

/// 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.
/// What the chat's mute currently is, drawn on the cover of both profile headers —
/// "Muted until 5:56 PM" for a timed mute, plain "Muted" for an indefinite one or a timed one too
/// wide for the space, 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.
Expand Down Expand Up @@ -74,16 +74,19 @@ struct ChatMuteStatusLabel: View {
}

if let text {
chipLabel(text)
.accessibilityIdentifier("chat-mute-status")
// Replaced rather than resized. A chip that keeps its identity across a text
// change interpolates its width, and the glyphs inside don't interpolate with
// it: they are drawn at the new string's size against a frame still growing
// from the old one's, so the label either spills out of the capsule or slides
// sideways into place. Keying identity on the string makes every change an
// insert and a remove, which has no width to interpolate.
.transition(.scale(scale: 0.85).combined(with: .opacity))
.id(text)
ViewThatFits(in: .horizontal) {
chipLabel(text)
chipLabel("Muted")
}
.accessibilityIdentifier("chat-mute-status")
// Replaced rather than resized. A chip that keeps its identity across a text
// change interpolates its width, and the glyphs inside don't interpolate with
// it: they are drawn at the new string's size against a frame still growing
// from the old one's, so the label either spills out of the capsule or slides
// sideways into place. Keying identity on the string makes every change an
// insert and a remove, which has no width to interpolate.
.transition(.scale(scale: 0.85).combined(with: .opacity))
.id(text)
}
}
.padding(.top, reservesSpace ? 3 : 0)
Expand All @@ -109,5 +112,6 @@ struct ChatMuteStatusLabel: View {

private func chipLabel(_ string: String) -> some View {
ProfileStatusChip(systemImage: "bell.slash", text: string, tint: .warning, fill: .warningSecondary)
.lineLimit(1)
}
}
6 changes: 5 additions & 1 deletion Flipcash/Core/Screens/Profile/ChatProfileScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,14 @@ struct ChatProfileScreen: View {
)
},
bannerControls: { EmptyView() },
rowActions: {
// On the cover rather than in the action row: a timed mute's label is
// wide enough to wrap Edit Group and push Share off the screen.
coverAccessory: {
if isMember {
ChatMuteStatusLabel(conversationID: conversationID, reservesSpace: false)
}
},
rowActions: {
if canEdit {
ProfileEditCapsule(title: "Edit Group") {
router.push(.editGroup(conversationID))
Expand Down
14 changes: 13 additions & 1 deletion Flipcash/Core/Screens/Profile/ProfileHeaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ enum ProfileHeaderMetrics {
///
/// The caller lets the view run under the status bar. A nil `title` leaves the text block out,
/// for a profile that has not named itself yet.
struct ProfileHeaderView<Avatar: View, BannerControls: View, RowActions: View, UnderSubtitle: View>: View {
struct ProfileHeaderView<Avatar: View, BannerControls: View, CoverAccessory: View, RowActions: View, UnderSubtitle: View>: View {

let cover: ProfileCover
let title: String?
Expand All @@ -32,6 +32,9 @@ struct ProfileHeaderView<Avatar: View, BannerControls: View, RowActions: View, U
/// Drawn at ``ProfileHeaderMetrics/avatarSize``; the header adds the ring and the overlap.
@ViewBuilder let avatar: () -> Avatar
@ViewBuilder let bannerControls: () -> BannerControls
/// Sits on the cover's bottom edge beside the avatar. The cover is a fixed height, so content
/// that arrives late, such as a status chip, never moves the rest of the header.
@ViewBuilder let coverAccessory: () -> CoverAccessory
@ViewBuilder let rowActions: () -> RowActions
@ViewBuilder let underSubtitle: () -> UnderSubtitle

Expand All @@ -41,6 +44,15 @@ struct ProfileHeaderView<Avatar: View, BannerControls: View, RowActions: View, U
cover: cover,
controls: bannerControls
)
.overlay(alignment: .bottomLeading) {
HStack(spacing: 0) {
coverAccessory()
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, ProfileHeaderMetrics.inset + ProfileHeaderMetrics.avatarSize + 12)
.padding(.trailing, ProfileHeaderMetrics.inset)
.padding(.bottom, 8)
}

HStack(alignment: .top, spacing: 0) {
avatar()
Expand Down
8 changes: 5 additions & 3 deletions Flipcash/Core/Screens/Profile/UserProfileScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ private struct UserProfileContent: View {
)
},
bannerControls: { EmptyView() },
coverAccessory: { statusBadges },
rowActions: {
statusBadges
shareButton
},
underSubtitle: { EmptyView() }
Expand Down Expand Up @@ -226,12 +226,14 @@ private struct UserProfileContent: View {

// MARK: - Status badges -

/// Blocked and muted are separate settings, so each gets its own badge. They sit in the action
/// row, whose height the share button sets, so one coming or going never moves the bio below.
/// Blocked and muted are separate settings, so each gets its own badge. They sit on the cover,
/// whose height is fixed, so one coming or going never moves the bio below.
private var statusBadges: some View {
HStack(spacing: 8) {
if isBlocked {
ProfileStatusChip(systemImage: "nosign", text: "Blocked", tint: .warning, fill: .warningSecondary)
// The mute chip gives up width first, falling back to plain "Muted".
.fixedSize()
.accessibilityIdentifier("profile-blocked-badge")
}
if let dmID {
Expand Down
Loading