11
damus/Assets.xcassets/AccentColor.colorset/Contents.json
Normal file
11
damus/Assets.xcassets/AccentColor.colorset/Contents.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
98
damus/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
98
damus/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
6
damus/Assets.xcassets/Contents.json
Normal file
6
damus/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
52
damus/ContentView.swift
Normal file
52
damus/ContentView.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2022-04-01.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Starscream
|
||||
|
||||
struct ContentView: View {
|
||||
@State var status: String = "Not connected"
|
||||
@State var events: [NostrEvent] = []
|
||||
@State var connection: NostrConnection? = nil
|
||||
|
||||
var body: some View {
|
||||
ForEach(events, id: \.id) {
|
||||
Text($0.content)
|
||||
.padding()
|
||||
}
|
||||
.onAppear() {
|
||||
let url = URL(string: "wss://nostr.bitcoiner.social")!
|
||||
let conn = NostrConnection(url: url, handleEvent: handle_event)
|
||||
conn.connect()
|
||||
self.connection = conn
|
||||
}
|
||||
}
|
||||
|
||||
func handle_event(conn_event: NostrConnectionEvent) {
|
||||
|
||||
switch conn_event {
|
||||
case .ws_event(let ev):
|
||||
if case .connected = ev {
|
||||
self.connection?.send(NostrFilter.filter_since(1648851447))
|
||||
}
|
||||
print("ws_event \(ev)")
|
||||
case .nostr_event(let ev):
|
||||
switch ev {
|
||||
case .event(_, let ev):
|
||||
self.events.append(ev)
|
||||
case .notice(let msg):
|
||||
print(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
183
damus/NostrConnection.swift
Normal file
183
damus/NostrConnection.swift
Normal file
@@ -0,0 +1,183 @@
|
||||
//
|
||||
// NostrConnection.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2022-04-02.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Starscream
|
||||
|
||||
struct OtherEvent {
|
||||
let event_id: String
|
||||
let relay_url: String
|
||||
}
|
||||
|
||||
struct KeyEvent {
|
||||
let key: String
|
||||
let relay_url: String
|
||||
}
|
||||
|
||||
enum NostrConnectionEvent {
|
||||
case ws_event(WebSocketEvent)
|
||||
case nostr_event(NostrResponse)
|
||||
}
|
||||
|
||||
enum NostrTag {
|
||||
case other_event(OtherEvent)
|
||||
case key_event(KeyEvent)
|
||||
}
|
||||
|
||||
struct NostrFilter: Codable {
|
||||
let ids: [String]?
|
||||
let kinds: [String]?
|
||||
let event_ids: [String]?
|
||||
let pubkeys: [String]?
|
||||
let since: Int64?
|
||||
let until: Int64?
|
||||
let authors: [String]?
|
||||
|
||||
private enum CodingKeys : String, CodingKey {
|
||||
case ids
|
||||
case kinds
|
||||
case event_ids = "#e"
|
||||
case pubkeys = "#p"
|
||||
case since
|
||||
case until
|
||||
case authors
|
||||
}
|
||||
|
||||
public static func filter_since(_ val: Int64) -> NostrFilter {
|
||||
return NostrFilter(ids: nil, kinds: nil, event_ids: nil, pubkeys: nil, since: val, until: nil, authors: nil)
|
||||
}
|
||||
}
|
||||
|
||||
enum NostrResponse: Decodable {
|
||||
case event(String, NostrEvent)
|
||||
case notice(String)
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
var container = try decoder.unkeyedContainer()
|
||||
|
||||
// Only use first item
|
||||
let typ = try container.decode(String.self)
|
||||
if typ == "EVENT" {
|
||||
let sub_id = try container.decode(String.self)
|
||||
var ev: NostrEvent
|
||||
do {
|
||||
ev = try container.decode(NostrEvent.self)
|
||||
} catch {
|
||||
print(error)
|
||||
throw error
|
||||
}
|
||||
self = .event(sub_id, ev)
|
||||
return
|
||||
} else if typ == "NOTICE" {
|
||||
let msg = try container.decode(String.self)
|
||||
self = .notice(msg)
|
||||
return
|
||||
}
|
||||
|
||||
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "expected EVENT or NOTICE, got \(typ)"))
|
||||
}
|
||||
}
|
||||
|
||||
struct NostrEvent: Decodable, Identifiable {
|
||||
let id: String
|
||||
let pubkey: String
|
||||
let created_at: Int64
|
||||
let kind: Int
|
||||
let tags: [[String]]
|
||||
let content: String
|
||||
let sig: String
|
||||
}
|
||||
|
||||
class NostrConnection: WebSocketDelegate {
|
||||
var isConnected: Bool = false
|
||||
var socket: WebSocket
|
||||
var handleEvent: (NostrConnectionEvent) -> ()
|
||||
|
||||
init(url: URL, handleEvent: @escaping (NostrConnectionEvent) -> ()) {
|
||||
var req = URLRequest(url: url)
|
||||
req.timeoutInterval = 5
|
||||
self.socket = WebSocket(request: req)
|
||||
self.handleEvent = handleEvent
|
||||
|
||||
socket.delegate = self
|
||||
}
|
||||
|
||||
func connect(){
|
||||
socket.connect()
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
socket.disconnect()
|
||||
}
|
||||
|
||||
func send(_ filter: NostrFilter) {
|
||||
guard let req = make_nostr_req(filter) else {
|
||||
print("failed to encode nostr req: \(filter)")
|
||||
return
|
||||
}
|
||||
socket.write(string: req)
|
||||
}
|
||||
|
||||
func didReceive(event: WebSocketEvent, client: WebSocket) {
|
||||
switch event {
|
||||
case .connected:
|
||||
self.isConnected = true
|
||||
|
||||
case .disconnected: fallthrough
|
||||
case .cancelled: fallthrough
|
||||
case .error:
|
||||
self.isConnected = false
|
||||
|
||||
case .text(let txt):
|
||||
if let ev = decode_nostr_event(txt: txt) {
|
||||
handleEvent(.nostr_event(ev))
|
||||
return
|
||||
}
|
||||
|
||||
print("decode failed for \(txt)")
|
||||
// TODO: trigger event error
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
handleEvent(.ws_event(event))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func decode_nostr_event(txt: String) -> NostrResponse? {
|
||||
return decode_data(Data(txt.utf8))
|
||||
}
|
||||
|
||||
func decode_data<T: Decodable>(_ data: Data) -> T? {
|
||||
let decoder = JSONDecoder()
|
||||
do {
|
||||
return try decoder.decode(T.self, from: data)
|
||||
} catch {
|
||||
print("decode_data failed for \(T.self): \(error)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func make_nostr_req(_ filter: NostrFilter) -> String? {
|
||||
let sub_id = UUID()
|
||||
var params: [Encodable] = []
|
||||
|
||||
params.append("REQ")
|
||||
params.append(sub_id)
|
||||
params.append(filter)
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
guard let filter_json = try? encoder.encode(filter) else {
|
||||
return nil
|
||||
}
|
||||
let filter_json_str = String(decoding: filter_json, as: UTF8.self)
|
||||
return "[\"REQ\",\"\(sub_id)\",\(filter_json_str)]"
|
||||
}
|
||||
|
||||
50
damus/WSConnection.swift
Normal file
50
damus/WSConnection.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// Connection.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2022-04-01.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Starscream
|
||||
|
||||
class WSConnection: WebSocketDelegate {
|
||||
var isConnected: Bool = false
|
||||
var socket: WebSocket
|
||||
var handleEvent: (WebSocketEvent) -> ()
|
||||
|
||||
init(url: URL, handleEvent: @escaping (WebSocketEvent) -> ()) {
|
||||
var req = URLRequest(url: url)
|
||||
req.timeoutInterval = 5
|
||||
self.socket = WebSocket(request: req)
|
||||
self.handleEvent = handleEvent
|
||||
|
||||
socket.delegate = self
|
||||
}
|
||||
|
||||
func connect(){
|
||||
socket.connect()
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
socket.disconnect()
|
||||
}
|
||||
|
||||
func didReceive(event: WebSocketEvent, client: WebSocket) {
|
||||
switch event {
|
||||
case .connected:
|
||||
self.isConnected = true
|
||||
|
||||
case .disconnected: fallthrough
|
||||
case .cancelled: fallthrough
|
||||
case .error:
|
||||
self.isConnected = false
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
handleEvent(event)
|
||||
}
|
||||
|
||||
}
|
||||
17
damus/damusApp.swift
Normal file
17
damus/damusApp.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// damusApp.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2022-04-01.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct damusApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user