|
@ -8,22 +8,47 @@ |
|
|
import SwiftUI |
|
|
import SwiftUI |
|
|
import Starscream |
|
|
import Starscream |
|
|
|
|
|
|
|
|
|
|
|
let PFP_SIZE: CGFloat? = 64 |
|
|
|
|
|
let CORNER_RADIUS: CGFloat = 32 |
|
|
|
|
|
|
|
|
|
|
|
struct TimestampedProfile { |
|
|
|
|
|
let profile: Profile |
|
|
|
|
|
let timestamp: Int64 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
struct EventView: View { |
|
|
struct EventView: View { |
|
|
let event: NostrEvent |
|
|
let event: NostrEvent |
|
|
let profile: Profile? |
|
|
let profile: Profile? |
|
|
|
|
|
|
|
|
var body: some View { |
|
|
var body: some View { |
|
|
VStack { |
|
|
HStack { |
|
|
Text(String(profile?.name ?? String(event.pubkey.prefix(16)))) |
|
|
if let pic = profile?.picture.flatMap { URL(string: $0) } { |
|
|
.bold() |
|
|
AsyncImage(url: pic) { img in |
|
|
.onTapGesture { |
|
|
img.resizable() |
|
|
UIPasteboard.general.string = event.pubkey |
|
|
} placeholder: { |
|
|
|
|
|
Color.purple.opacity(0.1) |
|
|
} |
|
|
} |
|
|
.frame(maxWidth: .infinity, alignment: .leading) |
|
|
.frame(width: PFP_SIZE, height: PFP_SIZE, alignment: .top) |
|
|
Text(event.content) |
|
|
.cornerRadius(CORNER_RADIUS) |
|
|
.textSelection(.enabled) |
|
|
} else { |
|
|
.frame(maxWidth: .infinity, alignment: .leading) |
|
|
Color.purple.opacity(0.1) |
|
|
Divider() |
|
|
.frame(width: PFP_SIZE, height: PFP_SIZE, alignment: .top) |
|
|
|
|
|
.cornerRadius(CORNER_RADIUS) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
VStack { |
|
|
|
|
|
Text(String(profile?.name ?? String(event.pubkey.prefix(16)))) |
|
|
|
|
|
.bold() |
|
|
|
|
|
.onTapGesture { |
|
|
|
|
|
UIPasteboard.general.string = event.pubkey |
|
|
|
|
|
} |
|
|
|
|
|
.frame(maxWidth: .infinity, alignment: .leading) |
|
|
|
|
|
Text(event.content) |
|
|
|
|
|
.textSelection(.enabled) |
|
|
|
|
|
.frame(maxWidth: .infinity, alignment: .leading) |
|
|
|
|
|
Divider() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
@ -39,20 +64,27 @@ enum Sheets: Identifiable { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
enum NostrKind: Int { |
|
|
|
|
|
case metadata = 0 |
|
|
|
|
|
case text = 1 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
struct ContentView: View { |
|
|
struct ContentView: View { |
|
|
@State var status: String = "Not connected" |
|
|
@State var status: String = "Not connected" |
|
|
@State var sub_id: String? = nil |
|
|
@State var sub_id: String? = nil |
|
|
@State var active_sheet: Sheets? = nil |
|
|
@State var active_sheet: Sheets? = nil |
|
|
@State var events: [NostrEvent] = [] |
|
|
@State var events: [NostrEvent] = [] |
|
|
@State var profiles: [String: Profile] = [:] |
|
|
@State var profiles: [String: TimestampedProfile] = [:] |
|
|
@State var has_events: [String: Bool] = [:] |
|
|
@State var has_events: [String: ()] = [:] |
|
|
|
|
|
@State var profile_count: Int = 0 |
|
|
|
|
|
@State var last_event_of_kind: [Int: NostrEvent] = [:] |
|
|
@State var loading: Bool = true |
|
|
@State var loading: Bool = true |
|
|
@State var pool: RelayPool? = nil |
|
|
@State var pool: RelayPool? = nil |
|
|
|
|
|
|
|
|
var MainContent: some View { |
|
|
var MainContent: some View { |
|
|
ScrollView { |
|
|
ScrollView { |
|
|
ForEach(events.reversed(), id: \.id) { |
|
|
ForEach(events, id: \.id) { |
|
|
EventView(event: $0, profile: profiles[$0.pubkey]) |
|
|
EventView(event: $0, profile: profiles[$0.pubkey]?.profile) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
@ -66,7 +98,7 @@ struct ContentView: View { |
|
|
|
|
|
|
|
|
HStack { |
|
|
HStack { |
|
|
Spacer() |
|
|
Spacer() |
|
|
PostButton { |
|
|
PostButton() { |
|
|
self.active_sheet = .post |
|
|
self.active_sheet = .post |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
@ -102,11 +134,44 @@ struct ContentView: View { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func handle_metadata_event(_ ev: NostrEvent) { |
|
|
func handle_metadata_event(_ ev: NostrEvent) { |
|
|
|
|
|
|
|
|
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else { |
|
|
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else { |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
self.profiles[ev.pubkey] = profile |
|
|
if let mprof = self.profiles[ev.pubkey] { |
|
|
|
|
|
if mprof.timestamp > ev.created_at { |
|
|
|
|
|
// skip if we already have an newer profile |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
self.profiles[ev.pubkey] = TimestampedProfile(profile: profile, timestamp: ev.created_at) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func send_filters(relay_id: String) { |
|
|
|
|
|
// TODO: since times should be based on events from a specific relay |
|
|
|
|
|
// perhaps we could mark this in the relay pool somehow |
|
|
|
|
|
|
|
|
|
|
|
let last_text_event = last_event_of_kind[NostrKind.text.rawValue] |
|
|
|
|
|
let since = get_since_time(last_event: last_text_event) |
|
|
|
|
|
var since_filter = NostrFilter.filter_text |
|
|
|
|
|
since_filter.since = since |
|
|
|
|
|
|
|
|
|
|
|
let last_metadata_event = last_event_of_kind[NostrKind.metadata.rawValue] |
|
|
|
|
|
var profile_filter = NostrFilter.filter_profiles |
|
|
|
|
|
if let prof_since = get_metadata_since_time(last_metadata_event) { |
|
|
|
|
|
profile_filter.since = prof_since |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
let filters = [since_filter, profile_filter] |
|
|
|
|
|
print("connected to \(relay_id), refreshing from \(since)") |
|
|
|
|
|
let sub_id = self.sub_id ?? UUID().description |
|
|
|
|
|
if self.sub_id != sub_id { |
|
|
|
|
|
self.sub_id = sub_id |
|
|
|
|
|
} |
|
|
|
|
|
print("subscribing to \(sub_id)") |
|
|
|
|
|
self.pool?.send(filters: filters, sub_id: sub_id) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func handle_event(relay_id: String, conn_event: NostrConnectionEvent) { |
|
|
func handle_event(relay_id: String, conn_event: NostrConnectionEvent) { |
|
@ -114,20 +179,14 @@ struct ContentView: View { |
|
|
case .ws_event(let ev): |
|
|
case .ws_event(let ev): |
|
|
switch ev { |
|
|
switch ev { |
|
|
case .connected: |
|
|
case .connected: |
|
|
// TODO: since times should be based on events from a specific relay |
|
|
send_filters(relay_id: relay_id) |
|
|
// perhaps we could mark this in the relay pool somehow |
|
|
case .disconnected: fallthrough |
|
|
|
|
|
|
|
|
let since = get_since_time(events: self.events) |
|
|
|
|
|
let filter = NostrFilter.filter_since(since) |
|
|
|
|
|
print("connected to \(relay_id), refreshing from \(since)") |
|
|
|
|
|
let sub_id = self.sub_id ?? UUID().description |
|
|
|
|
|
if self.sub_id != sub_id { |
|
|
|
|
|
self.sub_id = sub_id |
|
|
|
|
|
} |
|
|
|
|
|
print("subscribing to \(sub_id)") |
|
|
|
|
|
self.pool?.send(filter: filter, sub_id: sub_id) |
|
|
|
|
|
case .cancelled: |
|
|
case .cancelled: |
|
|
self.pool?.connect(to: [relay_id]) |
|
|
self.pool?.connect(to: [relay_id]) |
|
|
|
|
|
case .reconnectSuggested(let t): |
|
|
|
|
|
if t { |
|
|
|
|
|
self.pool?.connect(to: [relay_id]) |
|
|
|
|
|
} |
|
|
default: |
|
|
default: |
|
|
break |
|
|
break |
|
|
} |
|
|
} |
|
@ -141,10 +200,15 @@ struct ContentView: View { |
|
|
} |
|
|
} |
|
|
self.sub_id = sub_id |
|
|
self.sub_id = sub_id |
|
|
|
|
|
|
|
|
if !(has_events[ev.id] ?? false) { |
|
|
if has_events[ev.id] == nil { |
|
|
has_events[ev.id] = true |
|
|
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 ev.kind == 1 { |
|
|
self.events.append(ev) |
|
|
self.events.append(ev) |
|
|
|
|
|
self.events = self.events.sorted { $0.created_at > $1.created_at } |
|
|
} else if ev.kind == 0 { |
|
|
} else if ev.kind == 0 { |
|
|
handle_metadata_event(ev) |
|
|
handle_metadata_event(ev) |
|
|
} else if ev.kind == 3 { |
|
|
} else if ev.kind == 3 { |
|
@ -182,13 +246,20 @@ func PostButton(action: @escaping () -> ()) -> some View { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_metadata_since_time(_ metadata_event: NostrEvent?) -> Int64? { |
|
|
|
|
|
if metadata_event == nil { |
|
|
|
|
|
return nil |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return metadata_event!.created_at - 60 * 10 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
func get_since_time(events: [NostrEvent]) -> Int64 { |
|
|
func get_since_time(last_event: NostrEvent?) -> Int64 { |
|
|
if events.count == 0 { |
|
|
if last_event == nil { |
|
|
return Int64(Date().timeIntervalSince1970) - (24 * 60 * 60) |
|
|
return Int64(Date().timeIntervalSince1970) - (24 * 60 * 60 * 3) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return events.last!.created_at - 60 |
|
|
return last_event!.created_at - 60 * 10 |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/* |
|
|
/* |
|
|