diff --git a/Flipcash/Core/Screens/Main/You/YouScreen.swift b/Flipcash/Core/Screens/Main/You/YouScreen.swift index 414b3e0e1..e52957ebc 100644 --- a/Flipcash/Core/Screens/Main/You/YouScreen.swift +++ b/Flipcash/Core/Screens/Main/You/YouScreen.swift @@ -150,6 +150,7 @@ struct YouScreen: View { ) }, bannerControls: { EmptyView() }, + coverAccessory: { EmptyView() }, rowActions: { ProfileEditCapsule { router.push(.editProfile) diff --git a/Flipcash/Core/Screens/Profile/ChatMuteStatusLabel.swift b/Flipcash/Core/Screens/Profile/ChatMuteStatusLabel.swift index 90c84bc56..a67d562e3 100644 --- a/Flipcash/Core/Screens/Profile/ChatMuteStatusLabel.swift +++ b/Flipcash/Core/Screens/Profile/ChatMuteStatusLabel.swift @@ -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. @@ -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) @@ -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) } } diff --git a/Flipcash/Core/Screens/Profile/ChatProfileScreen.swift b/Flipcash/Core/Screens/Profile/ChatProfileScreen.swift index 3cc25e79f..a5570a85d 100644 --- a/Flipcash/Core/Screens/Profile/ChatProfileScreen.swift +++ b/Flipcash/Core/Screens/Profile/ChatProfileScreen.swift @@ -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)) diff --git a/Flipcash/Core/Screens/Profile/ProfileHeaderView.swift b/Flipcash/Core/Screens/Profile/ProfileHeaderView.swift index 1ea6b0331..065df1b2a 100644 --- a/Flipcash/Core/Screens/Profile/ProfileHeaderView.swift +++ b/Flipcash/Core/Screens/Profile/ProfileHeaderView.swift @@ -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: View { +struct ProfileHeaderView: View { let cover: ProfileCover let title: String? @@ -32,6 +32,9 @@ struct ProfileHeaderView 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 @@ -41,6 +44,15 @@ struct ProfileHeaderView