@@ -43,25 +43,54 @@ struct ContentView: View {
|
|||||||
@State var selected_timeline: Timeline? = .home
|
@State var selected_timeline: Timeline? = .home
|
||||||
@State var last_event_of_kind: [Int: NostrEvent] = [:]
|
@State var last_event_of_kind: [Int: NostrEvent] = [:]
|
||||||
@State var has_events: [String: ()] = [:]
|
@State var has_events: [String: ()] = [:]
|
||||||
|
@State var notifications_active: Bool = false
|
||||||
|
@State var new_notifications: Bool = false
|
||||||
|
|
||||||
@State var events: [NostrEvent] = []
|
@State var events: [NostrEvent] = []
|
||||||
@State var notifications: [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 sub_id = UUID().description
|
||||||
let pubkey = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"
|
let pubkey = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"
|
||||||
|
|
||||||
func TimelineButton(timeline: Timeline, img: String) -> some View {
|
var NotificationTab: some View {
|
||||||
NavigationLink(destination: Text("\(timeline.description)"), tag: timeline, selection: $selected_timeline){
|
ZStack(alignment: .center) {
|
||||||
Label("", systemImage: img)
|
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 {
|
var HomeTab: some View {
|
||||||
HStack {
|
Button(action: {switch_timeline(.home)}) {
|
||||||
TimelineButton(timeline: .home, img: selected == .home ? "house.fill" : "house")
|
Label("", systemImage: selected_timeline == .home ? "house.fill" : "house")
|
||||||
TimelineButton(timeline: .notifications, img: selected == .notifications ? "bell.fill" : "bell")
|
.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 {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
if self.loading {
|
if let pool = self.pool {
|
||||||
ProgressView()
|
NavigationView {
|
||||||
.progressViewStyle(.circular)
|
VStack {
|
||||||
.padding([.bottom], 4)
|
if self.loading {
|
||||||
}
|
ProgressView()
|
||||||
|
.progressViewStyle(.circular)
|
||||||
NavigationView {
|
.padding([.bottom], 4)
|
||||||
ZStack {
|
}
|
||||||
if let pool = self.pool {
|
|
||||||
TimelineView(events: $events, pool: pool)
|
PostingTimelineView
|
||||||
|
.onAppear() {
|
||||||
|
switch_timeline(.home)
|
||||||
|
}
|
||||||
|
|
||||||
|
let tlv = TimelineView(events: $notifications, pool: pool)
|
||||||
.environmentObject(profiles)
|
.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() {
|
.onAppear() {
|
||||||
self.connect()
|
self.connect()
|
||||||
@@ -116,6 +166,10 @@ struct ContentView: View {
|
|||||||
let new_ev = post.to_event(privkey: privkey, pubkey: pubkey)
|
let new_ev = post.to_event(privkey: privkey, pubkey: pubkey)
|
||||||
self.pool?.send(.event(new_ev))
|
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 {
|
func is_friend(_ pubkey: String) -> Bool {
|
||||||
@@ -123,7 +177,15 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func switch_timeline(_ timeline: Timeline) {
|
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) {
|
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) {
|
if let prof_since = get_metadata_since_time(last_metadata_event) {
|
||||||
profile_filter.since = prof_since
|
profile_filter.since = prof_since
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
var notification_filter = NostrFilter.filter_text
|
||||||
|
notification_filter.since = since
|
||||||
|
*/
|
||||||
|
|
||||||
var contacts_filter = NostrFilter.filter_contacts
|
var contacts_filter = NostrFilter.filter_contacts
|
||||||
contacts_filter.authors = [self.pubkey]
|
contacts_filter.authors = [self.pubkey]
|
||||||
@@ -199,8 +266,45 @@ struct ContentView: View {
|
|||||||
let filters = [since_filter, profile_filter, contacts_filter]
|
let filters = [since_filter, profile_filter, contacts_filter]
|
||||||
print("connected to \(relay_id), refreshing from \(since)")
|
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: 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) {
|
func handle_event(relay_id: String, conn_event: NostrConnectionEvent) {
|
||||||
switch conn_event {
|
switch conn_event {
|
||||||
case .ws_event(let ev):
|
case .ws_event(let ev):
|
||||||
@@ -211,10 +315,10 @@ struct ContentView: View {
|
|||||||
self.events.insert(wsev, at: 0)
|
self.events.insert(wsev, at: 0)
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
switch ev {
|
switch ev {
|
||||||
case .connected:
|
case .connected:
|
||||||
self.loading = ((self.pool?.num_connecting ?? 0) > 0)
|
|
||||||
send_filters(relay_id: relay_id)
|
send_filters(relay_id: relay_id)
|
||||||
case .error(let merr):
|
case .error(let merr):
|
||||||
let desc = merr.debugDescription
|
let desc = merr.debugDescription
|
||||||
@@ -231,6 +335,8 @@ struct ContentView: View {
|
|||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.loading = (self.pool?.num_connecting ?? 0) != 0
|
||||||
|
|
||||||
print("ws_event \(ev)")
|
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?
|
// TODO: other views like threads might have their own sub ids, so ignore those events... or should we?
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if has_events[ev.id] == nil {
|
self.process_event(ev)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case .notice(let msg):
|
case .notice(let msg):
|
||||||
self.events.insert(NostrEvent(content: "NOTICE from \(relay_id): \(msg)", pubkey: "system"), at: 0)
|
self.events.insert(NostrEvent(content: "NOTICE from \(relay_id): \(msg)", pubkey: "system"), at: 0)
|
||||||
print(msg)
|
print(msg)
|
||||||
@@ -342,3 +432,44 @@ func ws_nostr_event(relay: String, ev: WebSocketEvent) -> NostrEvent? {
|
|||||||
return NostrEvent(content: "reconnectSuggested \(b)", pubkey: relay)
|
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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class RelayConnection: WebSocketDelegate {
|
|||||||
var isConnected: Bool = false
|
var isConnected: Bool = false
|
||||||
var isConnecting: Bool = false
|
var isConnecting: Bool = false
|
||||||
var isReconnecting: Bool = false
|
var isReconnecting: Bool = false
|
||||||
|
var last_connection_attempt: Double = 0
|
||||||
var socket: WebSocket
|
var socket: WebSocket
|
||||||
var handleEvent: (NostrConnectionEvent) -> ()
|
var handleEvent: (NostrConnectionEvent) -> ()
|
||||||
let url: URL
|
let url: URL
|
||||||
@@ -25,9 +26,9 @@ class RelayConnection: WebSocketDelegate {
|
|||||||
self.url = url
|
self.url = url
|
||||||
self.handleEvent = handleEvent
|
self.handleEvent = handleEvent
|
||||||
// just init, we don't actually use this one
|
// 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() {
|
func reconnect() {
|
||||||
if self.isConnected {
|
if self.isConnected {
|
||||||
self.isReconnecting = true
|
self.isReconnecting = true
|
||||||
@@ -45,10 +46,11 @@ class RelayConnection: WebSocketDelegate {
|
|||||||
|
|
||||||
var req = URLRequest(url: self.url)
|
var req = URLRequest(url: self.url)
|
||||||
req.timeoutInterval = 5
|
req.timeoutInterval = 5
|
||||||
socket = WebSocket(request: req, compressionHandler: .none)
|
socket = make_websocket(url: url)
|
||||||
socket.delegate = self
|
socket.delegate = self
|
||||||
|
|
||||||
isConnecting = true
|
isConnecting = true
|
||||||
|
last_connection_attempt = Date().timeIntervalSince1970
|
||||||
socket.connect()
|
socket.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,3 +147,7 @@ func make_nostr_subscription_req(_ filters: [NostrFilter], sub_id: String) -> St
|
|||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func make_websocket(url: URL) -> WebSocket {
|
||||||
|
return WebSocket(request: URLRequest(url: url), compressionHandler: .none)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,16 @@ class RelayPool {
|
|||||||
let relay = Relay(descriptor: descriptor, connection: conn)
|
let relay = Relay(descriptor: descriptor, connection: conn)
|
||||||
self.relays.append(relay)
|
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) {
|
func reconnect(to: [String]? = nil) {
|
||||||
let relays = to.map{ get_relays($0) } ?? self.relays
|
let relays = to.map{ get_relays($0) } ?? self.relays
|
||||||
for relay in relays {
|
for relay in relays {
|
||||||
|
|||||||
@@ -169,7 +169,6 @@ struct EventDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
.onDisappear() {
|
.onDisappear() {
|
||||||
unsubscribe_to_thread()
|
unsubscribe_to_thread()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ struct EventView: View {
|
|||||||
}
|
}
|
||||||
.padding([.leading], 2)
|
.padding([.leading], 2)
|
||||||
}
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
.id(event.id)
|
.id(event.id)
|
||||||
.frame(minHeight: PFP_SIZE)
|
.frame(minHeight: PFP_SIZE)
|
||||||
.padding([.bottom], 4)
|
.padding([.bottom], 4)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ struct TimelineView: View {
|
|||||||
ForEach(events, id: \.id) { (ev: NostrEvent) in
|
ForEach(events, id: \.id) { (ev: NostrEvent) in
|
||||||
let evdet = EventDetailView(event: ev, pool: pool)
|
let evdet = EventDetailView(event: ev, pool: pool)
|
||||||
.navigationBarTitle("Thread")
|
.navigationBarTitle("Thread")
|
||||||
|
.padding([.leading, .trailing], 6)
|
||||||
.environmentObject(profiles)
|
.environmentObject(profiles)
|
||||||
NavigationLink(destination: evdet) {
|
NavigationLink(destination: evdet) {
|
||||||
EventView(event: ev, highlight: .none, has_action_bar: true)
|
EventView(event: ev, highlight: .none, has_action_bar: true)
|
||||||
@@ -25,6 +26,7 @@ struct TimelineView: View {
|
|||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.padding([.leading, .trailing], 6)
|
||||||
.environmentObject(profiles)
|
.environmentObject(profiles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user