From aed556ae076b065c2954d1c5fb769b674cb697df Mon Sep 17 00:00:00 2001 From: bwees Date: Fri, 3 Jul 2026 15:58:51 -0500 Subject: [PATCH 1/7] refactor: contact, room, and channel detail pages styling --- MC1/Views/Chats/Room/RoomInfoSheet.swift | 29 ++++-- MC1/Views/Chats/Sheets/ChannelInfoSheet.swift | 34 +++---- .../ConversationQuickActionsSection.swift | 11 +-- MC1/Views/Components/FavoriteToggleRow.swift | 22 ----- MC1/Views/Contacts/ContactDetailView.swift | 88 +++++++++---------- 5 files changed, 86 insertions(+), 98 deletions(-) delete mode 100644 MC1/Views/Components/FavoriteToggleRow.swift diff --git a/MC1/Views/Chats/Room/RoomInfoSheet.swift b/MC1/Views/Chats/Room/RoomInfoSheet.swift index 661b6c64..fc967270 100644 --- a/MC1/Views/Chats/Room/RoomInfoSheet.swift +++ b/MC1/Views/Chats/Room/RoomInfoSheet.swift @@ -27,17 +27,25 @@ struct RoomInfoSheet: View { NavigationStack { List { Section { - HStack { - Spacer() - NodeAvatar(publicKey: session.publicKey, role: .roomServer, size: 80) - Spacer() + VStack(spacing: 12) { + NodeAvatar(publicKey: session.publicKey, role: .roomServer, size: 150) + + VStack(spacing: 4) { + Text(session.name) + .font(.title2) + .bold() + + Text(L10n.RemoteNodes.RemoteNodes.Auth.typeRoom) + .font(.subheadline) + .foregroundStyle(.secondary) + } } + .frame(maxWidth: .infinity) .listRowBackground(Color.clear) } ConversationQuickActionsSection( notificationLevel: $notificationLevel, - isFavorite: $isFavorite, availableLevels: NotificationLevel.roomLevels ) .onChange(of: notificationLevel) { _, newValue in @@ -105,7 +113,16 @@ struct RoomInfoSheet: View { .navigationTitle(Strings.infoTitle) .navigationBarTitleDisplayMode(.inline) .toolbar { - ToolbarItem(placement: .cancellationAction) { + ToolbarItem(placement: .topBarLeading) { + Button { + isFavorite.toggle() + } label: { + Image(systemName: isFavorite ? "star.fill" : "star") + .foregroundStyle(isFavorite ? .yellow : .secondary) + } + } + + ToolbarItem(placement: .confirmationAction) { Button(L10n.Localizable.Common.done) { dismiss() } } } diff --git a/MC1/Views/Chats/Sheets/ChannelInfoSheet.swift b/MC1/Views/Chats/Sheets/ChannelInfoSheet.swift index 91eefd22..1a5c2058 100644 --- a/MC1/Views/Chats/Sheets/ChannelInfoSheet.swift +++ b/MC1/Views/Chats/Sheets/ChannelInfoSheet.swift @@ -53,7 +53,6 @@ struct ChannelInfoSheet: View { // Quick Actions Section ConversationQuickActionsSection( notificationLevel: $notificationLevel, - isFavorite: $isFavorite, availableLevels: NotificationLevel.channelLevels ) .onChange(of: notificationLevel) { _, newValue in @@ -126,7 +125,16 @@ struct ChannelInfoSheet: View { .navigationTitle(L10n.Chats.Chats.ChannelInfo.title) .navigationBarTitleDisplayMode(.inline) .toolbar { - ToolbarItem(placement: .cancellationAction) { + ToolbarItem(placement: .topBarLeading) { + Button { + isFavorite.toggle() + } label: { + Image(systemName: isFavorite ? "star.fill" : "star") + .foregroundStyle(isFavorite ? .yellow : .secondary) + } + } + + ToolbarItem(placement: .confirmationAction) { Button(L10n.Chats.Chats.Common.done) { dismiss() } @@ -367,11 +375,10 @@ private struct ChannelInfoHeaderSection: View { var body: some View { Section { - HStack { - Spacer() - VStack(spacing: 12) { - ChannelAvatar(channel: channel, size: 80) + VStack(spacing: 12) { + ChannelAvatar(channel: channel, size: 150) + VStack(spacing: 4) { Text(channel.displayName) .font(.title2) .bold() @@ -380,8 +387,8 @@ private struct ChannelInfoHeaderSection: View { .font(.subheadline) .foregroundStyle(.secondary) } - Spacer() } + .frame(maxWidth: .infinity) .listRowBackground(Color.clear) } } @@ -491,29 +498,24 @@ private struct ChannelInfoActionsSection: View { showingClearMessagesConfirmation = true } label: { HStack { - Spacer() + Label(L10n.Chats.Chats.ChannelInfo.clearMessagesButton, systemImage: "xmark.circle") if isClearingMessages { + Spacer() ProgressView() - } else { - Label(L10n.Chats.Chats.ChannelInfo.clearMessagesButton, systemImage: "xmark.circle") } - Spacer() } } .disabled(isActionInProgress) - .alignmentGuide(.listRowSeparatorLeading) { _ in 0 } Button(role: .destructive) { showingDeleteConfirmation = true } label: { HStack { - Spacer() + Label(L10n.Chats.Chats.ChannelInfo.deleteButton, systemImage: "trash") if isDeleting { + Spacer() ProgressView() - } else { - Label(L10n.Chats.Chats.ChannelInfo.deleteButton, systemImage: "trash") } - Spacer() } } .disabled(isActionInProgress) diff --git a/MC1/Views/Components/ConversationQuickActionsSection.swift b/MC1/Views/Components/ConversationQuickActionsSection.swift index aa6375dc..38ecb223 100644 --- a/MC1/Views/Components/ConversationQuickActionsSection.swift +++ b/MC1/Views/Components/ConversationQuickActionsSection.swift @@ -4,27 +4,20 @@ import SwiftUI struct ConversationQuickActionsSection: View { @Environment(\.appTheme) private var theme @Binding var notificationLevel: NotificationLevel - @Binding var isFavorite: Bool let availableLevels: [NotificationLevel] init( notificationLevel: Binding, - isFavorite: Binding, availableLevels: [NotificationLevel] = NotificationLevel.allCases ) { _notificationLevel = notificationLevel - _isFavorite = isFavorite self.availableLevels = availableLevels } var body: some View { Section { - VStack(spacing: 16) { - NotificationLevelPicker(selection: $notificationLevel, availableLevels: availableLevels) - - FavoriteToggleRow(isFavorite: $isFavorite) - } - .listRowInsets(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)) + NotificationLevelPicker(selection: $notificationLevel, availableLevels: availableLevels) + .listRowInsets(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)) } .themedRowBackground(theme) } diff --git a/MC1/Views/Components/FavoriteToggleRow.swift b/MC1/Views/Components/FavoriteToggleRow.swift deleted file mode 100644 index c0949c9f..00000000 --- a/MC1/Views/Components/FavoriteToggleRow.swift +++ /dev/null @@ -1,22 +0,0 @@ -import SwiftUI - -struct FavoriteToggleRow: View { - @Binding var isFavorite: Bool - - var body: some View { - HStack { - Image(systemName: isFavorite ? "star.fill" : "star") - .foregroundStyle(isFavorite ? .yellow : .secondary) - - Text(L10n.Chats.Chats.Row.favorite) - - Spacer() - - Toggle("", isOn: $isFavorite) - .labelsHidden() - } - .accessibilityElement(children: .combine) - .accessibilityLabel(L10n.Chats.Chats.Row.favorite) - .accessibilityValue(isFavorite ? L10n.Localizable.Accessibility.on : L10n.Localizable.Accessibility.off) - } -} diff --git a/MC1/Views/Contacts/ContactDetailView.swift b/MC1/Views/Contacts/ContactDetailView.swift index e670d946..7e32702c 100644 --- a/MC1/Views/Contacts/ContactDetailView.swift +++ b/MC1/Views/Contacts/ContactDetailView.swift @@ -115,7 +115,6 @@ struct ContactDetailView: View { currentContact: currentContact, showFromDirectChat: showFromDirectChat, isPinging: isPinging, - isTogglingFavorite: isTogglingFavorite, pingResult: pingResult, onJoinRoom: { showRoomJoinSheet = true }, onShowTelemetry: { @@ -130,7 +129,6 @@ struct ContactDetailView: View { showRepeaterAdminAuth = true }, onPingRepeater: { Task { await pingRepeater() } }, - onToggleFavorite: { Task { await toggleFavorite() } }, onShareQR: { showQRShareSheet = true }, onShareViaAdvert: { Task { await shareContact() } }, isSharing: isSharing, @@ -189,6 +187,23 @@ struct ContactDetailView: View { .errorAlert($errorMessage) .navigationTitle(contactTypeLabel) .navigationBarTitleDisplayMode(.inline) + .contentMargins(.top, 0, for: .scrollContent) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button { + Task { await toggleFavorite() } + } label: { + Image(systemName: currentContact.isFavorite ? "star.fill" : "star") + .foregroundStyle(currentContact.isFavorite ? .yellow : .secondary) + } + .disabled(isTogglingFavorite) + .radioDisabled(for: appState.connectionState) + } + + ToolbarItem(placement: .confirmationAction) { + Button(L10n.Localizable.Common.done) { dismiss() } + } + } .alert(L10n.Contacts.Contacts.Detail.Alert.Block.title, isPresented: $showingBlockAlert) { Button(L10n.Contacts.Contacts.Common.cancel, role: .cancel) {} Button(L10n.Contacts.Contacts.Action.block, role: .destructive) { @@ -504,11 +519,11 @@ private struct ContactDetailAvatarView: View { var body: some View { switch contact.type { case .chat: - ContactAvatar(contact: contact, size: 100) + ContactAvatar(contact: contact, size: 150) case .repeater: - NodeAvatar(publicKey: contact.publicKey, role: .repeater, size: 100) + NodeAvatar(publicKey: contact.publicKey, role: .repeater, size: 150) case .room: - NodeAvatar(publicKey: contact.publicKey, role: .roomServer, size: 100) + NodeAvatar(publicKey: contact.publicKey, role: .roomServer, size: 150) } } } @@ -519,7 +534,7 @@ private struct ContactProfileSection: View { var body: some View { Section { - VStack(spacing: 16) { + VStack(spacing: 12) { ContactDetailAvatarView(contact: currentContact) VStack(spacing: 4) { @@ -532,25 +547,26 @@ private struct ContactProfileSection: View { .foregroundStyle(.secondary) // Status indicators - HStack(spacing: 12) { - if currentContact.isFavorite { - Label(L10n.Contacts.Contacts.Detail.favorite, systemImage: "star.fill") - .font(.caption) - .foregroundStyle(.yellow) - } - + VStack(spacing: 8) { if currentContact.isBlocked { - Label(L10n.Contacts.Contacts.Detail.blocked, systemImage: "hand.raised.fill") - .font(.caption) - .foregroundStyle(.orange) + HStack(spacing: 4) { + Image(systemName: "hand.raised.fill") + Text(L10n.Contacts.Contacts.Detail.blocked) + } + .font(.caption) + .foregroundStyle(.red) } if currentContact.hasLocation { - Label(L10n.Contacts.Contacts.Detail.hasLocation, systemImage: "location.fill") - .font(.caption) - .foregroundStyle(.green) + HStack(spacing: 4) { + Image(systemName: "location.fill") + Text(L10n.Contacts.Contacts.Detail.hasLocation) + } + .font(.caption) + .foregroundStyle(.purple) } } + .padding(.top, 4) } } .frame(maxWidth: .infinity) @@ -565,13 +581,11 @@ private struct ContactActionsSection: View { let currentContact: ContactDTO let showFromDirectChat: Bool let isPinging: Bool - let isTogglingFavorite: Bool let pingResult: PingResult? let onJoinRoom: () -> Void let onShowTelemetry: () -> Void let onShowAdminAccess: () -> Void let onPingRepeater: () -> Void - let onToggleFavorite: () -> Void let onShareQR: () -> Void let onShareViaAdvert: () -> Void let isSharing: Bool @@ -636,22 +650,6 @@ private struct ContactActionsSection: View { } } - // Toggle favorite (for all contact types) - Button(action: onToggleFavorite) { - HStack { - Label( - currentContact.isFavorite ? L10n.Contacts.Contacts.Detail.removeFromFavorites : L10n.Contacts.Contacts.Detail.addToFavorites, - systemImage: currentContact.isFavorite ? "star.slash" : "star" - ) - if isTogglingFavorite { - Spacer() - ProgressView() - } - } - } - .disabled(isTogglingFavorite) - .radioDisabled(for: appState.connectionState) - // Share Contact via QR Button(action: onShareQR) { Label(L10n.Contacts.Contacts.Detail.shareContact, systemImage: "square.and.arrow.up") @@ -1139,6 +1137,14 @@ private struct ContactDangerSection: View { var body: some View { Section { if currentContact.type == .chat { + Button(action: onToggleBlock) { + Label( + currentContact.isBlocked ? L10n.Contacts.Contacts.Detail.unblockContact : L10n.Contacts.Contacts.Detail.blockContact, + systemImage: currentContact.isBlocked ? "hand.raised.slash" : "hand.raised" + ) + } + .radioDisabled(for: appState.connectionState) + Button(role: .destructive, action: onClearMessages) { HStack { Label(L10n.Contacts.Contacts.Detail.clearMessages, systemImage: "xmark.circle") @@ -1149,14 +1155,6 @@ private struct ContactDangerSection: View { } } .disabled(isClearingMessages) - - Button(action: onToggleBlock) { - Label( - currentContact.isBlocked ? L10n.Contacts.Contacts.Detail.unblockContact : L10n.Contacts.Contacts.Detail.blockContact, - systemImage: currentContact.isBlocked ? "hand.raised.slash" : "hand.raised" - ) - } - .radioDisabled(for: appState.connectionState) } Button(role: .destructive, action: onDelete) { From 62bb7c632ea883315db237b29f105cc2e3bf8957 Mon Sep 17 00:00:00 2001 From: bwees Date: Fri, 3 Jul 2026 16:17:00 -0500 Subject: [PATCH 2/7] chore: fix button destructive styling and scroll now reveals the sheet title once past header --- MC1/Extensions/View+ScrollRevealTitle.swift | 34 +++++++++++++++++++ MC1/Views/Chats/Room/RoomInfoSheet.swift | 2 +- MC1/Views/Chats/Sheets/ChannelInfoSheet.swift | 4 +-- MC1/Views/Contacts/ContactDetailView.swift | 2 +- 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 MC1/Extensions/View+ScrollRevealTitle.swift diff --git a/MC1/Extensions/View+ScrollRevealTitle.swift b/MC1/Extensions/View+ScrollRevealTitle.swift new file mode 100644 index 00000000..1200fa2c --- /dev/null +++ b/MC1/Extensions/View+ScrollRevealTitle.swift @@ -0,0 +1,34 @@ +import SwiftUI + +extension View { + /// Leaves the navigation bar title empty while the header is on screen, then fades + /// in `title` (the entity name) once the user scrolls past `revealAfter` points. + /// Pairs with `.navigationBarTitleDisplayMode(.inline)`. + func scrollRevealNavigationTitle(_ title: String, revealAfter: CGFloat = 150) -> some View { + modifier(ScrollRevealNavigationTitle(title: title, revealAfter: revealAfter)) + } +} + +private struct ScrollRevealNavigationTitle: ViewModifier { + let title: String + let revealAfter: CGFloat + + @State private var isRevealed = false + + func body(content: Content) -> some View { + content + .onScrollGeometryChange(for: Bool.self) { geometry in + geometry.contentOffset.y > revealAfter + } action: { _, revealed in + guard revealed != isRevealed else { return } + withAnimation(.easeInOut(duration: 0.2)) { isRevealed = revealed } + } + .toolbar { + ToolbarItem(placement: .principal) { + Text(title) + .font(.headline) + .opacity(isRevealed ? 1 : 0) + } + } + } +} diff --git a/MC1/Views/Chats/Room/RoomInfoSheet.swift b/MC1/Views/Chats/Room/RoomInfoSheet.swift index fc967270..8bbe274e 100644 --- a/MC1/Views/Chats/Room/RoomInfoSheet.swift +++ b/MC1/Views/Chats/Room/RoomInfoSheet.swift @@ -110,8 +110,8 @@ struct RoomInfoSheet: View { .themedRowBackground(theme) } .themedCanvas(theme) - .navigationTitle(Strings.infoTitle) .navigationBarTitleDisplayMode(.inline) + .scrollRevealNavigationTitle(session.name) .toolbar { ToolbarItem(placement: .topBarLeading) { Button { diff --git a/MC1/Views/Chats/Sheets/ChannelInfoSheet.swift b/MC1/Views/Chats/Sheets/ChannelInfoSheet.swift index 1a5c2058..afe85cd7 100644 --- a/MC1/Views/Chats/Sheets/ChannelInfoSheet.swift +++ b/MC1/Views/Chats/Sheets/ChannelInfoSheet.swift @@ -122,8 +122,8 @@ struct ChannelInfoSheet: View { ) } .themedCanvas(theme) - .navigationTitle(L10n.Chats.Chats.ChannelInfo.title) .navigationBarTitleDisplayMode(.inline) + .scrollRevealNavigationTitle(channel.displayName) .toolbar { ToolbarItem(placement: .topBarLeading) { Button { @@ -494,7 +494,7 @@ private struct ChannelInfoActionsSection: View { var body: some View { Section { - Button { + Button(role: .destructive) { showingClearMessagesConfirmation = true } label: { HStack { diff --git a/MC1/Views/Contacts/ContactDetailView.swift b/MC1/Views/Contacts/ContactDetailView.swift index 7e32702c..639d4781 100644 --- a/MC1/Views/Contacts/ContactDetailView.swift +++ b/MC1/Views/Contacts/ContactDetailView.swift @@ -185,8 +185,8 @@ struct ContactDetailView: View { } .themedCanvas(theme) .errorAlert($errorMessage) - .navigationTitle(contactTypeLabel) .navigationBarTitleDisplayMode(.inline) + .scrollRevealNavigationTitle(currentContact.displayName) .contentMargins(.top, 0, for: .scrollContent) .toolbar { ToolbarItem(placement: .topBarLeading) { From 5d84225f8a9b7f06efefb3e06e37f9dd7f56d0ca Mon Sep 17 00:00:00 2001 From: Brandon Wees Date: Fri, 3 Jul 2026 18:19:52 -0500 Subject: [PATCH 3/7] chore: revert location color --- MC1/Views/Contacts/ContactDetailView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MC1/Views/Contacts/ContactDetailView.swift b/MC1/Views/Contacts/ContactDetailView.swift index 639d4781..63551271 100644 --- a/MC1/Views/Contacts/ContactDetailView.swift +++ b/MC1/Views/Contacts/ContactDetailView.swift @@ -563,7 +563,7 @@ private struct ContactProfileSection: View { Text(L10n.Contacts.Contacts.Detail.hasLocation) } .font(.caption) - .foregroundStyle(.purple) + .foregroundStyle(.green) } } .padding(.top, 4) From 2b029b83f11970e9272e3543c68f7b76739bc774 Mon Sep 17 00:00:00 2001 From: bwees Date: Fri, 3 Jul 2026 18:42:49 -0500 Subject: [PATCH 4/7] chore: small ui tweaks --- MC1/Views/Contacts/ContactDetailView.swift | 4 +++- MC1/Views/Contacts/ContactQRShareSheet.swift | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/MC1/Views/Contacts/ContactDetailView.swift b/MC1/Views/Contacts/ContactDetailView.swift index 63551271..94e2e79a 100644 --- a/MC1/Views/Contacts/ContactDetailView.swift +++ b/MC1/Views/Contacts/ContactDetailView.swift @@ -748,8 +748,10 @@ private struct ContactInfoSection: View { Text(currentContact.nickname ?? L10n.Contacts.Contacts.Detail.nicknameNone) .foregroundStyle(.secondary) - Button(L10n.Contacts.Contacts.Common.edit) { + Button(action: { isEditingNickname = true + }) { + Image(systemName: "pencil") } .buttonStyle(.borderless) } diff --git a/MC1/Views/Contacts/ContactQRShareSheet.swift b/MC1/Views/Contacts/ContactQRShareSheet.swift index 1770032d..6552cb75 100644 --- a/MC1/Views/Contacts/ContactQRShareSheet.swift +++ b/MC1/Views/Contacts/ContactQRShareSheet.swift @@ -55,6 +55,7 @@ struct ContactQRShareSheet: View { } .sensoryFeedback(.success, trigger: copyHapticTrigger) } + .presentationBackground(theme.surfaces?.canvas ?? Color(.systemGroupedBackground)) } // MARK: - Private Methods From 9cd3298e695d4d2d6bbab87c03cf7809d461218c Mon Sep 17 00:00:00 2001 From: bwees Date: Mon, 6 Jul 2026 20:56:21 -0500 Subject: [PATCH 5/7] chore: code review --- MC1/Extensions/View+ScrollRevealTitle.swift | 10 ++++ MC1/Views/Chats/Room/RoomInfoSheet.swift | 15 ++---- MC1/Views/Chats/Sheets/ChannelInfoSheet.swift | 18 +++---- .../ConversationQuickActionsSection.swift | 7 +++ MC1/Views/Contacts/ContactDetailView.swift | 47 ++++++++++--------- 5 files changed, 55 insertions(+), 42 deletions(-) diff --git a/MC1/Extensions/View+ScrollRevealTitle.swift b/MC1/Extensions/View+ScrollRevealTitle.swift index 1200fa2c..90e6e3bb 100644 --- a/MC1/Extensions/View+ScrollRevealTitle.swift +++ b/MC1/Extensions/View+ScrollRevealTitle.swift @@ -3,10 +3,20 @@ import SwiftUI extension View { /// Leaves the navigation bar title empty while the header is on screen, then fades /// in `title` (the entity name) once the user scrolls past `revealAfter` points. + /// Pass the header's measured height (see `scrollRevealHeaderHeight`) so the reveal + /// tracks the real header size and adapts to Dynamic Type. /// Pairs with `.navigationBarTitleDisplayMode(.inline)`. func scrollRevealNavigationTitle(_ title: String, revealAfter: CGFloat = 150) -> some View { modifier(ScrollRevealNavigationTitle(title: title, revealAfter: revealAfter)) } + + /// Reports the measured height of the scroll-reveal header into `height`, so the + /// title reveals based on the header's real (Dynamic Type-aware) size rather than a + /// fixed threshold. Attach to the header content, then feed `height` to + /// `scrollRevealNavigationTitle(_:revealAfter:)`. + func scrollRevealHeaderHeight(_ height: Binding) -> some View { + onGeometryChange(for: CGFloat.self) { $0.size.height } action: { height.wrappedValue = $0 } + } } private struct ScrollRevealNavigationTitle: ViewModifier { diff --git a/MC1/Views/Chats/Room/RoomInfoSheet.swift b/MC1/Views/Chats/Room/RoomInfoSheet.swift index 8bbe274e..0eb6e8e0 100644 --- a/MC1/Views/Chats/Room/RoomInfoSheet.swift +++ b/MC1/Views/Chats/Room/RoomInfoSheet.swift @@ -16,6 +16,7 @@ struct RoomInfoSheet: View { @State private var favoriteTask: Task? @State private var showTelemetry = false @State private var showSettings = false + @State private var headerHeight: CGFloat = 150 init(session: RemoteNodeSessionDTO) { self.session = session @@ -41,10 +42,12 @@ struct RoomInfoSheet: View { } } .frame(maxWidth: .infinity) + .scrollRevealHeaderHeight($headerHeight) .listRowBackground(Color.clear) } ConversationQuickActionsSection( + isFavorite: $isFavorite, notificationLevel: $notificationLevel, availableLevels: NotificationLevel.roomLevels ) @@ -111,17 +114,9 @@ struct RoomInfoSheet: View { } .themedCanvas(theme) .navigationBarTitleDisplayMode(.inline) - .scrollRevealNavigationTitle(session.name) + .scrollRevealNavigationTitle(session.name, revealAfter: headerHeight) + .contentMargins(.top, 0, for: .scrollContent) .toolbar { - ToolbarItem(placement: .topBarLeading) { - Button { - isFavorite.toggle() - } label: { - Image(systemName: isFavorite ? "star.fill" : "star") - .foregroundStyle(isFavorite ? .yellow : .secondary) - } - } - ToolbarItem(placement: .confirmationAction) { Button(L10n.Localizable.Common.done) { dismiss() } } diff --git a/MC1/Views/Chats/Sheets/ChannelInfoSheet.swift b/MC1/Views/Chats/Sheets/ChannelInfoSheet.swift index afe85cd7..080bb950 100644 --- a/MC1/Views/Chats/Sheets/ChannelInfoSheet.swift +++ b/MC1/Views/Chats/Sheets/ChannelInfoSheet.swift @@ -34,6 +34,7 @@ struct ChannelInfoSheet: View { @State private var discoveredNewRegions: [String] = [] @State private var showingDiscoveryResults = false @State private var selectedFloodScope: ChannelFloodScope + @State private var headerHeight: CGFloat = 150 init(channel: ChannelDTO, onClearMessages: @escaping () -> Void, onDelete: @escaping () -> Void) { self.channel = channel @@ -48,10 +49,11 @@ struct ChannelInfoSheet: View { NavigationStack { Form { // Channel Header Section - ChannelInfoHeaderSection(channel: channel) + ChannelInfoHeaderSection(channel: channel, measuredHeight: $headerHeight) // Quick Actions Section ConversationQuickActionsSection( + isFavorite: $isFavorite, notificationLevel: $notificationLevel, availableLevels: NotificationLevel.channelLevels ) @@ -123,17 +125,9 @@ struct ChannelInfoSheet: View { } .themedCanvas(theme) .navigationBarTitleDisplayMode(.inline) - .scrollRevealNavigationTitle(channel.displayName) + .scrollRevealNavigationTitle(channel.displayName, revealAfter: headerHeight) + .contentMargins(.top, 0, for: .scrollContent) .toolbar { - ToolbarItem(placement: .topBarLeading) { - Button { - isFavorite.toggle() - } label: { - Image(systemName: isFavorite ? "star.fill" : "star") - .foregroundStyle(isFavorite ? .yellow : .secondary) - } - } - ToolbarItem(placement: .confirmationAction) { Button(L10n.Chats.Chats.Common.done) { dismiss() @@ -362,6 +356,7 @@ struct ChannelInfoSheet: View { private struct ChannelInfoHeaderSection: View { let channel: ChannelDTO + @Binding var measuredHeight: CGFloat private var channelTypeLabel: String { if channel.isPublicChannel { @@ -389,6 +384,7 @@ private struct ChannelInfoHeaderSection: View { } } .frame(maxWidth: .infinity) + .scrollRevealHeaderHeight($measuredHeight) .listRowBackground(Color.clear) } } diff --git a/MC1/Views/Components/ConversationQuickActionsSection.swift b/MC1/Views/Components/ConversationQuickActionsSection.swift index 38ecb223..0ac7f822 100644 --- a/MC1/Views/Components/ConversationQuickActionsSection.swift +++ b/MC1/Views/Components/ConversationQuickActionsSection.swift @@ -3,19 +3,26 @@ import SwiftUI struct ConversationQuickActionsSection: View { @Environment(\.appTheme) private var theme + @Binding var isFavorite: Bool @Binding var notificationLevel: NotificationLevel let availableLevels: [NotificationLevel] init( + isFavorite: Binding, notificationLevel: Binding, availableLevels: [NotificationLevel] = NotificationLevel.allCases ) { + _isFavorite = isFavorite _notificationLevel = notificationLevel self.availableLevels = availableLevels } var body: some View { Section { + Toggle(isOn: $isFavorite) { + Label(L10n.Chats.Chats.Action.favorite, systemImage: "star") + } + NotificationLevelPicker(selection: $notificationLevel, availableLevels: availableLevels) .listRowInsets(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)) } diff --git a/MC1/Views/Contacts/ContactDetailView.swift b/MC1/Views/Contacts/ContactDetailView.swift index 94e2e79a..bdc2c326 100644 --- a/MC1/Views/Contacts/ContactDetailView.swift +++ b/MC1/Views/Contacts/ContactDetailView.swift @@ -46,6 +46,7 @@ struct ContactDetailView: View { @Environment(\.appState) private var appState @Environment(\.appTheme) private var theme @Environment(\.dismiss) private var dismiss + @Environment(\.isPresented) private var isPresented let contact: ContactDTO let showFromDirectChat: Bool @@ -78,7 +79,8 @@ struct ContactDetailView: View { @State private var showingClearMessagesAlert = false @State private var isClearingMessages = false @State private var isSaving = false - @State private var isTogglingFavorite = false + @State private var isFavorite: Bool + @State private var favoriteTask: Task? @State private var errorMessage: String? @State private var pathViewModel = PathManagementViewModel() @State private var showRoomJoinSheet = false @@ -94,12 +96,14 @@ struct ContactDetailView: View { @State private var pingResult: PingResult? @State private var isSharing = false @State private var showShareSuccess = false + @State private var headerHeight: CGFloat = 150 init(contact: ContactDTO, showFromDirectChat: Bool = false, onClearMessages: @escaping () -> Void = {}) { self.contact = contact self.showFromDirectChat = showFromDirectChat self.onClearMessages = onClearMessages _currentContact = State(initialValue: contact) + _isFavorite = State(initialValue: contact.isFavorite) } var body: some View { @@ -107,12 +111,14 @@ struct ContactDetailView: View { // Profile header ContactProfileSection( currentContact: currentContact, - contactTypeLabel: contactTypeLabel + contactTypeLabel: contactTypeLabel, + measuredHeight: $headerHeight ) // Quick actions ContactActionsSection( currentContact: currentContact, + isFavorite: $isFavorite, showFromDirectChat: showFromDirectChat, isPinging: isPinging, pingResult: pingResult, @@ -135,6 +141,10 @@ struct ContactDetailView: View { showShareSuccess: showShareSuccess ) .themedRowBackground(theme) + .onChange(of: isFavorite) { _, newValue in + favoriteTask?.cancel() + favoriteTask = Task { await setFavorite(newValue) } + } // Info section ContactInfoSection( @@ -186,22 +196,13 @@ struct ContactDetailView: View { .themedCanvas(theme) .errorAlert($errorMessage) .navigationBarTitleDisplayMode(.inline) - .scrollRevealNavigationTitle(currentContact.displayName) + .scrollRevealNavigationTitle(currentContact.displayName, revealAfter: headerHeight) .contentMargins(.top, 0, for: .scrollContent) .toolbar { - ToolbarItem(placement: .topBarLeading) { - Button { - Task { await toggleFavorite() } - } label: { - Image(systemName: currentContact.isFavorite ? "star.fill" : "star") - .foregroundStyle(currentContact.isFavorite ? .yellow : .secondary) + if isPresented { + ToolbarItem(placement: .confirmationAction) { + Button(L10n.Localizable.Common.done) { dismiss() } } - .disabled(isTogglingFavorite) - .radioDisabled(for: appState.connectionState) - } - - ToolbarItem(placement: .confirmationAction) { - Button(L10n.Localizable.Common.done) { dismiss() } } } .alert(L10n.Contacts.Contacts.Detail.Alert.Block.title, isPresented: $showingBlockAlert) { @@ -398,16 +399,12 @@ struct ContactDetailView: View { // MARK: - Actions - private func toggleFavorite() async { - isTogglingFavorite = true - defer { isTogglingFavorite = false } - + private func setFavorite(_ isFavorite: Bool) async { do { try await appState.services?.contactService.setContactFavorite( currentContact.id, - isFavorite: !currentContact.isFavorite + isFavorite: isFavorite ) - await refreshContact() } catch { errorMessage = error.userFacingMessage } @@ -531,6 +528,7 @@ private struct ContactDetailAvatarView: View { private struct ContactProfileSection: View { let currentContact: ContactDTO let contactTypeLabel: String + @Binding var measuredHeight: CGFloat var body: some View { Section { @@ -570,6 +568,7 @@ private struct ContactProfileSection: View { } } .frame(maxWidth: .infinity) + .scrollRevealHeaderHeight($measuredHeight) .listRowBackground(Color.clear) } } @@ -579,6 +578,7 @@ private struct ContactActionsSection: View { @Environment(\.appState) private var appState let currentContact: ContactDTO + @Binding var isFavorite: Bool let showFromDirectChat: Bool let isPinging: Bool let pingResult: PingResult? @@ -593,6 +593,10 @@ private struct ContactActionsSection: View { var body: some View { Section { + Toggle(isOn: $isFavorite) { + Label(L10n.Contacts.Contacts.Detail.favorite, systemImage: "star") + } + // Role-specific actions based on contact type switch currentContact.type { case .room: @@ -754,6 +758,7 @@ private struct ContactInfoSection: View { Image(systemName: "pencil") } .buttonStyle(.borderless) + .accessibilityLabel(L10n.Contacts.Contacts.Common.edit) } } From 4153c3b80f79c29ae689c7d91fa986b94fc9fd2f Mon Sep 17 00:00:00 2001 From: bwees Date: Tue, 7 Jul 2026 21:51:15 -0500 Subject: [PATCH 6/7] fix: move favorite to bottom of first list section --- .../Components/ConversationQuickActionsSection.swift | 6 +++--- MC1/Views/Contacts/ContactDetailView.swift | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/MC1/Views/Components/ConversationQuickActionsSection.swift b/MC1/Views/Components/ConversationQuickActionsSection.swift index 0ac7f822..50af1379 100644 --- a/MC1/Views/Components/ConversationQuickActionsSection.swift +++ b/MC1/Views/Components/ConversationQuickActionsSection.swift @@ -19,12 +19,12 @@ struct ConversationQuickActionsSection: View { var body: some View { Section { + NotificationLevelPicker(selection: $notificationLevel, availableLevels: availableLevels) + .listRowInsets(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)) + Toggle(isOn: $isFavorite) { Label(L10n.Chats.Chats.Action.favorite, systemImage: "star") } - - NotificationLevelPicker(selection: $notificationLevel, availableLevels: availableLevels) - .listRowInsets(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)) } .themedRowBackground(theme) } diff --git a/MC1/Views/Contacts/ContactDetailView.swift b/MC1/Views/Contacts/ContactDetailView.swift index bdc2c326..d6297012 100644 --- a/MC1/Views/Contacts/ContactDetailView.swift +++ b/MC1/Views/Contacts/ContactDetailView.swift @@ -593,10 +593,6 @@ private struct ContactActionsSection: View { var body: some View { Section { - Toggle(isOn: $isFavorite) { - Label(L10n.Contacts.Contacts.Detail.favorite, systemImage: "star") - } - // Role-specific actions based on contact type switch currentContact.type { case .room: @@ -670,6 +666,10 @@ private struct ContactActionsSection: View { } } .radioDisabled(for: appState.connectionState, or: isSharing || showShareSuccess) + + Toggle(isOn: $isFavorite) { + Label(L10n.Contacts.Contacts.Detail.favorite, systemImage: "star") + } } } } From e594a594999bff4d47c764ba7421cacab3542bd2 Mon Sep 17 00:00:00 2001 From: bwees Date: Tue, 7 Jul 2026 21:51:57 -0500 Subject: [PATCH 7/7] chore: code review nit --- MC1/Extensions/View+ScrollRevealTitle.swift | 2 +- MC1/Views/Chats/Room/RoomInfoSheet.swift | 2 +- MC1/Views/Chats/Sheets/ChannelInfoSheet.swift | 2 +- MC1/Views/Contacts/ContactDetailView.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MC1/Extensions/View+ScrollRevealTitle.swift b/MC1/Extensions/View+ScrollRevealTitle.swift index 90e6e3bb..62d49a9f 100644 --- a/MC1/Extensions/View+ScrollRevealTitle.swift +++ b/MC1/Extensions/View+ScrollRevealTitle.swift @@ -14,7 +14,7 @@ extension View { /// title reveals based on the header's real (Dynamic Type-aware) size rather than a /// fixed threshold. Attach to the header content, then feed `height` to /// `scrollRevealNavigationTitle(_:revealAfter:)`. - func scrollRevealHeaderHeight(_ height: Binding) -> some View { + func scrollRevealHeaderHeight(into height: Binding) -> some View { onGeometryChange(for: CGFloat.self) { $0.size.height } action: { height.wrappedValue = $0 } } } diff --git a/MC1/Views/Chats/Room/RoomInfoSheet.swift b/MC1/Views/Chats/Room/RoomInfoSheet.swift index 0eb6e8e0..3d9768da 100644 --- a/MC1/Views/Chats/Room/RoomInfoSheet.swift +++ b/MC1/Views/Chats/Room/RoomInfoSheet.swift @@ -42,7 +42,7 @@ struct RoomInfoSheet: View { } } .frame(maxWidth: .infinity) - .scrollRevealHeaderHeight($headerHeight) + .scrollRevealHeaderHeight(into: $headerHeight) .listRowBackground(Color.clear) } diff --git a/MC1/Views/Chats/Sheets/ChannelInfoSheet.swift b/MC1/Views/Chats/Sheets/ChannelInfoSheet.swift index 080bb950..35ecdd31 100644 --- a/MC1/Views/Chats/Sheets/ChannelInfoSheet.swift +++ b/MC1/Views/Chats/Sheets/ChannelInfoSheet.swift @@ -384,7 +384,7 @@ private struct ChannelInfoHeaderSection: View { } } .frame(maxWidth: .infinity) - .scrollRevealHeaderHeight($measuredHeight) + .scrollRevealHeaderHeight(into: $measuredHeight) .listRowBackground(Color.clear) } } diff --git a/MC1/Views/Contacts/ContactDetailView.swift b/MC1/Views/Contacts/ContactDetailView.swift index d6297012..853f6301 100644 --- a/MC1/Views/Contacts/ContactDetailView.swift +++ b/MC1/Views/Contacts/ContactDetailView.swift @@ -568,7 +568,7 @@ private struct ContactProfileSection: View { } } .frame(maxWidth: .infinity) - .scrollRevealHeaderHeight($measuredHeight) + .scrollRevealHeaderHeight(into: $measuredHeight) .listRowBackground(Color.clear) } }