Implement quote reposting
This commit is contained in:
@@ -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.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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] = [:]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user