diff --git a/Darwin/Assets.xcassets/AccentColor.colorset/Contents.json b/Darwin/Assets.xcassets/AccentColor.colorset/Contents.json index 4e213e6..eb87897 100644 --- a/Darwin/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Darwin/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,15 +1,6 @@ { "colors" : [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.167", - "green" : "0.317", - "red" : "1.000" - } - }, "idiom" : "universal" } ], diff --git a/Darwin/FireSide.xcodeproj/project.pbxproj b/Darwin/FireSide.xcodeproj/project.pbxproj index f59ea2d..2d8d61f 100644 --- a/Darwin/FireSide.xcodeproj/project.pbxproj +++ b/Darwin/FireSide.xcodeproj/project.pbxproj @@ -217,8 +217,10 @@ isa = XCBuildConfiguration; buildSettings = { ENABLE_PREVIEWS = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 14.0; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -231,8 +233,10 @@ isa = XCBuildConfiguration; buildSettings = { ENABLE_PREVIEWS = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 14.0; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -245,8 +249,10 @@ isa = XCBuildConfiguration; buildSettings = { ENABLE_PREVIEWS = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 14.0; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/Darwin/FireSide.xcodeproj/xcshareddata/xcschemes/FireSide.xcscheme b/Darwin/FireSide.xcodeproj/xcshareddata/xcschemes/FireSide.xcscheme new file mode 100644 index 0000000..4a90d0a --- /dev/null +++ b/Darwin/FireSide.xcodeproj/xcshareddata/xcschemes/FireSide.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Package.swift b/Package.swift index 36f340c..d7b2869 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,7 @@ import PackageDescription let package = Package( name: "skipapp-fireside", defaultLocalization: "en", - platforms: [.iOS(.v16), .macOS(.v13), .tvOS(.v16), .watchOS(.v9), .macCatalyst(.v16)], + platforms: [.iOS(.v17), .macOS(.v14), .tvOS(.v16), .watchOS(.v9), .macCatalyst(.v17)], products: [ .library(name: "FireSideApp", type: .dynamic, targets: ["FireSide"]), .library(name: "FireSideModel", targets: ["FireSideModel"]), diff --git a/Sources/FireSide/ContentView.swift b/Sources/FireSide/ContentView.swift index aba745f..ae343c0 100644 --- a/Sources/FireSide/ContentView.swift +++ b/Sources/FireSide/ContentView.swift @@ -1,7 +1,7 @@ import SwiftUI import FireSideModel -let fireSide = try! FireSideStore() +let firestore = try! FireSideStore() public struct ContentView: View { @AppStorage("setting") var setting = true @@ -24,17 +24,7 @@ public struct ContentView: View { } NavigationStack { - List { - ForEach(1..<1_000) { i in - NavigationLink("Home \(i)", value: i) - } - } - .navigationTitle("Navigation") - .navigationDestination(for: Int.self) { i in - Text("Destination \(i)") - .font(.title) - .navigationTitle("Navigation \(i)") - } + MessagesListView() } .tag(1) .tabItem { Label("Home", systemImage: "house.fill") } @@ -50,6 +40,70 @@ public struct ContentView: View { } } +struct MessagesListView : View { + @State var messageList: MessageList? = nil + + var body: some View { + VStack { + List { + if let messageList = messageList { + ForEach(messageList.messages) { m in + NavigationLink(value: m) { + HStack { + Text(m.message) + .font(.title) + Text(m.time.description) + .font(Font.callout) + } + } + } + } + } + .navigationTitle("Navigation") + .navigationDestination(for: Message.self) { msg in + VStack { + Text(msg.message) + .font(.title) + .navigationTitle("Message") + } + } + + HStack { + ForEach(["♥️", "💙", "💜", "💛", "💚"], id: \.self) { emoji in + Button(emoji) { + Task.detached { + let isJava = ProcessInfo.processInfo.environment["java.io.tmpdir"] != nil + let msg = emoji + " from " + (isJava ? "Android" : "iOS") + await sendMessage(msg) + } + } + .buttonStyle(.bordered) + } + } + .padding() + } + .task { + do { + let messageList = try await firestore.watchMessageList() + self.messageList = messageList + } catch { + logger.error("error getting message list: \(error)") + } + } + } + + func sendMessage(_ message: String) async { + logger.log("sendMessage: \(message)") + do { + let msg = try await firestore.sendMessage(message) + logger.error("sent message: \(msg)") + } catch { + logger.error("error sending message: \(error)") + } + + } +} + let chatKeyCount = 8 struct JoinChatView : View { @@ -108,10 +162,10 @@ struct JoinChatView : View { self.lastError = nil // clear the most recent error if chatKey.count == chatKeyCount { logger.log("joinChat: \(chatKey)") - try await fireSide.joinChat(chatKey: chatKey) + try await firestore.joinChat(chatKey: chatKey) } else { logger.log("startNewChat") - chatKey = try await fireSide.startNewChat() + chatKey = try await firestore.startNewChat() } } catch { logger.log("joinChat error: \(error)") @@ -120,6 +174,10 @@ struct JoinChatView : View { } } -#Preview { - ContentView() -} +//#Preview { +// if #available(iOS 17.0, *) { +// ContentView() +// } else { +// // Fallback on earlier versions +// } +//} diff --git a/Sources/FireSide/Resources/Localizable.xcstrings b/Sources/FireSide/Resources/Localizable.xcstrings index 84143ca..40abd19 100644 --- a/Sources/FireSide/Resources/Localizable.xcstrings +++ b/Sources/FireSide/Resources/Localizable.xcstrings @@ -3,23 +3,17 @@ "strings" : { "Chat Key" : { - }, - "Destination %lld" : { - }, "Home" : { - }, - "Home %lld" : { - }, "Join Chat" : { }, - "Navigation" : { + "Message" : { }, - "Navigation %lld" : { + "Navigation" : { }, "New Chat" : { diff --git a/Sources/FireSideModel/FireSideModel.swift b/Sources/FireSideModel/FireSideModel.swift index 5594319..45cd1ad 100644 --- a/Sources/FireSideModel/FireSideModel.swift +++ b/Sources/FireSideModel/FireSideModel.swift @@ -31,6 +31,7 @@ public actor FireSideStore { let senderId = options["GCM_SENDER_ID"] else { throw InvalidConfigurationError(errorDescription: "configuration options are missing required attributes") } + let opts = FirebaseOptions(googleAppID: appId, gcmSenderID: senderId) if let apiKey = options["API_KEY"] { opts.apiKey = apiKey @@ -41,9 +42,6 @@ public actor FireSideStore { if let storageBucket = options["STORAGE_BUCKET"] { opts.storageBucket = storageBucket } -// if let bundleID = options["BUNDLE_ID"] { -// opts.bundleID = bundleID -// } FirebaseApp.configure(options: opts) self.firestore = Firestore.firestore() @@ -53,16 +51,48 @@ public actor FireSideStore { logger.info("joinChat: \(chatKey)") } + @MainActor public func watchMessageList() async throws -> MessageList { + MessageList(firestore.collection("messages")) + } + + /// "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) + return msg + } + @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, @@ -70,14 +100,16 @@ public actor FireSideStore { logger.log("created document: \(dref.documentID)") + print("changeCount: \(changeCount) listener: \(lreg)") + lreg.remove() + return dref.documentID } @MainActor public func runTask() async throws { - let dbname = "(default)" + //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())") @@ -86,14 +118,75 @@ public actor FireSideStore { let id = UUID() let bos = cref.document("msg-\(id.uuidString)") - try await bos.setData([ - "k": "message", - "t": Date.now.timeIntervalSince1970, - "c": "message content" - ]) + try await bos.setData(Message(id: UUID(), message: "message", time: Date.now).data) } public struct InvalidConfigurationError : LocalizedError { public var errorDescription: String? } } + +/// A live list of all the messages, updated using a Firestore snapshot listenr on the "messages" collection. +@Observable public class MessageList { + private var listener: ListenerRegistration? = nil + public var messages: [Message] = [] + + fileprivate init(_ collection: CollectionReference) { + let listener = collection.addSnapshotListener(includeMetadataChanges: true, listener: { [weak self] snap, err in + logger.log("snapshot: \(snap) error=\(err)") + var msgs: [Message] = [] + if let snap = snap { + for doc in snap.documents { + if let msg = Message.from(data: doc.data()) { + msgs.append(msg) + } else { + logger.warning("could not create message from data: \(doc.data())") + } + } + } + msgs.sort { + $0.time > $1.time + } + + self?.messages = msgs + }) + + self.listener = listener + } +} + +/// An individual message +public struct Message: Hashable, Identifiable, Codable, CustomStringConvertible { + public let id: UUID + public var message: String + public var time: Date + + public var description: String { + return "Message: id=\(id.uuidString) message=\(message) time=\(time.timeIntervalSince1970)" + } + + static func from(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, + ] + } +} diff --git a/Tests/FireSideModelTests/FireSideModelTests.swift b/Tests/FireSideModelTests/FireSideModelTests.swift index e8df919..99a1e16 100644 --- a/Tests/FireSideModelTests/FireSideModelTests.swift +++ b/Tests/FireSideModelTests/FireSideModelTests.swift @@ -9,21 +9,23 @@ let logger: Logger = Logger(subsystem: "FireSideModel", category: "Tests") @available(macOS 13, *) final class FireSideModelTests: XCTestCase { // values from Darwin/GoogleService-Info.plist - static let store = try! FireSideStore(options: [ - "API_KEY": "AIzaSyCjhtnQ4GE010ED8hRMaGZjpdApSk43z1I", - "GCM_SENDER_ID": "1058155430593", - //"BUNDLE_ID": "skip.fireside.App", - "PROJECT_ID": "skip-fireside", - "STORAGE_BUCKET": "skip-fireside.appspot.com", - "GOOGLE_APP_ID": "1:1058155430593:ios:d3a7a76d92b20132370a40", - ]) + static let store: Result = Result { + try FireSideStore(options: [ + "API_KEY": "AIzaSyCjhtnQ4GE010ED8hRMaGZjpdApSk43z1I", + "GCM_SENDER_ID": "1058155430593", + //"BUNDLE_ID": "skip.fireside.App", + "PROJECT_ID": "skip-fireside", + "STORAGE_BUCKET": "skip-fireside.appspot.com", + "GOOGLE_APP_ID": "1:1058155430593:ios:d3a7a76d92b20132370a40", + ]) + } func testFireSideStore() throws { let _ = Self.store } func testFireSideModel() async throws { - let chatKey = try await Self.store.startNewChat() + let chatKey = try await Self.store.get().startNewChat() } }