damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | LICENSE

commit 8568d4abc7cd95b432b511f5a8786341b44a713a
parent 020a1a4e6d4491b4d59fe77b48313357a7c93531
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 19 Apr 2022 09:26:29 -0700

fix up many things

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus/ContentView.swift | 225++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mdamus/Nostr/RelayConnection.swift | 12+++++++++---
Mdamus/Nostr/RelayPool.swift | 11++++++++++-
Mdamus/Views/EventDetailView.swift | 1-
Mdamus/Views/EventView.swift | 1+
Mdamus/Views/TimelineView.swift | 2++
6 files changed, 200 insertions(+), 52 deletions(-)

diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -43,25 +43,54 @@ struct ContentView: View { @State var selected_timeline: Timeline? = .home @State var last_event_of_kind: [Int: NostrEvent] = [:] @State var has_events: [String: ()] = [:] + @State var notifications_active: Bool = false + @State var new_notifications: Bool = false @State var events: [NostrEvent] = [] @State var notifications: [NostrEvent] = [] + + // connect retry timer + let timer = Timer.publish(every: 60, on: .main, in: .common).autoconnect() let sub_id = UUID().description let pubkey = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245" - - func TimelineButton(timeline: Timeline, img: String) -> some View { - NavigationLink(destination: Text("\(timeline.description)"), tag: timeline, selection: $selected_timeline){ - Label("", systemImage: img) + + var NotificationTab: some View { + ZStack(alignment: .center) { + Button(action: {switch_timeline(.notifications)}) { + Label("", systemImage: selected_timeline == .notifications ? "bell.fill" : "bell") + .contentShape(Rectangle()) + .frame(maxWidth: .infinity, minHeight: 30.0) + } + .foregroundColor(selected_timeline != .notifications ? .gray : .primary) + + if new_notifications { + Circle() + .size(CGSize(width: 8, height: 8)) + .frame(width: 10, height: 10, alignment: .topTrailing) + .alignmentGuide(VerticalAlignment.center) { a in a.height + 2.0 } + .alignmentGuide(HorizontalAlignment.center) { a in a.width - 12.0 } + .foregroundColor(.accentColor) + } } - .frame(maxWidth: .infinity) - .foregroundColor(selected_timeline != timeline ? .gray : .primary) } - - func TopBar(selected: Timeline) -> some View { - HStack { - TimelineButton(timeline: .home, img: selected == .home ? "house.fill" : "house") - TimelineButton(timeline: .notifications, img: selected == .notifications ? "bell.fill" : "bell") + + var HomeTab: some View { + Button(action: {switch_timeline(.home)}) { + Label("", systemImage: selected_timeline == .home ? "house.fill" : "house") + .contentShape(Rectangle()) + .frame(maxWidth: .infinity, minHeight: 30.0) + } + .foregroundColor(selected_timeline != .home ? .gray : .primary) + } + + var TabBar: some View { + VStack { + Divider() + HStack { + HomeTab + NotificationTab + } } } @@ -77,28 +106,49 @@ struct ContentView: View { } } } - + + var PostingTimelineView: some View { + ZStack { + if let pool = self.pool { + TimelineView(events: $events, pool: pool) + .environmentObject(profiles) + } + PostButtonContainer + } + } + var body: some View { VStack { - if self.loading { - ProgressView() - .progressViewStyle(.circular) - .padding([.bottom], 4) - } - - NavigationView { - ZStack { - if let pool = self.pool { - TimelineView(events: $events, pool: pool) + if let pool = self.pool { + NavigationView { + VStack { + if self.loading { + ProgressView() + .progressViewStyle(.circular) + .padding([.bottom], 4) + } + + PostingTimelineView + .onAppear() { + switch_timeline(.home) + } + + let tlv = TimelineView(events: $notifications, pool: pool) .environmentObject(profiles) - .padding() + .navigationTitle("Notifications") + .navigationBarBackButtonHidden(true) + + NavigationLink(destination: tlv, isActive: $notifications_active) { + EmptyView() + } } - PostButtonContainer + .navigationBarTitle("Damus", displayMode: .inline) + } - .navigationBarTitle("Damus", displayMode: .inline) + .padding([.bottom], -8.0) } - TopBar(selected: selected_timeline ?? .home) + TabBar } .onAppear() { self.connect() @@ -116,6 +166,10 @@ struct ContentView: View { let new_ev = post.to_event(privkey: privkey, pubkey: pubkey) self.pool?.send(.event(new_ev)) } + .onReceive(timer) { n in + self.pool?.connect_to_disconnected() + self.loading = (self.pool?.num_connecting ?? 0) != 0 + } } func is_friend(_ pubkey: String) -> Bool { @@ -123,7 +177,15 @@ struct ContentView: View { } func switch_timeline(_ timeline: Timeline) { - self.selected_timeline = timeline + if timeline == .notifications { + self.notifications_active = true + self.selected_timeline = .notifications + new_notifications = false + } else { + self.notifications_active = false + self.selected_timeline = .home + } + //self.selected_timeline = timeline } func add_relay(_ pool: RelayPool, _ relay: String) { @@ -192,6 +254,11 @@ struct ContentView: View { if let prof_since = get_metadata_since_time(last_metadata_event) { profile_filter.since = prof_since } + + /* + var notification_filter = NostrFilter.filter_text + notification_filter.since = since + */ var contacts_filter = NostrFilter.filter_contacts contacts_filter.authors = [self.pubkey] @@ -199,8 +266,45 @@ struct ContentView: View { let filters = [since_filter, profile_filter, contacts_filter] print("connected to \(relay_id), refreshing from \(since)") self.pool?.send(.subscribe(.init(filters: filters, sub_id: sub_id))) + //self.pool?.send(.subscribe(.init(filters: [notification_filter], sub_id: "notifications"))) } - + + func handle_notification(ev: NostrEvent) { + notifications.append(ev) + notifications = notifications.sorted { $0.created_at > $1.created_at } + + let last_notified = get_last_notified() + + if last_notified == nil || last_notified!.created_at < ev.created_at { + save_last_notified(ev) + new_notifications = true + } + } + + func process_event(_ ev: NostrEvent) { + if has_events[ev.id] == nil { + has_events[ev.id] = () + let last_k = last_event_of_kind[ev.kind] + if last_k == nil || ev.created_at > last_k!.created_at { + last_event_of_kind[ev.kind] = ev + } + if ev.kind == 1 { + if !should_hide_event(ev) { + self.events.append(ev) + self.events = self.events.sorted { $0.created_at > $1.created_at } + + if is_notification(ev: ev, pubkey: pubkey) { + handle_notification(ev: ev) + } + } + } else if ev.kind == 0 { + handle_metadata_event(ev) + } else if ev.kind == 3 { + handle_contact_event(ev) + } + } + } + func handle_event(relay_id: String, conn_event: NostrConnectionEvent) { switch conn_event { case .ws_event(let ev): @@ -211,10 +315,10 @@ struct ContentView: View { self.events.insert(wsev, at: 0) } */ + switch ev { case .connected: - self.loading = ((self.pool?.num_connecting ?? 0) > 0) send_filters(relay_id: relay_id) case .error(let merr): let desc = merr.debugDescription @@ -231,6 +335,8 @@ struct ContentView: View { default: break } + + self.loading = (self.pool?.num_connecting ?? 0) != 0 print("ws_event \(ev)") @@ -241,24 +347,8 @@ struct ContentView: View { // TODO: other views like threads might have their own sub ids, so ignore those events... or should we? return } - - if has_events[ev.id] == nil { - has_events[ev.id] = () - let last_k = last_event_of_kind[ev.kind] - if last_k == nil || ev.created_at > last_k!.created_at { - last_event_of_kind[ev.kind] = ev - } - if ev.kind == 1 { - if !should_hide_event(ev) { - self.events.append(ev) - } - self.events = self.events.sorted { $0.created_at > $1.created_at } - } else if ev.kind == 0 { - handle_metadata_event(ev) - } else if ev.kind == 3 { - handle_contact_event(ev) - } - } + + self.process_event(ev) case .notice(let msg): self.events.insert(NostrEvent(content: "NOTICE from \(relay_id): \(msg)", pubkey: "system"), at: 0) print(msg) @@ -342,3 +432,44 @@ func ws_nostr_event(relay: String, ev: WebSocketEvent) -> NostrEvent? { return NostrEvent(content: "reconnectSuggested \(b)", pubkey: relay) } } + +func is_notification(ev: NostrEvent, pubkey: String) -> Bool { + if ev.pubkey == pubkey { + return false + } + return ev.references(id: pubkey, key: "p") +} + + +extension UINavigationController: UIGestureRecognizerDelegate { + override open func viewDidLoad() { + super.viewDidLoad() + interactivePopGestureRecognizer?.delegate = self + } + + public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return viewControllers.count > 1 + } +} + +struct LastNotification { + let id: String + let created_at: Int64 +} + +func get_last_notified() -> LastNotification? { + let last = UserDefaults.standard.string(forKey: "last_notification") + let last_created = UserDefaults.standard.string(forKey: "last_notification_time") + .flatMap { Int64($0) } + + return last.flatMap { id in + last_created.map { created in + return LastNotification(id: id, created_at: created) + } + } +} + +func save_last_notified(_ ev: NostrEvent) { + UserDefaults.standard.set(ev.id, forKey: "last_notification") + UserDefaults.standard.set(String(ev.created_at), forKey: "last_notification_time") +} diff --git a/damus/Nostr/RelayConnection.swift b/damus/Nostr/RelayConnection.swift @@ -17,6 +17,7 @@ class RelayConnection: WebSocketDelegate { var isConnected: Bool = false var isConnecting: Bool = false var isReconnecting: Bool = false + var last_connection_attempt: Double = 0 var socket: WebSocket var handleEvent: (NostrConnectionEvent) -> () let url: URL @@ -25,9 +26,9 @@ class RelayConnection: WebSocketDelegate { self.url = url self.handleEvent = handleEvent // just init, we don't actually use this one - self.socket = WebSocket(request: URLRequest(url: self.url), compressionHandler: .none) + self.socket = make_websocket(url: url) } - + func reconnect() { if self.isConnected { self.isReconnecting = true @@ -45,10 +46,11 @@ class RelayConnection: WebSocketDelegate { var req = URLRequest(url: self.url) req.timeoutInterval = 5 - socket = WebSocket(request: req, compressionHandler: .none) + socket = make_websocket(url: url) socket.delegate = self isConnecting = true + last_connection_attempt = Date().timeIntervalSince1970 socket.connect() } @@ -145,3 +147,7 @@ func make_nostr_subscription_req(_ filters: [NostrFilter], sub_id: String) -> St return req } +func make_websocket(url: URL) -> WebSocket { + return WebSocket(request: URLRequest(url: url), compressionHandler: .none) +} + diff --git a/damus/Nostr/RelayPool.swift b/damus/Nostr/RelayPool.swift @@ -60,7 +60,16 @@ class RelayPool { let relay = Relay(descriptor: descriptor, connection: conn) self.relays.append(relay) } - + + /// This is used to retry dead connections + func connect_to_disconnected() { + for relay in relays { + if !relay.connection.isConnected && !relay.connection.isConnecting { + relay.connection.connect() + } + } + } + func reconnect(to: [String]? = nil) { let relays = to.map{ get_relays($0) } ?? self.relays for relay in relays { diff --git a/damus/Views/EventDetailView.swift b/damus/Views/EventDetailView.swift @@ -169,7 +169,6 @@ struct EventDetailView: View { } } } - .padding() .onDisappear() { unsubscribe_to_thread() } diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift @@ -79,6 +79,7 @@ struct EventView: View { } .padding([.leading], 2) } + .contentShape(Rectangle()) .id(event.id) .frame(minHeight: PFP_SIZE) .padding([.bottom], 4) diff --git a/damus/Views/TimelineView.swift b/damus/Views/TimelineView.swift @@ -18,6 +18,7 @@ struct TimelineView: View { ForEach(events, id: \.id) { (ev: NostrEvent) in let evdet = EventDetailView(event: ev, pool: pool) .navigationBarTitle("Thread") + .padding([.leading, .trailing], 6) .environmentObject(profiles) NavigationLink(destination: evdet) { EventView(event: ev, highlight: .none, has_action_bar: true) @@ -25,6 +26,7 @@ struct TimelineView: View { .buttonStyle(PlainButtonStyle()) } } + .padding([.leading, .trailing], 6) .environmentObject(profiles) } }