From 3ce918ca1887092e85cde288560052fa4a39f23e Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:19:34 +0400 Subject: [PATCH] improve banner --- .../Views/Badges/SupportSimpleXBanner.swift | 103 ++++++++++++++---- .../Shared/Views/ChatList/ChatListView.swift | 18 ++- 2 files changed, 98 insertions(+), 23 deletions(-) diff --git a/apps/ios/Shared/Views/Badges/SupportSimpleXBanner.swift b/apps/ios/Shared/Views/Badges/SupportSimpleXBanner.swift index df4fccd2ae..faab73c970 100644 --- a/apps/ios/Shared/Views/Badges/SupportSimpleXBanner.swift +++ b/apps/ios/Shared/Views/Badges/SupportSimpleXBanner.swift @@ -11,18 +11,38 @@ import SimpleXChat // A dismissible chat-list card promoting the badges purchase flow. Tapping the card opens the // badges entry view (BadgesSupportSimplexView) — the same first screen the settings row opens. -// The dismiss X hides the card permanently until DEFAULT_SUPPORTER_BANNER_SHOWN is reset. +// The dismiss X shows a confirmation alert (matching the Reachable-toolbar banner), and once +// dismissed the banner stays hidden until DEFAULT_SUPPORTER_BANNER_SHOWN is reset. The row-level +// show gate (chat count > 2 counting only visible chats) lives in ChatListView. struct SupportSimpleXBanner: View { @EnvironmentObject var theme: AppTheme @Environment(\.colorScheme) var colorScheme: ColorScheme @AppStorage(DEFAULT_SUPPORTER_BANNER_SHOWN) private var supporterBannerShown = false + @State private var showDismissAlert = false let onTap: () -> Void + private let cardCornerRadius: CGFloat = 24 + private let cardHeight: CGFloat = 72 + private let cardHorizontalPadding: CGFloat = 16 + private let heroWidth: CGFloat = 90 + private let heroHeight: CGFloat = 110 + // Trailing padding keeps the hero's right edge to the LEFT of the dismiss X's left edge + // (X's claim from the card right is trailing 16 + width 12 = 28pt, matching OneHandUICard, + // so hero trailing 32 gives a ~4pt gap between hero right and X left). + private let heroTrailingPadding: CGFloat = 32 + private let textToHeroGap: CGFloat = 8 + var body: some View { if !supporterBannerShown { + // The card Button is the base (its natural size is the card rectangle, cardHeight tall) + // and the hero image is added as an .overlay so it can visually extend past the card top + // without changing the layout size. The dismiss X lives as a sibling in the outer + // ZStack, aligned .topTrailing so it sits at the card's top-right; the X label uses + // asymmetric vertical padding (12 top / 4 bottom) so the icon has a visible gap from + // the card top edge without being pinned to the very top. ZStack(alignment: .topTrailing) { Button(action: onTap) { - HStack(alignment: .center, spacing: 8) { + HStack(spacing: 0) { VStack(alignment: .leading, spacing: 4) { Text("Support SimpleX") .font(.headline) @@ -31,26 +51,40 @@ struct SupportSimpleXBanner: View { .font(.subheadline) .foregroundColor(theme.colors.onBackground) } - Spacer() - heroThumbnail() + Spacer(minLength: heroWidth + heroTrailingPadding + textToHeroGap) } - .padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)) + .padding(.horizontal, cardHorizontalPadding) + .frame(height: cardHeight) .background(gradientBackground()) - .clipShape(RoundedRectangle(cornerRadius: 20)) + .clipShape(RoundedRectangle(cornerRadius: cardCornerRadius)) } .buttonStyle(.plain) + .overlay(alignment: .bottomTrailing) { + heroThumbnail() + .padding(.trailing, heroTrailingPadding) + .allowsHitTesting(false) + } - Button { - withAnimation { supporterBannerShown = true } - } label: { - Image(systemName: "xmark") - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(theme.colors.secondary) - .frame(width: 24, height: 24) - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - .padding(6) + // X, sibling of the Button in ZStack, aligned to CARD top-right. + Image(systemName: "multiply") + .foregroundColor(theme.colors.secondary) + .frame(width: 12, height: 12) + .padding(.top, 12) + .padding(.bottom, 4) + .padding(.trailing, 16) + .padding(.leading, 4) + .contentShape(Rectangle()) + .onTapGesture { showDismissAlert = true } + } + .zIndex(1) + .alert(isPresented: $showDismissAlert) { + Alert( + title: Text("Support SimpleX"), + message: Text("You can support SimpleX later in Settings."), + dismissButton: .default(Text("Ok")) { + withAnimation { supporterBannerShown = true } + } + ) } } } @@ -58,19 +92,24 @@ struct SupportSimpleXBanner: View { @ViewBuilder private func heroThumbnail() -> some View { #if SIMPLEX_ASSETS + // .resizable + .frame (no aspect modifier) stretches the asset to fill the frame exactly. + // The asset aspect (0.795) is close enough to the frame aspect (heroWidth/heroHeight ≈ 0.82) + // that horizontal stretching is barely perceptible (~3%), and this guarantees the phone + // illustration's bottom lines up with the frame bottom = card bottom (no whitespace gap + // from .scaledToFit centering, no overhang from .aspectRatio(.fill) leaking past the clip). Image(colorScheme == .light ? "phone-supporter" : "phone-supporter-light") .resizable() - .scaledToFit() - .frame(width: 90, height: 90) + .frame(width: heroWidth, height: heroHeight) + .clipShape(HeroBottomRightRoundedShape(cornerRadius: cardCornerRadius)) #else Image("badge-supporter") .resizable() .scaledToFit() - .frame(width: 56, height: 56) + .frame(width: 48, height: 48) + .padding(.top, (cardHeight - 48) / 2) #endif } - // Matches the onboarding card gradient so the banner reads as part of the same visual family. private func gradientBackground() -> some View { let gp = OnboardingCardView.gradientPoints(aspectRatio: 4.0, scale: colorScheme == .light ? 1.2 : 1.5) return LinearGradient( @@ -81,6 +120,28 @@ struct SupportSimpleXBanner: View { } } +// Rounds only the bottom-right corner of the hero to match the card's rounded bottom-right — +// leaves the top, left and bottom-left edges straight so the hero can extend above the card +// (top) and reach into the card without extra rounding on the sides. +private struct HeroBottomRightRoundedShape: Shape { + let cornerRadius: CGFloat + + func path(in rect: CGRect) -> Path { + var p = Path() + p.move(to: CGPoint(x: rect.minX, y: rect.minY)) + p.addLine(to: CGPoint(x: rect.maxX, y: rect.minY)) + p.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerRadius)) + p.addQuadCurve( + to: CGPoint(x: rect.maxX - cornerRadius, y: rect.maxY), + control: CGPoint(x: rect.maxX, y: rect.maxY) + ) + p.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) + p.addLine(to: CGPoint(x: rect.minX, y: rect.minY)) + p.closeSubpath() + return p + } +} + struct SupportSimpleXBanner_Previews: PreviewProvider { static var previews: some View { SupportSimpleXBanner(onTap: {}) diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index b5052046f3..d80ef139b0 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -374,6 +374,19 @@ struct ChatListView: View { !addressCreationCardShown && !chatModel.chats.isEmpty && !hasConversations } + // Count of user-facing conversations (excludes pending contact requests/connections, deleted + // chats and contact cards) — used as the show gate for SupportSimpleXBanner so the banner only + // appears once the user actually has a few conversations in the list. + private var visibleChatCount: Int { + chatModel.chats.reduce(into: 0) { count, chat in + switch chat.chatInfo { + case let .direct(contact): if !contact.chatDeleted && !contact.isContactCard { count += 1 } + case .group, .local: count += 1 + case .contactRequest, .contactConnection, .invalidJSON: break + } + } + } + private var hasConversations: Bool { chatModel.chats.contains { chat in switch chat.chatInfo { @@ -425,12 +438,13 @@ struct ChatListView: View { .listRowSeparator(.hidden) .listRowBackground(Color.clear) } - if !supporterBannerShown { + if !supporterBannerShown && visibleChatCount > 2 { SupportSimpleXBanner(onTap: { showBadgesSheet = true }) - .padding(.vertical, 6) + .padding(.vertical, 3) .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) .listRowSeparator(.hidden) .listRowBackground(Color.clear) + .zIndex(1) } if #available(iOS 16.0, *) { ForEach(cs, id: \.viewId) { chat in