From 6ebc629cf9236f197b0a7e66f8065874c8d99b23 Mon Sep 17 00:00:00 2001 From: marcprux Date: Mon, 8 Jan 2024 20:06:28 -0500 Subject: [PATCH] Add swipe to delete message --- Sources/FireSide/ContentView.swift | 82 ++++++++++-- .../FireSide/Resources/Localizable.xcstrings | 16 ++- Sources/FireSideModel/FireSideModel.swift | 124 +++++++++--------- 3 files changed, 146 insertions(+), 76 deletions(-) diff --git a/Sources/FireSide/ContentView.swift b/Sources/FireSide/ContentView.swift index ae343c0..b37bbd4 100644 --- a/Sources/FireSide/ContentView.swift +++ b/Sources/FireSide/ContentView.swift @@ -14,7 +14,7 @@ public struct ContentView: View { TabView(selection: $selectedTab) { JoinChatView() .tag(0) - .tabItem { Label("Welcome", systemImage: "star") } + .tabItem { Label("Join", systemImage: "star") } .task { //do { // try await fireSide.runTask() @@ -27,7 +27,7 @@ public struct ContentView: View { MessagesListView() } .tag(1) - .tabItem { Label("Home", systemImage: "house.fill") } + .tabItem { Label("Messages", systemImage: "list.bullet") } Form { Text("Settings") @@ -40,36 +40,78 @@ public struct ContentView: View { } } +let fmt: DateFormatter = { + let f = DateFormatter() + f.dateStyle = .short + f.timeStyle = .short + return f +}() + +let fmt2: DateFormatter = { + let f = DateFormatter() + f.dateStyle = .long + f.timeStyle = .long + return f +}() + struct MessagesListView : View { @State var messageList: MessageList? = nil var body: some View { - VStack { + VStack(spacing: 0.0) { List { if let messageList = messageList { ForEach(messageList.messages) { m in NavigationLink(value: m) { HStack { Text(m.message) - .font(.title) - Text(m.time.description) + .font(.title2) + + Text(fmt.string(from: m.time)) .font(Font.callout) + .frame(maxWidth: .infinity, alignment: .trailing) } + .lineLimit(1) + } + } + .onDelete { indices in + Task.detached { + await deleteMessages(indices) } } } } - .navigationTitle("Navigation") + #if !SKIP + // the Observable is in FireSideModel, which doesn't know about SwiftUI and so cannot perform the update in a `withAnimation` + .animation(.default, value: messageList?.messages) + #endif + .navigationTitle("Messages: \(messageList?.messages.count ?? 0)") .navigationDestination(for: Message.self) { msg in - VStack { - Text(msg.message) - .font(.title) - .navigationTitle("Message") + Form { + HStack { + Text("ID") + Text(msg.id ?? "NO ID") + } + + HStack { + Text("Message") + Text(msg.message) + } + + HStack { + Text("Date") + Text(fmt2.string(from: msg.time)) + .font(Font.subheadline) + } + } + .navigationTitle("Message") } + Divider() + HStack { - ForEach(["♥️", "💙", "💜", "💛", "💚"], id: \.self) { emoji in + ForEach(["♥️", "💙", "💛", "💚"], id: \.self) { emoji in Button(emoji) { Task.detached { let isJava = ProcessInfo.processInfo.environment["java.io.tmpdir"] != nil @@ -77,6 +119,7 @@ struct MessagesListView : View { await sendMessage(msg) } } + .font(.largeTitle) .buttonStyle(.bordered) } } @@ -102,6 +145,23 @@ struct MessagesListView : View { } } + + func deleteMessages(_ indices: IndexSet) async { + logger.log("deleteMessages: \(indices)") + do { + if let messages = self.messageList?.messages { + let ids = indices.compactMap({ messages[$0].id }) + self.messageList?.messages.removeAll(where: { + ids.contains($0.id ?? "") + }) + try await firestore.deleteMessages(ids) + logger.error("deleted ids: \(ids)") + } + } catch { + logger.error("error deleteMessages: \(error)") + } + + } } let chatKeyCount = 8 diff --git a/Sources/FireSide/Resources/Localizable.xcstrings b/Sources/FireSide/Resources/Localizable.xcstrings index 40abd19..77d8186 100644 --- a/Sources/FireSide/Resources/Localizable.xcstrings +++ b/Sources/FireSide/Resources/Localizable.xcstrings @@ -4,7 +4,13 @@ "Chat Key" : { }, - "Home" : { + "Date" : { + + }, + "ID" : { + + }, + "Join" : { }, "Join Chat" : { @@ -13,7 +19,10 @@ "Message" : { }, - "Navigation" : { + "Messages" : { + + }, + "Messages: %lld" : { }, "New Chat" : { @@ -24,9 +33,6 @@ }, "Settings" : { - }, - "Welcome" : { - } }, "version" : "1.0" diff --git a/Sources/FireSideModel/FireSideModel.swift b/Sources/FireSideModel/FireSideModel.swift index 45cd1ad..c4103c7 100644 --- a/Sources/FireSideModel/FireSideModel.swift +++ b/Sources/FireSideModel/FireSideModel.swift @@ -58,68 +58,80 @@ public actor FireSideStore { /// "Sends" a message by adding it to the document @MainActor public func sendMessage(_ message: String) async throws -> Message { logger.info("sendMessage: \(message)") - let msg = Message(id: UUID(), message: message, time: Date.now) - let dref = try await firestore.collection("messages").addDocument(data: msg.data) + let msg = Message(id: nil, message: message, time: Date.now) + let _ = try await firestore.collection("messages").addDocument(data: msg.data) return msg } + @MainActor public func deleteMessages(_ ids: [String]) async throws { + for doc in try await firestore.collection("messages") + .whereField(FieldPath.documentID(), in: ids.map({ $0 as Any })) + .getDocuments() + .documents { + try await doc.reference.delete() + } + + } + @MainActor public func startNewChat() async throws -> String { logger.info("startNewChat") let cref = firestore.collection("messages") //let q = cref.whereField("t", isGreaterThan: 100.0).limit(to: 4) - let snapshot = try await cref.getDocuments() - logger.log("cref document: \(snapshot)") - for document in snapshot.documents { - logger.log("read cref: \(document.documentID) => \(document.data())") - } - - var changeCount = 0 - let lreg = cref.addSnapshotListener { q, e in - if let q = q { - logger.log(" addSnapshotListener: \(q) count: \(q.documentChanges.count)") - for change in q.documentChanges { - changeCount += 1 - let t = change.type - logger.log(" - change: \(String(describing: t)) \(change.document) \(change)") - let data = change.document.data() - logger.log(" - change data: \(data)") - } - } else { - logger.log(" addSnapshotListener: NO QUERY error=\(e)") - } - } - - logger.log("added snapshot listener") - - let dref = try await cref.addDocument(data: [ - "m": "some message", - "t": Date.now.timeIntervalSince1970, - ]) - - logger.log("created document: \(dref.documentID)") - - print("changeCount: \(changeCount) listener: \(lreg)") - lreg.remove() - - return dref.documentID + //let snapshot = try await cref.getDocuments() + //logger.log("cref document: \(snapshot)") + //for document in snapshot.documents { + // logger.log("read cref: \(document.documentID) => \(document.data())") + //} + + //var changeCount = 0 + //let lreg = cref.addSnapshotListener { q, e in + // if let q = q { + // logger.log(" addSnapshotListener: \(q) count: \(q.documentChanges.count)") + // for change in q.documentChanges { + // changeCount += 1 + // let t = change.type + // logger.log(" - change: \(String(describing: t)) \(change.document) \(change)") + // let data = change.document.data() + // logger.log(" - change data: \(data)") + // } + // } else { + // logger.log(" addSnapshotListener: NO QUERY error=\(e)") + // } + //} + + //logger.log("added snapshot listener") + + //let dref = try await cref.addDocument(data: [ + // "m": "some message", + // "t": Date.now.timeIntervalSince1970, + //]) + + //logger.log("created document: \(dref.documentID)") + + //print("changeCount: \(changeCount) listener: \(lreg)") + //lreg.remove() + + //return dref.documentID + + return "ABCDEF" } - @MainActor public func runTask() async throws { - //let dbname = "(default)" + //@MainActor public func runTask() async throws { + // //let dbname = "(default)" - let cref = firestore.collection("messages") - let snapshot = try await cref.getDocuments() - for document in snapshot.documents { - logger.log("read cref: \(document.documentID) => \(document.data())") - } + // let cref = firestore.collection("messages") + // let snapshot = try await cref.getDocuments() + // for document in snapshot.documents { + // logger.log("read cref: \(document.documentID) => \(document.data())") + // } - let id = UUID() - let bos = cref.document("msg-\(id.uuidString)") + // let id = UUID() + // let bos = cref.document("msg-\(id.uuidString)") - try await bos.setData(Message(id: UUID(), message: "message", time: Date.now).data) - } + // try await bos.setData(Message(id: UUID(), message: "message", time: Date.now).data) + //} public struct InvalidConfigurationError : LocalizedError { public var errorDescription: String? @@ -137,7 +149,7 @@ public actor FireSideStore { var msgs: [Message] = [] if let snap = snap { for doc in snap.documents { - if let msg = Message.from(data: doc.data()) { + if let msg = Message.from(id: doc.documentID, data: doc.data()) { msgs.append(msg) } else { logger.warning("could not create message from data: \(doc.data())") @@ -157,34 +169,26 @@ public actor FireSideStore { /// An individual message public struct Message: Hashable, Identifiable, Codable, CustomStringConvertible { - public let id: UUID + public let id: String? public var message: String public var time: Date public var description: String { - return "Message: id=\(id.uuidString) message=\(message) time=\(time.timeIntervalSince1970)" + return "Message: id=\(id ?? "NONE") message=\(message) time=\(time.timeIntervalSince1970)" } - static func from(data: [String: Any]) -> Message? { + static func from(id: String, data: [String: Any]) -> Message? { guard let message = data["m"] as? String else { return nil } - guard let time = data["t"] as? TimeInterval else { return nil } - - guard let uuid = data["id"] as? String, - let id = UUID(uuidString: uuid) else { - return nil - } - return Message(id: id, message: message, time: Date(timeIntervalSince1970: time)) } var data: [String: Any] { [ - "id": id.uuidString, "m": message, "t": time.timeIntervalSince1970, ]