From acfcffd03ab189bfd57c51c739e5734a428794a0 Mon Sep 17 00:00:00 2001 From: Suhail Saqan Date: Thu, 1 Jun 2023 14:49:56 -0500 Subject: [PATCH 01/18] add separate_images and separate_invoices --- damus/Nostr/NostrEvent.swift | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift index 18a6be608..d3d6f9241 100644 --- a/damus/Nostr/NostrEvent.swift +++ b/damus/Nostr/NostrEvent.swift @@ -955,6 +955,29 @@ func first_eref_mention(ev: NostrEvent, privkey: Privkey?) -> Mention? { return nil } +func separate_images(ev: NostrEvent, privkey: String?) -> [MediaUrl]? { + let urlBlocks: [URL] = ev.blocks(privkey).reduce(into: []) { urls, block in + guard case .url(let url) = block else { + return + } + if classify_url(url).is_img != nil { + urls.append(url) + } + } + let mediaUrls = urlBlocks.map { MediaUrl.image($0) } + return mediaUrls.isEmpty ? nil : mediaUrls +} + +func separate_invoices(ev: NostrEvent, privkey: String?) -> [Invoice]? { + let invoiceBlocks: [Invoice] = ev.blocks(privkey).reduce(into: []) { invoices, block in + guard case .invoice(let invoice) = block else { + return + } + invoices.append(invoice) + } + return invoiceBlocks.isEmpty ? nil : invoiceBlocks +} + /** Transforms a `NostrEvent` of known kind `NostrKind.like`to a human-readable emoji. If the known kind is not a `NostrKind.like`, it will return `nil`. From 5f51979f3a391eefc6cba4e26ca29b32c4497225 Mon Sep 17 00:00:00 2001 From: Suhail Saqan Date: Thu, 1 Jun 2023 15:07:17 -0500 Subject: [PATCH 02/18] add only_text setting to NoteContentView --- damus/Util/EventCache.swift | 10 +++++----- damus/Views/DirectMessagesView.swift | 4 ++-- damus/Views/Events/TextEvent.swift | 1 + damus/Views/NoteContentView.swift | 16 ++++++++++++---- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/damus/Util/EventCache.swift b/damus/Util/EventCache.swift index 57235f8c8..5db3b5116 100644 --- a/damus/Util/EventCache.swift +++ b/damus/Util/EventCache.swift @@ -411,7 +411,7 @@ func is_animated_image(url: URL) -> Bool { return ext == "gif" } -func preload_event(plan: PreloadPlan, state: DamusState) async { +func preload_event(plan: PreloadPlan, state: DamusState, only_text: Bool = false) async { var artifacts: NoteArtifacts? = plan.data.artifacts.artifacts let settings = state.settings let profiles = state.profiles @@ -420,7 +420,7 @@ func preload_event(plan: PreloadPlan, state: DamusState) async { print("Preloading event \(plan.event.content)") if artifacts == nil && plan.load_artifacts { - let arts = render_note_content(ev: plan.event, profiles: profiles, privkey: our_keypair.privkey) + let arts = render_note_content(ev: plan.event, profiles: profiles, privkey: our_keypair.privkey, only_text: only_text) artifacts = arts // we need these asap @@ -441,7 +441,7 @@ func preload_event(plan: PreloadPlan, state: DamusState) async { } if plan.load_preview, note_artifact_is_separated(kind: plan.event.known_kind) { - let arts = artifacts ?? render_note_content(ev: plan.event, profiles: profiles, privkey: our_keypair.privkey) + let arts = artifacts ?? render_note_content(ev: plan.event, profiles: profiles, privkey: our_keypair.privkey, only_text: only_text) // only separated artifacts have previews if case .separated(let sep) = arts { @@ -479,7 +479,7 @@ func preload_event(plan: PreloadPlan, state: DamusState) async { } -func preload_events(state: DamusState, events: [NostrEvent]) { +func preload_events(state: DamusState, events: [NostrEvent], only_text: Bool = false) { let event_cache = state.events let our_keypair = state.keypair let settings = state.settings @@ -494,7 +494,7 @@ func preload_events(state: DamusState, events: [NostrEvent]) { Task { for plan in plans { - await preload_event(plan: plan, state: state) + await preload_event(plan: plan, state: state, only_text: only_text) } } } diff --git a/damus/Views/DirectMessagesView.swift b/damus/Views/DirectMessagesView.swift index d6e4d0ec6..a4445f0d2 100644 --- a/damus/Views/DirectMessagesView.swift +++ b/damus/Views/DirectMessagesView.swift @@ -38,10 +38,10 @@ struct DirectMessagesView: View { var options: EventViewOptions { if self.damus_state.settings.translate_dms { - return [.truncate_content, .no_action_bar] + return [.truncate_content, .no_action_bar, .only_text] } - return [.truncate_content, .no_action_bar, .no_translate] + return [.truncate_content, .no_action_bar, .no_translate, .only_text] } func MaybeEvent(_ model: DirectMessageModel) -> some View { diff --git a/damus/Views/Events/TextEvent.swift b/damus/Views/Events/TextEvent.swift index d37a84b34..e9005653c 100644 --- a/damus/Views/Events/TextEvent.swift +++ b/damus/Views/Events/TextEvent.swift @@ -19,6 +19,7 @@ struct EventViewOptions: OptionSet { static let nested = EventViewOptions(rawValue: 1 << 7) static let top_zap = EventViewOptions(rawValue: 1 << 8) static let no_mentions = EventViewOptions(rawValue: 1 << 9) + static let only_text = EventViewOptions(rawValue: 1 << 10) static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested] } diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift index d8cc9766d..413b1c9bd 100644 --- a/damus/Views/NoteContentView.swift +++ b/damus/Views/NoteContentView.swift @@ -60,6 +60,10 @@ struct NoteContentView: View { return options.contains(.wide) } + var only_txt: Bool { + return options.contains(.only_text) + } + var preview: LinkViewRepresentable? { guard show_images, case .loaded(let preview) = preview_model.state, @@ -178,9 +182,9 @@ struct NoteContentView: View { if force_artifacts { plan.load_artifacts = true } - await preload_event(plan: plan, state: damus_state) + await preload_event(plan: plan, state: damus_state, only_text: only_txt) } else if force_artifacts { - let arts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey) + let arts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey, only_text: only_txt) self.artifacts_model.state = .loaded(arts) } } @@ -394,7 +398,7 @@ func note_artifact_is_separated(kind: NostrKind?) -> Bool { return kind != .longform } -func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: Privkey?) -> NoteArtifacts { +func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: Privkey?, only_text: Bool = false) -> NoteArtifacts { let blocks = ev.blocks(privkey) if ev.known_kind == .longform { @@ -439,7 +443,7 @@ func reduce_text_block(blocks: [Block], ind: Int, txt: String, one_note_ref: Boo return CompatibleText(stringLiteral: trimmed) } -func render_blocks(blocks bs: Blocks, profiles: Profiles) -> NoteArtifactsSeparated { +func render_blocks(blocks bs: Blocks, profiles: Profiles, only_text: Bool = false) -> NoteArtifactsSeparated { var invoices: [Invoice] = [] var urls: [UrlType] = [] let blocks = bs.blocks @@ -482,6 +486,10 @@ func render_blocks(blocks bs: Blocks, profiles: Profiles) -> NoteArtifactsSepara } } + if (only_text) { + return NoteArtifactsSeparated(content: txt, words: bs.words, urls: [], invoices: []) + } + return NoteArtifactsSeparated(content: txt, words: bs.words, urls: urls, invoices: invoices) } From 91cf5bf6c2071a6a1a3dcee0271a300a55e18dfb Mon Sep 17 00:00:00 2001 From: Suhail Saqan Date: Thu, 1 Jun 2023 15:29:04 -0500 Subject: [PATCH 03/18] add only_text to dm_options --- damus/Views/DMView.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/damus/Views/DMView.swift b/damus/Views/DMView.swift index 1290cd8b5..48f263987 100644 --- a/damus/Views/DMView.swift +++ b/damus/Views/DMView.swift @@ -26,11 +26,13 @@ struct DMView: View { } var dm_options: EventViewOptions { - if self.damus_state.settings.translate_dms { - return [] + var options: EventViewOptions = [.only_text] + + if !self.damus_state.settings.translate_dms { + options.insert(.no_translate) } - return [.no_translate] + return options } var DM: some View { From bd7b18941040a471ca27fd1ac8c1fbb6fdf8a970 Mon Sep 17 00:00:00 2001 From: Suhail Saqan Date: Thu, 1 Jun 2023 15:31:39 -0500 Subject: [PATCH 04/18] add timestamp for dms --- damus/Views/DMView.swift | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/damus/Views/DMView.swift b/damus/Views/DMView.swift index 48f263987..9f12372d3 100644 --- a/damus/Views/DMView.swift +++ b/damus/Views/DMView.swift @@ -35,6 +35,13 @@ struct DMView: View { return options } + func format_timestamp(timestamp: Int64) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "h:mm a" + let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) + return dateFormatter.string(from: date) + } + var DM: some View { HStack { if is_ours { @@ -65,6 +72,25 @@ struct DMView: View { } } } + + func TimeStamp() -> some View { + return Group { + HStack { + if is_ours { + Spacer(minLength: UIScreen.main.bounds.width * 0.1) + } + + Text(format_timestamp(timestamp: event.created_at)) + .font(.system(size: 11)) + .font(.footnote) + .foregroundColor(.gray) + + if !is_ours { + Spacer(minLength: UIScreen.main.bounds.width * 0.1) + } + } + } + } var body: some View { VStack { From 59b43739e13208bbd9b0ba059fc270f30c2149c7 Mon Sep 17 00:00:00 2001 From: Suhail Saqan Date: Thu, 1 Jun 2023 15:33:37 -0500 Subject: [PATCH 05/18] add filter_content func to classify non-text content --- damus/Views/DMView.swift | 59 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/damus/Views/DMView.swift b/damus/Views/DMView.swift index 9f12372d3..c8f2546e4 100644 --- a/damus/Views/DMView.swift +++ b/damus/Views/DMView.swift @@ -91,7 +91,64 @@ struct DMView: View { } } } - + + func filter_content(blocks: [Block], profiles: Profiles, privkey: String?) -> (Bool, CompatibleText?) { + let one_note_ref = blocks + .filter({ $0.is_note_mention }) + .count == 1 + + var ind: Int = -1 + var show_text: Bool = false + let txt: CompatibleText = blocks.reduce(CompatibleText()) { str, block in + ind = ind + 1 + + switch block { + case .mention(let m): + if m.type == .event && one_note_ref { + return str + } + if m.type == .pubkey { + show_text = true + } + return str + mention_str(m, profiles: profiles) + case .text(let txt): + var trimmed = txt + if let prev = blocks[safe: ind-1], case .url(let u) = prev, classify_url(u).is_media != nil { + trimmed = " " + trim_prefix(trimmed) + } + + if let next = blocks[safe: ind+1] { + if case .url(let u) = next, classify_url(u).is_media != nil { + trimmed = trim_suffix(trimmed) + } else if case .mention(let m) = next, m.type == .event, one_note_ref { + trimmed = trim_suffix(trimmed) + } + } + if (!trimmed.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) { + show_text = true + } + return str + CompatibleText(stringLiteral: trimmed) + case .relay(let relay): + show_text = true + return str + CompatibleText(stringLiteral: relay) + case .hashtag(let htag): + show_text = true + return str + hashtag_str(htag) + case .invoice: + return str + case .url(let url): + if !(classify_url(url).is_media != nil) { + show_text = true + return str + url_str(url) + } else { + return str + } + } + } + + return (show_text, txt) + } + var body: some View { VStack { Mention From 15b52f9d57ebaca99825d048c4708cc2e62756e6 Mon Sep 17 00:00:00 2001 From: Suhail Saqan Date: Thu, 1 Jun 2023 15:35:14 -0500 Subject: [PATCH 06/18] add ChatBubbleShape shape --- damus/Views/DMView.swift | 105 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/damus/Views/DMView.swift b/damus/Views/DMView.swift index c8f2546e4..ba3a33d4a 100644 --- a/damus/Views/DMView.swift +++ b/damus/Views/DMView.swift @@ -158,6 +158,111 @@ struct DMView: View { } } +struct ChatBubbleShape: Shape { + enum Direction { + case left + case right + case none + } + + let direction: Direction + + func path(in rect: CGRect) -> Path { + return (direction == .none) ? getBubblePath(in: rect) : ( (direction == .left) ? getLeftBubblePath(in: rect) : getRightBubblePath(in: rect) ) + } + + private func getBubblePath(in rect: CGRect) -> Path { + let width = rect.width + let height = rect.height + let cornerRadius: CGFloat = 17 + let path = Path { p in + p.move(to: CGPoint(x: cornerRadius, y: height)) + p.addLine(to: CGPoint(x: width - cornerRadius, y: height)) + p.addCurve(to: CGPoint(x: width, y: height - cornerRadius), + control1: CGPoint(x: width - cornerRadius/2, y: height), + control2: CGPoint(x: width, y: height - cornerRadius/2)) + p.addLine(to: CGPoint(x: width, y: cornerRadius)) + p.addCurve(to: CGPoint(x: width - cornerRadius, y: 0), + control1: CGPoint(x: width, y: cornerRadius/2), + control2: CGPoint(x: width - cornerRadius/2, y: 0)) + p.addLine(to: CGPoint(x: cornerRadius, y: 0)) + p.addCurve(to: CGPoint(x: 0, y: cornerRadius), + control1: CGPoint(x: cornerRadius/2, y: 0), + control2: CGPoint(x: 0, y: cornerRadius/2)) + p.addLine(to: CGPoint(x: 0, y: height - cornerRadius)) + p.addCurve(to: CGPoint(x: cornerRadius, y: height), + control1: CGPoint(x: 0, y: height - cornerRadius/2), + control2: CGPoint(x: cornerRadius/2, y: height)) + } + return path + } + + private func getLeftBubblePath(in rect: CGRect) -> Path { + let width = rect.width + let height = rect.height + let path = Path { p in + p.move(to: CGPoint(x: 25, y: height)) + p.addLine(to: CGPoint(x: width - 20, y: height)) + p.addCurve(to: CGPoint(x: width, y: height - 20), + control1: CGPoint(x: width - 8, y: height), + control2: CGPoint(x: width, y: height - 8)) + p.addLine(to: CGPoint(x: width, y: 20)) + p.addCurve(to: CGPoint(x: width - 20, y: 0), + control1: CGPoint(x: width, y: 8), + control2: CGPoint(x: width - 8, y: 0)) + p.addLine(to: CGPoint(x: 21, y: 0)) + p.addCurve(to: CGPoint(x: 4, y: 20), + control1: CGPoint(x: 12, y: 0), + control2: CGPoint(x: 4, y: 8)) + p.addLine(to: CGPoint(x: 4, y: height - 11)) + p.addCurve(to: CGPoint(x: 0, y: height), + control1: CGPoint(x: 4, y: height - 1), + control2: CGPoint(x: 0, y: height)) + p.addLine(to: CGPoint(x: -0.05, y: height - 0.01)) + p.addCurve(to: CGPoint(x: 11.0, y: height - 4.0), + control1: CGPoint(x: 4.0, y: height + 0.5), + control2: CGPoint(x: 8, y: height - 1)) + p.addCurve(to: CGPoint(x: 25, y: height), + control1: CGPoint(x: 16, y: height), + control2: CGPoint(x: 20, y: height)) + + } + return path + } + + private func getRightBubblePath(in rect: CGRect) -> Path { + let width = rect.width + let height = rect.height + let path = Path { p in + p.move(to: CGPoint(x: 25, y: height)) + p.addLine(to: CGPoint(x: 20, y: height)) + p.addCurve(to: CGPoint(x: 0, y: height - 20), + control1: CGPoint(x: 8, y: height), + control2: CGPoint(x: 0, y: height - 8)) + p.addLine(to: CGPoint(x: 0, y: 20)) + p.addCurve(to: CGPoint(x: 20, y: 0), + control1: CGPoint(x: 0, y: 8), + control2: CGPoint(x: 8, y: 0)) + p.addLine(to: CGPoint(x: width - 21, y: 0)) + p.addCurve(to: CGPoint(x: width - 4, y: 20), + control1: CGPoint(x: width - 12, y: 0), + control2: CGPoint(x: width - 4, y: 8)) + p.addLine(to: CGPoint(x: width - 4, y: height - 11)) + p.addCurve(to: CGPoint(x: width, y: height), + control1: CGPoint(x: width - 4, y: height - 1), + control2: CGPoint(x: width, y: height)) + p.addLine(to: CGPoint(x: width + 0.05, y: height - 0.01)) + p.addCurve(to: CGPoint(x: width - 11, y: height - 4), + control1: CGPoint(x: width - 4, y: height + 0.5), + control2: CGPoint(x: width - 8, y: height - 1)) + p.addCurve(to: CGPoint(x: width - 25, y: height), + control1: CGPoint(x: width - 16, y: height), + control2: CGPoint(x: width - 20, y: height)) + } + return path + } +} + struct DMView_Previews: PreviewProvider { static var previews: some View { let ev = NostrEvent(content: "Hey there *buddy*, want to grab some drinks later? 🍻", keypair: test_keypair, kind: 1, tags: [])! From 16098798996ed8ec06883672467198ce6abfb2a9 Mon Sep 17 00:00:00 2001 From: Suhail Saqan Date: Thu, 1 Jun 2023 15:35:57 -0500 Subject: [PATCH 07/18] add Invoice view for dms --- damus/Views/DMView.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/damus/Views/DMView.swift b/damus/Views/DMView.swift index ba3a33d4a..b631d6947 100644 --- a/damus/Views/DMView.swift +++ b/damus/Views/DMView.swift @@ -73,6 +73,24 @@ struct DMView: View { } } + func Invoice(invoices: [Invoice]) -> some View { + return Group { + HStack { + if is_ours { + Spacer(minLength: UIScreen.main.bounds.width * 0.1) + } + + InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: invoices, settings: damus_state.settings) + .clipShape(ChatBubbleShape(direction: isLastInGroup ? (is_ours ? ChatBubbleShape.Direction.right: ChatBubbleShape.Direction.left): ChatBubbleShape.Direction.none)) + .contextMenu{MenuItems(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads)} + + if !is_ours { + Spacer(minLength: UIScreen.main.bounds.width * 0.1) + } + } + } + } + func TimeStamp() -> some View { return Group { HStack { From f042f1f06d23ad1232b7e2c48540f12bd2fc6b47 Mon Sep 17 00:00:00 2001 From: Suhail Saqan Date: Thu, 1 Jun 2023 15:36:48 -0500 Subject: [PATCH 08/18] add Image view for dms --- damus/Views/DMView.swift | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/damus/Views/DMView.swift b/damus/Views/DMView.swift index b631d6947..49a055658 100644 --- a/damus/Views/DMView.swift +++ b/damus/Views/DMView.swift @@ -73,6 +73,35 @@ struct DMView: View { } } + func Image(urls: [MediaUrl]) -> some View { + return Group { + HStack { + if is_ours { + Spacer(minLength: UIScreen.main.bounds.width * 0.1) + } + + let should_show_img = should_show_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey) + if should_show_img { + ImageCarousel(state: damus_state, evid: event.id, urls: urls) + .clipShape(ChatBubbleShape(direction: isLastInGroup ? (is_ours ? ChatBubbleShape.Direction.right: ChatBubbleShape.Direction.left): ChatBubbleShape.Direction.none)) + .contextMenu{MenuItems(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads)} + } else if !should_show_img { + ZStack { + ImageCarousel(state: damus_state, evid: event.id, urls: urls) + Blur() + .disabled(true) + } + .clipShape(ChatBubbleShape(direction: isLastInGroup ? (is_ours ? ChatBubbleShape.Direction.right: ChatBubbleShape.Direction.left): ChatBubbleShape.Direction.none)) + .contextMenu{MenuItems(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads)} + } + + if !is_ours { + Spacer(minLength: UIScreen.main.bounds.width * 0.1) + } + } + } + } + func Invoice(invoices: [Invoice]) -> some View { return Group { HStack { From 2960b7ffb2535a9559afd77f39d1b8027cfff0df Mon Sep 17 00:00:00 2001 From: Suhail Saqan Date: Thu, 1 Jun 2023 15:41:16 -0500 Subject: [PATCH 09/18] make different content of an event appear in split and change design of chat bubble --- damus/Views/DMView.swift | 90 +++++++++++++++++++++++++++++----------- 1 file changed, 65 insertions(+), 25 deletions(-) diff --git a/damus/Views/DMView.swift b/damus/Views/DMView.swift index 49a055658..c846caef7 100644 --- a/damus/Views/DMView.swift +++ b/damus/Views/DMView.swift @@ -10,6 +10,7 @@ import SwiftUI struct DMView: View { let event: NostrEvent let damus_state: DamusState + let isLastInGroup: Bool var is_ours: Bool { event.pubkey == damus_state.pubkey @@ -41,34 +42,60 @@ struct DMView: View { let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) return dateFormatter.string(from: date) } + + let LINEAR_GRADIENT_DM = LinearGradient(gradient: Gradient(colors: [ + DamusColors.purple, + .pink + ]), startPoint: .topTrailing, endPoint: .bottomTrailing) + + func DM(content: CompatibleText, isLastInDM: Bool) -> some View { + return Group { + HStack { + if is_ours { + Spacer(minLength: UIScreen.main.bounds.width * 0.1) + } - var DM: some View { - HStack { - if is_ours { - Spacer(minLength: UIScreen.main.bounds.width * 0.2) - } - - let should_show_img = should_show_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey) + let should_show_img = should_show_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey) - VStack(alignment: .trailing) { NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: .normal, options: dm_options) - .fixedSize(horizontal: false, vertical: true) - .padding([.top, .leading, .trailing], 10) - .padding([.bottom], 25) - .background(VisualEffectView(effect: UIBlurEffect(style: .prominent)) - .background(is_ours ? Color.accentColor.opacity(0.9) : Color.secondary.opacity(0.15)) + .frame(minWidth: 30) + .contentShape(Rectangle()) + .padding(.horizontal, 12.5) + .padding(.vertical, 9) + .foregroundColor(.primary) + .background( + Group { + if is_ours { + LINEAR_GRADIENT_DM.opacity(0.75) + } else { + Color.secondary.opacity(0.15) + } + } ) - .cornerRadius(8.0) - .tint(is_ours ? Color.white : Color.accentColor) + .background(VisualEffectView(effect: UIBlurEffect(style: .prominent))) + .clipShape(ChatBubbleShape(direction: (isLastInGroup && isLastInDM) ? (is_ours ? ChatBubbleShape.Direction.right: ChatBubbleShape.Direction.left): ChatBubbleShape.Direction.none)) + .contextMenu{MenuItems(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads)} - Text(format_relative_time(event.created_at)) - .font(.footnote) - .foregroundColor(.gray) - .opacity(0.8) + if !is_ours { + Spacer(minLength: UIScreen.main.bounds.width * 0.1) + } } + } + } - if !is_ours { - Spacer(minLength: UIScreen.main.bounds.width * 0.2) + func Mention(mention: Mention) -> some View { + Group { + HStack { + if is_ours { + Spacer(minLength: UIScreen.main.bounds.width * 0.1) + } + + BuilderEventView(damus: damus_state, event_id: mention.ref) + .contextMenu{MenuItems(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads)} + + if !is_ours { + Spacer(minLength: UIScreen.main.bounds.width * 0.1) + } } } } @@ -198,10 +225,23 @@ struct DMView: View { var body: some View { VStack { - Mention - DM + let (show_text, filtered_content): (Bool, CompatibleText?) = filter_content(blocks: event.blocks(damus_state.keypair.privkey), profiles: damus_state.profiles, privkey: damus_state.keypair.privkey) + if show_text, let filtered_content = filtered_content { + DM(content: filtered_content).padding(.bottom, isLastInGroup ? 0 : -6) + } + if let mention = first_eref_mention(ev: event, privkey: damus_state.keypair.privkey) { + Mention(mention: mention).padding(.bottom, isLastInGroup ? 0 : -6) + } + if let url = separate_images(ev: event, privkey: damus_state.keypair.privkey) { + Image(urls: url).padding(.bottom, isLastInGroup ? 0 : -6) + } + if let invoices = separate_invoices(ev: event, privkey: damus_state.keypair.privkey) { + Invoice(invoices: invoices).padding(.bottom, isLastInGroup ? 0 : -6) + } + if (isLastInGroup) { + TimeStamp().padding(.top, -5) + } } - } } @@ -313,6 +353,6 @@ struct ChatBubbleShape: Shape { struct DMView_Previews: PreviewProvider { static var previews: some View { let ev = NostrEvent(content: "Hey there *buddy*, want to grab some drinks later? 🍻", keypair: test_keypair, kind: 1, tags: [])! - DMView(event: ev, damus_state: test_damus_state()) + DMView(event: ev, damus_state: test_damus_state(), isLastInGroup: false) } } From 4e1e6646877730042c0216677e122d5b93bb5cc1 Mon Sep 17 00:00:00 2001 From: Suhail Saqan Date: Thu, 1 Jun 2023 15:56:42 -0500 Subject: [PATCH 10/18] add groupEventsByDateAndPubkey function --- damus/Views/DMChatView.swift | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift index a905c68a3..3d3b66778 100644 --- a/damus/Views/DMChatView.swift +++ b/damus/Views/DMChatView.swift @@ -17,6 +17,38 @@ struct DMChatView: View, KeyboardReadable { dms.pubkey } + // Group events by date and then by pubkey and sort them by creation time + func groupEventsByDateAndPubkey(events: [NostrEvent]) -> [Date: [String: [NostrEvent]]] { + let groups = Dictionary(grouping: events) { event in + Calendar.current.startOfDay(for: Date(timeIntervalSince1970: TimeInterval(event.created_at))) + }.mapValues { events in + var lastEventDate: Date? + var groupCounter = 0 + + var eventGroups = [String: [NostrEvent]]() + + for event in events { + let currentEventDate = Date(timeIntervalSince1970: TimeInterval(event.created_at)) + defer { lastEventDate = currentEventDate } + + let pubkey = event.pubkey + if let lastDate = lastEventDate, + currentEventDate.timeIntervalSince(lastDate) <= 120, + let lastEvent = eventGroups["\(groupCounter)-\(pubkey)"]?.last, + lastEvent.pubkey == pubkey { + eventGroups["\(groupCounter)-\(pubkey)"]?.append(event) + } else { + groupCounter += 1 + eventGroups["\(groupCounter)-\(pubkey)"] = [event] + } + } + + return eventGroups.mapValues { $0.sorted(by: { $0.created_at < $1.created_at }) } + } as [Date: [String: [NostrEvent]]] + + return groups + } + var Messages: some View { ScrollViewReader { scroller in ScrollView { From 13d95d875c07981ef57f2166e5c6f3d3d7b7688c Mon Sep 17 00:00:00 2001 From: Suhail Saqan Date: Thu, 1 Jun 2023 16:01:23 -0500 Subject: [PATCH 11/18] improve InputField and Footer --- damus/Views/DMChatView.swift | 91 ++++++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 14 deletions(-) diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift index 3d3b66778..6d965a55a 100644 --- a/damus/Views/DMChatView.swift +++ b/damus/Views/DMChatView.swift @@ -12,11 +12,12 @@ struct DMChatView: View, KeyboardReadable { let damus_state: DamusState @ObservedObject var dms: DirectMessageModel @State var showPrivateKeyWarning: Bool = false + @State private var textHeight: CGFloat = 0 var pubkey: Pubkey { dms.pubkey } - + // Group events by date and then by pubkey and sort them by creation time func groupEventsByDateAndPubkey(events: [NostrEvent]) -> [Date: [String: [NostrEvent]]] { let groups = Dictionary(grouping: events) { event in @@ -104,20 +105,82 @@ struct DMChatView: View, KeyboardReadable { } var InputField: some View { - TextEditor(text: $dms.draft) - .textEditorBackground { - InputBackground() + ZStack(alignment: .bottomTrailing) { + VStack(alignment: .leading) { + ZStack(alignment: .topLeading) { + HStack { + TextEditor(text: $dms.draft) + .font(.body) + .frame(minHeight: 15, maxHeight: 150) + .foregroundColor(Color.primary) + .padding(.horizontal, 10) + .fixedSize(horizontal: false, vertical: true) + .focused($focusedField, equals: .message) + .overlay( + GeometryReader { geo in + Color.clear + .preference(key: ViewHeightKey.self, value: geo.size.height) + } + ) + .onPreferenceChange(ViewHeightKey.self) { + textHeight = $0 + } + .onChange(of: dms.draft) { _ in + DispatchQueue.main.async { + textHeight = getTextHeight() + } + } + + if !dms.draft.isEmpty { + Button( + role: .none, + action: { + showPrivateKeyWarning = contentContainsPrivateKey(dms.draft) + + if !showPrivateKeyWarning { + send_message() + } + } + ) { + Label("", image: "send") + .font(.title) + .foregroundStyle(LINEAR_GRADIENT) + } + } + } + + RoundedRectangle(cornerRadius: 20) + .stroke(style: .init(lineWidth: 0.75)) + .foregroundColor(.secondary.opacity(0.35)) + + Text(dms.draft == "" ? placeholder : "") + .font(.body) + .padding(.leading, 15) + .foregroundColor(.gray) + .opacity(dms.draft == "" ? 0.35 : 0) + .frame(minHeight: 15, maxHeight: 150, alignment: .center) + .onTapGesture(perform: { + focusedField = .message + }) + } } - .cornerRadius(8) - .background( - RoundedRectangle(cornerRadius: 8) - .stroke(style: .init(lineWidth: 2)) - .foregroundColor(.secondary.opacity(0.2)) - ) - .padding(16) - .foregroundColor(Color.primary) - .frame(minHeight: 70, maxHeight: 150, alignment: .bottom) - .fixedSize(horizontal: false, vertical: true) + } + .frame(height: textHeight) + } + + private func getTextHeight() -> CGFloat { + let textHeight = dms.draft.isEmpty ? 15 : dms.draft.getHeight(width: UIScreen.main.bounds.width - 32, font: .systemFont(ofSize: 16)) + let height = textHeight < 150 ? textHeight : 150 + return height + 16 + } + + var Footer: some View { + VStack { + Divider() + InputField + .padding(10) + .padding(.top, -5) + } } @Environment(\.colorScheme) var colorScheme From 9b490e8b0199c7e097265998c41285a3da9464f7 Mon Sep 17 00:00:00 2001 From: Suhail Saqan Date: Thu, 1 Jun 2023 16:02:09 -0500 Subject: [PATCH 12/18] remove unused code --- damus/Views/DMChatView.swift | 39 ------------------------------------ 1 file changed, 39 deletions(-) diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift index 6d965a55a..dc8e4f69a 100644 --- a/damus/Views/DMChatView.swift +++ b/damus/Views/DMChatView.swift @@ -183,45 +183,6 @@ struct DMChatView: View, KeyboardReadable { } } - @Environment(\.colorScheme) var colorScheme - - func InputBackground() -> Color { - if colorScheme == .light { - return Color.init(.sRGB, red: 0.9, green: 0.9, blue: 0.9, opacity: 1.0) - } else { - return Color.init(.sRGB, red: 0.1, green: 0.1, blue: 0.1, opacity: 1.0) - } - } - - var Footer: some View { - - HStack(spacing: 0) { - InputField - - if !dms.draft.isEmpty { - Button( - role: .none, - action: { - showPrivateKeyWarning = contentContainsPrivateKey(dms.draft) - - if !showPrivateKeyWarning { - send_message() - } - } - ) { - Label("", image: "send") - .font(.title) - } - } - } - - /* - Text(dms.draft).opacity(0).padding(.all, 8) - .fixedSize(horizontal: false, vertical: true) - .frame(minHeight: 70, maxHeight: 150, alignment: .bottom) - */ - } - func send_message() { let tags = [["p", pubkey.hex()]] let post_blocks = parse_post_blocks(content: dms.draft) From 68d91556a05580922ea7fe2e1395e38a89a08f08 Mon Sep 17 00:00:00 2001 From: Suhail Saqan Date: Thu, 1 Jun 2023 16:04:15 -0500 Subject: [PATCH 13/18] add DM Header and reorganize DMChatView body --- damus/Views/DMChatView.swift | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift index dc8e4f69a..db4fc6f87 100644 --- a/damus/Views/DMChatView.swift +++ b/damus/Views/DMChatView.swift @@ -104,6 +104,19 @@ struct DMChatView: View, KeyboardReadable { .buttonStyle(PlainButtonStyle()) } + var Header: some View { + let profile = damus_state.profiles.lookup(id: pubkey) + let profile_page = ProfileView(damus_state: damus_state, pubkey: pubkey) + return NavigationLink(destination: profile_page) { + HStack { + ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) + + ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_nip5_domain: false) + } + } + .buttonStyle(PlainButtonStyle()) + } + var InputField: some View { ZStack(alignment: .bottomTrailing) { VStack(alignment: .leading) { @@ -203,18 +216,19 @@ struct DMChatView: View, KeyboardReadable { } var body: some View { - ZStack { - Messages - - Text("Send a message to start the conversation...", comment: "Text prompt for user to send a message to the other user.") - .lineLimit(nil) - .multilineTextAlignment(.center) - .padding(.horizontal, 40) - .opacity(((dms.events.count == 0) ? 1.0 : 0.0)) - .foregroundColor(.gray) + VStack(spacing: 0) { + VStack(spacing: 0){ + Messages + .dismissKeyboardOnTap() + } + Footer } .navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for DMs view, where DM is the English abbreviation for Direct Message.")) - .toolbar { Header } + .toolbar { + ToolbarItem(placement: .principal) { + Header + } + } .onDisappear { if dms.draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { dms.draft = "" From 9fa9c641fd0520913716e7821ad71530165442a6 Mon Sep 17 00:00:00 2001 From: Suhail Saqan Date: Thu, 1 Jun 2023 16:09:25 -0500 Subject: [PATCH 14/18] make Messages appear in scrollview based on grouping --- damus/Views/DMChatView.swift | 58 ++++++++++++++++++++++++------------ damus/Views/DMView.swift | 10 +++---- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift index db4fc6f87..5c942a8d1 100644 --- a/damus/Views/DMChatView.swift +++ b/damus/Views/DMChatView.swift @@ -13,6 +13,13 @@ struct DMChatView: View, KeyboardReadable { @ObservedObject var dms: DirectMessageModel @State var showPrivateKeyWarning: Bool = false @State private var textHeight: CGFloat = 0 + + enum FocusedField { + case message + } + @FocusState private var focusedField: FocusedField? + + let placeholder = "Send a message" var pubkey: Pubkey { dms.pubkey @@ -53,32 +60,45 @@ struct DMChatView: View, KeyboardReadable { var Messages: some View { ScrollViewReader { scroller in ScrollView { - LazyVStack(alignment: .leading) { - ForEach(Array(zip(dms.events, dms.events.indices)), id: \.0.id) { (ev, ind) in - DMView(event: dms.events[ind], damus_state: damus_state) - .contextMenu{MenuItems(event: ev, keypair: damus_state.keypair, target_pubkey: ev.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads, settings: damus_state.settings)} - } - EndBlock(height: 1) + if (dms.events.isEmpty) { + Text("Send a message to start the conversation...", comment: "Text prompt for user to send a message to the other user.") + .lineLimit(nil) + .multilineTextAlignment(.center) + .padding(.horizontal, 40) + .padding(.vertical) + .foregroundColor(.gray) + } else { + VStack(alignment: .leading) { + let groups = groupEventsByDateAndPubkey(events: dms.events) + + ForEach(Array(groups.keys).sorted(), id: \.self) { date in + VStack(alignment: .leading) { + Text(date, style: .date) + .font(.footnote) + .fontWeight(.bold) + .foregroundColor(.gray) + .padding(.horizontal) + .frame(maxWidth: .infinity) + + ForEach(Array(groups[date]!.keys).sorted(), id: \.self) { key in + let events = groups[date]![key]! + ForEach(events) { event in + DMView(event: event, damus_state: damus_state, isLastInGroup: event == events.last) + } + } + } + } + EndBlock(height: 5) + }.padding(.horizontal) } - .padding(.horizontal) - } - .dismissKeyboardOnTap() .onAppear { scroll_to_end(scroller) }.onChange(of: dms.events.count) { _ in scroll_to_end(scroller, animated: true) + }.onChange(of: textHeight) { _ in + scroll_to_end(scroller, animated: true) } - - Footer - .onReceive(keyboardPublisher) { visible in - guard visible else { - return - } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - scroll_to_end(scroller, animated: true) - } - } } } diff --git a/damus/Views/DMView.swift b/damus/Views/DMView.swift index c846caef7..20bdef3af 100644 --- a/damus/Views/DMView.swift +++ b/damus/Views/DMView.swift @@ -74,7 +74,7 @@ struct DMView: View { ) .background(VisualEffectView(effect: UIBlurEffect(style: .prominent))) .clipShape(ChatBubbleShape(direction: (isLastInGroup && isLastInDM) ? (is_ours ? ChatBubbleShape.Direction.right: ChatBubbleShape.Direction.left): ChatBubbleShape.Direction.none)) - .contextMenu{MenuItems(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads)} + .contextMenu{MenuItems(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads, settings: damus_state.settings)} if !is_ours { Spacer(minLength: UIScreen.main.bounds.width * 0.1) @@ -91,7 +91,7 @@ struct DMView: View { } BuilderEventView(damus: damus_state, event_id: mention.ref) - .contextMenu{MenuItems(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads)} + .contextMenu{MenuItems(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads, settings: damus_state.settings)} if !is_ours { Spacer(minLength: UIScreen.main.bounds.width * 0.1) @@ -111,7 +111,7 @@ struct DMView: View { if should_show_img { ImageCarousel(state: damus_state, evid: event.id, urls: urls) .clipShape(ChatBubbleShape(direction: isLastInGroup ? (is_ours ? ChatBubbleShape.Direction.right: ChatBubbleShape.Direction.left): ChatBubbleShape.Direction.none)) - .contextMenu{MenuItems(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads)} + .contextMenu{MenuItems(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads, settings: damus_state.settings)} } else if !should_show_img { ZStack { ImageCarousel(state: damus_state, evid: event.id, urls: urls) @@ -119,7 +119,7 @@ struct DMView: View { .disabled(true) } .clipShape(ChatBubbleShape(direction: isLastInGroup ? (is_ours ? ChatBubbleShape.Direction.right: ChatBubbleShape.Direction.left): ChatBubbleShape.Direction.none)) - .contextMenu{MenuItems(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads)} + .contextMenu{MenuItems(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads, settings: damus_state.settings)} } if !is_ours { @@ -138,7 +138,7 @@ struct DMView: View { InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: invoices, settings: damus_state.settings) .clipShape(ChatBubbleShape(direction: isLastInGroup ? (is_ours ? ChatBubbleShape.Direction.right: ChatBubbleShape.Direction.left): ChatBubbleShape.Direction.none)) - .contextMenu{MenuItems(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads)} + .contextMenu{MenuItems(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads, settings: damus_state.settings)} if !is_ours { Spacer(minLength: UIScreen.main.bounds.width * 0.1) From bea4cbc38b14061bf20d3c1239ce4d7da7e38d25 Mon Sep 17 00:00:00 2001 From: Suhail Saqan Date: Thu, 1 Jun 2023 16:10:40 -0500 Subject: [PATCH 15/18] add functions to set height of input field --- damus/Views/DMChatView.swift | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift index 5c942a8d1..0d6d13d1b 100644 --- a/damus/Views/DMChatView.swift +++ b/damus/Views/DMChatView.swift @@ -309,7 +309,22 @@ func create_encrypted_event(_ message: String, to_pk: Pubkey, tags: [[String]], return NostrEvent(content: enc_content, keypair: keypair.to_keypair(), kind: kind, tags: tags, createdAt: created_at) } -func create_dm(_ message: String, to_pk: Pubkey, tags: [[String]], keypair: Keypair, created_at: UInt32? = nil) -> NostrEvent? +struct ViewHeightKey: PreferenceKey { + static var defaultValue: CGFloat = 0 + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = nextValue() + } +} + +extension String { + func getHeight(width: CGFloat, font: UIFont) -> CGFloat { + let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude) + let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font], context: nil) + return ceil(boundingBox.height) + } +} + +func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keypair, created_at: UInt32? = nil) -> NostrEvent? { let created = created_at ?? UInt32(Date().timeIntervalSince1970) From 2858d93dfe3c11bf539feebde08c9faf099a0fb2 Mon Sep 17 00:00:00 2001 From: Suhail Saqan Date: Thu, 1 Jun 2023 16:11:20 -0500 Subject: [PATCH 16/18] remove unused code --- damus/Views/DMChatView.swift | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift index 0d6d13d1b..f38d29485 100644 --- a/damus/Views/DMChatView.swift +++ b/damus/Views/DMChatView.swift @@ -115,19 +115,6 @@ struct DMChatView: View, KeyboardReadable { var Header: some View { let profile = damus_state.profiles.lookup(id: pubkey) return NavigationLink(value: Route.ProfileByKey(pubkey: pubkey)) { - HStack { - ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) - - ProfileName(pubkey: pubkey, profile: profile, damus: damus_state) - } - } - .buttonStyle(PlainButtonStyle()) - } - - var Header: some View { - let profile = damus_state.profiles.lookup(id: pubkey) - let profile_page = ProfileView(damus_state: damus_state, pubkey: pubkey) - return NavigationLink(destination: profile_page) { HStack { ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) From 4c8b58c7e3b6d580620c97c85881476201f5156b Mon Sep 17 00:00:00 2001 From: Suhail Saqan Date: Wed, 7 Jun 2023 15:23:55 -0500 Subject: [PATCH 17/18] fix spacing and shape based on order of DM content --- damus/Views/DMView.swift | 53 +++++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/damus/Views/DMView.swift b/damus/Views/DMView.swift index 20bdef3af..01032980b 100644 --- a/damus/Views/DMView.swift +++ b/damus/Views/DMView.swift @@ -100,7 +100,7 @@ struct DMView: View { } } - func Image(urls: [MediaUrl]) -> some View { + func Image(urls: [MediaUrl], isLastInDM: Bool) -> some View { return Group { HStack { if is_ours { @@ -110,7 +110,7 @@ struct DMView: View { let should_show_img = should_show_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey) if should_show_img { ImageCarousel(state: damus_state, evid: event.id, urls: urls) - .clipShape(ChatBubbleShape(direction: isLastInGroup ? (is_ours ? ChatBubbleShape.Direction.right: ChatBubbleShape.Direction.left): ChatBubbleShape.Direction.none)) + .clipShape(ChatBubbleShape(direction: (isLastInGroup && isLastInDM) ? (is_ours ? ChatBubbleShape.Direction.right: ChatBubbleShape.Direction.left): ChatBubbleShape.Direction.none)) .contextMenu{MenuItems(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads, settings: damus_state.settings)} } else if !should_show_img { ZStack { @@ -118,7 +118,7 @@ struct DMView: View { Blur() .disabled(true) } - .clipShape(ChatBubbleShape(direction: isLastInGroup ? (is_ours ? ChatBubbleShape.Direction.right: ChatBubbleShape.Direction.left): ChatBubbleShape.Direction.none)) + .clipShape(ChatBubbleShape(direction: (isLastInGroup && isLastInDM) ? (is_ours ? ChatBubbleShape.Direction.right: ChatBubbleShape.Direction.left): ChatBubbleShape.Direction.none)) .contextMenu{MenuItems(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads, settings: damus_state.settings)} } @@ -129,7 +129,7 @@ struct DMView: View { } } - func Invoice(invoices: [Invoice]) -> some View { + func Invoice(invoices: [Invoice], isLastInDM: Bool) -> some View { return Group { HStack { if is_ours { @@ -137,7 +137,7 @@ struct DMView: View { } InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: invoices, settings: damus_state.settings) - .clipShape(ChatBubbleShape(direction: isLastInGroup ? (is_ours ? ChatBubbleShape.Direction.right: ChatBubbleShape.Direction.left): ChatBubbleShape.Direction.none)) + .clipShape(ChatBubbleShape(direction: (isLastInGroup && isLastInDM) ? (is_ours ? ChatBubbleShape.Direction.right: ChatBubbleShape.Direction.left): ChatBubbleShape.Direction.none)) .contextMenu{MenuItems(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads, settings: damus_state.settings)} if !is_ours { @@ -222,21 +222,43 @@ struct DMView: View { return (show_text, txt) } + + func getLastInDM(text: CompatibleText?, mention: Mention?, url: [MediaUrl]?, invoices: [Invoice]?) -> DMContentType? { + var last: DMContentType? + if let text { + last = .text + } + if let mention { + last = .mention + } + if let url { + last = .url + } + if let invoices { + last = .invoice + } + return last + } var body: some View { VStack { let (show_text, filtered_content): (Bool, CompatibleText?) = filter_content(blocks: event.blocks(damus_state.keypair.privkey), profiles: damus_state.profiles, privkey: damus_state.keypair.privkey) + let mention = first_eref_mention(ev: event, privkey: damus_state.keypair.privkey) + let url = separate_images(ev: event, privkey: damus_state.keypair.privkey) + let invoices = separate_invoices(ev: event, privkey: damus_state.keypair.privkey) + let lastInDM = getLastInDM(text: filtered_content, mention: mention, url: url, invoices: invoices) + if show_text, let filtered_content = filtered_content { - DM(content: filtered_content).padding(.bottom, isLastInGroup ? 0 : -6) + DM(content: filtered_content, isLastInDM: lastInDM == .text).padding(.bottom, (isLastInGroup && lastInDM == .text) ? 0 : -6) } - if let mention = first_eref_mention(ev: event, privkey: damus_state.keypair.privkey) { - Mention(mention: mention).padding(.bottom, isLastInGroup ? 0 : -6) + if let mention { + Mention(mention: mention).padding(.bottom, (isLastInGroup && lastInDM == .mention) ? 0 : -6) } - if let url = separate_images(ev: event, privkey: damus_state.keypair.privkey) { - Image(urls: url).padding(.bottom, isLastInGroup ? 0 : -6) + if let url { + Image(urls: url, isLastInDM: lastInDM == .url).padding(.bottom, (isLastInGroup && lastInDM == .url) ? 0 : -6) } - if let invoices = separate_invoices(ev: event, privkey: damus_state.keypair.privkey) { - Invoice(invoices: invoices).padding(.bottom, isLastInGroup ? 0 : -6) + if let invoices { + Invoice(invoices: invoices, isLastInDM: lastInDM == .invoice).padding(.bottom, (isLastInGroup && lastInDM == .invoice) ? 0 : -6) } if (isLastInGroup) { TimeStamp().padding(.top, -5) @@ -245,6 +267,13 @@ struct DMView: View { } } +enum DMContentType { + case text + case mention + case url + case invoice +} + struct ChatBubbleShape: Shape { enum Direction { case left From 3180829fe16bcdf19e61eea13f39e4adfb5410e5 Mon Sep 17 00:00:00 2001 From: Suhail Saqan Date: Mon, 7 Aug 2023 23:21:08 -0500 Subject: [PATCH 18/18] rebase --- damus/Nostr/NostrEvent.swift | 8 +-- damus/Views/DMChatView.swift | 91 +++++++++++++++++++++---------- damus/Views/DMView.swift | 21 ++++--- damus/Views/NoteContentView.swift | 2 +- 4 files changed, 78 insertions(+), 44 deletions(-) diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift index d3d6f9241..e4d03e2fc 100644 --- a/damus/Nostr/NostrEvent.swift +++ b/damus/Nostr/NostrEvent.swift @@ -955,8 +955,8 @@ func first_eref_mention(ev: NostrEvent, privkey: Privkey?) -> Mention? { return nil } -func separate_images(ev: NostrEvent, privkey: String?) -> [MediaUrl]? { - let urlBlocks: [URL] = ev.blocks(privkey).reduce(into: []) { urls, block in +func separate_images(ev: NostrEvent, privkey: Privkey?) -> [MediaUrl]? { + let urlBlocks: [URL] = ev.blocks(privkey).blocks.reduce(into: []) { urls, block in guard case .url(let url) = block else { return } @@ -968,8 +968,8 @@ func separate_images(ev: NostrEvent, privkey: String?) -> [MediaUrl]? { return mediaUrls.isEmpty ? nil : mediaUrls } -func separate_invoices(ev: NostrEvent, privkey: String?) -> [Invoice]? { - let invoiceBlocks: [Invoice] = ev.blocks(privkey).reduce(into: []) { invoices, block in +func separate_invoices(ev: NostrEvent, privkey: Privkey?) -> [Invoice]? { + let invoiceBlocks: [Invoice] = ev.blocks(privkey).blocks.reduce(into: []) { invoices, block in guard case .invoice(let invoice) = block else { return } diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift index f38d29485..c6bb01537 100644 --- a/damus/Views/DMChatView.swift +++ b/damus/Views/DMChatView.swift @@ -60,36 +60,10 @@ struct DMChatView: View, KeyboardReadable { var Messages: some View { ScrollViewReader { scroller in ScrollView { - if (dms.events.isEmpty) { - Text("Send a message to start the conversation...", comment: "Text prompt for user to send a message to the other user.") - .lineLimit(nil) - .multilineTextAlignment(.center) - .padding(.horizontal, 40) - .padding(.vertical) - .foregroundColor(.gray) + if dms.events.isEmpty { + EmptyMessageView() } else { - VStack(alignment: .leading) { - let groups = groupEventsByDateAndPubkey(events: dms.events) - - ForEach(Array(groups.keys).sorted(), id: \.self) { date in - VStack(alignment: .leading) { - Text(date, style: .date) - .font(.footnote) - .fontWeight(.bold) - .foregroundColor(.gray) - .padding(.horizontal) - .frame(maxWidth: .infinity) - - ForEach(Array(groups[date]!.keys).sorted(), id: \.self) { key in - let events = groups[date]![key]! - ForEach(events) { event in - DMView(event: event, damus_state: damus_state, isLastInGroup: event == events.last) - } - } - } - } - EndBlock(height: 5) - }.padding(.horizontal) + GroupedEventsView(groups: groupEventsByDateAndPubkey(events: dms.events), damus_state: damus_state) } } .onAppear { @@ -102,6 +76,63 @@ struct DMChatView: View, KeyboardReadable { } } + struct EmptyMessageView: View { + var body: some View { + Text("Send a message to start the conversation...", comment: "Text prompt for user to send a message to the other user.") + .lineLimit(nil) + .multilineTextAlignment(.center) + .padding(.horizontal, 40) + .padding(.vertical) + .foregroundColor(.gray) + } + } + + struct GroupedEventsView: View { + var groups: [Date: [String: [NostrEvent]]] + var damus_state: DamusState + + var body: some View { + VStack(alignment: .leading) { + ForEach(Array(groups.keys).sorted(), id: \.self) { date in + GroupedDateView(date: date, events: groups[date]!, damus_state: damus_state) + } + EndBlock(height: 5) + }.padding(.horizontal) + } + } + + struct GroupedDateView: View { + var date: Date + var events: [String: [NostrEvent]] + var damus_state: DamusState + + var body: some View { + VStack(alignment: .leading) { + Text(date, style: .date) + .font(.footnote) + .fontWeight(.bold) + .foregroundColor(.gray) + .padding(.horizontal) + .frame(maxWidth: .infinity) + + ForEach(Array(events.keys).sorted(), id: \.self) { key in + GroupedEventView(events: events[key]!, damus_state: damus_state) + } + } + } + } + + struct GroupedEventView: View { + var events: [NostrEvent] + var damus_state: DamusState + + var body: some View { + ForEach(events, id: \.self) { event in + DMView(event: event, damus_state: damus_state, isLastInGroup: event == events.last) + } + } + } + func scroll_to_end(_ scroller: ScrollViewProxy, animated: Bool = false) { if animated { withAnimation { @@ -311,7 +342,7 @@ extension String { } } -func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keypair, created_at: UInt32? = nil) -> NostrEvent? +func create_dm(_ message: String, to_pk: Pubkey, tags: [[String]], keypair: Keypair, created_at: UInt32? = nil) -> NostrEvent? { let created = created_at ?? UInt32(Date().timeIntervalSince1970) diff --git a/damus/Views/DMView.swift b/damus/Views/DMView.swift index 01032980b..001ec2962 100644 --- a/damus/Views/DMView.swift +++ b/damus/Views/DMView.swift @@ -36,7 +36,7 @@ struct DMView: View { return options } - func format_timestamp(timestamp: Int64) -> String { + func format_timestamp(timestamp: UInt32) -> String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "h:mm a" let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) @@ -83,7 +83,7 @@ struct DMView: View { } } - func Mention(mention: Mention) -> some View { + func Mention(mention: Mention) -> some View { Group { HStack { if is_ours { @@ -100,6 +100,7 @@ struct DMView: View { } } + @MainActor func Image(urls: [MediaUrl], isLastInDM: Bool) -> some View { return Group { HStack { @@ -166,7 +167,9 @@ struct DMView: View { } } - func filter_content(blocks: [Block], profiles: Profiles, privkey: String?) -> (Bool, CompatibleText?) { + func filter_content(blocks bs: Blocks, profiles: Profiles, privkey: Privkey?) -> (Bool, CompatibleText?) { + let blocks = bs.blocks + let one_note_ref = blocks .filter({ $0.is_note_mention }) .count == 1 @@ -178,10 +181,10 @@ struct DMView: View { switch block { case .mention(let m): - if m.type == .event && one_note_ref { + if case .note = m.ref, one_note_ref { return str } - if m.type == .pubkey { + if case .pubkey(_) = m.ref { show_text = true } return str + mention_str(m, profiles: profiles) @@ -192,9 +195,9 @@ struct DMView: View { } if let next = blocks[safe: ind+1] { - if case .url(let u) = next, classify_url(u).is_media != nil { + if case .url(let u) = next, classify_url(u).is_media != nil { trimmed = trim_suffix(trimmed) - } else if case .mention(let m) = next, m.type == .event, one_note_ref { + } else if case .mention(let m) = next, case .note = m.ref, one_note_ref { trimmed = trim_suffix(trimmed) } } @@ -219,11 +222,11 @@ struct DMView: View { } } } - + return (show_text, txt) } - func getLastInDM(text: CompatibleText?, mention: Mention?, url: [MediaUrl]?, invoices: [Invoice]?) -> DMContentType? { + func getLastInDM(text: CompatibleText?, mention: Mention?, url: [MediaUrl]?, invoices: [Invoice]?) -> DMContentType? { var last: DMContentType? if let text { last = .text diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift index 413b1c9bd..c20dee95e 100644 --- a/damus/Views/NoteContentView.swift +++ b/damus/Views/NoteContentView.swift @@ -405,7 +405,7 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: Privkey?, return .longform(LongformContent(ev.content)) } - return .separated(render_blocks(blocks: blocks, profiles: profiles)) + return .separated(render_blocks(blocks: blocks, profiles: profiles, only_text: only_text)) } fileprivate func artifact_part_last_text_ind(parts: [ArtifactPart]) -> (Int, Text)? {