Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions DamusNotificationService/NotificationExtensionState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ struct NotificationExtensionState: HeadlessDamusState {
let keypair: Keypair
let profiles: Profiles
let zaps: Zaps
let polls: PollResultsStore
let lnurls: LNUrls

init?() {
Expand All @@ -32,6 +33,7 @@ struct NotificationExtensionState: HeadlessDamusState {
self.keypair = keypair
self.profiles = Profiles(ndb: ndb)
self.zaps = Zaps(our_pubkey: keypair.pubkey)
self.polls = PollResultsStore()
self.lnurls = LNUrls()
}

Expand Down
128 changes: 108 additions & 20 deletions damus.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions damus/Core/Nostr/NostrKind.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ enum NostrKind: UInt32, Codable {
case delete = 5
case boost = 6
case like = 7
case poll_response = 1018
case chat = 42
case poll = 1068
case mute_list = 10000
case relay_list = 10002
case interest_list = 10015
Expand Down
34 changes: 33 additions & 1 deletion damus/Core/Storage/DamusState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class DamusState: HeadlessDamusState {
let dms: DirectMessagesModel
let previews: PreviewCache
let zaps: Zaps
let polls: PollResultsStore
let lnurls: LNUrls
let settings: UserSettingsStore
let relay_filters: RelayFilters
Expand All @@ -39,7 +40,35 @@ class DamusState: HeadlessDamusState {
let favicon_cache: FaviconCache
private(set) var nostrNetwork: NostrNetworkManager

init(keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider, favicon_cache: FaviconCache) {
init(
keypair: Keypair,
likes: EventCounter,
boosts: EventCounter,
contacts: Contacts,
mutelist_manager: MutelistManager,
profiles: Profiles,
dms: DirectMessagesModel,
previews: PreviewCache,
zaps: Zaps,
polls: PollResultsStore,
lnurls: LNUrls,
settings: UserSettingsStore,
relay_filters: RelayFilters,
relay_model_cache: RelayModelCache,
drafts: Drafts,
events: EventCache,
bookmarks: BookmarksManager,
replies: ReplyCounter,
wallet: WalletModel,
nav: NavigationCoordinator,
music: MusicController?,
video: DamusVideoCoordinator,
ndb: Ndb,
purple: DamusPurple? = nil,
quote_reposts: EventCounter,
emoji_provider: EmojiProvider,
favicon_cache: FaviconCache
) {
self.keypair = keypair
self.likes = likes
self.boosts = boosts
Expand All @@ -49,6 +78,7 @@ class DamusState: HeadlessDamusState {
self.dms = dms
self.previews = previews
self.zaps = zaps
self.polls = polls
self.lnurls = lnurls
self.settings = settings
self.relay_filters = relay_filters
Expand Down Expand Up @@ -114,6 +144,7 @@ class DamusState: HeadlessDamusState {
dms: home.dms,
previews: PreviewCache(),
zaps: Zaps(our_pubkey: pubkey),
polls: PollResultsStore(),
lnurls: LNUrls(),
settings: settings,
relay_filters: relay_filters,
Expand Down Expand Up @@ -183,6 +214,7 @@ class DamusState: HeadlessDamusState {
dms: DirectMessagesModel(our_pubkey: empty_pub),
previews: PreviewCache(),
zaps: Zaps(our_pubkey: empty_pub),
polls: PollResultsStore(),
lnurls: LNUrls(),
settings: UserSettingsStore(),
relay_filters: RelayFilters(our_pubkey: empty_pub),
Expand Down
4 changes: 3 additions & 1 deletion damus/Features/Events/EventView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ struct EventView: View {
LongformPreview(state: damus, ev: event, options: options)
} else if event.known_kind == .highlight {
HighlightView(state: damus, event: event, options: options)
} else if event.known_kind == .poll, let poll = PollEvent(event: event) {
damus.polls.registerPollEvent(event)
PollEventView(damus: damus, event: event, poll: poll, options: options)
} else {
TextEvent(damus: damus, event: event, pubkey: pubkey, options: options)
//.padding([.top], 6)
Expand Down Expand Up @@ -158,4 +161,3 @@ struct EventView_Previews: PreviewProvider {
.padding()
}
}

56 changes: 56 additions & 0 deletions damus/Features/Polls/Models/PollDraft.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// PollDraft.swift
// damus
//
// Created by ChatGPT on 2025-04-02.
Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The creation date 2025-04-02 appears to be in the future relative to when this was likely created. Consider updating to the actual creation date.

Suggested change
// Created by ChatGPT on 2025-04-02.
// Created by ChatGPT on 2024-04-02.

Copilot uses AI. Check for mistakes.

//

import Foundation

struct PollDraftOption: Identifiable, Equatable {
let id: UUID
var text: String
}

struct PollDraft: Equatable {
var options: [PollDraftOption]
var pollType: PollType
var endsAt: Date?

static let minimumOptions: Int = 2
static let maximumOptions: Int = 6

static func makeDefault() -> PollDraft {
PollDraft(
options: (0..<PollDraft.minimumOptions).map { _ in PollDraftOption(id: UUID(), text: "") },
pollType: .singleChoice,
endsAt: nil
)
}

mutating func addOption() {
guard options.count < PollDraft.maximumOptions else { return }
options.append(PollDraftOption(id: UUID(), text: ""))
}

mutating func removeOption(id: UUID) {
guard options.count > PollDraft.minimumOptions else { return }
options.removeAll { $0.id == id }
}

mutating func ensureMinimumOptions() {
if options.count < PollDraft.minimumOptions {
let missing = PollDraft.minimumOptions - options.count
for _ in 0..<missing {
options.append(PollDraftOption(id: UUID(), text: ""))
}
}
}

var normalizedOptionLabels: [String] {
options
.map { $0.text.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
}
}

159 changes: 159 additions & 0 deletions damus/Features/Polls/Models/PollModels.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
//
// PollModels.swift
// damus
//
// Created by ChatGPT on 2025-04-02.
Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The creation date 2025-04-02 appears to be in the future relative to when this was likely created. Consider updating to the actual creation date.

Suggested change
// Created by ChatGPT on 2025-04-02.
// Created by ChatGPT on 2024-06-10.

Copilot uses AI. Check for mistakes.

//

import Foundation

enum PollType: String {
case singleChoice = "singlechoice"
case multipleChoice = "multiplechoice"

static var `default`: PollType { .singleChoice }
}

struct PollOption: Identifiable, Hashable {
let id: String
let label: String
}

struct PollEvent {
let id: NoteId
let author: Pubkey
let createdAt: UInt32
let question: String
let options: [PollOption]
let pollType: PollType
let relayHints: [RelayURL]
let endsAt: UInt32?

private let optionIdSet: Set<String>

init?(event: NostrEvent) {
guard event.known_kind == .poll else { return nil }

var options: [PollOption] = []
var optionIds: Set<String> = []
var relayHints: [RelayURL] = []
var pollType: PollType = .default
var endsAt: UInt32? = nil

for tag in event.tags {
guard tag.count >= 2 else { continue }
var iterator = tag.makeIterator()
guard let keyElem = iterator.next() else { continue }

switch keyElem.string() {
case "option":
guard tag.count >= 3,
let idElem = iterator.next(),
let labelElem = iterator.next()
else { continue }
let optionId = idElem.string()
let optionLabel = labelElem.string()
guard !optionId.isEmpty, !optionLabel.isEmpty, !optionIds.contains(optionId) else { continue }
optionIds.insert(optionId)
options.append(PollOption(id: optionId, label: optionLabel))

case "relay":
guard let relayElem = iterator.next(),
let relay = RelayURL(relayElem.string())
else { continue }
if !relayHints.contains(relay) {
relayHints.append(relay)
}

case "polltype":
guard let typeElem = iterator.next() else { continue }
pollType = PollType(rawValue: typeElem.string()) ?? .default

case "endsAt":
guard let endsElem = iterator.next() else { continue }
if let raw = endsElem.u64() {
if raw > UInt64(UInt32.max) {
endsAt = UInt32.max
} else {
endsAt = UInt32(raw)
}
}

default:
continue
}
}

guard options.count >= 2 else { return nil }

self.id = event.id
self.author = event.pubkey
self.createdAt = event.created_at
self.question = event.content
self.options = options
self.pollType = pollType
self.relayHints = relayHints
self.endsAt = endsAt
self.optionIdSet = optionIds
}

func containsOption(_ optionId: String) -> Bool {
optionIdSet.contains(optionId)
}

func isExpired(at timestamp: UInt32) -> Bool {
guard let endsAt else { return false }
return timestamp > endsAt
}

func isExpired(now: Date = .now) -> Bool {
guard let endsAt else { return false }
return UInt32(now.timeIntervalSince1970) > endsAt
}
}

struct PollResponse {
let pollId: NoteId
let responseId: NoteId
let responder: Pubkey
let createdAt: UInt32
let optionIds: [String]

init?(event: NostrEvent) {
guard event.known_kind == .poll_response else { return nil }

guard let pollReference = event.referenced_ids.first(where: { ref in
if case .event = ref {
return true
}
return false
}),
case .event(let pollId) = pollReference
else {
return nil
}

var responses: [String] = []
for tag in event.tags {
guard tag.count >= 2 else { continue }
var iterator = tag.makeIterator()
guard let keyElem = iterator.next(), keyElem.string() == "response",
let responseElem = iterator.next()
else {
continue
}
let optionId = responseElem.string()
guard !optionId.isEmpty else { continue }
responses.append(optionId)
}

guard !responses.isEmpty else { return nil }

self.pollId = pollId
self.responseId = event.id
self.responder = event.pubkey
self.createdAt = event.created_at
self.optionIds = responses
}
}

Loading