This commit is contained in:
spaced4ndy
2026-07-30 23:36:25 +04:00
parent e640932703
commit 90a8793d7c
6 changed files with 93 additions and 51 deletions
@@ -0,0 +1,44 @@
//
// BadgeUserPreview.swift
// SimpleX (iOS)
//
// Created by spaced4ndy on 30.07.2026.
// Copyright © 2026 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
// Shows the user's avatar and display name with a synthesized preview badge at the selected
// level, using the same NameWithBadge helper that renders badges everywhere else in the app
// so the preview matches what will actually appear on the user's name. Any picker affordance
// (e.g. a chevron) is supplied by the caller via `trailing`, and is laid out next to the
// name row without leaking into this view.
struct BadgeUserPreview<Trailing: View>: View {
@EnvironmentObject var chatModel: ChatModel
let level: BadgeLevel
let trailing: () -> Trailing
init(level: BadgeLevel, @ViewBuilder trailing: @escaping () -> Trailing = { EmptyView() }) {
self.level = level
self.trailing = trailing
}
var body: some View {
let user = chatModel.currentUser
let displayName = user?.displayName ?? NSLocalizedString("My nickname", comment: "badges preview placeholder")
let previewBadge = LocalBadge(
badge: BadgeInfo(badgeType: level.badgeType),
status: .active
)
return VStack(spacing: 12) {
ProfileImage(imageStr: user?.image, size: 128)
HStack(alignment: .center, spacing: 6) {
NameWithBadge(Text(displayName).font(.largeTitle), previewBadge, .largeTitle)
.lineLimit(1)
.minimumScaleFactor(0.75)
trailing()
}
}
}
}
@@ -33,7 +33,6 @@ enum BadgePeriod: String, CaseIterable, Identifiable {
}
struct BadgesPayView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
let level: BadgeLevel
@State private var selectedPeriod: BadgePeriod = .subscribe
@@ -49,7 +48,7 @@ struct BadgesPayView: View {
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
userPreview()
BadgeUserPreview(level: level)
.padding(.top, 4)
Text(level.tagline)
@@ -86,23 +85,6 @@ struct BadgesPayView: View {
.frame(maxHeight: .infinity)
}
private func userPreview() -> some View {
let user = chatModel.currentUser
return VStack(spacing: 12) {
ProfileImage(imageStr: user?.image, size: 128)
HStack(alignment: .center, spacing: 6) {
Text(user?.displayName ?? NSLocalizedString("My nickname", comment: "badges preview placeholder"))
.font(.largeTitle)
.lineLimit(1)
.minimumScaleFactor(0.75)
Image(level.badgeAsset)
.resizable()
.scaledToFit()
.frame(width: 28, height: 28)
}
}
}
private func periodCard(_ period: BadgePeriod) -> some View {
let isSelected = period == selectedPeriod
return Button {
@@ -72,10 +72,16 @@ enum BadgeLevel: String, CaseIterable, Identifiable {
case .legend: "badge-legend"
}
}
var badgeType: BadgeType {
switch self {
case .supporter: .supporter
case .legend: .legend
}
}
}
struct BadgesYourLevelView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@State private var selectedLevel: BadgeLevel = .supporter
@State private var continueActive = false
@@ -92,8 +98,12 @@ struct BadgesYourLevelView: View {
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
userPreview()
.padding(.top, 4)
BadgeUserPreview(level: selectedLevel) {
Image(systemName: "chevron.down")
.font(.body)
.foregroundColor(theme.colors.primary)
}
.padding(.top, 4)
HStack(alignment: .top, spacing: 12) {
levelCard(.supporter)
@@ -118,29 +128,6 @@ struct BadgesYourLevelView: View {
.frame(maxHeight: .infinity)
}
// The avatar + name preview shows the user how their profile will look with the selected badge.
// Uses the current user's real profile image and display name; when SIMPLEX_ASSETS is absent the
// avatar falls back to ProfileImage's own default. TODO [badges] wire real LocalBadge preview.
private func userPreview() -> some View {
let user = chatModel.currentUser
return VStack(spacing: 12) {
ProfileImage(imageStr: user?.image, size: 128)
HStack(alignment: .center, spacing: 6) {
Text(user?.displayName ?? NSLocalizedString("My nickname", comment: "badges preview placeholder"))
.font(.largeTitle)
.lineLimit(1)
.minimumScaleFactor(0.75)
Image(selectedLevel.badgeAsset)
.resizable()
.scaledToFit()
.frame(width: 28, height: 28)
Image(systemName: "chevron.down")
.font(.body)
.foregroundColor(theme.colors.primary)
}
}
}
private func levelCard(_ level: BadgeLevel) -> some View {
let isSelected = level == selectedLevel
return Button {
@@ -123,13 +123,27 @@ struct SupportSimpleXBanner: View {
#endif
}
@ViewBuilder
private func gradientBackground() -> some View {
let stops = colorScheme == .light ? OnboardingCardView.lightStops : OnboardingCardView.darkStops
return LinearGradient(
colors: [stops.first!.color, stops.last!.color],
startPoint: .bottomLeading,
endPoint: .topTrailing
)
// Light: just the first and last stops of OnboardingCardView.lightStops as a simple
// 2-color gradient the intermediate stops there include a hard blue plateau (0.00.5)
// that reads as "almost entirely blue" on this wide-short banner.
// Dark: the full OnboardingCardView.darkStops (all 5 locations) the intermediate blue-to-
// navy transitions land nicely on this shape, giving depth we'd lose with only 2 colors.
if colorScheme == .light {
let stops = OnboardingCardView.lightStops
LinearGradient(
colors: [stops.first!.color, stops.last!.color],
startPoint: .bottomLeading,
endPoint: .topTrailing
)
} else {
LinearGradient(
stops: OnboardingCardView.darkStops,
startPoint: .bottomLeading,
endPoint: .topTrailing
)
}
}
}
@@ -256,6 +256,7 @@
E5BADE0107000000BADE0004 /* BadgesRedeemCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5BADE0108000000BADE0004 /* BadgesRedeemCodeView.swift */; };
E5BADE0109000000BADE0005 /* BadgesHowItWorksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5BADE010A000000BADE0005 /* BadgesHowItWorksView.swift */; };
E5BADE010B000000BADE0006 /* SupportSimpleXBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5BADE010C000000BADE0006 /* SupportSimpleXBanner.swift */; };
E5BADE010D000000BADE0007 /* BadgeUserPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5BADE010E000000BADE0007 /* BadgeUserPreview.swift */; };
E559A0A12E3F77EE00B26F74 /* CommandsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E559A0A02E3F77EE00B26F74 /* CommandsMenuView.swift */; };
E5A0B0012F960000AAAA0001 /* YourNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5A0B0022F960000AAAA0001 /* YourNetwork.swift */; };
E5AEC0AB2F91A6EB00270665 /* CIChatLinkHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AEC0AA2F91A6EA00270665 /* CIChatLinkHeader.swift */; };
@@ -638,6 +639,7 @@
E5BADE0108000000BADE0004 /* BadgesRedeemCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesRedeemCodeView.swift; sourceTree = "<group>"; };
E5BADE010A000000BADE0005 /* BadgesHowItWorksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesHowItWorksView.swift; sourceTree = "<group>"; };
E5BADE010C000000BADE0006 /* SupportSimpleXBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportSimpleXBanner.swift; sourceTree = "<group>"; };
E5BADE010E000000BADE0007 /* BadgeUserPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeUserPreview.swift; sourceTree = "<group>"; };
E559A0A02E3F77EE00B26F74 /* CommandsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandsMenuView.swift; sourceTree = "<group>"; };
E5A0B0022F960000AAAA0001 /* YourNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourNetwork.swift; sourceTree = "<group>"; };
E5AEC0AA2F91A6EA00270665 /* CIChatLinkHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIChatLinkHeader.swift; sourceTree = "<group>"; };
@@ -1064,6 +1066,7 @@
E5BADE0108000000BADE0004 /* BadgesRedeemCodeView.swift */,
E5BADE010A000000BADE0005 /* BadgesHowItWorksView.swift */,
E5BADE010C000000BADE0006 /* SupportSimpleXBanner.swift */,
E5BADE010E000000BADE0007 /* BadgeUserPreview.swift */,
);
path = Badges;
sourceTree = "<group>";
@@ -1563,6 +1566,7 @@
E5BADE0107000000BADE0004 /* BadgesRedeemCodeView.swift in Sources */,
E5BADE0109000000BADE0005 /* BadgesHowItWorksView.swift in Sources */,
E5BADE010B000000BADE0006 /* SupportSimpleXBanner.swift in Sources */,
E5BADE010D000000BADE0007 /* BadgeUserPreview.swift in Sources */,
5C65DAF929D0CC20003CEE45 /* DeveloperView.swift in Sources */,
5C36027327F47AD5009F19D9 /* AppDelegate.swift in Sources */,
5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */,
+11
View File
@@ -275,11 +275,22 @@ public struct BadgeInfo: Codable, Hashable {
public var badgeType: BadgeType
public var badgeExpiry: Date?
public var badgeExtra: String
public init(badgeType: BadgeType, badgeExpiry: Date? = nil, badgeExtra: String = "") {
self.badgeType = badgeType
self.badgeExpiry = badgeExpiry
self.badgeExtra = badgeExtra
}
}
public struct LocalBadge: Codable, Hashable {
public var badge: BadgeInfo
public var status: BadgeStatus
public init(badge: BadgeInfo, status: BadgeStatus) {
self.badge = badge
self.status = status
}
}
// the wire proof carried on a profile - opaque to the UI, only round-tripped back to the core (apiPrepareContact)