mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-16 04:40:25 +00:00
improve banner
This commit is contained in:
@@ -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: {})
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user