diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 71888053..8d3476ab 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -192,6 +192,8 @@ 4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; }; 4CA3FA1029F593D000FDB3C3 /* ZapTypePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA3FA0F29F593D000FDB3C3 /* ZapTypePicker.swift */; }; 4CA5588329F33F5B00DC6A45 /* StringCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA5588229F33F5B00DC6A45 /* StringCodable.swift */; }; + 4CA9275D2A28FF630098A105 /* LongformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA9275C2A28FF630098A105 /* LongformView.swift */; }; + 4CA9275F2A2902B20098A105 /* LongformPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA9275E2A2902B20098A105 /* LongformPreview.swift */; }; 4CA927612A290E340098A105 /* EventShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA927602A290E340098A105 /* EventShell.swift */; }; 4CA927632A290EB10098A105 /* EventTop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA927622A290EB10098A105 /* EventTop.swift */; }; 4CA927652A290F1A0098A105 /* TimeDot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA927642A290F1A0098A105 /* TimeDot.swift */; }; @@ -670,6 +672,8 @@ 4CA927712A2A5D480098A105 /* error.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = error.h; sourceTree = ""; }; 4CA927742A2A5E2F0098A105 /* varint.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = varint.h; sourceTree = ""; }; 4CA927752A2A5E2F0098A105 /* typedefs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = typedefs.h; sourceTree = ""; }; + 4CA9275C2A28FF630098A105 /* LongformView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongformView.swift; sourceTree = ""; }; + 4CA9275E2A2902B20098A105 /* LongformPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongformPreview.swift; sourceTree = ""; }; 4CA927602A290E340098A105 /* EventShell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventShell.swift; sourceTree = ""; }; 4CA927622A290EB10098A105 /* EventTop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTop.swift; sourceTree = ""; }; 4CA927642A290F1A0098A105 /* TimeDot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeDot.swift; sourceTree = ""; }; @@ -1270,6 +1274,15 @@ ); path = nostrscript; }; + 4CA9275B2A28FF570098A105 /* Longform */ = { + isa = PBXGroup; + children = ( + 4CA9275C2A28FF630098A105 /* LongformView.swift */, + 4CA9275E2A2902B20098A105 /* LongformPreview.swift */, + ); + path = Longform; + sourceTree = ""; + }; 4CA927682A290F8F0098A105 /* Components */ = { isa = PBXGroup; children = ( @@ -1362,6 +1375,7 @@ 4C3D52B5298DB4E6001C5831 /* ZapEvent.swift */, 4C3D52B7298DB5C6001C5831 /* TextEvent.swift */, 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */, + 4CA9275B2A28FF570098A105 /* Longform */, 4CA927602A290E340098A105 /* EventShell.swift */, ); path = Events; @@ -1826,6 +1840,7 @@ 4C3EA67728FF7A9800C48A62 /* talstr.c in Sources */, 4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */, 4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */, + 4CA9275D2A28FF630098A105 /* LongformView.swift in Sources */, 4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */, 504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */, 3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */, @@ -2023,6 +2038,7 @@ 4C3EA66028FF5E7700C48A62 /* node_id.c in Sources */, 4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */, 4C363A962827096D006E126D /* PostBlock.swift in Sources */, + 4CA9275F2A2902B20098A105 /* LongformPreview.swift in Sources */, 4C5F9116283D855D0052CD1C /* EventsModel.swift in Sources */, 4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */, 4C06670E28FDEAA000038D2A /* utf8.c in Sources */, diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift index 24c8be8d..817b9b9a 100644 --- a/damus/Views/EventView.swift +++ b/damus/Views/EventView.swift @@ -42,6 +42,8 @@ struct EventView: View { } else { EmptyView() } + } else if event.known_kind == .longform { + LongformPreview(state: damus, ev: event) } else { TextEvent(damus: damus, event: event, pubkey: pubkey, options: options) //.padding([.top], 6) diff --git a/damus/Views/Events/Longform/LongformPreview.swift b/damus/Views/Events/Longform/LongformPreview.swift new file mode 100644 index 00000000..fdfa8a44 --- /dev/null +++ b/damus/Views/Events/Longform/LongformPreview.swift @@ -0,0 +1,51 @@ +// +// LongformPreview.swift +// damus +// +// Created by William Casarin on 2023-06-01. +// + +import SwiftUI + +struct LongformPreview: View { + let state: DamusState + let event: LongformEvent + @ObservedObject var artifacts: NoteArtifactsModel + + init(state: DamusState, ev: NostrEvent) { + self.state = state + self.event = LongformEvent.parse(from: ev) + self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.id).artifacts_model) + } + + func Words(_ words: Int) -> Text { + Text(verbatim: words.description) + Text(verbatim: " ") + Text("Words") + } + + var body: some View { + EventShell(state: state, event: event.event, options: [.no_mentions]) { + NavigationLink(destination: LongformView(event: event)) { + VStack(alignment: .leading, spacing: 10) { + Text(event.title ?? "Untitled") + .font(.title) + + Text(event.summary ?? "") + .foregroundColor(.gray) + + if case .loaded(let arts) = artifacts.state { + Words(arts.words) + .font(.footnote) + } + } + .padding() + } + .buttonStyle(.plain) + } + } +} + +struct LongformPreview_Previews: PreviewProvider { + static var previews: some View { + LongformPreview(state: test_damus_state(), ev: test_longform_event.event) + } +} diff --git a/damus/Views/Events/Longform/LongformView.swift b/damus/Views/Events/Longform/LongformView.swift new file mode 100644 index 00000000..7e967f05 --- /dev/null +++ b/damus/Views/Events/Longform/LongformView.swift @@ -0,0 +1,90 @@ +// +// LongformEvent.swift +// damus +// +// Created by William Casarin on 2023-06-01. +// + +import SwiftUI + +struct LongformEvent { + let event: NostrEvent + + var title: String? = nil + var image: URL? = nil + var summary: String? = nil + var published_at: Date? = nil + + static func parse(from ev: NostrEvent) -> LongformEvent { + var longform = LongformEvent(event: ev) + + for tag in ev.tags { + guard tag.count >= 2 else { continue } + switch tag[0] { + case "title": longform.title = tag[1] + case "image": longform.image = URL(string: tag[1]) + case "summary": longform.summary = tag[1] + case "published_at": + longform.published_at = Double(tag[1]).map { d in Date(timeIntervalSince1970: d) } + default: + break + } + } + + return longform + } +} + +struct LongformView: View { + let state: DamusState + let event: LongformEvent + @ObservedObject var artifacts: NoteArtifactsModel + + init(state: DamusState, event: LongformEvent) { + self.state = state + self.event = event + self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(event.event.id).artifacts_model) + } + + var options: EventViewOptions { + return [.wide, .no_mentions, .no_replying_to] + } + + var body: some View { + EventShell(state: state, event: event.event, options: options) { + + Content + } + } + + var Content: some View { + Group { + if case .loaded(let artifacts) = artifacts.state { + SelectableText(attributedString: artifacts.content.attributed, size: .selected) + .padding(.horizontal) + } else { + Text("") + } + } + } +} + +let test_longform_event = LongformEvent.parse(from: + .init(content: "## Let me tell you why coffee is awesome\n**IT JUST IS**", + pubkey: "pk", + kind: NostrKind.longform.rawValue, + tags: [ + ["title", "Coffee is awesome"], + ["summary", "Did you know coffee is awesome?"], + ["published_at", "1685638715"], + ["t", "coffee"], + ["t", "coffeechain"], + ["image", "https://cdn.jb55.com/s/038fe8f558153b52.jpg"], + ]) +) + +struct LongformView_Previews: PreviewProvider { + static var previews: some View { + LongformView(state: test_damus_state(), event: test_longform_event) + } +} diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift index 7dbc79e4..21659d09 100644 --- a/damus/Views/NoteContentView.swift +++ b/damus/Views/NoteContentView.swift @@ -314,6 +314,65 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) - return render_blocks(blocks: blocks, profiles: profiles) } +func render_blocks_longform(blocks bs: Blocks) -> NoteArtifacts { + var invoices: [Invoice] = [] + var urls: [UrlType] = [] + let blocks = bs.blocks + + var ind: Int = -1 + let txt: CompatibleText = blocks.reduce(CompatibleText()) { str, block in + ind = ind + 1 + + switch block { + case .mention(let m): + return str + mention_str(m, profiles: profiles) + case .text(let txt): + return str + reduce_text_block(blocks: blocks, ind: ind, txt: txt, one_note_ref: false) + + case .relay(let relay): + return str + CompatibleText(stringLiteral: relay) + + case .hashtag(let htag): + return str + hashtag_str(htag) + case .invoice(let invoice): + invoices.append(invoice) + return str + case .url(let url): + let url_type = classify_url(url) + switch url_type { + case .media: + urls.append(url_type) + return str + case .link(let url): + urls.append(url_type) + return str + url_str(url) + } + } + } + + return NoteArtifacts(content: txt, words: bs.words, urls: urls, invoices: invoices) +} + +func reduce_text_block(blocks: [Block], ind: Int, txt: String, one_note_ref: Bool) -> CompatibleText { + 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) + } + } + + return CompatibleText(stringLiteral: trimmed) +} + func render_blocks(blocks bs: Blocks, profiles: Profiles) -> NoteArtifacts { var invoices: [Invoice] = [] var urls: [UrlType] = [] @@ -334,22 +393,8 @@ func render_blocks(blocks bs: Blocks, profiles: Profiles) -> NoteArtifacts { } 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) - } + return str + reduce_text_block(blocks: blocks, ind: ind, txt: txt, one_note_ref: one_note_ref) - 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) - } - } - - return str + CompatibleText(stringLiteral: trimmed) case .relay(let relay): return str + CompatibleText(stringLiteral: relay)