diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index b79b8a611..c44e045f0 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -38,6 +38,9 @@ 3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; }; 3AFE89C32BD4156F00AD31EF /* MCEmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 3AFE89C22BD4156F00AD31EF /* MCEmojiPicker */; }; 3CCD1E6A2A874C4E0099A953 /* Nip98HTTPAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */; }; + 4C011B5E2BD0A56A002F2F9B /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B5C2BD0A56A002F2F9B /* ChatView.swift */; }; + 4C011B5F2BD0A56A002F2F9B /* ChatroomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B5D2BD0A56A002F2F9B /* ChatroomView.swift */; }; + 4C011B612BD0B25C002F2F9B /* ReplyQuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B602BD0B25C002F2F9B /* ReplyQuoteView.swift */; }; 4C06670128FC7C5900038D2A /* RelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670028FC7C5900038D2A /* RelayView.swift */; }; 4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C06670328FC7EC500038D2A /* Kingfisher */; }; 4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670528FCB08600038D2A /* ImageCarousel.swift */; }; @@ -98,6 +101,7 @@ 4C2B10282A7B0F5C008AA43E /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2B10272A7B0F5C008AA43E /* Log.swift */; }; 4C2B7BF22A71B6540049DEE7 /* Id.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2B7BF12A71B6540049DEE7 /* Id.swift */; }; 4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */; }; + 4C2D34412BDAF1B300F9FB44 /* NIP10Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */; }; 4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */; }; 4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7329A5680900E2BD5A /* EventGroupView.swift */; }; 4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */; }; @@ -822,6 +826,9 @@ 3AF6336929884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = ""; }; 3AF6336A29884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-PT"; path = "pt-PT.lproj/Localizable.stringsdict"; sourceTree = ""; }; 3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nip98HTTPAuth.swift; sourceTree = ""; }; + 4C011B5C2BD0A56A002F2F9B /* ChatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = ""; }; + 4C011B5D2BD0A56A002F2F9B /* ChatroomView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatroomView.swift; sourceTree = ""; }; + 4C011B602BD0B25C002F2F9B /* ReplyQuoteView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyQuoteView.swift; sourceTree = ""; }; 4C06670028FC7C5900038D2A /* RelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayView.swift; sourceTree = ""; }; 4C06670528FCB08600038D2A /* ImageCarousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCarousel.swift; sourceTree = ""; }; 4C06670828FDE64700038D2A /* damus-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "damus-Bridging-Header.h"; sourceTree = ""; }; @@ -885,6 +892,7 @@ 4C2B10272A7B0F5C008AA43E /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; 4C2B7BF12A71B6540049DEE7 /* Id.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Id.swift; sourceTree = ""; }; 4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = ""; }; + 4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP10Tests.swift; sourceTree = ""; }; 4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; 4C30AC7329A5680900E2BD5A /* EventGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroupView.swift; sourceTree = ""; }; 4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemView.swift; sourceTree = ""; }; @@ -1986,6 +1994,9 @@ 4C75EFA227FA576C0006080F /* Views */ = { isa = PBXGroup; children = ( + 4C011B602BD0B25C002F2F9B /* ReplyQuoteView.swift */, + 4C011B5D2BD0A56A002F2F9B /* ChatroomView.swift */, + 4C011B5C2BD0A56A002F2F9B /* ChatView.swift */, D71AC4CA2BA8E3320076268E /* Extensions */, BA3759952ABCCF360018D73B /* Camera */, F71694E82A66221E001F4053 /* Onboarding */, @@ -2555,6 +2566,7 @@ D7CBD1D52B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift */, D72927AC2BAB515C00F93E90 /* RelayURLTests.swift */, D7831AF72BBE11E2005DA780 /* VideoCacheTests.swift */, + 4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */, ); path = damusTests; sourceTree = ""; @@ -3226,6 +3238,7 @@ F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */, 4CC14FEF2A73FCCB007AEB17 /* IdType.swift in Sources */, 4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */, + 4C011B612BD0B25C002F2F9B /* ReplyQuoteView.swift in Sources */, D71AC4CC2BA8E3480076268E /* VisibilityTracker.swift in Sources */, 4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */, 4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */, @@ -3313,6 +3326,7 @@ 4C64305C2A945AFF00B0C0E9 /* MusicController.swift in Sources */, 5053ACA72A56DF3B00851AE3 /* DeveloperSettingsView.swift in Sources */, F79C7FAD29D5E9620000F946 /* EditPictureControl.swift in Sources */, + 4C011B5F2BD0A56A002F2F9B /* ChatroomView.swift in Sources */, 4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */, 4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */, 4C1A9A2A29DDF54400516EAC /* DamusVideoPlayer.swift in Sources */, @@ -3483,6 +3497,7 @@ 4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */, 4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */, D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */, + 4C011B5E2BD0A56A002F2F9B /* ChatView.swift in Sources */, 4C32B95A2A9AD44700DC3548 /* Verifiable.swift in Sources */, 4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */, 501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */, @@ -3517,6 +3532,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4C2D34412BDAF1B300F9FB44 /* NIP10Tests.swift in Sources */, 4C684A572A7FFAE6005E6031 /* UrlTests.swift in Sources */, 3A90B1832A4EA3C600000D94 /* UserSearchCacheTests.swift in Sources */, 4C9B0DEE2A65A75F00CBDA21 /* AttrStringTestExtensions.swift in Sources */, diff --git a/damus/ContentParsing.swift b/damus/ContentParsing.swift index 96de61623..0b7f03d7f 100644 --- a/damus/ContentParsing.swift +++ b/damus/ContentParsing.swift @@ -76,27 +76,33 @@ func interpret_event_refs_ndb(blocks: [Block], tags: TagsSequence) -> [EventRef] } func interp_event_refs_without_mentions_ndb(_ ev_tags: References) -> [EventRef] { - - var count = 0 var evrefs: [EventRef] = [] var first: Bool = true - var first_ref: NoteRef? = nil + var root_id: NoteRef? = nil for ref in ev_tags { - if first { - first_ref = ref - evrefs.append(.thread_id(ref)) - first = false + if let marker = ref.marker { + switch marker { + case .root: root_id = ref + case .reply: evrefs.append(.reply(ref)) + case .mention: evrefs.append(.mention(.noteref(ref))) + } } else { - - evrefs.append(.reply(ref)) + if first { + root_id = ref + first = false + } else { + evrefs.append(.reply(ref)) + } } - count += 1 } - if let first_ref, count == 1 { - let r = first_ref - return [.reply_to_root(r)] + if let root_id { + if evrefs.count == 0 { + return [.reply_to_root(root_id)] + } else { + evrefs.append(.thread_id(root_id)) + } } return evrefs diff --git a/damus/Models/EventRef.swift b/damus/Models/EventRef.swift index 50d9b0a33..12c36df6e 100644 --- a/damus/Models/EventRef.swift +++ b/damus/Models/EventRef.swift @@ -13,6 +13,15 @@ enum EventRef: Equatable { case reply(NoteRef) case reply_to_root(NoteRef) + var note_ref: NoteRef { + switch self { + case .mention(let mnref): return mnref.ref + case .thread_id(let ref): return ref + case .reply(let ref): return ref + case .reply_to_root(let ref): return ref + } + } + var is_mention: NoteRef? { if case .mention(let m) = self { return m.ref } return nil diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift index c105d34c6..bacd87557 100644 --- a/damus/Models/ThreadModel.swift +++ b/damus/Models/ThreadModel.swift @@ -20,7 +20,18 @@ class ThreadModel: ObservableObject { self.original_event = event add_event(event, keypair: damus_state.keypair) } - + + func events() -> [NostrEvent] { + return Array(event_map).sorted(by: { a, b in + if a.created_at == b.created_at { + return false + } else if a.created_at < b.created_at { + return true + } + return false + }) + } + var is_original: Bool { return original_event.id == event.id } diff --git a/damus/Models/VideoCache.swift b/damus/Models/VideoCache.swift index 1931a0b52..538fbad0f 100644 --- a/damus/Models/VideoCache.swift +++ b/damus/Models/VideoCache.swift @@ -8,8 +8,8 @@ import Foundation import CryptoKit import AVKit -// Default expiry time of only 1 day to prevent using too much storage -fileprivate let DEFAULT_EXPIRY_TIME: TimeInterval = 60*60*24 +// Default expiry time of only 2 hours to prevent using too much storage +fileprivate let DEFAULT_EXPIRY_TIME: TimeInterval = 60*60*2 // Default cache directory is in the system-provided caches directory, so that the operating system can delete files when it needs storage space // (https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html) fileprivate let DEFAULT_CACHE_DIRECTORY_PATH: URL? = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("video_cache") diff --git a/damus/Util/EventCache.swift b/damus/Util/EventCache.swift index 3f6d54192..b81994b5c 100644 --- a/damus/Util/EventCache.swift +++ b/damus/Util/EventCache.swift @@ -97,10 +97,10 @@ class EventCache { // TODO: remove me and change code to use ndb directly private let ndb: Ndb private var events: [NoteId: NostrEvent] = [:] - private var replies = ReplyMap() private var cancellable: AnyCancellable? private var image_metadata: [String: ImageMetadataState] = [:] // lowercased URL key private var event_data: [NoteId: EventData] = [:] + var replies = ReplyMap() //private var thread_latest: [String: Int64] @@ -187,7 +187,7 @@ class EventCache { replies.add(id: reply, reply_id: ev.id) } } - + func child_events(event: NostrEvent) -> [NostrEvent] { guard let xs = replies.lookup(event.id) else { return [] diff --git a/damus/Util/Router.swift b/damus/Util/Router.swift index b737446bd..c1c25f194 100644 --- a/damus/Util/Router.swift +++ b/damus/Util/Router.swift @@ -93,7 +93,8 @@ enum Route: Hashable { case .FirstAidSettings(settings: let settings): FirstAidSettingsView(damus_state: damusState, settings: settings) case .Thread(let thread): - ThreadView(state: damusState, thread: thread) + ChatroomView(damus: damusState, thread: thread) + //ThreadView(state: damusState, thread: thread) case .Reposts(let reposts): RepostsView(damus_state: damusState, model: reposts) case .QuoteReposts(let quote_reposts): diff --git a/damus/Views/ChatView.swift b/damus/Views/ChatView.swift new file mode 100644 index 000000000..a2b38585e --- /dev/null +++ b/damus/Views/ChatView.swift @@ -0,0 +1,177 @@ +// +// ChatView.swift +// damus +// +// Created by William Casarin on 2022-04-19. +// + +import SwiftUI + +struct ChatView: View { + let event: NostrEvent + let prev_ev: NostrEvent? + let next_ev: NostrEvent? + + let damus_state: DamusState + var thread: ThreadModel + + @State var expand_reply: Bool = false + + var just_started: Bool { + return prev_ev == nil || prev_ev!.pubkey != event.pubkey + } + + func next_replies_to_this() -> Bool { + guard let next = next_ev else { + return false + } + + return damus_state.events.replies.lookup(next.id) != nil + } + + func is_reply_to_prev(ref_id: NoteId) -> Bool { + guard let prev = prev_ev else { + return true + } + + if let rep = damus_state.events.replies.lookup(event.id) { + return rep.contains(prev.id) + } + + return false + } + + var is_active: Bool { + return thread.event.id == event.id + } + + func prev_reply_is_same() -> NoteId? { + return damus.prev_reply_is_same(event: event, prev_ev: prev_ev, replies: damus_state.events.replies) + } + + func reply_is_new() -> NoteId? { + guard let prev = self.prev_ev else { + // if they are both null they are the same? + return nil + } + + if damus_state.events.replies.lookup(prev.id) != damus_state.events.replies.lookup(event.id) { + return prev.id + } + + return nil + } + + @Environment(\.colorScheme) var colorScheme + + var disable_animation: Bool { + self.damus_state.settings.disable_animation + } + + var options: EventViewOptions { + if expand_reply { + return [.no_previews, .no_action_bar] + } else { + return [.no_previews, .no_action_bar, .truncate_content] + } + } + + var body: some View { + HStack { + VStack { + if is_active || just_started { + ProfilePicView(pubkey: event.pubkey, size: 32, highlight: is_active ? .main : .none, profiles: damus_state.profiles, disable_animation: disable_animation) + } + + Spacer() + } + .frame(maxWidth: 32) + + Group { + VStack(alignment: .leading) { + HStack { + ProfileName(pubkey: event.pubkey, damus: damus_state) + .foregroundColor(colorScheme == .dark ? id_to_color(event.pubkey) : Color.black) + //.shadow(color: Color.black, radius: 2) + Text(verbatim: "\(format_relative_time(event.created_at))") + .foregroundColor(.gray) + } + + if let replying_to = event.direct_replies(damus_state.keypair).first, + let prev = self.prev_ev, + replying_to != prev.id + { + //if !is_reply_to_prev(ref_id) { + ReplyQuoteView(keypair: damus_state.keypair, quoter: event, event_id: replying_to, state: damus_state, thread: thread, options: options) + .onTapGesture { + expand_reply = !expand_reply + } + } + + let blur_images = should_blur_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey) + NoteContentView(damus_state: damus_state, event: event, blur_images: blur_images, size: .normal, options: []) + + if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey { + let bar = make_actionbar_model(ev: event.id, damus: damus_state) + EventActionBar(damus_state: damus_state, event: event, bar: bar) + } + + //Spacer() + } + .padding(6) + } + .background(Color.secondary.opacity(0.1)) + .cornerRadius(8.0) + + //.border(Color.red) + } + .contentShape(Rectangle()) + .id(event.id) + //.frame(minHeight: just_started ? PFP_SIZE : 0) + .padding([.bottom], 6) + //.border(Color.green) + + } +} + +extension Notification.Name { + static var toggle_thread_view: Notification.Name { + return Notification.Name("convert_to_thread") + } +} + + +/* +struct ChatView_Previews: PreviewProvider { + static var previews: some View { + ChatView() + } +} + +*/ + + +func prev_reply_is_same(event: NostrEvent, prev_ev: NostrEvent?, replies: ReplyMap) -> NoteId? { + if let prev = prev_ev { + if let prev_reply_id = replies.lookup(prev.id) { + if let cur_reply_id = replies.lookup(event.id) { + if prev_reply_id != cur_reply_id { + return cur_reply_id.first + } + } + } + } + return nil +} + + +func id_to_color(_ pubkey: Pubkey) -> Color { + return Color( + .sRGB, + red: Double(pubkey.id[0]) / 255, + green: Double(pubkey.id[1]) / 255, + blue: Double(pubkey.id[2]) / 255, + opacity: 1 + ) + +} diff --git a/damus/Views/ChatroomView.swift b/damus/Views/ChatroomView.swift new file mode 100644 index 000000000..8eb41fa7b --- /dev/null +++ b/damus/Views/ChatroomView.swift @@ -0,0 +1,90 @@ +// +// ChatroomView.swift +// damus +// +// Created by William Casarin on 2022-04-19. +// + +import SwiftUI + +struct ChatroomView: View { + @Environment(\.dismiss) var dismiss + @State var once: Bool = false + let damus: DamusState + @ObservedObject var thread: ThreadModel + + var body: some View { + ScrollViewReader { scroller in + ScrollView(.vertical) { + LazyVStack(alignment: .leading) { + let events = thread.events() + let count = events.count + ForEach(Array(zip(events, events.indices)), id: \.0.id) { (ev, ind) in + ChatView(event: events[ind], + prev_ev: ind > 0 ? events[ind-1] : nil, + next_ev: ind == count-1 ? nil : events[ind+1], + damus_state: damus, + thread: thread + ) + /* + .contextMenu{MenuItems(event: ev, keypair: damus.keypair, target_pubkey: ev.pubkey, profileModel: ProfileModel(pubkey: ev.pubkey, damus: damus))} + */ + .onTapGesture { + if thread.event.id == ev.id { + //dismiss() + toggle_thread_view() + } else { + //thread.set_active_event(ev, privkey: damus.keypair.privkey) + } + } + } + + } + .padding(.horizontal) + .padding(.top) + + EndBlock() + } + /* + .onReceive(NotificationCenter.default.publisher(for: .select_quote)) { notif in + let ev = notif.object as! NostrEvent + if ev.id != thread.event.id { + thread.set_active_event(ev, privkey: damus.keypair.privkey) + } + scroll_to_event(scroller: scroller, id: ev.id, delay: 0, animate: true) + } + .onChange(of: thread.loading) { _ in + guard !thread.loading && !once else { + return + } + scroll_after_load(thread: thread, proxy: scroller) + once = true + } + */ + .onAppear() { + thread.subscribe() + scroll_to_event(scroller: scroller, id: thread.event.id, delay: 0.1, animate: false) + } + .onDisappear() { + thread.unsubscribe() + } + } + } + + func toggle_thread_view() { + NotificationCenter.default.post(name: .toggle_thread_view, object: nil) + } +} + + + + +struct ChatroomView_Previews: PreviewProvider { + static var previews: some View { + ChatroomView(damus: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state)) + } +} + +func scroll_after_load(thread: ThreadModel, proxy: ScrollViewProxy) { + scroll_to_event(scroller: proxy, id: thread.event.id, delay: 0.1, animate: false) +} diff --git a/damus/Views/Events/TextEvent.swift b/damus/Views/Events/TextEvent.swift index c1bedb6dc..8834d50f4 100644 --- a/damus/Views/Events/TextEvent.swift +++ b/damus/Views/Events/TextEvent.swift @@ -21,9 +21,10 @@ struct EventViewOptions: OptionSet { static let no_mentions = EventViewOptions(rawValue: 1 << 9) static let no_media = EventViewOptions(rawValue: 1 << 10) static let truncate_content_very_short = EventViewOptions(rawValue: 1 << 11) + static let no_previews = EventViewOptions(rawValue: 1 << 12) static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested] - static let embedded_text_only: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested, .no_media, .truncate_content_very_short] + static let embedded_text_only: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested, .no_media, .truncate_content_very_short, .no_previews] } struct TextEvent: View { diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift index 491fe4ac2..fd0801b1b 100644 --- a/damus/Views/NoteContentView.swift +++ b/damus/Views/NoteContentView.swift @@ -186,7 +186,7 @@ struct NoteContentView: View { } } - if damus_state.settings.media_previews { + if damus_state.settings.media_previews, has_previews { if with_padding { previewView(links: artifacts.links).padding(.horizontal) } else { @@ -196,7 +196,11 @@ struct NoteContentView: View { } } - + + var has_previews: Bool { + !options.contains(.no_previews) + } + func loadMediaButton(artifacts: NoteArtifactsSeparated) -> some View { Button(action: { load_media = true diff --git a/damus/Views/ReplyQuoteView.swift b/damus/Views/ReplyQuoteView.swift new file mode 100644 index 000000000..40793552d --- /dev/null +++ b/damus/Views/ReplyQuoteView.swift @@ -0,0 +1,67 @@ +// +// SwiftUIView.swift +// damus +// +// Created by William Casarin on 2022-04-19. +// + +import SwiftUI + +struct ReplyQuoteView: View { + let keypair: Keypair + let quoter: NostrEvent + let event_id: NoteId + let state: DamusState + @ObservedObject var thread: ThreadModel + let options: EventViewOptions + + func MainContent(event: NostrEvent) -> some View { + HStack(alignment: .top) { + Rectangle() + .frame(width: 2) + .padding([.leading], 4) + .foregroundColor(.accentColor) + + VStack(alignment: .leading) { + HStack(alignment: .top) { + ProfilePicView(pubkey: event.pubkey, size: 16, highlight: .reply, profiles: state.profiles, disable_animation: false) + ProfileName(pubkey: event.pubkey, damus: state) + .foregroundColor(.accentColor) + RelativeTime(time: state.events.get_cache_data(event.id).relative_time) + } + + let blur_images = should_blur_images(settings: state.settings, contacts: state.contacts, ev: event, our_pubkey: state.pubkey) + NoteContentView(damus_state: state, event: event, blur_images: blur_images, size: .normal, options: options) + .font(.callout) + .foregroundColor(.accentColor) + + //Spacer() + } + //.border(Color.red) + } + //.border(Color.green) + } + + var body: some View { + Group { + if let event = state.events.lookup(event_id) { + VStack { + MainContent(event: event) + .padding(4) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + + ReplyDescription(event: event, replying_to: event, ndb: state.ndb) + } + } + } + } +} + +struct ReplyQuoteView_Previews: PreviewProvider { + static var previews: some View { + let s = test_damus_state + let quoter = test_note + ReplyQuoteView(keypair: s.keypair, quoter: quoter, event_id: test_note.id, state: s, thread: ThreadModel(event: quoter, damus_state: s), options: [.no_media, .truncate_content]) + } +} diff --git a/damusTests/NIP10Tests.swift b/damusTests/NIP10Tests.swift new file mode 100644 index 000000000..7a112b8f0 --- /dev/null +++ b/damusTests/NIP10Tests.swift @@ -0,0 +1,171 @@ +// +// NIP10Tests.swift +// damusTests +// +// Created by William Casarin on 2024-04-25. +// + +import XCTest +@testable import damus + +final class NIP10Tests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func test_new_nip10() { + let root_note_id_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d52" + let direct_reply_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d51" + let reply_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d53" + + let tags = [ + ["e", direct_reply_hex, "", "reply"], + ["e", root_note_id_hex, "", "root"], + ["e", reply_hex, "", "reply"], + ["e", "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d54", "", "mention"], + ] + + let root_note_id = NoteId(hex: root_note_id_hex)! + let direct_reply_id = NoteId(hex: direct_reply_hex)! + let reply_id = NoteId(hex: reply_hex)! + + let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)! + let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs) + + XCTAssertEqual(refs.reduce(into: Array(), { xs, r in + if let note_id = r.is_thread_id?.note_id { xs.append(note_id) } + }), [root_note_id]) + + XCTAssertEqual(refs.reduce(into: Array(), { xs, r in + if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) } + }), [direct_reply_id, reply_id]) + + XCTAssertEqual(refs.reduce(into: Array(), { xs, r in + if let note_id = r.is_reply?.note_id { xs.append(note_id) } + }), [direct_reply_id, reply_id]) + } + + func test_repost_root() { + let mention_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d52" + let tags = [ + ["e", mention_hex, "", "mention"], + ] + + let mention_id = NoteId(hex: mention_hex)! + let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)! + let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs) + + XCTAssertEqual(refs.reduce(into: Array(), { xs, r in + if let note_id = r.is_thread_id?.note_id { xs.append(note_id) } + }), []) + + XCTAssertEqual(refs.reduce(into: Array(), { xs, r in + if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) } + }), []) + + XCTAssertEqual(refs.reduce(into: Array(), { xs, r in + if let note_id = r.is_reply?.note_id { xs.append(note_id) } + }), []) + } + + func test_direct_reply_old_nip10() { + let root_note_id_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d52" + let tags = [ + ["e", root_note_id_hex], + ] + + let root_note_id = NoteId(hex: root_note_id_hex)! + + let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)! + let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs) + + XCTAssertEqual(refs.reduce(into: Array(), { xs, r in + if let note_id = r.is_thread_id?.note_id { xs.append(note_id) } + }), [root_note_id]) + + XCTAssertEqual(refs.reduce(into: Array(), { xs, r in + if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) } + }), [root_note_id]) + + XCTAssertEqual(refs.reduce(into: Array(), { xs, r in + if let note_id = r.is_reply?.note_id { xs.append(note_id) } + }), [root_note_id]) + } + + func test_direct_reply_new_nip10() { + let root_note_id_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d52" + let tags = [ + ["e", root_note_id_hex, "", "root"], + ] + + let root_note_id = NoteId(hex: root_note_id_hex)! + + let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)! + let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs) + + XCTAssertEqual(refs.reduce(into: Array(), { xs, r in + if let note_id = r.is_thread_id?.note_id { xs.append(note_id) } + }), [root_note_id]) + + XCTAssertEqual(refs.reduce(into: Array(), { xs, r in + if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) } + }), [root_note_id]) + + XCTAssertEqual(refs.reduce(into: Array(), { xs, r in + if let note_id = r.is_reply?.note_id { xs.append(note_id) } + }), [root_note_id]) + } + + func test_deprecated_nip10() { + let root_note_id_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d52" + let direct_reply_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d51" + let reply_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d53" + let tags = [ + ["e", root_note_id_hex], + ["e", direct_reply_hex], + ["e", reply_hex], + ] + + let root_note_id = NoteId(hex: root_note_id_hex)! + let direct_reply_id = NoteId(hex: direct_reply_hex)! + let reply_id = NoteId(hex: reply_hex)! + + let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)! + let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs) + + XCTAssertEqual(refs.reduce(into: Array(), { xs, r in + if let note_id = r.is_thread_id?.note_id { xs.append(note_id) } + }), [root_note_id]) + + XCTAssertEqual(refs.reduce(into: Array(), { xs, r in + if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) } + }), [direct_reply_id, reply_id]) + + XCTAssertEqual(refs.reduce(into: Array(), { xs, r in + if let note_id = r.is_reply?.note_id { xs.append(note_id) } + }), [direct_reply_id, reply_id]) + } + + + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +}