@@ -40,6 +40,7 @@
|
|||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
4C75EFA327FA577B0006080F /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
4CE6DEE827F7A08100C66700 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
@@ -110,6 +111,7 @@
|
|||||||
4CE6DEE527F7A08100C66700 /* damus */ = {
|
4CE6DEE527F7A08100C66700 /* damus */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
4C75EFA72804823E0006080F /* Info.plist */,
|
||||||
4C75EFA227FA576C0006080F /* Views */,
|
4C75EFA227FA576C0006080F /* Views */,
|
||||||
4CE6DEE627F7A08100C66700 /* damusApp.swift */,
|
4CE6DEE627F7A08100C66700 /* damusApp.swift */,
|
||||||
4CE6DEE827F7A08100C66700 /* ContentView.swift */,
|
4CE6DEE827F7A08100C66700 /* ContentView.swift */,
|
||||||
@@ -450,6 +452,7 @@
|
|||||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = damus/Info.plist;
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
@@ -479,6 +482,7 @@
|
|||||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = damus/Info.plist;
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
|
|||||||
@@ -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? {
|
||||||
func get_since_time(events: [NostrEvent]) -> Int64 {
|
if metadata_event == nil {
|
||||||
if events.count == 0 {
|
return nil
|
||||||
return Int64(Date().timeIntervalSince1970) - (24 * 60 * 60)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return events.last!.created_at - 60
|
return metadata_event!.created_at - 60 * 10
|
||||||
|
}
|
||||||
|
|
||||||
|
func get_since_time(last_event: NostrEvent?) -> Int64 {
|
||||||
|
if last_event == nil {
|
||||||
|
return Int64(Date().timeIntervalSince1970) - (24 * 60 * 60 * 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
return last_event!.created_at - 60 * 10
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
11
damus/Info.plist
Normal file
11
damus/Info.plist
Normal file
@@ -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>
|
||||||
@@ -9,9 +9,9 @@ import Foundation
|
|||||||
|
|
||||||
|
|
||||||
struct Profile: Decodable {
|
struct Profile: Decodable {
|
||||||
let name: String
|
let name: String?
|
||||||
let about: String
|
let about: String?
|
||||||
let picture: String
|
let picture: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ struct NostrSubscription {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct NostrFilter: Codable {
|
struct NostrFilter: Codable {
|
||||||
let ids: [String]?
|
var ids: [String]?
|
||||||
let kinds: [String]?
|
var kinds: [Int]?
|
||||||
let referenced_ids: [String]?
|
var referenced_ids: [String]?
|
||||||
let pubkeys: [String]?
|
var pubkeys: [String]?
|
||||||
let since: Int64?
|
var since: Int64?
|
||||||
let until: Int64?
|
var until: Int64?
|
||||||
let authors: [String]?
|
var authors: [String]?
|
||||||
|
|
||||||
private enum CodingKeys : String, CodingKey {
|
private enum CodingKeys : String, CodingKey {
|
||||||
case ids
|
case ids
|
||||||
@@ -52,6 +52,14 @@ struct NostrFilter: Codable {
|
|||||||
case authors
|
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 {
|
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)
|
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
|
let relays = to.map{ get_relays($0) } ?? self.relays
|
||||||
|
|
||||||
for relay in relays {
|
for relay in relays {
|
||||||
if relay.connection.isConnected {
|
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()
|
socket.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
func send(_ filter: NostrFilter, sub_id: String) {
|
func send(_ filters: [NostrFilter], sub_id: String) {
|
||||||
guard let req = make_nostr_req(filter, sub_id: sub_id) else {
|
guard let req = make_nostr_req(filters, sub_id: sub_id) else {
|
||||||
print("failed to encode nostr req: \(filter)")
|
print("failed to encode nostr req: \(filters)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
socket.write(string: req)
|
socket.write(string: req)
|
||||||
@@ -262,12 +270,19 @@ func decode_data<T: Decodable>(_ data: Data) -> T? {
|
|||||||
return nil
|
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()
|
let encoder = JSONEncoder()
|
||||||
guard let filter_json = try? encoder.encode(filter) else {
|
var req = "[\"REQ\",\"\(sub_id)\""
|
||||||
return nil
|
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)
|
req += "]"
|
||||||
return "[\"REQ\",\"\(sub_id)\",\(filter_json_str)]"
|
print("req: \(req)")
|
||||||
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user