@@ -26,6 +26,7 @@
|
|||||||
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
|
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
|
||||||
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */; };
|
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */; };
|
||||||
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */; };
|
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */; };
|
||||||
|
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; };
|
||||||
4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DEE627F7A08100C66700 /* damusApp.swift */; };
|
4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DEE627F7A08100C66700 /* damusApp.swift */; };
|
||||||
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DEE827F7A08100C66700 /* ContentView.swift */; };
|
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DEE827F7A08100C66700 /* ContentView.swift */; };
|
||||||
4CE6DEEB27F7A08200C66700 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4CE6DEEA27F7A08200C66700 /* Assets.xcassets */; };
|
4CE6DEEB27F7A08200C66700 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4CE6DEEA27F7A08200C66700 /* Assets.xcassets */; };
|
||||||
@@ -83,6 +84,7 @@
|
|||||||
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
|
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
|
||||||
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = "<group>"; };
|
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = "<group>"; };
|
||||||
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profiles.swift; sourceTree = "<group>"; };
|
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profiles.swift; sourceTree = "<group>"; };
|
||||||
|
4CE4F8CC281352B30009DFBB /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; 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>"; };
|
||||||
@@ -211,6 +213,7 @@
|
|||||||
4CE6DEEA27F7A08200C66700 /* Assets.xcassets */,
|
4CE6DEEA27F7A08200C66700 /* Assets.xcassets */,
|
||||||
4CE6DEEC27F7A08200C66700 /* Preview Content */,
|
4CE6DEEC27F7A08200C66700 /* Preview Content */,
|
||||||
4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */,
|
4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */,
|
||||||
|
4CE4F8CC281352B30009DFBB /* Notifications.swift */,
|
||||||
);
|
);
|
||||||
path = damus;
|
path = damus;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -386,6 +389,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */,
|
||||||
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
|
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
|
||||||
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
|
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
|
||||||
4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */,
|
4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */,
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ enum Sheets: Identifiable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ThreadState {
|
||||||
|
case event_details
|
||||||
|
case chatroom
|
||||||
|
}
|
||||||
|
|
||||||
enum Timeline: String, CustomStringConvertible {
|
enum Timeline: String, CustomStringConvertible {
|
||||||
case home
|
case home
|
||||||
case notifications
|
case notifications
|
||||||
@@ -42,11 +47,13 @@ struct ContentView: View {
|
|||||||
@State var loading: Bool = true
|
@State var loading: Bool = true
|
||||||
@State var pool: RelayPool? = nil
|
@State var pool: RelayPool? = nil
|
||||||
@State var selected_timeline: Timeline? = .home
|
@State var selected_timeline: Timeline? = .home
|
||||||
|
@StateObject var thread: ThreadModel = ThreadModel()
|
||||||
|
@State var is_thread_open: Bool = false
|
||||||
@State var last_event_of_kind: [String: [Int: NostrEvent]] = [:]
|
@State var last_event_of_kind: [String: [Int: NostrEvent]] = [:]
|
||||||
@State var has_events: [String: ()] = [:]
|
@State var has_events: [String: ()] = [:]
|
||||||
@State var has_friend_event: [String: ()] = [:]
|
@State var has_friend_event: [String: ()] = [:]
|
||||||
@State var new_notifications: Bool = false
|
@State var new_notifications: Bool = false
|
||||||
|
@State var event: NostrEvent? = nil
|
||||||
@State var events: [NostrEvent] = []
|
@State var events: [NostrEvent] = []
|
||||||
@State var friend_events: [NostrEvent] = []
|
@State var friend_events: [NostrEvent] = []
|
||||||
@State var notifications: [NostrEvent] = []
|
@State var notifications: [NostrEvent] = []
|
||||||
@@ -129,6 +136,7 @@ struct ContentView: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
if let pool = self.pool {
|
if let pool = self.pool {
|
||||||
TimelineView(events: $friend_events, pool: pool)
|
TimelineView(events: $friend_events, pool: pool)
|
||||||
|
.environmentObject(thread)
|
||||||
.environmentObject(profiles)
|
.environmentObject(profiles)
|
||||||
}
|
}
|
||||||
PostButtonContainer
|
PostButtonContainer
|
||||||
@@ -138,24 +146,35 @@ struct ContentView: View {
|
|||||||
func MainContent(pool: RelayPool) -> some View {
|
func MainContent(pool: RelayPool) -> some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
VStack {
|
VStack {
|
||||||
PostingTimelineView
|
switch selected_timeline {
|
||||||
.onAppear() {
|
case .home:
|
||||||
switch_timeline(.home)
|
PostingTimelineView
|
||||||
}
|
.onAppear() {
|
||||||
|
switch_timeline(.home)
|
||||||
|
}
|
||||||
|
|
||||||
let notif = TimelineView(events: $notifications, pool: pool)
|
case .notifications:
|
||||||
|
TimelineView(events: $notifications, pool: pool)
|
||||||
|
.environmentObject(profiles)
|
||||||
|
.navigationTitle("Notifications")
|
||||||
|
|
||||||
|
case .global:
|
||||||
|
|
||||||
|
TimelineView(events: $events, pool: pool)
|
||||||
|
.environmentObject(profiles)
|
||||||
|
.navigationTitle("Global")
|
||||||
|
case .none:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
let tv = ThreadView()
|
||||||
|
.environmentObject(thread)
|
||||||
.environmentObject(profiles)
|
.environmentObject(profiles)
|
||||||
.navigationTitle("Notifications")
|
.padding([.leading, .trailing], 6)
|
||||||
.navigationBarBackButtonHidden(true)
|
|
||||||
|
|
||||||
let global = TimelineView(events: $events, pool: pool)
|
NavigationLink(destination: tv, isActive: $is_thread_open) {
|
||||||
.environmentObject(profiles)
|
EmptyView()
|
||||||
.navigationTitle("Global")
|
}
|
||||||
.navigationBarBackButtonHidden(true)
|
|
||||||
|
|
||||||
NavigationLink(destination: notif, tag: .notifications, selection: $selected_timeline) { EmptyView() }
|
|
||||||
|
|
||||||
NavigationLink(destination: global, tag: .global, selection: $selected_timeline) { EmptyView() }
|
|
||||||
}
|
}
|
||||||
.navigationBarTitle("Damus", displayMode: .inline)
|
.navigationBarTitle("Damus", displayMode: .inline)
|
||||||
}
|
}
|
||||||
@@ -183,6 +202,16 @@ struct ContentView: View {
|
|||||||
PostView(references: [])
|
PostView(references: [])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: .open_thread)) { obj in
|
||||||
|
let ev = obj.object as! NostrEvent
|
||||||
|
thread.reset_events()
|
||||||
|
thread.set_active_event(ev)
|
||||||
|
is_thread_open = true
|
||||||
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: .broadcast_event)) { obj in
|
||||||
|
let ev = obj.object as! NostrEvent
|
||||||
|
self.pool?.send(.event(ev))
|
||||||
|
}
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .post)) { obj in
|
.onReceive(NotificationCenter.default.publisher(for: .post)) { obj in
|
||||||
|
|
||||||
let post_res = obj.object as! NostrPostResult
|
let post_res = obj.object as! NostrPostResult
|
||||||
@@ -253,11 +282,10 @@ struct ContentView: View {
|
|||||||
add_relay(pool, "wss://nostr-relay.freeberty.net")
|
add_relay(pool, "wss://nostr-relay.freeberty.net")
|
||||||
add_relay(pool, "wss://nostr-relay.untethr.me")
|
add_relay(pool, "wss://nostr-relay.untethr.me")
|
||||||
|
|
||||||
pool.register_handler(sub_id: sub_id) { (relay_id, ev) in
|
pool.register_handler(sub_id: sub_id, handler: handle_event)
|
||||||
self.handle_event(relay_id: relay_id, conn_event: ev)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.pool = pool
|
self.pool = pool
|
||||||
|
self.thread.pool = pool
|
||||||
pool.connect()
|
pool.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,11 +467,13 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
struct ContentView_Previews: PreviewProvider {
|
struct ContentView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ContentView()
|
ContentView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
func get_metadata_since_time(_ metadata_event: NostrEvent?) -> Int64? {
|
func get_metadata_since_time(_ metadata_event: NostrEvent?) -> Int64? {
|
||||||
@@ -547,3 +577,4 @@ func save_last_notified(_ ev: NostrEvent) {
|
|||||||
UserDefaults.standard.set(ev.id, forKey: "last_notification")
|
UserDefaults.standard.set(ev.id, forKey: "last_notification")
|
||||||
UserDefaults.standard.set(String(ev.created_at), forKey: "last_notification_time")
|
UserDefaults.standard.set(String(ev.created_at), forKey: "last_notification_time")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,39 +9,53 @@ import Foundation
|
|||||||
|
|
||||||
/// manages the lifetime of a thread
|
/// manages the lifetime of a thread
|
||||||
class ThreadModel: ObservableObject {
|
class ThreadModel: ObservableObject {
|
||||||
@Published var event: NostrEvent
|
@Published var event: NostrEvent? = nil
|
||||||
@Published var events: [NostrEvent] = []
|
@Published var events: [NostrEvent] = []
|
||||||
@Published var event_map: [String: Int] = [:]
|
@Published var event_map: [String: Int] = [:]
|
||||||
var replies: ReplyMap = ReplyMap()
|
var replies: ReplyMap = ReplyMap()
|
||||||
|
|
||||||
let pool: RelayPool
|
var pool: RelayPool? = nil
|
||||||
let sub_id = UUID().description
|
var sub_id = UUID().description
|
||||||
|
|
||||||
init(event: NostrEvent, pool: RelayPool) {
|
deinit {
|
||||||
self.event = event
|
unsubscribe()
|
||||||
self.pool = pool
|
|
||||||
add_event(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe() {
|
func unsubscribe() {
|
||||||
|
guard let event = self.event else {
|
||||||
|
return
|
||||||
|
}
|
||||||
print("unsubscribing from thread \(event.id) with sub_id \(sub_id)")
|
print("unsubscribing from thread \(event.id) with sub_id \(sub_id)")
|
||||||
self.pool.remove_handler(sub_id: sub_id)
|
self.pool?.remove_handler(sub_id: sub_id)
|
||||||
self.pool.send(.unsubscribe(sub_id))
|
self.pool?.send(.unsubscribe(sub_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
func subscribe() {
|
func reset_events() {
|
||||||
|
self.events.removeAll()
|
||||||
|
self.event_map.removeAll()
|
||||||
|
self.replies.replies.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
func set_active_event(_ ev: NostrEvent) {
|
||||||
|
unsubscribe()
|
||||||
|
self.event = ev
|
||||||
|
add_event(ev)
|
||||||
|
subscribe(ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func subscribe(_ ev: NostrEvent) {
|
||||||
var ref_events = NostrFilter.filter_text
|
var ref_events = NostrFilter.filter_text
|
||||||
var events = NostrFilter.filter_text
|
var events_filter = NostrFilter.filter_text
|
||||||
|
|
||||||
// TODO: add referenced relays
|
// TODO: add referenced relays
|
||||||
ref_events.referenced_ids = event.referenced_ids.map { $0.ref_id }
|
ref_events.referenced_ids = ev.referenced_ids.map { $0.ref_id }
|
||||||
ref_events.referenced_ids!.append(event.id)
|
ref_events.referenced_ids!.append(ev.id)
|
||||||
|
|
||||||
events.ids = ref_events.referenced_ids!
|
events_filter.ids = ref_events.referenced_ids!
|
||||||
|
|
||||||
print("subscribing to thread \(event.id) with sub_id \(sub_id)")
|
print("subscribing to thread \(ev.id) with sub_id \(sub_id)")
|
||||||
pool.register_handler(sub_id: sub_id, handler: handle_event)
|
pool?.register_handler(sub_id: sub_id, handler: handle_event)
|
||||||
pool.send(.subscribe(.init(filters: [ref_events, events], sub_id: sub_id)))
|
pool?.send(.subscribe(.init(filters: [ref_events, events_filter], sub_id: sub_id)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookup(_ event_id: String) -> NostrEvent? {
|
func lookup(_ event_id: String) -> NostrEvent? {
|
||||||
@@ -83,7 +97,7 @@ class ThreadModel: ObservableObject {
|
|||||||
case .notice(let note):
|
case .notice(let note):
|
||||||
if note.contains("Too many subscription filters") {
|
if note.contains("Too many subscription filters") {
|
||||||
// TODO: resend filters?
|
// TODO: resend filters?
|
||||||
pool.reconnect(to: [relay_id])
|
pool?.reconnect(to: [relay_id])
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ func make_nostr_subscription_req(_ filters: [NostrFilter], sub_id: String) -> St
|
|||||||
}
|
}
|
||||||
|
|
||||||
func make_websocket(url: URL) -> WebSocket {
|
func make_websocket(url: URL) -> WebSocket {
|
||||||
var req = URLRequest(url: url)
|
let req = URLRequest(url: url)
|
||||||
//req.setValue("chat,superchat", forHTTPHeaderField: "Sec-WebSocket-Protocol")
|
//req.setValue("chat,superchat", forHTTPHeaderField: "Sec-WebSocket-Protocol")
|
||||||
return WebSocket(request: req, compressionHandler: .none)
|
return WebSocket(request: req, compressionHandler: .none)
|
||||||
}
|
}
|
||||||
|
|||||||
38
damus/Notifications.swift
Normal file
38
damus/Notifications.swift
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
//
|
||||||
|
// Notifications.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2022-04-22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Notification.Name {
|
||||||
|
static var thread_focus: Notification.Name {
|
||||||
|
return Notification.Name("thread focus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Notification.Name {
|
||||||
|
static var select_event: Notification.Name {
|
||||||
|
return Notification.Name("select_event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Notification.Name {
|
||||||
|
static var broadcast_event: Notification.Name {
|
||||||
|
return Notification.Name("broadcast event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Notification.Name {
|
||||||
|
static var open_thread: Notification.Name {
|
||||||
|
return Notification.Name("open thread")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Notification.Name {
|
||||||
|
static var post: Notification.Name {
|
||||||
|
return Notification.Name("send post")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,10 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var is_active: Bool {
|
var is_active: Bool {
|
||||||
thread.event.id == event.id
|
guard let ev = thread.event else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return ev.id == event.id
|
||||||
}
|
}
|
||||||
|
|
||||||
func prev_reply_is_same() -> String? {
|
func prev_reply_is_same() -> String? {
|
||||||
@@ -98,7 +101,7 @@ struct ChatView: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.textSelection(.enabled)
|
.textSelection(.enabled)
|
||||||
|
|
||||||
if next_ev == nil || next_ev!.pubkey != event.pubkey {
|
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
|
||||||
EventActionBar(event: event)
|
EventActionBar(event: event)
|
||||||
.environmentObject(profiles)
|
.environmentObject(profiles)
|
||||||
}
|
}
|
||||||
@@ -112,30 +115,14 @@ struct ChatView: View {
|
|||||||
.id(event.id)
|
.id(event.id)
|
||||||
.frame(minHeight: just_started ? PFP_SIZE : 0)
|
.frame(minHeight: just_started ? PFP_SIZE : 0)
|
||||||
.padding([.bottom], next_ev == nil ? 4 : 0)
|
.padding([.bottom], next_ev == nil ? 4 : 0)
|
||||||
.onTapGesture {
|
|
||||||
if is_active {
|
|
||||||
convert_to_thread()
|
|
||||||
} else {
|
|
||||||
thread.event = event
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//.border(Color.green)
|
//.border(Color.green)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Environment(\.presentationMode) var presmode
|
|
||||||
|
|
||||||
func dismiss() {
|
|
||||||
presmode.wrappedValue.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
func convert_to_thread() {
|
|
||||||
NotificationCenter.default.post(name: .convert_to_thread, object: nil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
static var convert_to_thread: Notification.Name {
|
static var toggle_thread_view: Notification.Name {
|
||||||
return Notification.Name("convert_to_thread")
|
return Notification.Name("convert_to_thread")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,3 +136,5 @@ struct ChatView_Previews: PreviewProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,26 +9,39 @@ import SwiftUI
|
|||||||
|
|
||||||
struct ChatroomView: View {
|
struct ChatroomView: View {
|
||||||
@EnvironmentObject var thread: ThreadModel
|
@EnvironmentObject var thread: ThreadModel
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollViewReader { scroller in
|
ScrollViewReader { scroller in
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack {
|
LazyVStack {
|
||||||
let count = thread.events.count
|
let count = thread.events.count
|
||||||
ForEach(Array(zip(thread.events, thread.events.indices)), id: \.0.id) { (ev, ind) in
|
ForEach(Array(zip(thread.events, thread.events.indices)), id: \.0.id) { (ev, ind) in
|
||||||
ChatView(event: thread.events[ind],
|
ChatView(event: thread.events[ind],
|
||||||
prev_ev: ind > 0 ? thread.events[ind-1] : nil,
|
prev_ev: ind > 0 ? thread.events[ind-1] : nil,
|
||||||
next_ev: ind == count-1 ? nil : thread.events[ind+1]
|
next_ev: ind == count-1 ? nil : thread.events[ind+1]
|
||||||
)
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
if thread.event!.id == ev.id {
|
||||||
|
//dismiss()
|
||||||
|
toggle_thread_view()
|
||||||
|
} else {
|
||||||
|
thread.set_active_event(ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
.environmentObject(thread)
|
.environmentObject(thread)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
scroll_to_event(scroller: scroller, id: thread.event.id, delay: 0.5, animate: true, anchor: .center)
|
scroll_to_event(scroller: scroller, id: thread.event!.id, delay: 0.3, animate: true, anchor: .bottom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toggle_thread_view() {
|
||||||
|
NotificationCenter.default.post(name: .toggle_thread_view, object: nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,6 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var thread_focus: Notification.Name {
|
|
||||||
return Notification.Name("thread focus")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ActionBarSheet: Identifiable {
|
enum ActionBarSheet: Identifiable {
|
||||||
case reply
|
case reply
|
||||||
|
|
||||||
@@ -50,6 +44,15 @@ struct EventActionBar: View {
|
|||||||
case .reply:
|
case .reply:
|
||||||
ReplyView(replying_to: event)
|
ReplyView(replying_to: event)
|
||||||
.environmentObject(profiles)
|
.environmentObject(profiles)
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: .post)) { obj in
|
||||||
|
let res = obj.object as! NostrPostResult
|
||||||
|
switch res {
|
||||||
|
case .cancel:
|
||||||
|
self.sheet = nil
|
||||||
|
case .post:
|
||||||
|
self.sheet = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ struct EventDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func OurEventView(proxy: ScrollViewProxy, ev: NostrEvent, highlight: Highlight, collapsed_events: [CollapsedEvent]) -> some View {
|
/*
|
||||||
|
func OldEventView(proxy: ScrollViewProxy, ev: NostrEvent, highlight: Highlight, collapsed_events: [CollapsedEvent]) -> some View {
|
||||||
Group {
|
Group {
|
||||||
if ev.id == thread.event.id {
|
if ev.id == thread.event.id {
|
||||||
EventView(event: ev, highlight: .main, has_action_bar: true)
|
EventView(event: ev, highlight: .main, has_action_bar: true)
|
||||||
@@ -76,6 +77,7 @@ struct EventDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
func uncollapse_section(scroller: ScrollViewProxy, c: CollapsedEvents)
|
func uncollapse_section(scroller: ScrollViewProxy, c: CollapsedEvents)
|
||||||
{
|
{
|
||||||
@@ -86,27 +88,51 @@ struct EventDetailView: View {
|
|||||||
toggle_collapse_thread(scroller: scroller, id: start_id, animate: true, anchor: .top)
|
toggle_collapse_thread(scroller: scroller, id: start_id, animate: true, anchor: .top)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
func CollapsedEventView(_ cev: CollapsedEvent, scroller: ScrollViewProxy) -> some View {
|
||||||
ScrollViewReader { proxy in
|
Group {
|
||||||
ScrollView {
|
switch cev {
|
||||||
let collapsed_events = calculated_collapsed_events(collapsed: self.collapsed, active: thread.event, events: thread.events)
|
case .collapsed(let c):
|
||||||
ForEach(collapsed_events, id: \.id) { cev in
|
Text("··· \(c.count) other replies ···")
|
||||||
switch cev {
|
.font(.footnote)
|
||||||
case .collapsed(let c):
|
.foregroundColor(.gray)
|
||||||
Text("··· \(c.count) other replies ···")
|
.onTapGesture {
|
||||||
.font(.footnote)
|
//self.uncollapse_section(scroller: proxy, c: c)
|
||||||
.foregroundColor(.gray)
|
//self.toggle_collapse_thread(scroller: proxy, id: nil)
|
||||||
.onTapGesture {
|
toggle_thread_view()
|
||||||
self.uncollapse_section(scroller: proxy, c: c)
|
}
|
||||||
//self.toggle_collapse_thread(scroller: proxy, id: nil)
|
case .event(let ev, let highlight):
|
||||||
}
|
EventView(event: ev, highlight: highlight, has_action_bar: true)
|
||||||
case .event(let ev, let highlight):
|
.onTapGesture {
|
||||||
OurEventView(proxy: proxy, ev: ev, highlight: highlight, collapsed_events: collapsed_events)
|
if thread.event!.id == ev.id {
|
||||||
|
toggle_thread_view()
|
||||||
|
} else {
|
||||||
|
thread.set_active_event(ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear() {
|
||||||
|
if highlight == .main {
|
||||||
|
scroll_to_event(scroller: scroller, id: ev.id, delay: 0.5, animate: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollViewReader { proxy in
|
||||||
|
ScrollView {
|
||||||
|
let collapsed_events = calculated_collapsed_events(collapsed: self.collapsed, active: thread.event, events: thread.events)
|
||||||
|
ForEach(collapsed_events, id: \.id) { cev in
|
||||||
|
CollapsedEventView(cev, scroller: proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationBarTitle("Thread")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggle_thread_view() {
|
||||||
|
NotificationCenter.default.post(name: .toggle_thread_view, object: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,9 +240,13 @@ func determine_highlight(reply_map: [String: ()], current: NostrEvent, active: N
|
|||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculated_collapsed_events(collapsed: Bool, active: NostrEvent, events: [NostrEvent]) -> [CollapsedEvent] {
|
func calculated_collapsed_events(collapsed: Bool, active: NostrEvent?, events: [NostrEvent]) -> [CollapsedEvent] {
|
||||||
var count: Int = 0
|
var count: Int = 0
|
||||||
|
|
||||||
|
guard let active = active else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
let reply_map = make_reply_map(active: active, events: events)
|
let reply_map = make_reply_map(active: active, events: events)
|
||||||
|
|
||||||
if !collapsed {
|
if !collapsed {
|
||||||
|
|||||||
@@ -50,9 +50,6 @@ struct EventView: View {
|
|||||||
Text("\(format_relative_time(event.created_at))")
|
Text("\(format_relative_time(event.created_at))")
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
Spacer()
|
Spacer()
|
||||||
if (event.pow ?? 0) >= 10 {
|
|
||||||
PowView(event.pow)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.is_reply {
|
if event.is_reply {
|
||||||
@@ -82,6 +79,25 @@ struct EventView: View {
|
|||||||
.id(event.id)
|
.id(event.id)
|
||||||
.frame(minHeight: PFP_SIZE)
|
.frame(minHeight: PFP_SIZE)
|
||||||
.padding([.bottom], 4)
|
.padding([.bottom], 4)
|
||||||
|
.contextMenu {
|
||||||
|
Button {
|
||||||
|
UIPasteboard.general.string = event.content
|
||||||
|
} label: {
|
||||||
|
Label("Copy", systemImage: "doc.on.doc")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
UIPasteboard.general.string = event.id
|
||||||
|
} label: {
|
||||||
|
Label("Copy ID", systemImage: "tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
NotificationCenter.default.post(name: .broadcast_event, object: event)
|
||||||
|
} label: {
|
||||||
|
Label("Broadcast", systemImage: "globe")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,3 +135,5 @@ func reply_others_desc(n: Int, n_pubkeys: Int) -> String {
|
|||||||
let plural = other == 1 ? "" : "s"
|
let plural = other == 1 ? "" : "s"
|
||||||
return n > 1 ? " & \(other) other\(plural)" : ""
|
return n > 1 ? " & \(other) other\(plural)" : ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,9 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
extension Notification.Name {
|
enum NostrPostResult {
|
||||||
static var post: Notification.Name {
|
case post(NostrPost)
|
||||||
return Notification.Name("send post")
|
case cancel
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NostrPost {
|
struct NostrPost {
|
||||||
@@ -46,27 +45,32 @@ struct PostView: View {
|
|||||||
@FocusState var focus: Bool
|
@FocusState var focus: Bool
|
||||||
let references: [ReferencedId]
|
let references: [ReferencedId]
|
||||||
|
|
||||||
@Environment(\.presentationMode) var presmode
|
@Environment(\.presentationMode) var presentationMode
|
||||||
|
|
||||||
enum FocusField: Hashable {
|
enum FocusField: Hashable {
|
||||||
case post
|
case post
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cancel() {
|
||||||
|
NotificationCenter.default.post(name: .post, object: NostrPostResult.cancel)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
func dismiss() {
|
func dismiss() {
|
||||||
presmode.wrappedValue.dismiss()
|
self.presentationMode.wrappedValue.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
func send_post() {
|
func send_post() {
|
||||||
let new_post = NostrPost(content: self.post, references: references)
|
let new_post = NostrPost(content: self.post, references: references)
|
||||||
NotificationCenter.default.post(name: .post, object: new_post)
|
NotificationCenter.default.post(name: .post, object: NostrPostResult.post(new_post))
|
||||||
dismiss()
|
//dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
Button("Cancel") {
|
Button("Cancel") {
|
||||||
self.dismiss()
|
self.cancel()
|
||||||
}
|
}
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
|||||||
@@ -16,17 +16,19 @@ func id_to_color(_ id: String) -> Color {
|
|||||||
|
|
||||||
func highlight_color(_ h: Highlight) -> Color {
|
func highlight_color(_ h: Highlight) -> Color {
|
||||||
switch h {
|
switch h {
|
||||||
|
case .reply: fallthrough
|
||||||
case .none: return Color.black
|
case .none: return Color.black
|
||||||
case .main: return Color.red
|
case .main: return Color.red
|
||||||
case .reply: return Color.blue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func pfp_line_width(_ h: Highlight) -> CGFloat {
|
func pfp_line_width(_ h: Highlight) -> CGFloat {
|
||||||
if h.is_none {
|
switch h {
|
||||||
|
case .none: fallthrough
|
||||||
|
case .reply:
|
||||||
return 0
|
return 0
|
||||||
|
case .main: return 4
|
||||||
}
|
}
|
||||||
return 4
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ProfilePicView: View {
|
struct ProfilePicView: View {
|
||||||
|
|||||||
@@ -41,12 +41,15 @@ struct ReplyQuoteView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if let event = thread.lookup(event_id) {
|
if let event = thread.lookup(event_id) {
|
||||||
Group {
|
MainContent(event: event)
|
||||||
MainContent(event: event)
|
.padding(4)
|
||||||
.padding(4)
|
.frame(maxHeight: 100)
|
||||||
}
|
.background(event.id == thread.event!.id ? Color.red.opacity(0.2) : Color.secondary.opacity(0.2))
|
||||||
.background(Color.secondary.opacity(0.2))
|
|
||||||
.cornerRadius(8.0)
|
.cornerRadius(8.0)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
thread.set_active_event(event)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.progressViewStyle(.circular)
|
.progressViewStyle(.circular)
|
||||||
|
|||||||
@@ -7,30 +7,37 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
struct ThreadView: View {
|
struct ThreadView: View {
|
||||||
@StateObject var thread: ThreadModel
|
@State var is_chatroom: Bool = false
|
||||||
@State var is_thread: Bool = false
|
|
||||||
|
|
||||||
@EnvironmentObject var profiles: Profiles
|
@EnvironmentObject var profiles: Profiles
|
||||||
|
@EnvironmentObject var thread: ThreadModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
ChatroomView()
|
if is_chatroom {
|
||||||
.environmentObject(thread)
|
ChatroomView()
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .convert_to_thread)) { _ in
|
.navigationBarTitle("Chat")
|
||||||
is_thread = true
|
.environmentObject(profiles)
|
||||||
}
|
.environmentObject(thread)
|
||||||
|
} else {
|
||||||
|
EventDetailView(thread: thread)
|
||||||
|
.navigationBarTitle("Thread")
|
||||||
|
.environmentObject(profiles)
|
||||||
|
.environmentObject(thread)
|
||||||
|
}
|
||||||
|
|
||||||
let edv = EventDetailView(thread: thread).environmentObject(profiles)
|
|
||||||
NavigationLink(destination: edv, isActive: $is_thread) {
|
/*
|
||||||
|
NavigationLink(destination: edv, isActive: $is_chatroom) {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
.onDisappear() {
|
.onReceive(NotificationCenter.default.publisher(for: .toggle_thread_view)) { _ in
|
||||||
thread.unsubscribe()
|
is_chatroom = !is_chatroom
|
||||||
}
|
//print("is_chatroom: \(is_chatroom)")
|
||||||
.onAppear() {
|
|
||||||
thread.subscribe()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,25 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
enum TimelineAction {
|
||||||
|
case chillin
|
||||||
|
case navigating
|
||||||
|
}
|
||||||
|
|
||||||
struct TimelineView: View {
|
struct TimelineView: View {
|
||||||
@Binding var events: [NostrEvent]
|
@Binding var events: [NostrEvent]
|
||||||
|
|
||||||
@EnvironmentObject var profiles: Profiles
|
@EnvironmentObject var profiles: Profiles
|
||||||
|
|
||||||
let pool: RelayPool
|
let pool: RelayPool
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
MainContent
|
||||||
|
.padding([.leading, .trailing], 6)
|
||||||
|
.environmentObject(profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
var MainContent: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
ForEach(events, id: \.id) { (ev: NostrEvent) in
|
ForEach(events, id: \.id) { (ev: NostrEvent) in
|
||||||
@@ -24,20 +36,13 @@ struct TimelineView: View {
|
|||||||
.environmentObject(profiles)
|
.environmentObject(profiles)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let evdet = ThreadView(thread: ThreadModel(event: ev, pool: pool))
|
EventView(event: ev, highlight: .none, has_action_bar: true)
|
||||||
.navigationBarTitle("Chat")
|
.onTapGesture {
|
||||||
.padding([.leading, .trailing], 6)
|
NotificationCenter.default.post(name: .open_thread, object: ev)
|
||||||
.environmentObject(profiles)
|
}
|
||||||
|
|
||||||
NavigationLink(destination: evdet) {
|
|
||||||
EventView(event: ev, highlight: .none, has_action_bar: true)
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding([.leading, .trailing], 6)
|
|
||||||
.environmentObject(profiles)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,3 +53,14 @@ struct TimelineView_Previews: PreviewProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
struct NavigationLazyView<Content: View>: View {
|
||||||
|
let build: () -> Content
|
||||||
|
init(_ build: @autoclosure @escaping () -> Content) {
|
||||||
|
self.build = build
|
||||||
|
}
|
||||||
|
var body: Content {
|
||||||
|
build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user