commit d1fce5054ded058958baf27cb0c25a7191cc01cf
parent 300cd87fc2242ead441338c8e2f77b38cc3ff9c3
Author: William Casarin <jb55@jb55.com>
Date: Tue, 18 Apr 2023 12:16:11 -0700
Implement quote reposting
Diffstat:
5 files changed, 204 insertions(+), 89 deletions(-)
diff --git a/damus/ContentView.swift b/damus/ContentView.swift
@@ -15,17 +15,15 @@ struct TimestampedProfile {
}
enum Sheets: Identifiable {
- case post
+ case post(PostAction)
case report(ReportTarget)
- case reply(NostrEvent)
case event(NostrEvent)
case filter
var id: String {
switch self {
case .report: return "report"
- case .post: return "post"
- case .reply(let ev): return "reply-" + ev.id
+ case .post(let action): return "post-" + (action.ev?.id ?? "")
case .event(let ev): return "event-" + ev.id
case .filter: return "filter"
}
@@ -115,7 +113,7 @@ struct ContentView: View {
if privkey != nil {
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) {
- self.active_sheet = .post
+ self.active_sheet = .post(.posting)
}
}
}
@@ -311,10 +309,8 @@ struct ContentView: View {
switch item {
case .report(let target):
MaybeReportView(target: target)
- case .post:
- PostView(replying_to: nil, damus_state: damus_state!)
- case .reply(let event):
- PostView(replying_to: event, damus_state: damus_state!)
+ case .post(let action):
+ PostView(action: action, damus_state: damus_state!)
case .event:
EventDetailView()
case .filter:
@@ -354,14 +350,16 @@ struct ContentView: View {
}
.onReceive(handle_notify(.boost)) { notif in
- if let ev = (notif.object as? NostrEvent) {
- current_boost = ev
- shouldShowBoostAlert = true
+ guard let ev = notif.object as? NostrEvent else {
+ return
}
+
+ current_boost = ev
+ shouldShowBoostAlert = true
}
.onReceive(handle_notify(.reply)) { notif in
let ev = notif.object as! NostrEvent
- self.active_sheet = .reply(ev)
+ self.active_sheet = .post(.replying_to(ev))
}
.onReceive(handle_notify(.like)) { like in
}
@@ -587,17 +585,35 @@ struct ContentView: View {
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
}
})
- .alert(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post."), isPresented: $shouldShowBoostAlert) {
- Button(NSLocalizedString("Cancel", comment: "Button to cancel out of reposting a post.")) {
- current_boost = nil
+ .confirmationDialog("Repost", isPresented: $shouldShowBoostAlert) {
+ Button(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post.")) {
+ guard let current_boost else {
+ return
+ }
+
+ guard let privkey = self.damus_state?.keypair.privkey else {
+ return
+ }
+
+ guard let damus_state else {
+ return
+ }
+
+ let boost = make_boost_event(pubkey: damus_state.keypair.pubkey, privkey: privkey, boosted: current_boost)
+ damus_state.postbox.send(boost)
}
- Button(NSLocalizedString("Repost", comment: "Button to confirm reposting a post.")) {
- if let current_boost {
- self.damus_state?.postbox.send(current_boost)
+
+ Button(NSLocalizedString("Quote", comment: "Title of alert for confirming to make a quoted post.")) {
+ guard let current_boost else {
+ return
}
+ self.active_sheet = .post(.quoting(current_boost))
+ }
+ }
+ .onChange(of: shouldShowBoostAlert) { v in
+ if v == false {
+ self.current_boost = nil
}
- } message: {
- Text("Are you sure you want to repost this?", comment: "Alert message to ask if user wants to repost a post.")
}
}
diff --git a/damus/Models/DraftsModel.swift b/damus/Models/DraftsModel.swift
@@ -7,8 +7,23 @@
import Foundation
+class DraftArtifacts {
+ var content: NSMutableAttributedString
+ var media: [UploadedMedia]
+
+ init() {
+ self.content = NSMutableAttributedString(string: "")
+ self.media = []
+ }
+
+ init(content: NSMutableAttributedString, media: [UploadedMedia]) {
+ self.content = content
+ self.media = media
+ }
+}
+
class Drafts: ObservableObject {
- @Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "")
- @Published var replies: [NostrEvent: NSMutableAttributedString] = [:]
- @Published var medias: [UploadedMedia] = []
+ @Published var post: DraftArtifacts? = nil
+ @Published var replies: [NostrEvent: DraftArtifacts] = [:]
+ @Published var quotes: [NostrEvent: DraftArtifacts] = [:]
}
diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift
@@ -746,6 +746,15 @@ func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
return ids
}
+func gather_quote_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
+ var ids: [ReferencedId] = []
+ ids.append(contentsOf: uniq(from.referenced_pubkeys.filter { $0.ref_id != our_pubkey }))
+ if from.pubkey != our_pubkey {
+ ids.append(ReferencedId(ref_id: from.pubkey, relay_id: nil, key: "p"))
+ }
+ return ids
+}
+
func event_from_json(dat: String) -> NostrEvent? {
return try? JSONDecoder().decode(NostrEvent.self, from: Data(dat.utf8))
}
diff --git a/damus/Views/ActionBar/EventActionBar.swift b/damus/Views/ActionBar/EventActionBar.swift
@@ -137,17 +137,7 @@ struct EventActionBar: View {
}
func send_boost() {
- guard let privkey = self.damus_state.keypair.privkey else {
- return
- }
-
- let boost = make_boost_event(pubkey: damus_state.keypair.pubkey, privkey: privkey, boosted: self.event)
-
- // As we will still have to wait for the confirmation from alert for repost, we do not turn it green yet.
- // However, turning green handled from EventActionBar spontaneously once reposted
- // self.bar.our_boost = boost
-
- notify(.boost, boost)
+ notify(.boost, self.event)
}
func send_like() {
diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift
@@ -15,6 +15,23 @@ enum NostrPostResult {
let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Text box prompt to ask user to type their post.")
+enum PostAction {
+ case replying_to(NostrEvent)
+ case quoting(NostrEvent)
+ case posting
+
+ var ev: NostrEvent? {
+ switch self {
+ case .replying_to(let ev):
+ return ev
+ case .quoting(let ev):
+ return ev
+ case .posting:
+ return nil
+ }
+ }
+}
+
struct PostView: View {
@State var post: NSMutableAttributedString = NSMutableAttributedString()
@FocusState var focus: Bool
@@ -31,7 +48,7 @@ struct PostView: View {
@StateObject var image_upload: ImageUploadModel = ImageUploadModel()
- let replying_to: NostrEvent?
+ let action: PostAction
let damus_state: DamusState
@Environment(\.presentationMode) var presentationMode
@@ -51,7 +68,8 @@ struct PostView: View {
func send_post() {
var kind: NostrKind = .text
- if replying_to?.known_kind == .chat {
+
+ if case .replying_to(let ev) = action, ev.known_kind == .chat {
kind = .chat
}
@@ -61,25 +79,21 @@ struct PostView: View {
}
}
-
-
var content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ")
content.append(" " + imagesString + " ")
+ if case .quoting(let ev) = action, let id = bech32_note_id(ev.id) {
+ content.append(" nostr:" + id)
+ }
+
let new_post = NostrPost(content: content, references: references, kind: kind)
NotificationCenter.default.post(name: .post, object: NostrPostResult.post(new_post))
-
- if let replying_to {
- damus_state.drafts.replies.removeValue(forKey: replying_to)
- } else {
- damus_state.drafts.post = NSMutableAttributedString(string: "")
- uploadedMedias = []
- damus_state.drafts.medias = []
- }
+
+ clear_draft()
dismiss()
}
@@ -131,17 +145,67 @@ struct PostView: View {
.clipShape(Capsule())
}
+ var isEmpty: Bool {
+ self.uploadedMedias.count == 0 &&
+ self.post.mutableString.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+ }
+
+ func clear_draft() {
+ switch action {
+ case .replying_to(let replying_to):
+ damus_state.drafts.replies.removeValue(forKey: replying_to)
+ case .quoting(let quoting):
+ damus_state.drafts.quotes.removeValue(forKey: quoting)
+ case .posting:
+ damus_state.drafts.post = nil
+ }
+
+ }
+
+ func load_draft() {
+ guard let draft = load_draft_for_post(drafts: self.damus_state.drafts, action: self.action) else {
+ self.post = NSMutableAttributedString("")
+ self.uploadedMedias = []
+ return
+ }
+
+ self.uploadedMedias = draft.media
+ self.post = draft.content
+ }
+
+ func post_changed(post: NSMutableAttributedString, media: [UploadedMedia]) {
+ switch action {
+ case .replying_to(let ev):
+ if let draft = damus_state.drafts.replies[ev] {
+ draft.content = post
+ draft.media = media
+ } else {
+ damus_state.drafts.replies[ev] = DraftArtifacts(content: post, media: media)
+ }
+ case .quoting(let ev):
+ if let draft = damus_state.drafts.quotes[ev] {
+ draft.content = post
+ draft.media = media
+ } else {
+ damus_state.drafts.quotes[ev] = DraftArtifacts(content: post, media: media)
+ }
+ case .posting:
+ if let draft = damus_state.drafts.post {
+ draft.content = post
+ draft.media = media
+ } else {
+ damus_state.drafts.post = DraftArtifacts(content: post, media: media)
+ }
+ }
+ }
+
var TextEntry: some View {
ZStack(alignment: .topLeading) {
TextViewWrapper(attributedText: $post)
.focused($focus)
.textInputAutocapitalization(.sentences)
- .onChange(of: post) { _ in
- if let replying_to {
- damus_state.drafts.replies[replying_to] = post
- } else {
- damus_state.drafts.post = post
- }
+ .onChange(of: post) { p in
+ post_changed(post: p, media: uploadedMedias)
}
if post.string.isEmpty {
@@ -207,6 +271,35 @@ struct PostView: View {
}
}
+ var has_artifacts: Bool {
+ if case .quoting = action {
+ return true
+ }
+ return !uploadedMedias.isEmpty
+ }
+
+ func Editor(deviceSize: GeometryProxy) -> some View {
+ VStack(alignment: .leading, spacing: 0) {
+ HStack(alignment: .top) {
+ ProfilePicView(pubkey: damus_state.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
+
+ TextEntry
+ }
+ .frame(height: has_artifacts ? deviceSize.size.height*0.4 : deviceSize.size.height)
+ .id("post")
+
+ PVImageCarouselView(media: $uploadedMedias, deviceWidth: deviceSize.size.width)
+ .onChange(of: uploadedMedias) { media in
+ post_changed(post: post, media: media)
+ }
+
+ if case .quoting(let ev) = action {
+ BuilderEventView(damus: damus_state, event: ev)
+ }
+ }
+ .padding(.horizontal)
+ }
+
var body: some View {
GeometryReader { (deviceSize: GeometryProxy) in
VStack(alignment: .leading, spacing: 0) {
@@ -217,27 +310,11 @@ struct PostView: View {
ScrollViewReader { scroller in
ScrollView {
- if let replying_to = replying_to {
+ if case .replying_to(let replying_to) = self.action {
ReplyView(replying_to: replying_to, damus: damus_state, originalReferences: $originalReferences, references: $references)
}
- VStack(alignment: .leading, spacing: 0) {
- HStack(alignment: .top) {
- ProfilePicView(pubkey: damus_state.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
-
- TextEntry
- }
- .frame(height: uploadedMedias.isEmpty ? deviceSize.size.height*0.78 : deviceSize.size.height*0.2)
- .id("post")
-
- PVImageCarouselView(media: $uploadedMedias, deviceWidth: deviceSize.size.width)
- .onChange(of: uploadedMedias) { _ in
- if replying_to == nil {
- damus_state.drafts.medias = uploadedMedias
- }
- }
-
- }
- .padding(.horizontal)
+
+ Editor(deviceSize: deviceSize)
}
.frame(maxHeight: searching == nil ? .infinity : 70)
.onAppear {
@@ -283,18 +360,17 @@ struct PostView: View {
}
}
.onAppear() {
- if let replying_to {
- references = gather_reply_ids(our_pubkey: damus_state.pubkey, from: replying_to)
+ load_draft()
+
+ switch action {
+ case .replying_to(let replying_to):
+ references = gather_reply_ids(our_pubkey: damus_state.pubkey, from: replying_to)
originalReferences = references
- if damus_state.drafts.replies[replying_to] == nil {
- damus_state.drafts.post = NSMutableAttributedString(string: "")
- }
- if let p = damus_state.drafts.replies[replying_to] {
- post = p
- }
- } else {
- post = damus_state.drafts.post
- uploadedMedias = damus_state.drafts.medias
+ case .quoting(let quoting):
+ references = gather_quote_ids(our_pubkey: damus_state.pubkey, from: quoting)
+ originalReferences = references
+ case .posting:
+ break
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
@@ -302,11 +378,8 @@ struct PostView: View {
}
}
.onDisappear {
- if let replying_to, let reply = damus_state.drafts.replies[replying_to], reply.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
- damus_state.drafts.replies.removeValue(forKey: replying_to)
- } else if replying_to == nil && damus_state.drafts.post.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
- damus_state.drafts.post = NSMutableAttributedString(string : "")
- damus_state.drafts.medias = uploadedMedias
+ if isEmpty {
+ clear_draft()
}
}
.alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: {
@@ -344,7 +417,7 @@ func get_searching_string(_ post: String) -> String? {
struct PostView_Previews: PreviewProvider {
static var previews: some View {
- PostView(replying_to: nil, damus_state: test_damus_state())
+ PostView(action: .posting, damus_state: test_damus_state())
}
}
@@ -429,3 +502,15 @@ struct UploadedMedia: Equatable {
let uploadedURL: URL
let representingImage: UIImage
}
+
+
+func load_draft_for_post(drafts: Drafts, action: PostAction) -> DraftArtifacts? {
+ switch action {
+ case .replying_to(let ev):
+ return drafts.replies[ev]
+ case .quoting(let ev):
+ return drafts.quotes[ev]
+ case .posting:
+ return drafts.post
+ }
+}