Implement quote reposting

This commit is contained in:
William Casarin
2023-04-18 12:16:11 -07:00
parent 300cd87fc2
commit d1fce5054d
5 changed files with 206 additions and 91 deletions

View File

@@ -15,17 +15,15 @@ struct TimestampedProfile {
} }
enum Sheets: Identifiable { enum Sheets: Identifiable {
case post case post(PostAction)
case report(ReportTarget) case report(ReportTarget)
case reply(NostrEvent)
case event(NostrEvent) case event(NostrEvent)
case filter case filter
var id: String { var id: String {
switch self { switch self {
case .report: return "report" case .report: return "report"
case .post: return "post" case .post(let action): return "post-" + (action.ev?.id ?? "")
case .reply(let ev): return "reply-" + ev.id
case .event(let ev): return "event-" + ev.id case .event(let ev): return "event-" + ev.id
case .filter: return "filter" case .filter: return "filter"
} }
@@ -115,7 +113,7 @@ struct ContentView: View {
if privkey != nil { if privkey != nil {
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) { 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 { switch item {
case .report(let target): case .report(let target):
MaybeReportView(target: target) MaybeReportView(target: target)
case .post: case .post(let action):
PostView(replying_to: nil, damus_state: damus_state!) PostView(action: action, damus_state: damus_state!)
case .reply(let event):
PostView(replying_to: event, damus_state: damus_state!)
case .event: case .event:
EventDetailView() EventDetailView()
case .filter: case .filter:
@@ -354,14 +350,16 @@ struct ContentView: View {
} }
.onReceive(handle_notify(.boost)) { notif in .onReceive(handle_notify(.boost)) { notif in
if let ev = (notif.object as? NostrEvent) { guard let ev = notif.object as? NostrEvent else {
current_boost = ev return
shouldShowBoostAlert = true
} }
current_boost = ev
shouldShowBoostAlert = true
} }
.onReceive(handle_notify(.reply)) { notif in .onReceive(handle_notify(.reply)) { notif in
let ev = notif.object as! NostrEvent let ev = notif.object as! NostrEvent
self.active_sheet = .reply(ev) self.active_sheet = .post(.replying_to(ev))
} }
.onReceive(handle_notify(.like)) { like in .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.") 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) { .confirmationDialog("Repost", isPresented: $shouldShowBoostAlert) {
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of reposting a post.")) { Button(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post.")) {
current_boost = nil guard let current_boost else {
} return
Button(NSLocalizedString("Repost", comment: "Button to confirm reposting a post.")) {
if let current_boost {
self.damus_state?.postbox.send(current_boost)
} }
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("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.")
} }
} }

View File

@@ -7,8 +7,23 @@
import Foundation import Foundation
class Drafts: ObservableObject { class DraftArtifacts {
@Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "") var content: NSMutableAttributedString
@Published var replies: [NostrEvent: NSMutableAttributedString] = [:] var media: [UploadedMedia]
@Published var medias: [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: DraftArtifacts? = nil
@Published var replies: [NostrEvent: DraftArtifacts] = [:]
@Published var quotes: [NostrEvent: DraftArtifacts] = [:]
} }

View File

@@ -746,6 +746,15 @@ func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
return ids 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? { func event_from_json(dat: String) -> NostrEvent? {
return try? JSONDecoder().decode(NostrEvent.self, from: Data(dat.utf8)) return try? JSONDecoder().decode(NostrEvent.self, from: Data(dat.utf8))
} }

View File

@@ -137,17 +137,7 @@ struct EventActionBar: View {
} }
func send_boost() { func send_boost() {
guard let privkey = self.damus_state.keypair.privkey else { notify(.boost, self.event)
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)
} }
func send_like() { func send_like() {

View File

@@ -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.") 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 { struct PostView: View {
@State var post: NSMutableAttributedString = NSMutableAttributedString() @State var post: NSMutableAttributedString = NSMutableAttributedString()
@FocusState var focus: Bool @FocusState var focus: Bool
@@ -31,7 +48,7 @@ struct PostView: View {
@StateObject var image_upload: ImageUploadModel = ImageUploadModel() @StateObject var image_upload: ImageUploadModel = ImageUploadModel()
let replying_to: NostrEvent? let action: PostAction
let damus_state: DamusState let damus_state: DamusState
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
@@ -51,7 +68,8 @@ struct PostView: View {
func send_post() { func send_post() {
var kind: NostrKind = .text var kind: NostrKind = .text
if replying_to?.known_kind == .chat {
if case .replying_to(let ev) = action, ev.known_kind == .chat {
kind = .chat kind = .chat
} }
@@ -61,25 +79,21 @@ struct PostView: View {
} }
} }
var content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) var content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ") let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ")
content.append(" " + imagesString + " ") 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) let new_post = NostrPost(content: content, references: references, kind: kind)
NotificationCenter.default.post(name: .post, object: NostrPostResult.post(new_post)) NotificationCenter.default.post(name: .post, object: NostrPostResult.post(new_post))
if let replying_to { clear_draft()
damus_state.drafts.replies.removeValue(forKey: replying_to)
} else {
damus_state.drafts.post = NSMutableAttributedString(string: "")
uploadedMedias = []
damus_state.drafts.medias = []
}
dismiss() dismiss()
} }
@@ -131,17 +145,67 @@ struct PostView: View {
.clipShape(Capsule()) .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 { var TextEntry: some View {
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
TextViewWrapper(attributedText: $post) TextViewWrapper(attributedText: $post)
.focused($focus) .focused($focus)
.textInputAutocapitalization(.sentences) .textInputAutocapitalization(.sentences)
.onChange(of: post) { _ in .onChange(of: post) { p in
if let replying_to { post_changed(post: p, media: uploadedMedias)
damus_state.drafts.replies[replying_to] = post
} else {
damus_state.drafts.post = post
}
} }
if post.string.isEmpty { 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 { var body: some View {
GeometryReader { (deviceSize: GeometryProxy) in GeometryReader { (deviceSize: GeometryProxy) in
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
@@ -217,27 +310,11 @@ struct PostView: View {
ScrollViewReader { scroller in ScrollViewReader { scroller in
ScrollView { 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) 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 Editor(deviceSize: deviceSize)
}
.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)
} }
.frame(maxHeight: searching == nil ? .infinity : 70) .frame(maxHeight: searching == nil ? .infinity : 70)
.onAppear { .onAppear {
@@ -283,18 +360,17 @@ struct PostView: View {
} }
} }
.onAppear() { .onAppear() {
if let replying_to { load_draft()
references = gather_reply_ids(our_pubkey: damus_state.pubkey, from: replying_to)
switch action {
case .replying_to(let replying_to):
references = gather_reply_ids(our_pubkey: damus_state.pubkey, from: replying_to)
originalReferences = references originalReferences = references
if damus_state.drafts.replies[replying_to] == nil { case .quoting(let quoting):
damus_state.drafts.post = NSMutableAttributedString(string: "") references = gather_quote_ids(our_pubkey: damus_state.pubkey, from: quoting)
} originalReferences = references
if let p = damus_state.drafts.replies[replying_to] { case .posting:
post = p break
}
} else {
post = damus_state.drafts.post
uploadedMedias = damus_state.drafts.medias
} }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
@@ -302,11 +378,8 @@ struct PostView: View {
} }
} }
.onDisappear { .onDisappear {
if let replying_to, let reply = damus_state.drafts.replies[replying_to], reply.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { if isEmpty {
damus_state.drafts.replies.removeValue(forKey: replying_to) clear_draft()
} 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
} }
} }
.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: { .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 { struct PostView_Previews: PreviewProvider {
static var previews: some View { 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 uploadedURL: URL
let representingImage: UIImage 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
}
}