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:
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: []
)
)
}