damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit 79407f17e85cfb35e756964eec939a78367700fd
parent 72c19fc411137a950cb39bae2f011c0368482f12
Author: Daniel D’Aquino <daniel@daquino.me>
Date:   Wed, 29 Jan 2025 16:33:22 -0800

Add double star for Purple members that have been active for over a year

This commit adds a special badge for purple members who have been active
for more than one entire year.

Closes: https://github.com/damus-io/damus/issues/2831
Changelog-Added: Purple members who have been active for more than a year now get a special badge
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>

Diffstat:
Mdamus/Components/SupporterBadge.swift | 171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mdamus/Models/Purple/DamusPurple.swift | 15++++++++++++++-
Mdamus/Views/Purple/DamusPurpleAccountView.swift | 6++++--
3 files changed, 181 insertions(+), 11 deletions(-)

diff --git a/damus/Components/SupporterBadge.swift b/damus/Components/SupporterBadge.swift @@ -12,6 +12,14 @@ struct SupporterBadge: View { let purple_account: DamusPurple.Account? let style: Style let text_color: Color + var badge_variant: BadgeVariant { + if purple_account?.attributes.contains(.memberForMoreThanOneYear) == true { + return .oneYearSpecial + } + else { + return .normal + } + } init(percent: Int?, purple_account: DamusPurple.Account? = nil, style: Style, text_color: Color = .secondary) { self.percent = percent @@ -26,13 +34,18 @@ struct SupporterBadge: View { HStack { if let purple_account, purple_account.active == true { HStack(spacing: 1) { - Image("star.fill") - .resizable() - .frame(width:size, height:size) - .foregroundStyle(GoldGradient) - if self.style == .full { - let date = format_date(date: purple_account.created_at, time_style: .none) - Text(date) + switch self.badge_variant { + case .normal: + StarShape() + .frame(width:size, height:size) + .foregroundStyle(GoldGradient) + case .oneYearSpecial: + DoubleStar(size: size) + } + + if self.style == .full, + let ordinal = self.purple_account?.ordinal() { + Text(ordinal) .foregroundStyle(text_color) .font(.caption) } @@ -56,8 +69,102 @@ struct SupporterBadge: View { case full // Shows the entire badge with a purple subscriber number if present case compact // Does not show purple subscriber number. Only shows the star (if applicable) } + + enum BadgeVariant { + /// A normal badge that people are used to + case normal + /// A special badge for users who have been members for more than a year + case oneYearSpecial + } +} + + +struct StarShape: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + let center = CGPoint(x: rect.midX, y: rect.midY) + let radius: CGFloat = min(rect.width, rect.height) / 2 + let points = 5 + let adjustment: CGFloat = .pi / 2 + + for i in 0..<points * 2 { + let angle = (CGFloat(i) * .pi / CGFloat(points)) - adjustment + let pointRadius = i % 2 == 0 ? radius : radius * 0.4 + let point = CGPoint(x: center.x + pointRadius * cos(angle), y: center.y + pointRadius * sin(angle)) + if i == 0 { + path.move(to: point) + } else { + path.addLine(to: point) + } + } + path.closeSubpath() + return path + } +} + +struct DoubleStar: View { + let size: CGFloat + var starOffset: CGFloat = 5 + + var body: some View { + if #available(iOS 17.0, *) { + DoubleStarShape(starOffset: starOffset) + .frame(width: size, height: size) + .foregroundStyle(GoldGradient) + .padding(.trailing, starOffset) + } else { + Fallback(size: size, starOffset: starOffset) + } + } + + @available(iOS 17.0, *) + struct DoubleStarShape: Shape { + var strokeSize: CGFloat = 3 + var starOffset: CGFloat + + func path(in rect: CGRect) -> Path { + let normalSizedStarPath = StarShape().path(in: rect) + let largerStarPath = StarShape().path(in: rect.insetBy(dx: -strokeSize, dy: -strokeSize)) + + let finalPath = normalSizedStarPath + .subtracting( + largerStarPath.offsetBy(dx: starOffset, dy: 0) + ) + .union( + normalSizedStarPath.offsetBy(dx: starOffset, dy: 0) + ) + + return finalPath + } + } + + /// A fallback view for those who cannot run iOS 17 + struct Fallback: View { + var size: CGFloat + var starOffset: CGFloat + + var body: some View { + HStack { + StarShape() + .frame(width: size, height: size) + .foregroundStyle(GoldGradient) + + StarShape() + .fill(GoldGradient) + .overlay( + StarShape() + .stroke(Color.damusAdaptableWhite, lineWidth: 1) + ) + .frame(width: size + 1, height: size + 1) + .padding(.leading, -size - starOffset) + } + .padding(.trailing, -3) + } + } } + + func support_level_color(_ percent: Int) -> Color { if percent == 0 { return .gray @@ -86,7 +193,7 @@ struct SupporterBadge_Previews: PreviewProvider { HStack(alignment: .center) { SupporterBadge( percent: nil, - purple_account: DamusPurple.Account(pubkey: test_pubkey, created_at: .now, expiry: .now.addingTimeInterval(10000), subscriber_number: subscriber_number, active: true), + purple_account: DamusPurple.Account(pubkey: test_pubkey, created_at: .now, expiry: .now.addingTimeInterval(10000), subscriber_number: subscriber_number, active: true, attributes: []), style: .full ) .frame(width: 100) @@ -118,4 +225,52 @@ struct SupporterBadge_Previews: PreviewProvider { } } +#Preview("1 yr badge") { + VStack { + HStack(alignment: .center) { + SupporterBadge( + percent: nil, + purple_account: DamusPurple.Account(pubkey: test_pubkey, created_at: .now, expiry: .now.addingTimeInterval(10000), subscriber_number: 3, active: true, attributes: []), + style: .full + ) + .frame(width: 100) + } + + HStack(alignment: .center) { + SupporterBadge( + percent: nil, + purple_account: DamusPurple.Account(pubkey: test_pubkey, created_at: .now, expiry: .now.addingTimeInterval(10000), subscriber_number: 3, active: true, attributes: [.memberForMoreThanOneYear]), + style: .full + ) + .frame(width: 100) + } + + Text("Double star (just shape itself, with alt background color, to show it adapts to background well)") + .multilineTextAlignment(.center) + + if #available(iOS 17.0, *) { + HStack(alignment: .center) { + DoubleStar.DoubleStarShape(starOffset: 5) + .frame(width: 17, height: 17) + .padding(.trailing, -8) + } + .background(Color.blue) + } + + Text("Double star (fallback for iOS 16 and below)") + + HStack(alignment: .center) { + DoubleStar.Fallback(size: 17, starOffset: 5) + } + + Text("Double star (fallback for iOS 16 and below, with alt color limitation shown)") + .multilineTextAlignment(.center) + + HStack(alignment: .center) { + DoubleStar.Fallback(size: 17, starOffset: 5) + } + .background(Color.blue) + } +} + diff --git a/damus/Models/Purple/DamusPurple.swift b/damus/Models/Purple/DamusPurple.swift @@ -418,6 +418,13 @@ class DamusPurple: StoreObserverDelegate { let expiry: Date let subscriber_number: Int let active: Bool + let attributes: PurpleAccountAttributes + + struct PurpleAccountAttributes: OptionSet { + let rawValue: Int + + static let memberForMoreThanOneYear = PurpleAccountAttributes(rawValue: 1 << 0) + } func ordinal() -> String? { let number = Int(self.subscriber_number) @@ -438,7 +445,8 @@ class DamusPurple: StoreObserverDelegate { created_at: Date.init(timeIntervalSince1970: TimeInterval(payload.created_at)), expiry: Date.init(timeIntervalSince1970: TimeInterval(payload.expiry)), subscriber_number: Int(payload.subscriber_number), - active: payload.active + active: payload.active, + attributes: (payload.attributes?.member_for_more_than_one_year ?? false) ? [.memberForMoreThanOneYear] : [] ) } @@ -448,6 +456,11 @@ class DamusPurple: StoreObserverDelegate { let expiry: UInt64 // Unix timestamp let subscriber_number: UInt let active: Bool + let attributes: Attributes? + + struct Attributes: Codable { + let member_for_more_than_one_year: Bool + } } } } diff --git a/damus/Views/Purple/DamusPurpleAccountView.swift b/damus/Views/Purple/DamusPurpleAccountView.swift @@ -136,7 +136,8 @@ struct DamusPurpleAccountView: View { created_at: Date.now, expiry: Date.init(timeIntervalSinceNow: 60 * 60 * 24 * 30), subscriber_number: 7, - active: true + active: true, + attributes: [] ) ) } @@ -149,7 +150,8 @@ struct DamusPurpleAccountView: View { created_at: Date.init(timeIntervalSinceNow: -60 * 60 * 24 * 37), expiry: Date.init(timeIntervalSinceNow: -60 * 60 * 24 * 7), subscriber_number: 7, - active: false + active: false, + attributes: [] ) ) }