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()
}
}