commit e2993898669606a3876ef47f81b58e8358ab9e47 parent cb2380e2186cfbb01c3b5326d93a53af57f150d1 Author: William Casarin <jb55@jb55.com> Date: Mon, 10 Jul 2023 18:29:18 -0700 Add initial longform note support Changelog-Added: Add initial longform note support Diffstat:
34 files changed, 841 insertions(+), 164 deletions(-)
diff --git a/damus-c/block.h b/damus-c/block.h @@ -46,6 +46,7 @@ typedef struct note_block { } block_t; typedef struct note_blocks { + int words; int num_blocks; struct note_block *blocks; } blocks_t; diff --git a/damus-c/damus.c b/damus-c/damus.c @@ -216,6 +216,7 @@ int damus_parse_content(struct note_blocks *blocks, const char *content) { struct note_block block; u8 *start, *pre_mention; + blocks->words = 0; blocks->num_blocks = 0; make_cursor((u8*)content, (u8*)content + strlen(content), &cur); @@ -224,6 +225,11 @@ int damus_parse_content(struct note_blocks *blocks, const char *content) { cp = peek_char(&cur, -1); c = peek_char(&cur, 0); + // new word + if (is_whitespace(cp) && !is_whitespace(c)) { + blocks->words++; + } + pre_mention = cur.p; if (cp == -1 || is_whitespace(cp) || c == '#') { if (c == '#' && (parse_mention_index(&cur, &block) || parse_hashtag(&cur, &block))) { diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -192,6 +192,14 @@ 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 */; }; + 4CA927672A290F8B0098A105 /* RelativeTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA927662A290F8B0098A105 /* RelativeTime.swift */; }; + 4CA9276A2A290FC00098A105 /* ContextButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA927692A290FC00098A105 /* ContextButton.swift */; }; + 4CA9276C2A2910D10098A105 /* ReplyPart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA9276B2A2910D10098A105 /* ReplyPart.swift */; }; 4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */; }; 4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */; }; 4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */; }; @@ -279,6 +287,7 @@ 4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABED29844B5500D66079 /* AnyEncodable.swift */; }; 4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; }; 4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABF52985CD5500D66079 /* UserSearch.swift */; }; + 4CFD502F2A2DA45800A229DB /* MediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFD502E2A2DA45800A229DB /* MediaView.swift */; }; 4CFF8F6329CC9AD7008DB934 /* ImageContextMenuModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */; }; 4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6629CC9E3A008DB934 /* ImageView.swift */; }; 4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */; }; @@ -664,6 +673,14 @@ 4CA927712A2A5D480098A105 /* error.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = error.h; sourceTree = "<group>"; }; 4CA927742A2A5E2F0098A105 /* varint.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = varint.h; sourceTree = "<group>"; }; 4CA927752A2A5E2F0098A105 /* typedefs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = typedefs.h; sourceTree = "<group>"; }; + 4CA9275C2A28FF630098A105 /* LongformView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongformView.swift; sourceTree = "<group>"; }; + 4CA9275E2A2902B20098A105 /* LongformPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongformPreview.swift; sourceTree = "<group>"; }; + 4CA927602A290E340098A105 /* EventShell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventShell.swift; sourceTree = "<group>"; }; + 4CA927622A290EB10098A105 /* EventTop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTop.swift; sourceTree = "<group>"; }; + 4CA927642A290F1A0098A105 /* TimeDot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeDot.swift; sourceTree = "<group>"; }; + 4CA927662A290F8B0098A105 /* RelativeTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelativeTime.swift; sourceTree = "<group>"; }; + 4CA927692A290FC00098A105 /* ContextButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextButton.swift; sourceTree = "<group>"; }; + 4CA9276B2A2910D10098A105 /* ReplyPart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyPart.swift; sourceTree = "<group>"; }; 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletion.swift; sourceTree = "<group>"; }; 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConfigView.swift; sourceTree = "<group>"; }; 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = "<group>"; }; @@ -754,6 +771,7 @@ 4CF0ABED29844B5500D66079 /* AnyEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = "<group>"; }; 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Object.swift; sourceTree = "<group>"; }; 4CF0ABF52985CD5500D66079 /* UserSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearch.swift; sourceTree = "<group>"; }; + 4CFD502E2A2DA45800A229DB /* MediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = "<group>"; }; 4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContextMenuModifier.swift; sourceTree = "<group>"; }; 4CFF8F6629CC9E3A008DB934 /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = "<group>"; }; 4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContainerView.swift; sourceTree = "<group>"; }; @@ -1257,6 +1275,27 @@ 4C9146FB2A2A77B300DDEA40 /* NostrScript.swift */, ); path = nostrscript; + }; + 4CA9275B2A28FF570098A105 /* Longform */ = { + isa = PBXGroup; + children = ( + 4CA9275C2A28FF630098A105 /* LongformView.swift */, + 4CA9275E2A2902B20098A105 /* LongformPreview.swift */, + ); + path = Longform; + sourceTree = "<group>"; + }; + 4CA927682A290F8F0098A105 /* Components */ = { + isa = PBXGroup; + children = ( + 4CA927642A290F1A0098A105 /* TimeDot.swift */, + 4CA927622A290EB10098A105 /* EventTop.swift */, + 4CC7AAF3297F18B400430951 /* ReplyDescription.swift */, + 4CA927662A290F8B0098A105 /* RelativeTime.swift */, + 4CA927692A290FC00098A105 /* ContextButton.swift */, + 4CA9276B2A2910D10098A105 /* ReplyPart.swift */, + ); + path = Components; sourceTree = "<group>"; }; 4CA927732A2A5DCC0098A105 /* nostrscript */ = { @@ -1328,8 +1367,8 @@ 4CC7AAEE297F11B300430951 /* Events */ = { isa = PBXGroup; children = ( + 4CA927682A290F8F0098A105 /* Components */, 4CC7AAEF297F11C700430951 /* SelectedEventView.swift */, - 4CC7AAF3297F18B400430951 /* ReplyDescription.swift */, 4CC7AAF5297F1A6A00430951 /* EventBody.swift */, 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */, 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */, @@ -1338,6 +1377,8 @@ 4C3D52B5298DB4E6001C5831 /* ZapEvent.swift */, 4C3D52B7298DB5C6001C5831 /* TextEvent.swift */, 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */, + 4CA9275B2A28FF570098A105 /* Longform */, + 4CA927602A290E340098A105 /* EventShell.swift */, ); path = Events; sourceTree = "<group>"; @@ -1557,6 +1598,7 @@ 4CFF8F6629CC9E3A008DB934 /* ImageView.swift */, 6439E013296790CF0020672B /* ProfilePicImageView.swift */, 4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */, + 4CFD502E2A2DA45800A229DB /* MediaView.swift */, ); path = Images; sourceTree = "<group>"; @@ -1782,6 +1824,7 @@ 4C3EA64428FF558100C48A62 /* sha256.c in Sources */, 4CCF9AAF2A1FDBDB00E03CFB /* VideoPlayer.swift in Sources */, 504323A72A34915F006AE6DC /* RelayModel.swift in Sources */, + 4CA9276A2A290FC00098A105 /* ContextButton.swift in Sources */, 4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */, 4C363AA828297703006E126D /* InsertSort.swift in Sources */, 4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */, @@ -1800,14 +1843,17 @@ 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 */, 4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */, 4C363AA228296A7E006E126D /* SearchView.swift in Sources */, 4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */, + 4CA927652A290F1A0098A105 /* TimeDot.swift in Sources */, 4CC6193A29DC777C006A86D1 /* RelayBootstrap.swift in Sources */, 4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */, + 4CFD502F2A2DA45800A229DB /* MediaView.swift in Sources */, 4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */, 4CA5588329F33F5B00DC6A45 /* StringCodable.swift in Sources */, 4C75EFB92804A2740006080F /* EventView.swift in Sources */, @@ -1835,6 +1881,7 @@ 3A5E47C52A4A6CF400C0D090 /* Trie.swift in Sources */, 4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */, 4C75EFA627FF87A20006080F /* Nostr.swift in Sources */, + 4CA927672A290F8B0098A105 /* RelativeTime.swift in Sources */, 4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */, 4C7D096D2A0AEA0400943473 /* CodeScanner.swift in Sources */, 4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */, @@ -1854,6 +1901,7 @@ 4C363A9A28283854006E126D /* Reply.swift in Sources */, BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */, 4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */, + 4CA927632A290EB10098A105 /* EventTop.swift in Sources */, 4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */, 4CB8838B296F6E1E00DC99E7 /* NIP05Badge.swift in Sources */, 4CA3FA1029F593D000FDB3C3 /* ZapTypePicker.swift in Sources */, @@ -1938,6 +1986,7 @@ 4C9146FD2A2A87C200DDEA40 /* wasm.c in Sources */, 4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */, 4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */, + 4CA9276C2A2910D10098A105 /* ReplyPart.swift in Sources */, 4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */, 4CE1399229F0666100AC6A0B /* ShareActionButton.swift in Sources */, 4C42812C298C848200DBF26F /* TranslateView.swift in Sources */, @@ -1981,6 +2030,7 @@ 4CDA128C29EB19C40006FA5A /* LocalNotification.swift in Sources */, 4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */, 4C7D09762A0AF19E00943473 /* FillAndStroke.swift in Sources */, + 4CA927612A290E340098A105 /* EventShell.swift in Sources */, 4C363AA428296DEE006E126D /* SearchModel.swift in Sources */, 4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */, 4C7D096F2A0AEA0400943473 /* ScannerViewController.swift in Sources */, @@ -1992,6 +2042,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/Components/TranslateView.swift b/damus/Components/TranslateView.swift @@ -10,7 +10,7 @@ import NaturalLanguage struct Translated: Equatable { - let artifacts: NoteArtifacts + let artifacts: NoteArtifactsSeparated let language: String } @@ -42,7 +42,7 @@ struct TranslateView: View { .translate_button_style() } - func TranslatedView(lang: String?, artifacts: NoteArtifacts) -> some View { + func TranslatedView(lang: String?, artifacts: NoteArtifactsSeparated) -> some View { return VStack(alignment: .leading) { let translatedFromLanguageString = String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang ?? "ja") Text(translatedFromLanguageString) diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -106,6 +106,7 @@ class HomeModel { switch kind { case .chat: fallthrough + case .longform: fallthrough case .text: handle_text_event(sub_id: sub_id, ev) case .contacts: @@ -406,8 +407,7 @@ class HomeModel { // TODO: separate likes? var home_filter_kinds: [NostrKind] = [ - .text, - .boost + .text, .longform, .boost ] if !damus_state.settings.onlyzaps_mode { home_filter_kinds.append(.like) @@ -1147,6 +1147,27 @@ func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale: } } +func render_notification_content_preview(cache: EventCache, ev: NostrEvent, profiles: Profiles, privkey: String?) -> String { + + let prefix_len = 50 + let artifacts = cache.get_cache_data(ev.id).artifacts.artifacts ?? render_note_content(ev: ev, profiles: profiles, privkey: privkey) + + // special case for longform events + if ev.known_kind == .longform { + let longform = LongformEvent(event: ev) + return longform.title ?? longform.summary ?? "Longform Event" + } + + switch artifacts { + case .parts: + // we should never hit this until we have more note types built out of parts + // since we handle this case above in known_kind == .longform + return String(ev.content.prefix(prefix_len)) + + case .separated(let artifacts): + return String(NSAttributedString(artifacts.content.attributed).string.prefix(prefix_len)) + } +} func process_local_notification(damus_state: DamusState, event ev: NostrEvent) { guard let type = ev.known_kind else { @@ -1170,23 +1191,22 @@ func process_local_notification(damus_state: DamusState, event ev: NostrEvent) { } if type == .text && damus_state.settings.mention_notification { - let blocks = ev.blocks(damus_state.keypair.privkey) + let blocks = ev.blocks(damus_state.keypair.privkey).blocks for case .mention(let mention) in blocks where mention.ref.ref_id == damus_state.keypair.pubkey { - let content = NSAttributedString(render_note_content(ev: ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content.attributed).string - - let notify = LocalNotification(type: .mention, event: ev, target: ev, content: content) + let content_preview = render_notification_content_preview(cache: damus_state.events, ev: ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey) + let notify = LocalNotification(type: .mention, event: ev, target: ev, content: content_preview) create_local_notification(profiles: damus_state.profiles, notify: notify ) } } else if type == .boost && damus_state.settings.repost_notification, let inner_ev = ev.get_inner_event(cache: damus_state.events) { - let content = NSAttributedString(render_note_content(ev: inner_ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content.attributed).string - let notify = LocalNotification(type: .repost, event: ev, target: inner_ev, content: content) + let content_preview = render_notification_content_preview(cache: damus_state.events, ev: inner_ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey) + let notify = LocalNotification(type: .repost, event: ev, target: inner_ev, content: content_preview) create_local_notification(profiles: damus_state.profiles, notify: notify) } else if type == .like && damus_state.settings.like_notification, let evid = ev.referenced_ids.last?.ref_id, let liked_event = damus_state.events.lookup(evid) { - let content = NSAttributedString(render_note_content(ev: liked_event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content.attributed).string - let notify = LocalNotification(type: .like, event: ev, target: liked_event, content: content) + let content_preview = render_notification_content_preview(cache: damus_state.events, ev: liked_event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey) + let notify = LocalNotification(type: .like, event: ev, target: liked_event, content: content_preview) create_local_notification(profiles: damus_state.profiles, notify: notify) } diff --git a/damus/Models/Mentions.swift b/damus/Models/Mentions.swift @@ -150,7 +150,12 @@ func render_blocks(blocks: [Block]) -> String { } } -func parse_mentions(content: String, tags: [[String]]) -> [Block] { +struct Blocks { + let words: Int + let blocks: [Block] +} + +func parse_mentions(content: String, tags: [[String]]) -> Blocks { var out: [Block] = [] var bs = note_blocks() @@ -174,9 +179,10 @@ func parse_mentions(content: String, tags: [[String]]) -> [Block] { i += 1 } + let words = Int(bs.words) blocks_free(&bs) - return out + return Blocks(words: words, blocks: out) } func strblock_to_string(_ s: str_block_t) -> String? { diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift @@ -69,8 +69,7 @@ class ProfileModel: ObservableObject, Equatable { } func subscribe() { - var text_filter = NostrFilter(kinds: [.text, .chat]) - + var text_filter = NostrFilter(kinds: [.text, .longform]) var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost]) profile_filter.authors = [pubkey] diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift @@ -68,7 +68,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has let content: String var is_textlike: Bool { - return kind == 1 || kind == 42 + return kind == 1 || kind == 42 || kind == 30023 } var too_big: Bool { @@ -83,8 +83,8 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has return calculate_event_id(ev: self) == self.id } - private var _blocks: [Block]? = nil - func blocks(_ privkey: String?) -> [Block] { + private var _blocks: Blocks? = nil + func blocks(_ privkey: String?) -> Blocks { if let bs = _blocks { return bs } @@ -93,7 +93,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has return blocks } - func get_blocks(content: String) -> [Block] { + func get_blocks(content: String) -> Blocks { return parse_mentions(content: content, tags: self.tags) } @@ -118,7 +118,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has if let rs = _event_refs { return rs } - let refs = interpret_event_refs(blocks: self.blocks(privkey), tags: self.tags) + let refs = interpret_event_refs(blocks: self.blocks(privkey).blocks, tags: self.tags) self._event_refs = refs return refs } @@ -232,7 +232,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has func note_language(_ privkey: String?) -> String? { // Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in // and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer. - let originalBlocks = blocks(privkey) + let originalBlocks = blocks(privkey).blocks let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ") // Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate. @@ -942,7 +942,7 @@ func last_etag(tags: [[String]]) -> String? { } func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? { - let blocks = ev.blocks(privkey).filter { block in + let blocks = ev.blocks(privkey).blocks.filter { block in guard case .mention(let mention) = block else { return false } diff --git a/damus/Nostr/NostrKind.swift b/damus/Nostr/NostrKind.swift @@ -20,6 +20,7 @@ enum NostrKind: Int, Codable { case channel_meta = 41 case chat = 42 case list = 30000 + case longform = 30023 case zap = 9735 case zap_request = 9734 case nwc_request = 23194 diff --git a/damus/Util/EventCache.swift b/damus/Util/EventCache.swift @@ -344,7 +344,7 @@ struct PreloadPlan { let load_preview: Bool } -func load_preview(artifacts: NoteArtifacts) async -> Preview? { +func load_preview(artifacts: NoteArtifactsSeparated) async -> Preview? { guard let link = artifacts.links.first else { return nil } @@ -442,14 +442,18 @@ func preload_event(plan: PreloadPlan, state: DamusState) async { } } - if plan.load_preview { + 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 preview = await load_preview(artifacts: arts) - DispatchQueue.main.async { - if let preview { - plan.data.preview_model.state = .loaded(preview) - } else { - plan.data.preview_model.state = .loaded(.failed) + + // only separated artifacts have previews + if case .separated(let sep) = arts { + let preview = await load_preview(artifacts: sep) + DispatchQueue.main.async { + if let preview { + plan.data.preview_model.state = .loaded(preview) + } else { + plan.data.preview_model.state = .loaded(.failed) + } } } } diff --git a/damus/Util/Markdown.swift b/damus/Util/Markdown.swift @@ -6,6 +6,32 @@ // import Foundation +import SwiftUI + +func count_leading_hashes(_ str: String) -> Int { + var count = 0 + for c in str { + if c == "#" { + count += 1 + } else { + break + } + } + + return count +} + +func get_heading_title_size(count: Int) -> SwiftUI.Font { + if count >= 3 { + return Font.title3 + } else if count >= 2 { + return Font.title2 + } else if count >= 1 { + return Font.title + } + + return Font.body +} public struct Markdown { private var detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) @@ -19,6 +45,18 @@ public struct Markdown { public static func parse(content: String) -> AttributedString { let md_opts: AttributedString.MarkdownParsingOptions = .init(interpretedSyntax: .inlineOnlyPreservingWhitespace) + + guard content.utf8.count > 0 else { + return AttributedString(stringLiteral: "") + } + + let leading_hashes = count_leading_hashes(content) + if leading_hashes > 0 { + if var str = try? AttributedString(markdown: content) { + str.font = get_heading_title_size(count: leading_hashes) + return str + } + } // TODO: escape unintentional markdown let escaped = content.replacingOccurrences(of: "\\_", with: "\\\\\\_") diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift @@ -12,6 +12,7 @@ enum EventViewKind { case small case normal case selected + case title case subheadline } @@ -42,6 +43,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) @@ -107,6 +110,8 @@ func eventviewsize_to_font(_ size: EventViewKind) -> Font { return .body case .selected: return .custom("selected", size: 21.0) + case .title: + return .title case .subheadline: return .subheadline } @@ -122,6 +127,8 @@ func eventviewsize_to_uifont(_ size: EventViewKind) -> UIFont { return .preferredFont(forTextStyle: .title2) case .subheadline: return .preferredFont(forTextStyle: .subheadline) + case .title: + return .preferredFont(forTextStyle: .title1) } } diff --git a/damus/Views/Events/Components/ContextButton.swift b/damus/Views/Events/Components/ContextButton.swift @@ -0,0 +1,20 @@ +// +// ContextButton.swift +// damus +// +// Created by William Casarin on 2023-06-01. +// + +import SwiftUI + +struct ContextButton: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +struct ContextButton_Previews: PreviewProvider { + static var previews: some View { + ContextButton() + } +} diff --git a/damus/Views/Events/Components/EventTop.swift b/damus/Views/Events/Components/EventTop.swift @@ -0,0 +1,44 @@ +// +// EventTop.swift +// damus +// +// Created by William Casarin on 2023-06-01. +// + +import SwiftUI + +struct EventTop: View { + let state: DamusState + let event: NostrEvent + let is_anon: Bool + + init(state: DamusState, event: NostrEvent, is_anon: Bool) { + self.state = state + self.event = event + self.is_anon = is_anon + } + + func ProfileName(is_anon: Bool) -> some View { + let profile = state.profiles.lookup(id: event.pubkey) + let pk = is_anon ? "anon" : event.pubkey + return EventProfileName(pubkey: pk, profile: profile, damus: state, size: .normal) + } + + var body: some View { + HStack(alignment: .center, spacing: 0) { + ProfileName(is_anon: is_anon) + TimeDot() + RelativeTime(time: state.events.get_cache_data(event.id).relative_time) + Spacer() + EventMenuContext(damus: state, event: event) + } + + .lineLimit(1) + } +} + +struct EventTop_Previews: PreviewProvider { + static var previews: some View { + EventTop(state: test_damus_state(), event: test_event, is_anon: false) + } +} diff --git a/damus/Views/Events/Components/RelativeTime.swift b/damus/Views/Events/Components/RelativeTime.swift @@ -0,0 +1,25 @@ +// +// RelativeTime.swift +// damus +// +// Created by William Casarin on 2023-06-01. +// + +import SwiftUI + +struct RelativeTime: View { + @ObservedObject var time: RelativeTimeModel + + var body: some View { + Text(verbatim: "\(time.value)") + .font(.system(size: 16)) + .foregroundColor(.gray) + } +} + + +struct RelativeTime_Previews: PreviewProvider { + static var previews: some View { + RelativeTime(time: RelativeTimeModel()) + } +} diff --git a/damus/Views/Events/ReplyDescription.swift b/damus/Views/Events/Components/ReplyDescription.swift diff --git a/damus/Views/Events/Components/ReplyPart.swift b/damus/Views/Events/Components/ReplyPart.swift @@ -0,0 +1,30 @@ +// +// ReplyPart.swift +// damus +// +// Created by William Casarin on 2023-06-01. +// + +import SwiftUI + +struct ReplyPart: View { + let event: NostrEvent + let privkey: String? + let profiles: Profiles + + var body: some View { + Group { + if event_is_reply(event, privkey: privkey) { + ReplyDescription(event: event, profiles: profiles) + } else { + EmptyView() + } + } + } +} + +struct ReplyPart_Previews: PreviewProvider { + static var previews: some View { + ReplyPart(event: test_event, privkey: nil, profiles: test_damus_state().profiles) + } +} diff --git a/damus/Views/Events/Components/TimeDot.swift b/damus/Views/Events/Components/TimeDot.swift @@ -0,0 +1,22 @@ +// +// TimeDot.swift +// damus +// +// Created by William Casarin on 2023-06-01. +// + +import SwiftUI + +struct TimeDot: View { + var body: some View { + Text(verbatim: "⋅") + .font(.footnote) + .foregroundColor(.gray) + } +} + +struct TimeDot_Previews: PreviewProvider { + static var previews: some View { + TimeDot() + } +} diff --git a/damus/Views/Events/EventBody.swift b/damus/Views/Events/EventBody.swift @@ -23,6 +23,14 @@ struct EventBody: View { } var body: some View { + if event.known_kind == .longform { + let longform = LongformEvent.parse(from: event) + + Text(longform.title ?? "Untitled") + .font(.title) + .padding(.horizontal) + } + NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: size, options: options) .frame(maxWidth: .infinity, alignment: .leading) } diff --git a/damus/Views/Events/EventMenu.swift b/damus/Views/Events/EventMenu.swift @@ -15,6 +15,15 @@ struct EventMenuContext: View { let muted_threads: MutedThreadsManager @ObservedObject var settings: UserSettingsStore + init(damus: DamusState, event: NostrEvent) { + self.event = event + self.keypair = damus.keypair + self.target_pubkey = event.pubkey + self.bookmarks = damus.bookmarks + self.muted_threads = damus.muted_threads + self._settings = ObservedObject(wrappedValue: damus.settings) + } + var body: some View { HStack { Menu { @@ -26,6 +35,7 @@ struct EventMenuContext: View { .foregroundColor(Color.gray) } } + .padding([.bottom], 4) .contentShape(Rectangle()) .onTapGesture {} } diff --git a/damus/Views/Events/EventProfile.swift b/damus/Views/Events/EventProfile.swift @@ -15,6 +15,8 @@ func eventview_pfp_size(_ size: EventViewKind) -> CGFloat { return PFP_SIZE case .selected: return PFP_SIZE + case .title: + return PFP_SIZE case .subheadline: return PFP_SIZE * 0.5 } diff --git a/damus/Views/Events/EventShell.swift b/damus/Views/Events/EventShell.swift @@ -0,0 +1,72 @@ +// +// EventShell.swift +// damus +// +// Created by William Casarin on 2023-06-01. +// + +import SwiftUI + +struct EventShell<Content: View>: View { + let state: DamusState + let event: NostrEvent + let options: EventViewOptions + let content: Content + + init(state: DamusState, event: NostrEvent, options: EventViewOptions, @ViewBuilder content: () -> Content) { + self.state = state + self.event = event + self.options = options + self.content = content() + } + + var has_action_bar: Bool { + !options.contains(.no_action_bar) + } + + func get_mention() -> Mention? { + if self.options.contains(.nested) { + return nil + } + + return first_eref_mention(ev: event, privkey: state.keypair.privkey) + } + + var body: some View { + VStack(alignment: .leading) { + let is_anon = event_is_anonymous(ev: event) + + HStack(spacing: 10) { + MaybeAnonPfpView(state: state, is_anon: is_anon, pubkey: event.pubkey, size: options.contains(.small_pfp) ? eventview_pfp_size(.small) : PFP_SIZE ) + + VStack { + EventTop(state: state, event: event, is_anon: is_anon) + ReplyPart(event: event, privkey: state.keypair.privkey, profiles: state.profiles) + } + } + .padding(.horizontal) + + content + + if !options.contains(.no_mentions), let mention = get_mention() { + + BuilderEventView(damus: state, event_id: mention.ref.id) + .padding(.horizontal) + } + + if has_action_bar { + //EmptyRect + EventActionBar(damus_state: state, event: event) + .padding(.horizontal) + } + } + } +} + +struct EventShell_Previews: PreviewProvider { + static var previews: some View { + EventShell(state: test_damus_state(), event: test_event, options: [.no_action_bar]) { + Text("Hello") + } + } +} diff --git a/damus/Views/Events/Longform/LongformPreview.swift b/damus/Views/Events/Longform/LongformPreview.swift @@ -0,0 +1,49 @@ +// +// 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]) { + VStack(alignment: .leading, spacing: 10) { + Text(event.title ?? "Untitled") + .font(.title) + + Text(event.summary ?? "") + .foregroundColor(.gray) + + if case .loaded(let arts) = artifacts.state, + case .parts(let parts) = arts + { + Words(parts.words).font(.footnote) + } + } + .padding() + } + } +} + +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 @@ -0,0 +1,87 @@ +// +// 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, artifacts: NoteArtifactsModel? = nil) { + self.state = state + self.event = event + self._artifacts = ObservedObject(wrappedValue: artifacts ?? 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) { + + VStack { + SelectableText(attributedString: AttributedString(stringLiteral: event.title ?? "Untitled"), size: .title) + + NoteContentView(damus_state: state, event: event.event, show_images: true, size: .selected, options: options) + } + } + } +} + +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 { + let st = test_damus_state() + let artifacts = render_note_content(ev: test_longform_event.event, profiles: st.profiles, privkey: nil) + + let model = NoteArtifactsModel(state: .loaded(artifacts)) + LongformView(state: st, event: test_longform_event, artifacts: model) + } +} diff --git a/damus/Views/Events/SelectedEventView.swift b/damus/Views/Events/SelectedEventView.swift @@ -35,9 +35,8 @@ struct SelectedEventView: View { Spacer() - EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks, muted_threads: damus.muted_threads, settings: damus.settings) + EventMenuContext(damus: damus, event: event) .padding([.bottom], 4) - } .padding(.horizontal) .minimumScaleFactor(0.75) diff --git a/damus/Views/Events/TextEvent.swift b/damus/Views/Events/TextEvent.swift @@ -19,20 +19,11 @@ struct EventViewOptions: OptionSet { static let small_pfp = EventViewOptions(rawValue: 1 << 7) static let nested = EventViewOptions(rawValue: 1 << 8) static let top_zap = EventViewOptions(rawValue: 1 << 9) + static let no_mentions = EventViewOptions(rawValue: 1 << 10) static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested] } -struct RelativeTime: View { - @ObservedObject var time: RelativeTimeModel - - var body: some View { - Text(verbatim: "\(time.value)") - .font(.system(size: 16)) - .foregroundColor(.gray) - } -} - struct TextEvent: View { let damus: DamusState let event: NostrEvent @@ -73,63 +64,20 @@ struct TextEvent: View { func TopPart(is_anon: Bool) -> some View { HStack(alignment: .center, spacing: 0) { ProfileName(is_anon: is_anon) - TimeDot + TimeDot() RelativeTime(time: self.evdata.relative_time) Spacer() - ContextButton + EventMenuContext(damus: damus, event: event) } .lineLimit(1) } - var ReplyPart: some View { - Group { - if event_is_reply(event, privkey: damus.keypair.privkey) { - ReplyDescription(event: event, profiles: damus.profiles) - } else { - EmptyView() - } - } - } - var WideStyle: some View { - VStack(alignment: .leading) { - let is_anon = event_is_anonymous(ev: event) - - HStack(spacing: 10) { - Pfp(is_anon: is_anon) - VStack { - TopPart(is_anon: is_anon) - ReplyPart - } - } - .padding(.horizontal) - + EventShell(state: damus, event: event, options: options) { EvBody(options: self.options.union(.pad_content)) - - if let mention = get_mention() { - Mention(mention) - .padding(.horizontal) - } - - if has_action_bar { - //EmptyRect - ActionBar - .padding(.horizontal) - } } } - var TimeDot: some View { - Text(verbatim: "⋅") - .font(.footnote) - .foregroundColor(.gray) - } - - var ContextButton: some View { - EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks, muted_threads: damus.muted_threads, settings: damus.settings) - .padding([.bottom], 4) - } - func ProfileName(is_anon: Bool) -> some View { let profile = damus.profiles.lookup(id: pubkey) let pk = is_anon ? ANON_PUBKEY : pubkey @@ -183,7 +131,7 @@ struct TextEvent: View { TopPart(is_anon: is_anon) if !options.contains(.no_replying_to) { - ReplyPart + ReplyPart(event: event, privkey: damus.keypair.privkey, profiles: damus.profiles) } EvBody(options: self.options) diff --git a/damus/Views/Images/MediaView.swift b/damus/Views/Images/MediaView.swift @@ -0,0 +1,50 @@ +// +// MediaView.swift +// damus +// +// Created by William Casarin on 2023-06-05. +// + +import SwiftUI + +/* + struct MediaView: View { + let geo: GeometryProxy + let url: MediaUrl + let index: Int + + var body: some View { + Group { + switch url { + case .image(let url): + Img(geo: geo, url: url, index: index) + .onTapGesture { + open_sheet = true + } + case .video(let url): + DamusVideoPlayer(url: url, model: video_model(url), video_size: $video_size) + .onChange(of: video_size) { size in + guard let size else { return } + + let fill = ImageFill.calculate_image_fill(geo_size: geo.size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight) + + print("video_size changed \(size)") + if self.image_fill == nil { + print("video_size firstImageHeight \(fill.height)") + firstImageHeight = fill.height + state.events.get_cache_data(evid).media_metadata_model.fill = fill + } + + self.image_fill = fill + } + } + } + } + } + + struct MediaView_Previews: PreviewProvider { + static var previews: some View { + MediaView() + } + } + */ diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift @@ -33,8 +33,8 @@ struct NoteContentView: View { @ObservedObject var artifacts_model: NoteArtifactsModel @ObservedObject var preview_model: PreviewModel - var artifacts: NoteArtifacts { - return self.artifacts_model.state.artifacts ?? .just_content(event.get_content(damus_state.keypair.privkey)) + var note_artifacts: NoteArtifacts { + return self.artifacts_model.state.artifacts ?? .separated(.just_content(event.get_content(damus_state.keypair.privkey))) } init(damus_state: DamusState, event: NostrEvent, show_images: Bool, size: EventViewKind, options: EventViewOptions) { @@ -67,27 +67,27 @@ struct NoteContentView: View { return LinkViewRepresentable(meta: .linkmeta(cached)) } - var truncatedText: some View { + func truncatedText(content: CompatibleText) -> some View { Group { if truncate { - TruncatedText(text: artifacts.content) + TruncatedText(text: content) .font(eventviewsize_to_font(size)) } else { - artifacts.content.text + content.text .font(eventviewsize_to_font(size)) } } } - var invoicesView: some View { - InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: artifacts.invoices, settings: damus_state.settings) + func invoicesView(invoices: [Invoice]) -> some View { + InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: invoices, settings: damus_state.settings) } var translateView: some View { TranslateView(damus_state: damus_state, event: event, size: self.size) } - var previewView: some View { + func previewView(links: [URL]) -> some View { Group { if let preview = self.preview, show_images { if let preview_height { @@ -96,14 +96,14 @@ struct NoteContentView: View { } else { preview } - } else if let link = artifacts.links.first { + } else if let link = links.first { LinkViewRepresentable(meta: .url(link)) .frame(height: 50) } } } - var MainContent: some View { + func MainContent(artifacts: NoteArtifactsSeparated) -> some View { VStack(alignment: .leading) { if size == .selected { if with_padding { @@ -114,10 +114,10 @@ struct NoteContentView: View { } } else { if with_padding { - truncatedText + truncatedText(content: artifacts.content) .padding(.horizontal) } else { - truncatedText + truncatedText(content: artifacts.content) } } @@ -143,17 +143,17 @@ struct NoteContentView: View { if artifacts.invoices.count > 0 { if with_padding { - invoicesView + invoicesView(invoices: artifacts.invoices) .padding(.horizontal) } else { - invoicesView + invoicesView(invoices: artifacts.invoices) } } if with_padding { - previewView.padding(.horizontal) + previewView(links: artifacts.links).padding(.horizontal) } else { - previewView + previewView(links: artifacts.links) } } @@ -183,12 +183,42 @@ struct NoteContentView: View { } } + func artifactPartsView(_ parts: [ArtifactPart]) -> some View { + + LazyVStack { + ForEach(parts.indices, id: \.self) { ind in + let part = parts[ind] + switch part { + case .text(let txt): + txt + .padding(.horizontal) + case .invoice(let inv): + InvoiceView(our_pubkey: damus_state.pubkey, invoice: inv, settings: damus_state.settings) + .padding(.horizontal) + case .media(let media): + Text("media \(media.url.absoluteString)") + } + } + } + } + + var ArtifactContent: some View { + Group { + switch self.note_artifacts { + case .parts(let parts): + artifactPartsView(parts.parts) + case .separated(let separated): + MainContent(artifacts: separated) + } + } + } + var body: some View { - MainContent + ArtifactContent .onReceive(handle_notify(.profile_updated)) { notif in let profile = notif.object as! ProfileUpdate let blocks = event.blocks(damus_state.keypair.privkey) - for block in blocks { + for block in blocks.blocks { switch block { case .mention(let m): if m.type == .pubkey && m.ref.ref_id == profile.pubkey { @@ -255,12 +285,58 @@ struct NoteContentView_Previews: PreviewProvider { } } -struct NoteArtifacts: Equatable { - static func == (lhs: NoteArtifacts, rhs: NoteArtifacts) -> Bool { + +enum NoteArtifacts { + case separated(NoteArtifactsSeparated) + case parts(NoteArtifactsParts) + + var images: [URL] { + switch self { + case .separated(let arts): + return arts.images + case .parts(let parts): + return parts.parts.reduce(into: [URL]()) { acc, part in + guard case .media(let m) = part, + case .image(let url) = m + else { return } + + acc.append(url) + } + } + } +} + +enum ArtifactPart { + case text(Text) + case media(MediaUrl) + case invoice(Invoice) + + var is_text: Bool { + switch self { + case .text: return true + case .media: return false + case .invoice: return false + } + } +} + +class NoteArtifactsParts { + var parts: [ArtifactPart] + var words: Int + + init(parts: [ArtifactPart], words: Int) { + self.parts = parts + self.words = words + } +} + +struct NoteArtifactsSeparated: Equatable { + static func == (lhs: NoteArtifactsSeparated, rhs: NoteArtifactsSeparated) -> Bool { return lhs.content == rhs.content } let content: CompatibleText + let words: Int let urls: [UrlType] let invoices: [Invoice] @@ -276,9 +352,9 @@ struct NoteArtifacts: Equatable { return urls.compactMap { url in url.is_link } } - static func just_content(_ content: String) -> NoteArtifacts { + static func just_content(_ content: String) -> NoteArtifactsSeparated { let txt = CompatibleText(attributed: AttributedString(stringLiteral: content)) - return NoteArtifacts(content: txt, urls: [], invoices: []) + return NoteArtifactsSeparated(content: txt, words: 0, urls: [], invoices: []) } } @@ -307,15 +383,119 @@ enum NoteArtifactState { } } +func note_artifact_is_separated(kind: NostrKind?) -> Bool { + return kind != .longform +} + func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> NoteArtifacts { let blocks = ev.blocks(privkey) - return render_blocks(blocks: blocks, profiles: profiles) + if ev.known_kind == .longform { + return .parts(render_blocks_parted(blocks: blocks, profiles: profiles)) + } + + return .separated(render_blocks(blocks: blocks, profiles: profiles)) } -func render_blocks(blocks: [Block], profiles: Profiles) -> NoteArtifacts { +fileprivate func artifact_part_last_text_ind(parts: [ArtifactPart]) -> (Int, Text)? { + let ind = parts.count - 1 + if ind < 0 { + return nil + } + + guard case .text(let txt) = parts[safe: ind] else { + return nil + } + + return (ind, txt) +} + +func render_blocks_parted(blocks bs: Blocks, profiles: Profiles) -> NoteArtifactsParts { + let blocks = bs.blocks + + let new_parts = NoteArtifactsParts(parts: [], words: bs.words) + + return blocks.reduce(into: new_parts) { parts, block in + + switch block { + case .mention(let m): + guard let (last_ind, txt) = artifact_part_last_text_ind(parts: parts.parts) else { + parts.parts.append(.text(mention_str(m, profiles: profiles).text)) + return + } + parts.parts[last_ind] = .text(txt + mention_str(m, profiles: profiles).text) + + case .text(let str): + guard let (last_ind, txt) = artifact_part_last_text_ind(parts: parts.parts) else { + // TODO: (jb55) md is longform specific + let md = Markdown.parse(content: str) + parts.parts.append(.text(Text(md))) + return + } + + parts.parts[last_ind] = .text(txt + Text(str)) + + case .relay(let relay): + guard let (last_ind, txt) = artifact_part_last_text_ind(parts: parts.parts) else { + parts.parts.append(.text(Text(relay))) + return + } + + parts.parts[last_ind] = .text(txt + Text(relay)) + + case .hashtag(let htag): + guard let (last_ind, txt) = artifact_part_last_text_ind(parts: parts.parts) else { + parts.parts.append(.text(hashtag_str(htag).text)) + return + } + + parts.parts[last_ind] = .text(txt + hashtag_str(htag).text) + + case .invoice(let invoice): + parts.parts.append(.invoice(invoice)) + return + + case .url(let url): + let url_type = classify_url(url) + switch url_type { + case .media(let media_url): + parts.parts.append(.media(media_url)) + case .link(let url): + guard let (last_ind, txt) = artifact_part_last_text_ind(parts: parts.parts) else { + parts.parts.append(.text(url_str(url).text)) + return + } + + parts.parts[last_ind] = .text(txt + url_str(url).text) + } + } + } +} + +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) -> NoteArtifactsSeparated { var invoices: [Invoice] = [] var urls: [UrlType] = [] + let blocks = bs.blocks let one_note_ref = blocks .filter({ $0.is_note_mention }) @@ -332,22 +512,8 @@ func render_blocks(blocks: [Block], 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) - } - - 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 + reduce_text_block(blocks: blocks, ind: ind, txt: txt, one_note_ref: one_note_ref) - return str + CompatibleText(stringLiteral: trimmed) case .relay(let relay): return str + CompatibleText(stringLiteral: relay) @@ -369,7 +535,7 @@ func render_blocks(blocks: [Block], profiles: Profiles) -> NoteArtifacts { } } - return NoteArtifacts(content: txt, urls: urls, invoices: invoices) + return NoteArtifactsSeparated(content: txt, words: bs.words, urls: urls, invoices: invoices) } enum MediaUrl { diff --git a/damus/Views/Reposts/RepostedEvent.swift b/damus/Views/Reposts/RepostedEvent.swift @@ -24,7 +24,7 @@ struct RepostedEvent: View { .buttonStyle(PlainButtonStyle()) //SelectedEventView(damus: damus, event: inner_ev, size: .normal) - TextEvent(damus: damus, event: inner_ev, pubkey: inner_ev.pubkey, options: options) + EventView(damus: damus, event: inner_ev, pubkey: inner_ev.pubkey, options: options) } } } diff --git a/damusTests/InvoiceTests.swift b/damusTests/InvoiceTests.swift @@ -20,7 +20,7 @@ final class InvoiceTests: XCTestCase { func testParseAnyAmountInvoice() throws { let invstr = "LNBC1P3MR5UJSP5G7SA48YD4JWTTPCHWMY4QYN4UWZQCJQ8NMWKD6QE3HCRVYTDLH9SPP57YM9TSA9NN4M4XU59XMJCXKR7YDV29DDP6LVQUT46ZW6CU3KE9GQDQ9V9H8JXQ8P3MYLZJCQPJRZJQF60PZDVNGGQWQDNERZSQN35L8CVQ3QG2Z5NSZYD0D3Q0JW2TL6VUZA7FYQQWKGQQYQQQQLGQQQQXJQQ9Q9QXPQYSGQ39EM4QJMQFKZGJXZVGL7QJMYNSWA8PGDTAGXXRG5Z92M7VLCGKQK2L2THDF8LM0AUKAURH7FVAWDLRNMVF38W4EYJDNVN9V4Z9CRS5CQCV465C" - let parsed = parse_mentions(content: invstr, tags: []) + let parsed = parse_mentions(content: invstr, tags: []).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 1) @@ -38,7 +38,7 @@ final class InvoiceTests: XCTestCase { let invstr = """ LNBC1P3MR5UJSP5G7SA48YD4JWTTPCHWMY4QYN4UWZQCJQ8NMWKD6QE3HCRVYTDLH9SPP57YM9TSA9NN4M4XU59XMJCXKR7YDV29DDP6LVQUT46ZW6CU3KE9GQDQ9V9H8JXQ8P3MYLZJCQPJRZJQF60PZDVNGGQWQDNERZSQN35L8CVQ3QG2Z5NSZYD0D3Q0JW2TL6VUZA7FYQQWKGQQYQQQQLGQQQQXJQQ9Q9QXPQYSGQ39EM4QJMQFKZGJXZVGL7QJMYNSWA8PGDTAGXXRG5Z92M7VLCGKQK2L2THDF8LM0AUKAURH7FVAWDLRNMVF38W4EYJDNVN9V4Z9CRS5CQCV465C hi there """ - let parsed = parse_mentions(content: invstr, tags: []) + let parsed = parse_mentions(content: invstr, tags: []).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 2) @@ -54,7 +54,7 @@ LNBC1P3MR5UJSP5G7SA48YD4JWTTPCHWMY4QYN4UWZQCJQ8NMWKD6QE3HCRVYTDLH9SPP57YM9TSA9NN func testParseInvoiceUpper() throws { let invstr = "LNBC100N1P357SL0SP5T9N56WDZTUN39LGDQLR30XQWKSG3K69Q4Q2RKR52APLUJW0ESN0QPP5MRQGLJK62Z20Q4NVGR6LZCYN6FHYLZCCWDVU4K77APG3ZMRKUJJQDPZW35XJUEQD9EJQCFQV3JHXCMJD9C8G6T0DCXQYJW5QCQPJRZJQT56H4GVP5YX36U2UZQA6QWCSK3E2DUUNFXPPZJ9VHYPC3WFE2WSWZ607UQQ3XQQQSQQQQQQQQQQQLQQYG9QYYSGQAGX5H20AEULJ3GDWX3KXS8U9F4MCAKDKWUAKASAMM9562FFYR9EN8YG20LG0YGNR9ZPWP68524KMDA0T5XP2WYTEX35PU8HAPYJAJXQPSQL29R" - let parsed = parse_mentions(content: invstr, tags: []) + let parsed = parse_mentions(content: invstr, tags: []).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 1) @@ -70,7 +70,7 @@ LNBC1P3MR5UJSP5G7SA48YD4JWTTPCHWMY4QYN4UWZQCJQ8NMWKD6QE3HCRVYTDLH9SPP57YM9TSA9NN func testParseInvoiceWithPrefix() throws { let invstr = "lightning:lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r" - let parsed = parse_mentions(content: invstr, tags: []) + let parsed = parse_mentions(content: invstr, tags: []).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 1) @@ -79,7 +79,7 @@ LNBC1P3MR5UJSP5G7SA48YD4JWTTPCHWMY4QYN4UWZQCJQ8NMWKD6QE3HCRVYTDLH9SPP57YM9TSA9NN func testParseInvoiceWithPrefixCapitalized() throws { let invstr = "LIGHTNING:LNBC100N1P357SL0SP5T9N56WDZTUN39LGDQLR30XQWKSG3K69Q4Q2RKR52APLUJW0ESN0QPP5MRQGLJK62Z20Q4NVGR6LZCYN6FHYLZCCWDVU4K77APG3ZMRKUJJQDPZW35XJUEQD9EJQCFQV3JHXCMJD9C8G6T0DCXQYJW5QCQPJRZJQT56H4GVP5YX36U2UZQA6QWCSK3E2DUUNFXPPZJ9VHYPC3WFE2WSWZ607UQQ3XQQQSQQQQQQQQQQQLQQYG9QYYSGQAGX5H20AEULJ3GDWX3KXS8U9F4MCAKDKWUAKASAMM9562FFYR9EN8YG20LG0YGNR9ZPWP68524KMDA0T5XP2WYTEX35PU8HAPYJAJXQPSQL29R" - let parsed = parse_mentions(content: invstr, tags: []) + let parsed = parse_mentions(content: invstr, tags: []).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 1) @@ -88,7 +88,7 @@ LNBC1P3MR5UJSP5G7SA48YD4JWTTPCHWMY4QYN4UWZQCJQ8NMWKD6QE3HCRVYTDLH9SPP57YM9TSA9NN func testParseInvoice() throws { let invstr = "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r" - let parsed = parse_mentions(content: invstr, tags: []) + let parsed = parse_mentions(content: invstr, tags: []).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 1) diff --git a/damusTests/MarkdownTests.swift b/damusTests/MarkdownTests.swift @@ -33,6 +33,18 @@ class MarkdownTests: XCTestCase { let expected = try AttributedString(markdown: "prologue [damus.io](https://damus.io) epilogue", options: md_opts) XCTAssertEqual(md, expected) } + + func test_longform_rendering() throws { + let st = test_damus_state() + let artifacts = render_note_content(ev: test_longform_event.event, profiles: st.profiles, privkey: st.keypair.privkey) + + switch artifacts { + case .separated: + XCTAssert(false) + case .parts(let parts): + XCTAssertEqual(parts.parts.count, 1) + } + } func test_convert_links() throws { let helper = Markdown() diff --git a/damusTests/NIP19Tests.swift b/damusTests/NIP19Tests.swift @@ -19,7 +19,7 @@ final class NIP19Tests: XCTestCase { } func test_parse_nprofile() throws { - let res = parse_mentions(content: "nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p", tags: []) + let res = parse_mentions(content: "nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p", tags: []).blocks XCTAssertEqual(res.count, 1) let expected_ref = ReferencedId(ref_id: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", relay_id: "wss://r.x.com", key: "p") let expected_mention = Mention(index: nil, type: .pubkey, ref: expected_ref) @@ -27,7 +27,7 @@ final class NIP19Tests: XCTestCase { } func test_parse_npub() throws { - let res = parse_mentions(content: "nostr:npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg ", tags: []) + let res = parse_mentions(content: "nostr:npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg ", tags: []).blocks XCTAssertEqual(res.count, 2) let expected_ref = ReferencedId(ref_id: "7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e", relay_id: nil, key: "p") let expected_mention = Mention(index: nil, type: .pubkey, ref: expected_ref) @@ -35,7 +35,7 @@ final class NIP19Tests: XCTestCase { } func test_parse_note() throws { - let res = parse_mentions(content: " nostr:note1s4p70596lv50x0zftuses32t6ck8x6wgd4edwacyetfxwns2jtysux7vep", tags: []) + let res = parse_mentions(content: " nostr:note1s4p70596lv50x0zftuses32t6ck8x6wgd4edwacyetfxwns2jtysux7vep", tags: []).blocks XCTAssertEqual(res.count, 2) let expected_ref = ReferencedId(ref_id: "8543e7d0bafb28f33c495f2198454bd62c7369c86d72d77704cad2674e0a92c9", relay_id: nil, key: "e") let expected_mention = Mention(index: nil, type: .event, ref: expected_ref) @@ -43,7 +43,7 @@ final class NIP19Tests: XCTestCase { } func test_mention_with_adjacent() throws { - let res = parse_mentions(content: " nostr:note1s4p70596lv50x0zftuses32t6ck8x6wgd4edwacyetfxwns2jtysux7vep?", tags: []) + let res = parse_mentions(content: " nostr:note1s4p70596lv50x0zftuses32t6ck8x6wgd4edwacyetfxwns2jtysux7vep?", tags: []).blocks XCTAssertEqual(res.count, 3) let expected_ref = ReferencedId(ref_id: "8543e7d0bafb28f33c495f2198454bd62c7369c86d72d77704cad2674e0a92c9", relay_id: nil, key: "e") let expected_mention = Mention(index: nil, type: .event, ref: expected_ref) diff --git a/damusTests/ReplyTests.swift b/damusTests/ReplyTests.swift @@ -21,7 +21,7 @@ class ReplyTests: XCTestCase { func testMentionIsntReply() throws { let content = "this is #[0] a mention" let tags = [["e", "event_id"]] - let blocks = parse_mentions(content: content, tags: tags) + let blocks = parse_mentions(content: content, tags: tags).blocks let event_refs = interpret_event_refs(blocks: blocks, tags: tags) XCTAssertEqual(event_refs.count, 1) @@ -96,7 +96,7 @@ class ReplyTests: XCTestCase { func testRootReplyWithMention() throws { let content = "this is #[1] a mention" let tags = [["e", "thread_id"], ["e", "mentioned_id"]] - let blocks = parse_mentions(content: content, tags: tags) + let blocks = parse_mentions(content: content, tags: tags).blocks let event_refs = interpret_event_refs(blocks: blocks, tags: tags) XCTAssertEqual(event_refs.count, 2) @@ -114,7 +114,7 @@ class ReplyTests: XCTestCase { func testEmptyMention() throws { let content = "this is some & content" let tags: [[String]] = [] - let blocks = parse_mentions(content: content, tags: tags) + let blocks = parse_mentions(content: content, tags: tags).blocks let post_blocks = parse_post_blocks(content: content) let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags, silent_mentions: false) let event_refs = interpret_event_refs(blocks: blocks, tags: tags) @@ -148,7 +148,7 @@ class ReplyTests: XCTestCase { func testManyMentions() throws { let content = "#[10]" let tags: [[String]] = [[],[],[],[],[],[],[],[],[],[],["p", "3e999f94e2cb34ef44a64b351141ac4e51b5121b2d31aed4a6c84602a1144692"]] - let blocks = parse_mentions(content: content, tags: tags) + let blocks = parse_mentions(content: content, tags: tags).blocks let mentions = blocks.filter { $0.is_mention } XCTAssertEqual(mentions.count, 1) } @@ -156,7 +156,7 @@ class ReplyTests: XCTestCase { func testThreadedReply() throws { let content = "this is some content" let tags = [["e", "thread_id"], ["e", "reply_id"]] - let blocks = parse_mentions(content: content, tags: tags) + let blocks = parse_mentions(content: content, tags: tags).blocks let event_refs = interpret_event_refs(blocks: blocks, tags: tags) XCTAssertEqual(event_refs.count, 2) @@ -172,7 +172,7 @@ class ReplyTests: XCTestCase { func testRootReply() throws { let content = "this is a reply" let tags = [["e", "thread_id"]] - let blocks = parse_mentions(content: content, tags: tags) + let blocks = parse_mentions(content: content, tags: tags).blocks let event_refs = interpret_event_refs(blocks: blocks, tags: tags) XCTAssertEqual(event_refs.count, 1) @@ -186,14 +186,14 @@ class ReplyTests: XCTestCase { func testNoReply() throws { let content = "this is a #[0] reply" - let blocks = parse_mentions(content: content, tags: []) + let blocks = parse_mentions(content: content, tags: []).blocks let event_refs = interpret_event_refs(blocks: blocks, tags: []) XCTAssertEqual(event_refs.count, 0) } func testParseMention() throws { - let parsed = parse_mentions(content: "this is #[0] a mention", tags: [["e", "event_id"]]) + let parsed = parse_mentions(content: "this is #[0] a mention", tags: [["e", "event_id"]]).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 3) @@ -522,7 +522,7 @@ class ReplyTests: XCTestCase { } func testParseInvalidMention() throws { - let parsed = parse_mentions(content: "this is #[0] a mention", tags: []) + let parsed = parse_mentions(content: "this is #[0] a mention", tags: []).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 3) diff --git a/damusTests/damusTests.swift b/damusTests/damusTests.swift @@ -71,7 +71,7 @@ class damusTests: XCTestCase { [my website](https://jb55.com) """ - let parsed = parse_mentions(content: md, tags: []) + let parsed = parse_mentions(content: md, tags: []).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 1) @@ -79,7 +79,7 @@ class damusTests: XCTestCase { } func testParseUrlUpper() { - let parsed = parse_mentions(content: "a HTTPS://jb55.COM b", tags: []) + let parsed = parse_mentions(content: "a HTTPS://jb55.COM b", tags: []).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 3) @@ -114,7 +114,7 @@ class damusTests: XCTestCase { } func testParseUrl() { - let parsed = parse_mentions(content: "a https://jb55.com b", tags: []) + let parsed = parse_mentions(content: "a https://jb55.com b", tags: []).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 3) @@ -122,7 +122,7 @@ class damusTests: XCTestCase { } func testParseUrlEnd() { - let parsed = parse_mentions(content: "a https://jb55.com", tags: []) + let parsed = parse_mentions(content: "a https://jb55.com", tags: []).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 2) @@ -131,7 +131,7 @@ class damusTests: XCTestCase { } func testParseUrlStart() { - let parsed = parse_mentions(content: "https://jb55.com br", tags: []) + let parsed = parse_mentions(content: "https://jb55.com br", tags: []).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 2) @@ -141,7 +141,7 @@ class damusTests: XCTestCase { func testNoParseUrlWithOnlyWhitespace() { let testString = "https:// " - let parsed = parse_mentions(content: testString, tags: []) + let parsed = parse_mentions(content: testString, tags: []).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed[0].is_text, testString) @@ -149,14 +149,14 @@ class damusTests: XCTestCase { func testNoParseUrlTrailingCharacters() { let testString = "https://foo.bar, " - let parsed = parse_mentions(content: testString, tags: []) + let parsed = parse_mentions(content: testString, tags: []).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed[0].is_url?.absoluteString, "https://foo.bar") } func testParseMentionBlank() { - let parsed = parse_mentions(content: "", tags: [["e", "event_id"]]) + let parsed = parse_mentions(content: "", tags: [["e", "event_id"]]).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 0) @@ -178,7 +178,7 @@ class damusTests: XCTestCase { } func testParseHashtag() { - let parsed = parse_mentions(content: "some hashtag #bitcoin derp", tags: []) + let parsed = parse_mentions(content: "some hashtag #bitcoin derp", tags: []).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 3) @@ -188,7 +188,7 @@ class damusTests: XCTestCase { } func testHashtagWithComma() { - let parsed = parse_mentions(content: "some hashtag #bitcoin, cool", tags: []) + let parsed = parse_mentions(content: "some hashtag #bitcoin, cool", tags: []).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 3) @@ -198,7 +198,7 @@ class damusTests: XCTestCase { } func testHashtagWithEmoji() { - let parsed = parse_mentions(content: "some hashtag #bitcoin☕️ cool", tags: []) + let parsed = parse_mentions(content: "some hashtag #bitcoin☕️ cool", tags: []).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 3) @@ -208,7 +208,7 @@ class damusTests: XCTestCase { } func testParseHashtagEnd() { - let parsed = parse_mentions(content: "some hashtag #bitcoin", tags: []) + let parsed = parse_mentions(content: "some hashtag #bitcoin", tags: []).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 2) @@ -217,7 +217,7 @@ class damusTests: XCTestCase { } func testParseMentionOnlyText() { - let parsed = parse_mentions(content: "there is no mention here", tags: [["e", "event_id"]]) + let parsed = parse_mentions(content: "there is no mention here", tags: [["e", "event_id"]]).blocks XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 1)