commit 37b5309dd4c6ea07d5219b0b5ba201e8fd73e952
parent 13b01381d70f3c7f5ff19667fddd9509a1bcce9b
Author: William Casarin <jb55@jb55.com>
Date: Mon, 11 Apr 2022 09:29:30 -0700
Profiles!
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
5 files changed, 155 insertions(+), 54 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -40,6 +40,7 @@
/* Begin PBXFileReference section */
4C75EFA327FA577B0006080F /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; };
4C75EFA527FF87A20006080F /* Nostr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nostr.swift; sourceTree = "<group>"; };
+ 4C75EFA72804823E0006080F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
4CE6DEE327F7A08100C66700 /* damus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = damus.app; sourceTree = BUILT_PRODUCTS_DIR; };
4CE6DEE627F7A08100C66700 /* damusApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusApp.swift; sourceTree = "<group>"; };
4CE6DEE827F7A08100C66700 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -110,6 +111,7 @@
4CE6DEE527F7A08100C66700 /* damus */ = {
isa = PBXGroup;
children = (
+ 4C75EFA72804823E0006080F /* Info.plist */,
4C75EFA227FA576C0006080F /* Views */,
4CE6DEE627F7A08100C66700 /* damusApp.swift */,
4CE6DEE827F7A08100C66700 /* ContentView.swift */,
@@ -450,6 +452,7 @@
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = damus/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -479,6 +482,7 @@
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = damus/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
diff --git a/damus/ContentView.swift b/damus/ContentView.swift
@@ -8,22 +8,47 @@
import SwiftUI
import Starscream
+let PFP_SIZE: CGFloat? = 64
+let CORNER_RADIUS: CGFloat = 32
+
+struct TimestampedProfile {
+ let profile: Profile
+ let timestamp: Int64
+}
+
struct EventView: View {
let event: NostrEvent
let profile: Profile?
var body: some View {
- VStack {
- Text(String(profile?.name ?? String(event.pubkey.prefix(16))))
- .bold()
- .onTapGesture {
- UIPasteboard.general.string = event.pubkey
+ HStack {
+ if let pic = profile?.picture.flatMap { URL(string: $0) } {
+ AsyncImage(url: pic) { img in
+ img.resizable()
+ } placeholder: {
+ Color.purple.opacity(0.1)
}
- .frame(maxWidth: .infinity, alignment: .leading)
- Text(event.content)
- .textSelection(.enabled)
- .frame(maxWidth: .infinity, alignment: .leading)
- Divider()
+ .frame(width: PFP_SIZE, height: PFP_SIZE, alignment: .top)
+ .cornerRadius(CORNER_RADIUS)
+ } else {
+ Color.purple.opacity(0.1)
+ .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 {
@State var status: String = "Not connected"
@State var sub_id: String? = nil
@State var active_sheet: Sheets? = nil
@State var events: [NostrEvent] = []
- @State var profiles: [String: Profile] = [:]
- @State var has_events: [String: Bool] = [:]
+ @State var profiles: [String: TimestampedProfile] = [:]
+ @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 pool: RelayPool? = nil
var MainContent: some View {
ScrollView {
- ForEach(events.reversed(), id: \.id) {
- EventView(event: $0, profile: profiles[$0.pubkey])
+ ForEach(events, id: \.id) {
+ EventView(event: $0, profile: profiles[$0.pubkey]?.profile)
}
}
}
@@ -66,7 +98,7 @@ struct ContentView: View {
HStack {
Spacer()
- PostButton {
+ PostButton() {
self.active_sheet = .post
}
}
@@ -102,11 +134,44 @@ struct ContentView: View {
}
func handle_metadata_event(_ ev: NostrEvent) {
+
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
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) {
@@ -114,20 +179,14 @@ struct ContentView: View {
case .ws_event(let ev):
switch ev {
case .connected:
- // TODO: since times should be based on events from a specific relay
- // perhaps we could mark this in the relay pool somehow
-
- 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)
+ send_filters(relay_id: relay_id)
+ case .disconnected: fallthrough
case .cancelled:
self.pool?.connect(to: [relay_id])
+ case .reconnectSuggested(let t):
+ if t {
+ self.pool?.connect(to: [relay_id])
+ }
default:
break
}
@@ -141,10 +200,15 @@ struct ContentView: View {
}
self.sub_id = sub_id
- if !(has_events[ev.id] ?? false) {
- has_events[ev.id] = true
+ 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 {
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 {
@@ -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 {
- if events.count == 0 {
- return Int64(Date().timeIntervalSince1970) - (24 * 60 * 60)
+func get_since_time(last_event: NostrEvent?) -> Int64 {
+ if last_event == nil {
+ return Int64(Date().timeIntervalSince1970) - (24 * 60 * 60 * 3)
}
- return events.last!.created_at - 60
+ return last_event!.created_at - 60 * 10
}
/*
diff --git a/damus/Info.plist b/damus/Info.plist
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>NSAppTransportSecurity</key>
+ <dict>
+ <key>NSAllowsArbitraryLoads</key>
+ <true/>
+ </dict>
+</dict>
+</plist>
diff --git a/damus/Nostr.swift b/damus/Nostr.swift
@@ -9,9 +9,9 @@ import Foundation
struct Profile: Decodable {
- let name: String
- let about: String
- let picture: String
+ let name: String?
+ let about: String?
+ let picture: String?
}
diff --git a/damus/RelayConnection.swift b/damus/RelayConnection.swift
@@ -34,13 +34,13 @@ struct NostrSubscription {
}
struct NostrFilter: Codable {
- let ids: [String]?
- let kinds: [String]?
- let referenced_ids: [String]?
- let pubkeys: [String]?
- let since: Int64?
- let until: Int64?
- let authors: [String]?
+ var ids: [String]?
+ var kinds: [Int]?
+ var referenced_ids: [String]?
+ var pubkeys: [String]?
+ var since: Int64?
+ var until: Int64?
+ var authors: [String]?
private enum CodingKeys : String, CodingKey {
case ids
@@ -52,6 +52,14 @@ struct NostrFilter: Codable {
case authors
}
+ public static var filter_text: NostrFilter {
+ NostrFilter(ids: nil, kinds: [1], referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil)
+ }
+
+ public static var filter_profiles: NostrFilter {
+ return NostrFilter(ids: nil, kinds: [0], referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil)
+ }
+
public static func filter_since(_ val: Int64) -> NostrFilter {
return NostrFilter(ids: nil, kinds: nil, referenced_ids: nil, pubkeys: nil, since: val, until: nil, authors: nil)
}
@@ -151,12 +159,12 @@ class RelayPool {
}
}
- func send(filter: NostrFilter, sub_id: String, to: [String]? = nil) {
+ func send(filters: [NostrFilter], sub_id: String, to: [String]? = nil) {
let relays = to.map{ get_relays($0) } ?? self.relays
for relay in relays {
if relay.connection.isConnected {
- relay.connection.send(filter, sub_id: sub_id)
+ relay.connection.send(filters, sub_id: sub_id)
}
}
}
@@ -211,9 +219,9 @@ class RelayConnection: WebSocketDelegate {
socket.disconnect()
}
- func send(_ filter: NostrFilter, sub_id: String) {
- guard let req = make_nostr_req(filter, sub_id: sub_id) else {
- print("failed to encode nostr req: \(filter)")
+ func send(_ filters: [NostrFilter], sub_id: String) {
+ guard let req = make_nostr_req(filters, sub_id: sub_id) else {
+ print("failed to encode nostr req: \(filters)")
return
}
socket.write(string: req)
@@ -262,12 +270,19 @@ func decode_data<T: Decodable>(_ data: Data) -> T? {
return nil
}
-func make_nostr_req(_ filter: NostrFilter, sub_id: String) -> String? {
+func make_nostr_req(_ filters: [NostrFilter], sub_id: String) -> String? {
let encoder = JSONEncoder()
- guard let filter_json = try? encoder.encode(filter) else {
- return nil
+ var req = "[\"REQ\",\"\(sub_id)\""
+ for filter in filters {
+ req += ","
+ guard let filter_json = try? encoder.encode(filter) else {
+ return nil
+ }
+ let filter_json_str = String(decoding: filter_json, as: UTF8.self)
+ req += filter_json_str
}
- let filter_json_str = String(decoding: filter_json, as: UTF8.self)
- return "[\"REQ\",\"\(sub_id)\",\(filter_json_str)]"
+ req += "]"
+ print("req: \(req)")
+ return req
}