500 lines
15 KiB
Swift
500 lines
15 KiB
Swift
//
|
|
// ContentView.swift
|
|
// damus
|
|
//
|
|
// Created by William Casarin on 2022-04-01.
|
|
//
|
|
|
|
import SwiftUI
|
|
import Starscream
|
|
|
|
struct TimestampedProfile {
|
|
let profile: Profile
|
|
let timestamp: Int64
|
|
}
|
|
|
|
enum Sheets: Identifiable {
|
|
case post
|
|
case reply(NostrEvent)
|
|
|
|
var id: String {
|
|
switch self {
|
|
case .post: return "post"
|
|
case .reply(let ev): return "reply-" + ev.id
|
|
}
|
|
}
|
|
}
|
|
|
|
enum ThreadState {
|
|
case event_details
|
|
case chatroom
|
|
}
|
|
|
|
struct ContentView: View {
|
|
let keypair: Keypair
|
|
|
|
var pubkey: String {
|
|
return keypair.pubkey
|
|
}
|
|
|
|
var privkey: String? {
|
|
return keypair.privkey
|
|
}
|
|
|
|
@State var status: String = "Not connected"
|
|
@State var active_sheet: Sheets? = nil
|
|
@State var damus_state: DamusState? = nil
|
|
@State var selected_timeline: Timeline? = .home
|
|
@State var is_thread_open: Bool = false
|
|
@State var is_profile_open: Bool = false
|
|
@State var event: NostrEvent? = nil
|
|
@State var active_profile: String? = nil
|
|
@State var active_search: NostrFilter? = nil
|
|
@State var active_event_id: String? = nil
|
|
@State var profile_open: Bool = false
|
|
@State var thread_open: Bool = false
|
|
@State var search_open: Bool = false
|
|
@StateObject var home: HomeModel = HomeModel()
|
|
|
|
// connect retry timer
|
|
let timer = Timer.publish(every: 60, on: .main, in: .common).autoconnect()
|
|
|
|
let sub_id = UUID().description
|
|
|
|
var LoadingContainer: some View {
|
|
VStack {
|
|
HStack {
|
|
Spacer()
|
|
|
|
if home.signal.signal != home.signal.max_signal {
|
|
Text("\(home.signal.signal)/\(home.signal.max_signal)")
|
|
.font(.callout)
|
|
.foregroundColor(.gray)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
}
|
|
|
|
var PostingTimelineView: some View {
|
|
ZStack {
|
|
if let damus = self.damus_state {
|
|
TimelineView(events: $home.events, damus: damus)
|
|
}
|
|
if privkey != nil {
|
|
PostButtonContainer {
|
|
self.active_sheet = .post
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func MainContent(damus: DamusState) -> some View {
|
|
NavigationView {
|
|
VStack {
|
|
NavigationLink(destination: MaybeProfileView, isActive: $profile_open) {
|
|
EmptyView()
|
|
}
|
|
NavigationLink(destination: MaybeThreadView, isActive: $thread_open) {
|
|
EmptyView()
|
|
}
|
|
NavigationLink(destination: MaybeSearchView, isActive: $search_open) {
|
|
EmptyView()
|
|
}
|
|
switch selected_timeline {
|
|
case .search:
|
|
SearchHomeView()
|
|
|
|
case .home:
|
|
PostingTimelineView
|
|
|
|
case .notifications:
|
|
TimelineView(events: $home.notifications, damus: damus)
|
|
.navigationTitle("Notifications")
|
|
|
|
case .none:
|
|
EmptyView()
|
|
}
|
|
}
|
|
.navigationBarTitle("Damus", displayMode: .inline)
|
|
|
|
}
|
|
.navigationViewStyle(.stack)
|
|
}
|
|
|
|
var MaybeSearchView: some View {
|
|
Group {
|
|
if let search = self.active_search {
|
|
SearchView(appstate: damus_state!, search: SearchModel(pool: damus_state!.pool, search: search))
|
|
} else {
|
|
EmptyView()
|
|
}
|
|
}
|
|
}
|
|
|
|
var MaybeThreadView: some View {
|
|
Group {
|
|
if let evid = self.active_event_id {
|
|
let thread_model = ThreadModel(evid: evid, pool: damus_state!.pool)
|
|
ThreadView(thread: thread_model, damus: damus_state!)
|
|
} else {
|
|
EmptyView()
|
|
}
|
|
}
|
|
}
|
|
|
|
var MaybeProfileView: some View {
|
|
Group {
|
|
if let pk = self.active_profile {
|
|
let profile_model = ProfileModel(pubkey: pk, damus: damus_state!)
|
|
ProfileView(damus_state: damus_state!, profile: profile_model)
|
|
} else {
|
|
EmptyView()
|
|
}
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
VStack {
|
|
if let damus = self.damus_state {
|
|
ZStack {
|
|
MainContent(damus: damus)
|
|
.padding([.bottom], -8.0)
|
|
|
|
LoadingContainer
|
|
}
|
|
}
|
|
|
|
TabBar(new_notifications: $home.new_notifications, selected: $selected_timeline, action: switch_timeline)
|
|
}
|
|
.onAppear() {
|
|
self.connect()
|
|
}
|
|
.sheet(item: $active_sheet) { item in
|
|
switch item {
|
|
case .post:
|
|
PostView(references: [])
|
|
case .reply(let event):
|
|
ReplyView(replying_to: event, damus: damus_state!)
|
|
}
|
|
}
|
|
.onOpenURL { url in
|
|
guard let link = decode_nostr_uri(url.absoluteString) else {
|
|
return
|
|
}
|
|
|
|
switch link {
|
|
case .ref(let ref):
|
|
if ref.key == "p" {
|
|
active_profile = ref.ref_id
|
|
profile_open = true
|
|
} else if ref.key == "e" {
|
|
active_event_id = ref.ref_id
|
|
thread_open = true
|
|
}
|
|
case .filter(let filt):
|
|
active_search = filt
|
|
search_open = true
|
|
break
|
|
// TODO: handle filter searches?
|
|
}
|
|
|
|
}
|
|
.onReceive(handle_notify(.boost)) { notif in
|
|
guard let privkey = self.privkey else {
|
|
return
|
|
}
|
|
|
|
let ev = notif.object as! NostrEvent
|
|
let boost = make_boost_event(pubkey: pubkey, privkey: privkey, boosted: ev)
|
|
self.damus_state?.pool.send(.event(boost))
|
|
}
|
|
.onReceive(handle_notify(.open_thread)) { obj in
|
|
//let ev = obj.object as! NostrEvent
|
|
//thread.set_active_event(ev)
|
|
//is_thread_open = true
|
|
}
|
|
.onReceive(handle_notify(.reply)) { notif in
|
|
let ev = notif.object as! NostrEvent
|
|
self.active_sheet = .reply(ev)
|
|
}
|
|
.onReceive(handle_notify(.like)) { like in
|
|
guard let privkey = self.privkey else {
|
|
return
|
|
}
|
|
let ev = like.object as! NostrEvent
|
|
let like_ev = make_like_event(pubkey: pubkey, privkey: privkey, liked: ev)
|
|
self.damus_state?.pool.send(.event(like_ev))
|
|
}
|
|
.onReceive(handle_notify(.broadcast_event)) { obj in
|
|
let ev = obj.object as! NostrEvent
|
|
self.damus_state?.pool.send(.event(ev))
|
|
}
|
|
.onReceive(handle_notify(.unfollow)) { notif in
|
|
guard let privkey = self.privkey else {
|
|
return
|
|
}
|
|
|
|
let pk = notif.object as! String
|
|
guard let damus = self.damus_state else {
|
|
return
|
|
}
|
|
|
|
if unfollow_user(pool: damus.pool,
|
|
our_contacts: damus.contacts.event,
|
|
pubkey: damus.pubkey,
|
|
privkey: privkey,
|
|
unfollow: pk) {
|
|
notify(.unfollowed, pk)
|
|
damus.contacts.remove_friend(pk)
|
|
//friend_events = friend_events.filter { $0.pubkey != pk }
|
|
}
|
|
}
|
|
.onReceive(handle_notify(.follow)) { notif in
|
|
guard let privkey = self.privkey else {
|
|
return
|
|
}
|
|
|
|
let fnotify = notif.object as! FollowTarget
|
|
guard let damus = self.damus_state else {
|
|
return
|
|
}
|
|
|
|
if follow_user(pool: damus.pool,
|
|
our_contacts: damus.contacts.event,
|
|
pubkey: damus.pubkey,
|
|
privkey: privkey,
|
|
follow: ReferencedId(ref_id: fnotify.pubkey, relay_id: nil, key: "p")) {
|
|
notify(.followed, fnotify.pubkey)
|
|
|
|
switch fnotify {
|
|
case .pubkey(let pk):
|
|
damus.contacts.add_friend_pubkey(pk)
|
|
case .contact(let ev):
|
|
damus.contacts.add_friend_contact(ev)
|
|
}
|
|
}
|
|
}
|
|
.onReceive(handle_notify(.post)) { obj in
|
|
guard let privkey = self.privkey else {
|
|
return
|
|
}
|
|
|
|
let post_res = obj.object as! NostrPostResult
|
|
switch post_res {
|
|
case .post(let post):
|
|
print("post \(post.content)")
|
|
let new_ev = post_to_event(post: post, privkey: privkey, pubkey: pubkey)
|
|
self.damus_state?.pool.send(.event(new_ev))
|
|
case .cancel:
|
|
active_sheet = nil
|
|
print("post cancelled")
|
|
}
|
|
}
|
|
.onReceive(timer) { n in
|
|
self.damus_state?.pool.connect_to_disconnected()
|
|
}
|
|
}
|
|
|
|
func is_friend_event(_ ev: NostrEvent) -> Bool {
|
|
return damus.is_friend_event(ev, our_pubkey: self.pubkey, contacts: self.damus_state!.contacts)
|
|
}
|
|
|
|
func switch_timeline(_ timeline: Timeline) {
|
|
if timeline == self.selected_timeline {
|
|
NotificationCenter.default.post(name: .scroll_to_top, object: nil)
|
|
return
|
|
}
|
|
|
|
if (timeline != .notifications && self.selected_timeline == .notifications) || timeline == .notifications {
|
|
home.new_notifications = false
|
|
}
|
|
self.selected_timeline = timeline
|
|
NotificationCenter.default.post(name: .switched_timeline, object: timeline)
|
|
//self.selected_timeline = timeline
|
|
}
|
|
|
|
func add_relay(_ pool: RelayPool, _ relay: String) {
|
|
//add_rw_relay(pool, "wss://nostr-pub.wellorder.net")
|
|
add_rw_relay(pool, relay)
|
|
/*
|
|
let profile = Profile(name: relay, about: nil, picture: nil)
|
|
let ts = Int64(Date().timeIntervalSince1970)
|
|
let tsprofile = TimestampedProfile(profile: profile, timestamp: ts)
|
|
damus!.profiles.add(id: relay, profile: tsprofile)
|
|
*/
|
|
}
|
|
|
|
func connect() {
|
|
let pool = RelayPool()
|
|
|
|
add_relay(pool, "wss://relay.damus.io")
|
|
add_relay(pool, "wss://nostr-pub.wellorder.net")
|
|
add_relay(pool, "wss://nostr.onsats.org")
|
|
add_relay(pool, "wss://nostr.bitcoiner.social")
|
|
add_relay(pool, "ws://monad.jb55.com:8080")
|
|
add_relay(pool, "wss://nostr-relay.freeberty.net")
|
|
add_relay(pool, "wss://nostr-relay.untethr.me")
|
|
|
|
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
|
|
|
self.damus_state = DamusState(pool: pool, keypair: keypair,
|
|
likes: EventCounter(our_pubkey: pubkey),
|
|
boosts: EventCounter(our_pubkey: pubkey),
|
|
contacts: Contacts(),
|
|
tips: TipCounter(our_pubkey: pubkey),
|
|
image_cache: ImageCache(),
|
|
profiles: Profiles()
|
|
)
|
|
home.damus_state = self.damus_state!
|
|
|
|
pool.connect()
|
|
}
|
|
|
|
|
|
}
|
|
|
|
/*
|
|
struct ContentView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
ContentView()
|
|
}
|
|
}
|
|
*/
|
|
|
|
|
|
func get_since_time(last_event: NostrEvent?) -> Int64? {
|
|
if let last_event = last_event {
|
|
return last_event.created_at - 60 * 10
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/*
|
|
func fetch_profiles(relay: URL, pubkeys: [String]) {
|
|
return NostrFilter(ids: nil, kinds: 3, event_ids: nil, pubkeys: pubkeys, since: nil, until: nil, authors: pubkeys)
|
|
}
|
|
|
|
|
|
func nostr_req(relays: [URL], filter: NostrFilter) {
|
|
if relays.count == 0 {
|
|
return
|
|
}
|
|
let conn = NostrConnection(url: relay) {
|
|
}
|
|
}
|
|
|
|
|
|
func get_profiles()
|
|
|
|
*/
|
|
|
|
|
|
func ws_nostr_event(relay: String, ev: WebSocketEvent) -> NostrEvent? {
|
|
switch ev {
|
|
case .binary(let dat):
|
|
return NostrEvent(content: "binary data? \(dat.count) bytes", pubkey: relay)
|
|
case .cancelled:
|
|
return NostrEvent(content: "cancelled", pubkey: relay)
|
|
case .connected:
|
|
return NostrEvent(content: "connected", pubkey: relay)
|
|
case .disconnected:
|
|
return NostrEvent(content: "disconnected", pubkey: relay)
|
|
case .error(let err):
|
|
return NostrEvent(content: "error \(err.debugDescription)", pubkey: relay)
|
|
case .text(let txt):
|
|
return NostrEvent(content: "text \(txt)", pubkey: relay)
|
|
case .pong:
|
|
return NostrEvent(content: "pong", pubkey: relay)
|
|
case .ping:
|
|
return NostrEvent(content: "ping", pubkey: relay)
|
|
case .viabilityChanged(let b):
|
|
return NostrEvent(content: "viabilityChanged \(b)", pubkey: relay)
|
|
case .reconnectSuggested(let b):
|
|
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")
|
|
}
|
|
|
|
|
|
func get_like_pow() -> [String] {
|
|
return ["00000"] // 20 bits
|
|
}
|
|
|
|
|
|
func update_filters_with_since(last_of_kind: [Int: NostrEvent], filters: [NostrFilter]) -> [NostrFilter] {
|
|
|
|
return filters.map { filter in
|
|
let kinds = filter.kinds ?? []
|
|
let initial: Int64? = nil
|
|
let earliest = kinds.reduce(initial) { earliest, kind in
|
|
let last = last_of_kind[kind]
|
|
let since: Int64? = get_since_time(last_event: last)
|
|
|
|
if earliest == nil {
|
|
if since == nil {
|
|
return nil
|
|
}
|
|
return since
|
|
}
|
|
|
|
if since == nil {
|
|
return earliest
|
|
}
|
|
|
|
return since! < earliest! ? since! : earliest!
|
|
}
|
|
|
|
if let earliest = earliest {
|
|
var with_since = NostrFilter.copy(from: filter)
|
|
with_since.since = earliest
|
|
return with_since
|
|
}
|
|
|
|
return filter
|
|
}
|
|
}
|
|
|