-
Notifications
You must be signed in to change notification settings - Fork 294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New feature: Polls #243
New feature: Polls #243
Changes from all commits
ebf80e2
74cf389
7d85954
8263ac6
08cde75
fef8455
45a3747
c9844ba
2be0b2c
80ed2bc
e4c7752
20963d4
da2bce1
d217420
4c631a9
aaa9292
c5ed601
1315154
3e71465
6023efa
47a62a6
7c9d6f3
ab88b91
fb9ced4
1b9ae2d
3811f71
e99fab5
8fdd190
8a8b897
6fc9c8d
c10cb8f
d8df64b
34602dd
17cf609
f11798d
c3d26dc
b07629b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,10 @@ struct ConfigView: View { | |
@State var relays: [RelayDescriptor] | ||
@EnvironmentObject var user_settings: UserSettingsStore | ||
|
||
/// True = poll will display votes from everyone, false = poll will display votes from friends only (and friends of friends) | ||
/// This option is included because of the potential problem with bots. | ||
@AppStorage("poll_results_everyone") var poll_results_everyone: Bool = false | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've never used AppStorage before. Does this just save it in UserSettings? I eventually want to scope user settings by pubkey so maybe its best to have it in there |
||
|
||
let generator = UIImpactFeedbackGenerator(style: .light) | ||
|
||
init(state: DamusState) { | ||
|
@@ -105,6 +109,9 @@ struct ConfigView: View { | |
} | ||
} | ||
|
||
Section("Poll feature") { | ||
Toggle("See everyone votes", isOn: $poll_results_everyone) | ||
} | ||
Section(NSLocalizedString("Wallet Selector", comment: "Section title for selection of wallet.")) { | ||
Toggle(NSLocalizedString("Show wallet selector", comment: "Toggle to show or hide selection of wallet."), isOn: $user_settings.show_wallet_selector).toggleStyle(.switch) | ||
Picker(NSLocalizedString("Select default wallet", comment: "Prompt selection of user's default wallet"), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -121,6 +121,12 @@ struct BuilderEventView: View { | |
} | ||
} | ||
|
||
enum PollResultsDisplay { | ||
case none | ||
case friends | ||
case everyone | ||
} | ||
|
||
struct EventView: View { | ||
let event: NostrEvent | ||
let highlight: Highlight | ||
|
@@ -131,6 +137,14 @@ struct EventView: View { | |
let size: EventViewKind | ||
|
||
@EnvironmentObject var action_bar: ActionBarModel | ||
|
||
@State var show_poll_results: Bool = false | ||
@State var subscription_poll_uuid: String = UUID().description | ||
|
||
// String = user pubkey and int is the choice | ||
@State var choices: [(String, Int)] = [] | ||
|
||
@AppStorage("poll_results_everyone") var poll_results_everyone: Bool = false | ||
|
||
init(event: NostrEvent, highlight: Highlight, has_action_bar: Bool, damus: DamusState, show_friend_icon: Bool, size: EventViewKind = .normal) { | ||
self.event = event | ||
|
@@ -161,6 +175,58 @@ struct EventView: View { | |
self.show_friend_icon = show_friend_icon | ||
self.size = size | ||
} | ||
|
||
func handle_event(relay_id: String, ev: NostrConnectionEvent) { | ||
guard case .nostr_event(let nostr_response) = ev else { | ||
return | ||
} | ||
|
||
guard case .event(let id, let nostr_event) = nostr_response else { | ||
return | ||
} | ||
|
||
// Is current event | ||
guard id == subscription_poll_uuid else { | ||
return | ||
} | ||
guard nostr_event.kind == 8 else { | ||
return | ||
} | ||
|
||
|
||
// If the pubkey is our, display the results | ||
if nostr_event.pubkey == damus.pubkey { | ||
show_poll_results = true | ||
} | ||
|
||
// Check if the choice is already submitted by the pubkey | ||
if choices.contains(where: { $0.0 == nostr_event.pubkey }) { | ||
return | ||
} | ||
|
||
// Check the choice | ||
guard let choice_int = Int(nostr_event.content) else { | ||
return | ||
} | ||
|
||
choices.append((nostr_event.pubkey, choice_int)) | ||
} | ||
|
||
func unsubscribe_poll() { | ||
damus.pool.unsubscribe(sub_id: subscription_poll_uuid) | ||
} | ||
|
||
func subscribe_poll() { | ||
let filters: [NostrFilter] = [ | ||
NostrFilter( | ||
kinds: [8], | ||
referenced_ids: [event.id] | ||
) | ||
] | ||
|
||
damus.pool.register_handler(sub_id: subscription_poll_uuid, handler: handle_event) | ||
damus.pool.send(.subscribe(.init(filters: filters, sub_id: subscription_poll_uuid))) | ||
} | ||
|
||
var body: some View { | ||
return Group { | ||
|
@@ -198,6 +264,12 @@ struct EventView: View { | |
func TextEvent(_ event: NostrEvent, pubkey: String) -> some View { | ||
let content = event.get_content(damus.keypair.privkey) | ||
|
||
let poll_choices = event.tags.filter { value in | ||
return value.count >= 2 && value[0] == "poll" && !value[1].isEmpty | ||
}.prefix(4).map { value in | ||
return value[1] | ||
} | ||
|
||
return HStack(alignment: .top) { | ||
let profile = damus.profiles.lookup(id: pubkey) | ||
|
||
|
@@ -247,6 +319,99 @@ struct EventView: View { | |
NoteContentView(privkey: damus.keypair.privkey, event: event, profiles: damus.profiles, previews: damus.previews, show_images: should_show_img, artifacts: .just_content(content), size: self.size) | ||
.frame(maxWidth: .infinity, alignment: .leading) | ||
|
||
// MARK: - Poll | ||
if poll_choices.count >= 2 { | ||
let filtered_choices = choices.filter({ poll_results_everyone ? true : damus.contacts.is_in_friendosphere($0.0) }) | ||
let total_count = filtered_choices.count == 0 ? 1 : filtered_choices.count | ||
|
||
VStack(alignment: .leading, spacing: 15) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should be its own view |
||
ForEach(0 ..< poll_choices.count, id: \.self) { index in | ||
let this_choice_count = filtered_choices.filter({ $0.1 == index }).count | ||
let percent = CGFloat(this_choice_count) / CGFloat(total_count) | ||
|
||
HStack { | ||
if show_poll_results { | ||
/// Placeholder text for the size | ||
Text("100%") | ||
.font(.caption) | ||
.opacity(0) | ||
.overlay { | ||
Text("\(Int(floor(percent * 100)))%") | ||
.font(.caption) | ||
} | ||
} | ||
|
||
HStack { | ||
Button { | ||
if !show_poll_results { | ||
send_poll_choice(index) | ||
show_poll_results = true | ||
} | ||
} label: { | ||
Text(poll_choices[index]) | ||
.font(.caption) | ||
.frame(minWidth: 0, maxWidth: .infinity) | ||
.padding(12) | ||
} | ||
} | ||
.background(alignment: .leading) { | ||
GeometryReader { geometry in | ||
withAnimation { | ||
Rectangle() | ||
.foregroundColor(.accentColor.opacity(0.2)) | ||
.frame( | ||
minWidth: 0, | ||
maxWidth: show_poll_results | ||
? percent * geometry.size.width | ||
: 0 | ||
) | ||
} | ||
} | ||
} | ||
.overlay( | ||
RoundedRectangle(cornerRadius: 10) | ||
.stroke(Color.gray.opacity(0.4), lineWidth: 2) | ||
) | ||
.cornerRadius(10) | ||
} | ||
} | ||
|
||
if !show_poll_results { | ||
Button { | ||
show_poll_results = true | ||
} label: { | ||
Label("Show result", systemImage: "number.circle") | ||
.font(.caption) | ||
.frame(minWidth: 0, maxWidth: .infinity) | ||
.padding(12) | ||
.overlay( | ||
RoundedRectangle(cornerRadius: 10) | ||
.stroke(Color.gray.opacity(0.4), lineWidth: 2) | ||
) | ||
.cornerRadius(10) | ||
} | ||
} | ||
|
||
Text("\(filtered_choices.count) vote\(filtered_choices.count >= 2 ? "s" : "")\(poll_results_everyone ? "" : " - close circle of friends votes only")") | ||
.font(.footnote) | ||
.foregroundColor(.gray) | ||
} | ||
.frame(minWidth: 0, maxWidth: .infinity) | ||
.padding() | ||
.overlay( | ||
RoundedRectangle(cornerRadius: 10) | ||
.stroke(Color.gray.opacity(0.4), lineWidth: 2) | ||
) | ||
.cornerRadius(10) | ||
.onAppear { | ||
subscribe_poll() | ||
} | ||
.onDisappear { | ||
unsubscribe_poll() | ||
} | ||
} | ||
|
||
// MARK: - Action bar | ||
if has_action_bar { | ||
if size == .selected { | ||
Text("\(format_date(event.created_at))") | ||
|
@@ -276,6 +441,16 @@ struct EventView: View { | |
.padding([.bottom], 2) | ||
.event_context_menu(event, pubkey: pubkey, privkey: damus.keypair.privkey) | ||
} | ||
|
||
func send_poll_choice(_ choice: Int) { | ||
guard let privkey = damus.keypair.privkey else { | ||
return | ||
} | ||
|
||
let choice_ev = make_poll_choice_event(pubkey: damus.pubkey, privkey: privkey, event: event, choice_index: choice) | ||
|
||
damus.pool.send(.event(choice_ev)) | ||
} | ||
} | ||
|
||
// blame the porn bots for this code | ||
|
@@ -411,6 +586,20 @@ func make_actionbar_model(ev: NostrEvent, damus: DamusState) -> ActionBarModel { | |
|
||
struct EventView_Previews: PreviewProvider { | ||
static var previews: some View { | ||
EventView( | ||
event: NostrEvent( | ||
content: "jb55.com/red-me.jb55 cool", | ||
pubkey: "pk", | ||
tags: [["random", "kind"], ["poll", "yes"], ["poll", "no"]], | ||
createdAt: Int64(Date().timeIntervalSince1970 - 100) | ||
), | ||
highlight: .none, | ||
has_action_bar: true, | ||
damus: test_damus_state(), | ||
show_friend_icon: true, | ||
size: .selected | ||
) | ||
|
||
VStack { | ||
EventView(damus: test_damus_state(), event: NostrEvent(content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool", pubkey: "pk"), show_friend_icon: true, size: .small) | ||
EventView(damus: test_damus_state(), event: NostrEvent(content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool", pubkey: "pk"), show_friend_icon: true, size: .normal) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this should be removed?