diff --git a/ios/RocketChat Watch App/Database/Database.swift b/ios/RocketChat Watch App/Database/Database.swift index 2e9d75f91..82e87273c 100644 --- a/ios/RocketChat Watch App/Database/Database.swift +++ b/ios/RocketChat Watch App/Database/Database.swift @@ -1,97 +1,109 @@ import CoreData import Foundation -final class Database { - private let container: NSPersistentContainer - - var viewContext: NSManagedObjectContext { - container.viewContext - } - - private static let model: NSManagedObjectModel = { - guard let url = Bundle.main.url(forResource: "Default", withExtension: "momd"), - let managedObjectModel = NSManagedObjectModel(contentsOf: url) else { - fatalError("Can't find Core Data Model") - } - - return managedObjectModel - }() - - init() { - container = NSPersistentContainer(name: "default", managedObjectModel: Self.model) - - container.loadPersistentStores { _, error in - if let error { fatalError("Can't load persistent stores: \(error)") } - } - - container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - } - - func save() { - guard container.viewContext.hasChanges else { - return - } - - try? container.viewContext.save() - } - - func server(url: URL) -> Server? { - let request = Server.fetchRequest() - request.predicate = NSPredicate(format: "url == %@", url.absoluteString) - - return try? viewContext.fetch(request).first - } - - func user(id: String) -> LoggedUser? { - let request = LoggedUser.fetchRequest() - request.predicate = NSPredicate(format: "id == %@", id) - - return try? viewContext.fetch(request).first - } - - func servers() -> [Server] { - let request = Server.fetchRequest() - - return (try? viewContext.fetch(request)) ?? [] - } - - func process(updatedServer: WatchMessage.Server) { - if let server = server(url: updatedServer.url) { - server.url = updatedServer.url - server.name = updatedServer.name - server.iconURL = updatedServer.iconURL - server.useRealName = updatedServer.useRealName - server.loggedUser = user(from: updatedServer.loggedUser) - } else { - Server( - context: viewContext, - iconURL: updatedServer.iconURL, - name: updatedServer.name, - url: updatedServer.url, - useRealName: updatedServer.useRealName, - loggedUser: user(from: updatedServer.loggedUser) - ) - } - - save() - } - - private func user(from updatedUser: WatchMessage.Server.LoggedUser) -> LoggedUser { - if let user = user(id: updatedUser.id) { - user.id = updatedUser.id - user.name = updatedUser.name - user.username = updatedUser.username - user.token = updatedUser.token - - return user - } - - return LoggedUser( - context: viewContext, - id: updatedUser.id, - name: updatedUser.name, - token: updatedUser.token, - username: updatedUser.username - ) - } +protocol ServersDatabase { + var viewContext: NSManagedObjectContext { get } + + func server(url: URL) -> Server? + func user(id: String) -> LoggedUser? + func servers() -> [Server] + + func save() + + func process(updatedServer: WatchMessage.Server) +} + +final class DefaultDatabase: ServersDatabase { + private let container: NSPersistentContainer + + var viewContext: NSManagedObjectContext { + container.viewContext + } + + private static let model: NSManagedObjectModel = { + guard let url = Bundle.main.url(forResource: "Default", withExtension: "momd"), + let managedObjectModel = NSManagedObjectModel(contentsOf: url) else { + fatalError("Can't find Core Data Model") + } + + return managedObjectModel + }() + + init() { + container = NSPersistentContainer(name: "default", managedObjectModel: Self.model) + + container.loadPersistentStores { _, error in + if let error { fatalError("Can't load persistent stores: \(error)") } + } + + container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + } + + func save() { + guard container.viewContext.hasChanges else { + return + } + + try? container.viewContext.save() + } + + func server(url: URL) -> Server? { + let request = Server.fetchRequest() + request.predicate = NSPredicate(format: "url == %@", url.absoluteString) + + return try? viewContext.fetch(request).first + } + + func user(id: String) -> LoggedUser? { + let request = LoggedUser.fetchRequest() + request.predicate = NSPredicate(format: "id == %@", id) + + return try? viewContext.fetch(request).first + } + + func servers() -> [Server] { + let request = Server.fetchRequest() + + return (try? viewContext.fetch(request)) ?? [] + } + + func process(updatedServer: WatchMessage.Server) { + if let server = server(url: updatedServer.url) { + server.url = updatedServer.url + server.name = updatedServer.name + server.iconURL = updatedServer.iconURL + server.useRealName = updatedServer.useRealName + server.loggedUser = user(from: updatedServer.loggedUser) + } else { + Server( + context: viewContext, + iconURL: updatedServer.iconURL, + name: updatedServer.name, + url: updatedServer.url, + useRealName: updatedServer.useRealName, + loggedUser: user(from: updatedServer.loggedUser) + ) + } + + save() + } + + private func user(from updatedUser: WatchMessage.Server.LoggedUser) -> LoggedUser { + if let user = user(id: updatedUser.id) { + user.id = updatedUser.id + user.name = updatedUser.name + user.username = updatedUser.username + user.token = updatedUser.token + + return user + } + + return LoggedUser( + context: viewContext, + id: updatedUser.id, + name: updatedUser.name, + token: updatedUser.token, + username: updatedUser.username + ) + } } diff --git a/ios/RocketChat Watch App/Database/Models/LoggedUser.swift b/ios/RocketChat Watch App/Database/Models/LoggedUser.swift index d602e80df..5408276ea 100644 --- a/ios/RocketChat Watch App/Database/Models/LoggedUser.swift +++ b/ios/RocketChat Watch App/Database/Models/LoggedUser.swift @@ -2,47 +2,47 @@ import CoreData @objc public final class LoggedUser: NSManagedObject { - - @nonobjc public class func fetchRequest() -> NSFetchRequest { - NSFetchRequest(entityName: "LoggedUser") - } - - @NSManaged public var id: String - @NSManaged public var name: String - @NSManaged public var token: String - @NSManaged public var username: String - - @available(*, unavailable) - init() { - fatalError() - } - - @available(*, unavailable) - init(context: NSManagedObjectContext) { - fatalError() - } - - public override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) { - super.init(entity: entity, insertInto: context) - } - - @discardableResult - public init( - context: NSManagedObjectContext, - id: String, - name: String, - token: String, - username: String - ) { - let entity = NSEntityDescription.entity(forEntityName: "LoggedUser", in: context)! - super.init(entity: entity, insertInto: context) - self.id = id - self.name = name - self.token = token - self.username = username - } + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + NSFetchRequest(entityName: "LoggedUser") + } + + @NSManaged public var id: String + @NSManaged public var name: String + @NSManaged public var token: String + @NSManaged public var username: String + + @available(*, unavailable) + init() { + fatalError() + } + + @available(*, unavailable) + init(context: NSManagedObjectContext) { + fatalError() + } + + public override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) { + super.init(entity: entity, insertInto: context) + } + + @discardableResult + public init( + context: NSManagedObjectContext, + id: String, + name: String, + token: String, + username: String + ) { + let entity = NSEntityDescription.entity(forEntityName: "LoggedUser", in: context)! + super.init(entity: entity, insertInto: context) + self.id = id + self.name = name + self.token = token + self.username = username + } } extension LoggedUser: Identifiable { - + } diff --git a/ios/RocketChat Watch App/Database/Models/Room.swift b/ios/RocketChat Watch App/Database/Models/Room.swift index a1a6efa55..780e9dc46 100644 --- a/ios/RocketChat Watch App/Database/Models/Room.swift +++ b/ios/RocketChat Watch App/Database/Models/Room.swift @@ -1,22 +1,32 @@ import CoreData extension Room { - var messagesRequest: NSFetchRequest { - let request = Message.fetchRequest() - - request.predicate = NSPredicate(format: "room == %@", self) - request.sortDescriptors = [NSSortDescriptor(keyPath: \Message.ts, ascending: true)] - - return request - } - - var lastMessage: Message? { - let request = Message.fetchRequest() - - request.predicate = NSPredicate(format: "room == %@", self) - request.sortDescriptors = [NSSortDescriptor(keyPath: \Message.ts, ascending: false)] - request.fetchLimit = 1 - - return try? managedObjectContext?.fetch(request).first - } + var messagesRequest: NSFetchRequest { + let request = Message.fetchRequest() + + request.predicate = NSPredicate(format: "room == %@", self) + request.sortDescriptors = [NSSortDescriptor(keyPath: \Message.ts, ascending: true)] + + return request + } + + var lastMessage: Message? { + let request = Message.fetchRequest() + + request.predicate = NSPredicate(format: "room == %@", self) + request.sortDescriptors = [NSSortDescriptor(keyPath: \Message.ts, ascending: false)] + request.fetchLimit = 1 + + return try? managedObjectContext?.fetch(request).first + } + + var firstMessage: Message? { + let request = Message.fetchRequest() + + request.predicate = NSPredicate(format: "room == %@", self) + request.sortDescriptors = [NSSortDescriptor(keyPath: \Message.ts, ascending: true)] + request.fetchLimit = 1 + + return try? managedObjectContext?.fetch(request).first + } } diff --git a/ios/RocketChat Watch App/Database/Models/Server.swift b/ios/RocketChat Watch App/Database/Models/Server.swift index f1b19fb17..d15f518ce 100644 --- a/ios/RocketChat Watch App/Database/Models/Server.swift +++ b/ios/RocketChat Watch App/Database/Models/Server.swift @@ -2,64 +2,64 @@ import CoreData @objc public final class Server: NSManagedObject { - - @nonobjc public class func fetchRequest() -> NSFetchRequest { - NSFetchRequest(entityName: "Server") - } - - @NSManaged public var iconURL: URL - @NSManaged public var name: String - @NSManaged public var updatedSince: Date? - @NSManaged public var url: URL - @NSManaged public var useRealName: Bool - @NSManaged public var loggedUser: LoggedUser - - @available(*, unavailable) - init() { - fatalError() - } - - @available(*, unavailable) - init(context: NSManagedObjectContext) { - fatalError() - } - - public override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) { - super.init(entity: entity, insertInto: context) - } - - @discardableResult - public init( - context: NSManagedObjectContext, - iconURL: URL, - name: String, - updatedSince: Date? = nil, - url: URL, - useRealName: Bool, - loggedUser: LoggedUser - ) { - let entity = NSEntityDescription.entity(forEntityName: "Server", in: context)! - super.init(entity: entity, insertInto: context) - self.iconURL = iconURL - self.name = name - self.updatedSince = updatedSince - self.url = url - self.useRealName = useRealName - self.loggedUser = loggedUser - } + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + NSFetchRequest(entityName: "Server") + } + + @NSManaged public var iconURL: URL + @NSManaged public var name: String + @NSManaged public var updatedSince: Date? + @NSManaged public var url: URL + @NSManaged public var useRealName: Bool + @NSManaged public var loggedUser: LoggedUser + + @available(*, unavailable) + init() { + fatalError() + } + + @available(*, unavailable) + init(context: NSManagedObjectContext) { + fatalError() + } + + public override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) { + super.init(entity: entity, insertInto: context) + } + + @discardableResult + public init( + context: NSManagedObjectContext, + iconURL: URL, + name: String, + updatedSince: Date? = nil, + url: URL, + useRealName: Bool, + loggedUser: LoggedUser + ) { + let entity = NSEntityDescription.entity(forEntityName: "Server", in: context)! + super.init(entity: entity, insertInto: context) + self.iconURL = iconURL + self.name = name + self.updatedSince = updatedSince + self.url = url + self.useRealName = useRealName + self.loggedUser = loggedUser + } } extension Server: Identifiable { - + } extension Server { - var roomsRequest: NSFetchRequest { - let request = Room.fetchRequest() - - request.predicate = NSPredicate(format: "archived == false") - request.sortDescriptors = [NSSortDescriptor(keyPath: \Room.ts, ascending: false)] - - return request - } + var roomsRequest: NSFetchRequest { + let request = Room.fetchRequest() + + request.predicate = NSPredicate(format: "archived == false") + request.sortDescriptors = [NSSortDescriptor(keyPath: \Room.ts, ascending: false)] + + return request + } } diff --git a/ios/RocketChat Watch App/Database/RocketChat.xcdatamodeld/RocketChat.xcdatamodel/contents b/ios/RocketChat Watch App/Database/RocketChat.xcdatamodeld/RocketChat.xcdatamodel/contents index 8040459ed..2b1d98675 100644 --- a/ios/RocketChat Watch App/Database/RocketChat.xcdatamodeld/RocketChat.xcdatamodel/contents +++ b/ios/RocketChat Watch App/Database/RocketChat.xcdatamodeld/RocketChat.xcdatamodel/contents @@ -24,6 +24,7 @@ + diff --git a/ios/RocketChat Watch App/Database/RocketChatDatabase.swift b/ios/RocketChat Watch App/Database/RocketChatDatabase.swift index 5f815f9d2..248cbe570 100644 --- a/ios/RocketChat Watch App/Database/RocketChatDatabase.swift +++ b/ios/RocketChat Watch App/Database/RocketChatDatabase.swift @@ -1,216 +1,219 @@ import CoreData -final class RocketChatDatabase { - private let container: NSPersistentContainer - - var viewContext: NSManagedObjectContext { - container.viewContext - } - - private static let model: NSManagedObjectModel = { - guard let url = Bundle.main.url(forResource: "RocketChat", withExtension: "momd"), - let managedObjectModel = NSManagedObjectModel(contentsOf: url) else { - fatalError("Can't find Core Data Model") - } - - return managedObjectModel - }() - - init(name: String) { - container = NSPersistentContainer(name: name, managedObjectModel: Self.model) - - container.loadPersistentStores { _, error in - if let error { fatalError("Can't load persistent stores: \(error)") } - } - - container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - } - - func save() { - guard container.viewContext.hasChanges else { - return - } - - try? container.viewContext.save() - } - - func createUser(id: String) -> User { - let user = User(context: viewContext) - user.id = id - - return user - } - - func createRoom(id: String) -> Room { - let room = Room(context: viewContext) - room.id = id - - return room - } - - func createMessage(id: String) -> Message { - let message = Message(context: viewContext) - message.id = id - message.ts = Date() - - return message - } - - func createAttachment(url: String) -> Attachment { - let attachment = Attachment(context: viewContext) - attachment.imageURL = URL(string: url) - - return attachment - } - - func createTempMessage(msg: String, in room: Room, for loggedUser: LoggedUser) -> String { - let id = String.random(17) - let message = message(id: id) ?? createMessage(id: id) - - message.id = id - message.ts = Date() - message.room = room - message.status = "temp" // TODO: - message.msg = msg - - let user = user(id: loggedUser.id) ?? createUser(id: loggedUser.id) - user.username = loggedUser.username - user.name = loggedUser.name - message.user = user - - return id - } - - func user(id: String) -> User? { - let user = User(context: viewContext) - user.id = id - - return user - } - - func room(id: String) -> Room? { - let request = Room.fetchRequest() - request.predicate = NSPredicate(format: "id == %@", id) - - return try? viewContext.fetch(request).first - } - - func message(id: String) -> Message? { - let request = Message.fetchRequest() - request.predicate = NSPredicate(format: "id == %@", id) - - return try? viewContext.fetch(request).first - } - - func attachment(url: String) -> Attachment? { - let request = Attachment.fetchRequest() - request.predicate = NSPredicate(format: "imageURL == %@", url) - - return try? viewContext.fetch(request).first - } - - func rooms(ids: [String]) -> [Room] { - let request = Room.fetchRequest() - request.predicate = NSPredicate(format: "ANY id IN %@", ids) - - return (try? viewContext.fetch(request)) ?? [] - } - - func process(updatedMessage: MessageResponse, in room: Room) { - let message = message(id: updatedMessage._id) ?? createMessage(id: updatedMessage._id) - - let user = user(id: updatedMessage.u._id) ?? createUser(id: updatedMessage.u._id) - user.name = updatedMessage.u.name - user.username = updatedMessage.u.username - - message.status = "received" // TODO: - message.id = updatedMessage._id - message.msg = updatedMessage.msg - message.room = room - message.ts = updatedMessage.ts - message.user = user - message.t = updatedMessage.t - message.groupable = updatedMessage.groupable ?? true - - updatedMessage.attachments?.forEach { attachment in - process(updatedAttachment: attachment, in: message) - } - - save() - } - - func process(updatedAttachment: AttachmentResponse, in message: Message) { - guard let url = updatedAttachment.imageURL?.absoluteString else { - return - } - - let attachment = attachment(url: url) ?? createAttachment(url: url) - - attachment.msg = updatedAttachment.description - attachment.message = message - attachment.width = updatedAttachment.dimensions?.width ?? 0 - attachment.height = updatedAttachment.dimensions?.height ?? 0 - } - - func process(subscription: SubscriptionsResponse.Subscription?, in updatedRoom: RoomsResponse.Room) { - let room = room(id: updatedRoom._id) ?? createRoom(id: updatedRoom._id) - - room.name = updatedRoom.name - room.fname = updatedRoom.fname - room.updatedAt = updatedRoom._updatedAt - room.t = updatedRoom.t - room.usernames = updatedRoom.usernames - room.uids = updatedRoom.uids - room.prid = updatedRoom.prid - room.isReadOnly = updatedRoom.ro ?? false - room.encrypted = updatedRoom.encrypted ?? false - room.teamMain = updatedRoom.teamMain ?? false - room.archived = updatedRoom.archived ?? false - room.broadcast = updatedRoom.broadcast ?? false - - if let subscription { - room.alert = subscription.alert - room.name = room.name ?? subscription.name - room.fname = room.fname ?? subscription.fname - room.unread = Int32(subscription.unread) - } - - if let lastMessage = updatedRoom.lastMessage?.value { - process(updatedMessage: lastMessage, in: room) - } - - let lastRoomUpdate = updatedRoom.lm ?? updatedRoom.ts ?? updatedRoom._updatedAt - - if let lr = subscription?.lr, let lastRoomUpdate { - room.ts = max(lr, lastRoomUpdate) - } else { - room.ts = lastRoomUpdate - } - - save() - } - - func process(subscription: SubscriptionsResponse.Subscription) { - let room = room(id: subscription.rid) ?? createRoom(id: subscription.rid) - - room.alert = subscription.alert - room.name = room.name ?? subscription.name - room.fname = room.fname ?? subscription.fname - room.unread = Int32(subscription.unread) - - if let lr = subscription.lr, let lastRoomUpdate = room.ts { - room.ts = max(lr, lastRoomUpdate) - } - - save() - } - - func markRead(in roomID: String) { - let room = room(id: roomID) ?? createRoom(id: roomID) - - room.alert = false - room.unread = 0 - - save() - } +protocol Database { + var viewContext: NSManagedObjectContext { get } + + func room(id: String) -> Room? + func message(id: String) -> Message? + func createTempMessage(msg: String, in room: Room, for loggedUser: LoggedUser) -> String + + func process(subscription: SubscriptionsResponse.Subscription) + func process(subscription: SubscriptionsResponse.Subscription?, in updatedRoom: RoomsResponse.Room) + func process(updatedMessage: MessageResponse, in room: Room) +} + +final class RocketChatDatabase: Database { + private let container: NSPersistentContainer + + var viewContext: NSManagedObjectContext { + container.viewContext + } + + private static let model: NSManagedObjectModel = { + guard let url = Bundle.main.url(forResource: "RocketChat", withExtension: "momd"), + let managedObjectModel = NSManagedObjectModel(contentsOf: url) else { + fatalError("Can't find Core Data Model") + } + + return managedObjectModel + }() + + init(name: String) { + container = NSPersistentContainer(name: name, managedObjectModel: Self.model) + + container.loadPersistentStores { _, error in + if let error { fatalError("Can't load persistent stores: \(error)") } + } + + container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + } + + private func save() { + guard container.viewContext.hasChanges else { + return + } + + try? container.viewContext.save() + } + + func createUser(id: String) -> User { + let user = User(context: viewContext) + user.id = id + + return user + } + + func createRoom(id: String) -> Room { + let room = Room(context: viewContext) + room.id = id + + return room + } + + func createMessage(id: String) -> Message { + let message = Message(context: viewContext) + message.id = id + message.ts = Date() + + return message + } + + func createAttachment(url: String) -> Attachment { + let attachment = Attachment(context: viewContext) + attachment.imageURL = URL(string: url) + + return attachment + } + + func createTempMessage(msg: String, in room: Room, for loggedUser: LoggedUser) -> String { + let id = String.random(17) + let message = message(id: id) ?? createMessage(id: id) + + message.id = id + message.ts = Date() + message.room = room + message.status = "temp" // TODO: + message.msg = msg + + let user = user(id: loggedUser.id) ?? createUser(id: loggedUser.id) + user.username = loggedUser.username + user.name = loggedUser.name + message.user = user + + return id + } + + func user(id: String) -> User? { + let user = User(context: viewContext) + user.id = id + + return user + } + + func room(id: String) -> Room? { + let request = Room.fetchRequest() + request.predicate = NSPredicate(format: "id == %@", id) + + return try? viewContext.fetch(request).first + } + + func message(id: String) -> Message? { + let request = Message.fetchRequest() + request.predicate = NSPredicate(format: "id == %@", id) + + return try? viewContext.fetch(request).first + } + + func attachment(url: String) -> Attachment? { + let request = Attachment.fetchRequest() + request.predicate = NSPredicate(format: "imageURL == %@", url) + + return try? viewContext.fetch(request).first + } + + func rooms(ids: [String]) -> [Room] { + let request = Room.fetchRequest() + request.predicate = NSPredicate(format: "ANY id IN %@", ids) + + return (try? viewContext.fetch(request)) ?? [] + } + + func process(updatedMessage: MessageResponse, in room: Room) { + let message = message(id: updatedMessage._id) ?? createMessage(id: updatedMessage._id) + + let user = user(id: updatedMessage.u._id) ?? createUser(id: updatedMessage.u._id) + user.name = updatedMessage.u.name + user.username = updatedMessage.u.username + + message.status = "received" // TODO: + message.id = updatedMessage._id + message.msg = updatedMessage.msg + message.room = room + message.ts = updatedMessage.ts + message.user = user + message.t = updatedMessage.t + message.groupable = updatedMessage.groupable ?? true + + updatedMessage.attachments?.forEach { attachment in + process(updatedAttachment: attachment, in: message) + } + + save() + } + + func process(updatedAttachment: AttachmentResponse, in message: Message) { + guard let url = updatedAttachment.imageURL?.absoluteString else { + return + } + + let attachment = attachment(url: url) ?? createAttachment(url: url) + + attachment.msg = updatedAttachment.description + attachment.message = message + attachment.width = updatedAttachment.dimensions?.width ?? 0 + attachment.height = updatedAttachment.dimensions?.height ?? 0 + } + + func process(subscription: SubscriptionsResponse.Subscription?, in updatedRoom: RoomsResponse.Room) { + let room = room(id: updatedRoom._id) ?? createRoom(id: updatedRoom._id) + + room.name = updatedRoom.name + room.fname = updatedRoom.fname + room.updatedAt = updatedRoom._updatedAt + room.t = updatedRoom.t + room.usernames = updatedRoom.usernames + room.uids = updatedRoom.uids + room.prid = updatedRoom.prid + room.isReadOnly = updatedRoom.ro ?? false + room.encrypted = updatedRoom.encrypted ?? false + room.teamMain = updatedRoom.teamMain ?? false + room.archived = updatedRoom.archived ?? false + room.broadcast = updatedRoom.broadcast ?? false + + if let subscription { + room.alert = subscription.alert + room.name = room.name ?? subscription.name + room.fname = room.fname ?? subscription.fname + room.unread = Int32(subscription.unread) + } + + if let lastMessage = updatedRoom.lastMessage?.value { + process(updatedMessage: lastMessage, in: room) + } + + let lastRoomUpdate = updatedRoom.lm ?? updatedRoom.ts ?? updatedRoom._updatedAt + + if let lr = subscription?.lr, let lastRoomUpdate { + room.ts = max(lr, lastRoomUpdate) + } else { + room.ts = lastRoomUpdate + } + + save() + } + + func process(subscription: SubscriptionsResponse.Subscription) { + let room = room(id: subscription.rid) ?? createRoom(id: subscription.rid) + + room.alert = subscription.alert + room.name = room.name ?? subscription.name + room.fname = room.fname ?? subscription.fname + room.unread = Int32(subscription.unread) + + if let lr = subscription.lr, let lastRoomUpdate = room.ts { + room.ts = max(lr, lastRoomUpdate) + } + + save() + } } diff --git a/ios/RocketChat Watch App/DependencyInjection/DependencyStore.swift b/ios/RocketChat Watch App/DependencyInjection/DependencyStore.swift new file mode 100644 index 000000000..2f11248b4 --- /dev/null +++ b/ios/RocketChat Watch App/DependencyInjection/DependencyStore.swift @@ -0,0 +1,29 @@ +final class DependencyStore { + func client(for server: Server) -> RocketChatClientProtocol { + RocketChatClient(server: server) + } + + let connection = WatchConnection(session: .default) + + let database = DefaultDatabase() + + private var activeDatabase: WeakRef? + + func database(for server: Server) -> RocketChatDatabase { + if let activeDatabase = activeDatabase?.value { + return activeDatabase + } + + let database = RocketChatDatabase(name: server.url.host ?? "server") + activeDatabase = WeakRef(value: database) + return database + } +} + +private final class WeakRef { + weak var value: T? + + init(value: T) { + self.value = value + } +} diff --git a/ios/RocketChat Watch App/DependencyStore.swift b/ios/RocketChat Watch App/DependencyStore.swift deleted file mode 100644 index 09e17581b..000000000 --- a/ios/RocketChat Watch App/DependencyStore.swift +++ /dev/null @@ -1,29 +0,0 @@ -final class DependencyStore { - func client(for server: Server) -> RocketChatClientProtocol { - RocketChatClient(server: server) - } - - let connection = WatchConnection(session: .default) - - let database = Database() - - private var activeDatabase: WeakRef? - - func database(for server: Server) -> RocketChatDatabase { - if let activeDatabase = activeDatabase?.value { - return activeDatabase - } - - let database = RocketChatDatabase(name: server.url.host ?? "server") - activeDatabase = WeakRef(value: database) - return database - } -} - -private final class WeakRef { - weak var value: T? - - init(value: T) { - self.value = value - } -} diff --git a/ios/RocketChat Watch App/ExtensionDelegate.swift b/ios/RocketChat Watch App/ExtensionDelegate.swift new file mode 100644 index 000000000..1a18803f1 --- /dev/null +++ b/ios/RocketChat Watch App/ExtensionDelegate.swift @@ -0,0 +1,34 @@ +import WatchKit +import UserNotifications + +final class ExtensionDelegate: NSObject, WKExtensionDelegate, UNUserNotificationCenterDelegate { + func applicationDidFinishLaunching() { + let center = UNUserNotificationCenter.current() + center.delegate = self + + let replyAction = UNTextInputNotificationAction( + identifier: "REPLY_ACTION", + title: "Reply", + options: [], + textInputButtonTitle: "Reply", + textInputPlaceholder: "Message" + ) + + let category = UNNotificationCategory( + identifier: "MESSAGE", + actions: [replyAction], + intentIdentifiers: [], + options: [] + ) + + UNUserNotificationCenter.current().setNotificationCategories([category]) + } + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + print(response.notification) + } +} diff --git a/ios/RocketChat Watch App/Extensions/Date+Extensions.swift b/ios/RocketChat Watch App/Extensions/Date+Extensions.swift index cc5c5cb59..3d0d95ffd 100644 --- a/ios/RocketChat Watch App/Extensions/Date+Extensions.swift +++ b/ios/RocketChat Watch App/Extensions/Date+Extensions.swift @@ -1,7 +1,7 @@ import Foundation extension Date { - static func - (lhs: Date, rhs: Date) -> TimeInterval { - return lhs.timeIntervalSinceReferenceDate - rhs.timeIntervalSinceReferenceDate - } + static func - (lhs: Date, rhs: Date) -> TimeInterval { + return lhs.timeIntervalSinceReferenceDate - rhs.timeIntervalSinceReferenceDate + } } diff --git a/ios/RocketChat Watch App/Formatters/MessageFormatter.swift b/ios/RocketChat Watch App/Formatters/MessageFormatter.swift index 63ce03d22..ef7be53bb 100644 --- a/ios/RocketChat Watch App/Formatters/MessageFormatter.swift +++ b/ios/RocketChat Watch App/Formatters/MessageFormatter.swift @@ -1,74 +1,74 @@ import Foundation final class MessageFormatter { - private let message: Message - private let previousMessage: Message? - - init(message: Message, previousMessage: Message?) { - self.message = message - self.previousMessage = previousMessage - } - - func hasDateSeparator() -> Bool { - if let previousMessage, - let previousMessageTS = previousMessage.ts, - let messageTS = message.ts, - Calendar.current.isDate(previousMessageTS, inSameDayAs: messageTS) { - return false - } - return true - } - - func isHeader() -> Bool { - if let previousMessage, - let previousMessageTS = previousMessage.ts, - let messageTS = message.ts, - Calendar.current.isDate(previousMessageTS, inSameDayAs: messageTS), - previousMessage.user?.username == message.user?.username, - !(previousMessage.groupable == false || message.groupable == false || message.room?.broadcast == true), - messageTS - previousMessageTS < 300, - message.t != "rm", - previousMessage.t != "rm" { - return false - } - - return true - } - - func info() -> String? { - switch message.t { - case "rm": - return "Message Removed" - case "e2e": - return "Encrypted message" - default: - return nil - } - } - - func date() -> String? { - guard let ts = message.ts else { return nil } - - let dateFormatter = DateFormatter() - - dateFormatter.locale = Locale.current - dateFormatter.timeZone = TimeZone.current - dateFormatter.timeStyle = .none - dateFormatter.dateStyle = .long - - return dateFormatter.string(from: ts) - } - - func time() -> String? { - guard let ts = message.ts else { return nil } - - let dateFormatter = DateFormatter() - - dateFormatter.locale = Locale.current - dateFormatter.timeZone = TimeZone.current - dateFormatter.timeStyle = .short - dateFormatter.dateStyle = .none - - return dateFormatter.string(from: ts) - } + private let message: Message + private let previousMessage: Message? + + init(message: Message, previousMessage: Message?) { + self.message = message + self.previousMessage = previousMessage + } + + func hasDateSeparator() -> Bool { + if let previousMessage, + let previousMessageTS = previousMessage.ts, + let messageTS = message.ts, + Calendar.current.isDate(previousMessageTS, inSameDayAs: messageTS) { + return false + } + return true + } + + func isHeader() -> Bool { + if let previousMessage, + let previousMessageTS = previousMessage.ts, + let messageTS = message.ts, + Calendar.current.isDate(previousMessageTS, inSameDayAs: messageTS), + previousMessage.user?.username == message.user?.username, + !(previousMessage.groupable == false || message.groupable == false || message.room?.broadcast == true), + messageTS - previousMessageTS < 300, + message.t != "rm", + previousMessage.t != "rm" { + return false + } + + return true + } + + func info() -> String? { + switch message.t { + case "rm": + return "Message Removed" + case "e2e": + return "Encrypted message" + default: + return nil + } + } + + func date() -> String? { + guard let ts = message.ts else { return nil } + + let dateFormatter = DateFormatter() + + dateFormatter.locale = Locale.current + dateFormatter.timeZone = TimeZone.current + dateFormatter.timeStyle = .none + dateFormatter.dateStyle = .long + + return dateFormatter.string(from: ts) + } + + func time() -> String? { + guard let ts = message.ts else { return nil } + + let dateFormatter = DateFormatter() + + dateFormatter.locale = Locale.current + dateFormatter.timeZone = TimeZone.current + dateFormatter.timeStyle = .short + dateFormatter.dateStyle = .none + + return dateFormatter.string(from: ts) + } } diff --git a/ios/RocketChat Watch App/Formatters/RoomFormatter.swift b/ios/RocketChat Watch App/Formatters/RoomFormatter.swift index fc50a117d..3b3d7eb34 100644 --- a/ios/RocketChat Watch App/Formatters/RoomFormatter.swift +++ b/ios/RocketChat Watch App/Formatters/RoomFormatter.swift @@ -1,46 +1,46 @@ import Foundation final class RoomFormatter { - private let room: Room - private let server: Server - - init(room: Room, server: Server) { - self.room = room - self.server = server - } - - var title: String? { - if isGroupChat, (room.name == nil || room.name?.isEmpty == true), let usernames = room.usernames { - return usernames - .filter { $0 == server.loggedUser.username } - .sorted() - .joined(separator: ", ") - } - - if room.t != "d" { - if let fname = room.fname { - return fname - } else if let name = room.name { - return name - } - } - - if room.prid != nil || server.useRealName { - return room.fname - } - - return room.name - } - - var isGroupChat: Bool { - if let uids = room.uids, uids.count > 2 { - return true - } - - if let usernames = room.usernames, usernames.count > 2 { - return true - } - - return false - } + private let room: Room + private let server: Server + + init(room: Room, server: Server) { + self.room = room + self.server = server + } + + var title: String? { + if isGroupChat, (room.name == nil || room.name?.isEmpty == true), let usernames = room.usernames { + return usernames + .filter { $0 == server.loggedUser.username } + .sorted() + .joined(separator: ", ") + } + + if room.t != "d" { + if let fname = room.fname { + return fname + } else if let name = room.name { + return name + } + } + + if room.prid != nil || server.useRealName { + return room.fname + } + + return room.name + } + + var isGroupChat: Bool { + if let uids = room.uids, uids.count > 2 { + return true + } + + if let usernames = room.usernames, usernames.count > 2 { + return true + } + + return false + } } diff --git a/ios/RocketChat Watch App/Loaders/CancelBag.swift b/ios/RocketChat Watch App/Loaders/CancelBag.swift new file mode 100644 index 000000000..4461ea559 --- /dev/null +++ b/ios/RocketChat Watch App/Loaders/CancelBag.swift @@ -0,0 +1,10 @@ +import Combine + +typealias CancelBag = Set + +extension CancelBag { + mutating func cancelAll() { + forEach { $0.cancel() } + removeAll() + } +} diff --git a/ios/RocketChat Watch App/Loaders/MessageSender.swift b/ios/RocketChat Watch App/Loaders/MessageSender.swift new file mode 100644 index 000000000..2af4fff16 --- /dev/null +++ b/ios/RocketChat Watch App/Loaders/MessageSender.swift @@ -0,0 +1,47 @@ +import Combine +import Foundation + +protocol MessageSending { + func sendMessage(_ msg: String, in room: Room) +} + +final class MessageSender { + private let client: RocketChatClientProtocol + private let database: Database + private let server: Server + + init( + client: RocketChatClientProtocol, + database: Database, + server: Server + ) { + self.client = client + self.database = database + self.server = server + } +} + +extension MessageSender: MessageSending { + func sendMessage(_ msg: String, in room: Room) { + guard let rid = room.id else { return } + + let messageID = database.createTempMessage(msg: msg, in: room, for: server.loggedUser) + + client.sendMessage(id: messageID, rid: rid, msg: msg) + .receive(on: DispatchQueue.main) + .subscribe(Subscribers.Sink { completion in + if case .failure(let error) = completion { + print(error) + } + } receiveValue: { [weak self] messageResponse in + guard let self else { + return + } + + let message = messageResponse.message + + database.process(updatedMessage: message, in: room) + }) + } +} + diff --git a/ios/RocketChat Watch App/Loaders/MessagesLoader.swift b/ios/RocketChat Watch App/Loaders/MessagesLoader.swift new file mode 100644 index 000000000..e5322cb51 --- /dev/null +++ b/ios/RocketChat Watch App/Loaders/MessagesLoader.swift @@ -0,0 +1,133 @@ +import CoreData +import Combine +import Foundation + +protocol MessagesLoading { + func start(on roomID: String) + func loadMore(from date: Date) + + func stop() +} + +final class MessagesLoader { + private var timer: Timer? + private var cancellable = CancelBag() + + private let client: RocketChatClientProtocol + private let database: Database + private let serversDB: ServersDatabase + + private var roomID: String? + + init( + client: RocketChatClientProtocol, + database: Database, + serversDB: ServersDatabase + ) { + self.client = client + self.database = database + self.serversDB = serversDB + } + + private func scheduledSyncMessages(in room: Room, from date: Date) { + timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] _ in + self?.syncMessages(in: room, from: date) + } + } + + private func syncMessages(in room: Room, from date: Date) { + guard let rid = room.id else { return } + + let newUpdatedSince = Date() + + client.syncMessages(rid: rid, updatedSince: date) + .receive(on: DispatchQueue.main) + .sink { completion in + if case .failure(let error) = completion { + print(error) + } + } receiveValue: { [weak self] messagesResponse in + let messages = messagesResponse.result.updated + + room.updatedSince = newUpdatedSince + + for message in messages { + self?.database.process(updatedMessage: message, in: room) + } + + self?.scheduledSyncMessages(in: room, from: newUpdatedSince) + + self?.markAsRead(in: room) + } + .store(in: &cancellable) + } + + private func loadMessages(in room: Room, from date: Date) { + guard let rid = room.id else { return } + + client.getHistory(rid: rid, t: room.t ?? "", latest: date) + .receive(on: DispatchQueue.main) + .sink { completion in + if case .failure(let error) = completion { + print(error) + } + } receiveValue: { [weak self] messagesResponse in + let messages = messagesResponse.messages + + if let lastMessage = messages.last, self?.database.message(id: lastMessage._id) == nil, messages.count == 20 { + room.hasMoreMessages = true + } + + for message in messages { + self?.database.process(updatedMessage: message, in: room) + } + } + .store(in: &cancellable) + } + + private func markAsRead(in room: Room) { + guard (room.unread > 0 || room.alert), let rid = room.id else { + return + } + + client.sendRead(rid: rid) + .receive(on: DispatchQueue.main) + .sink { completion in + if case .failure(let error) = completion { + print(error) + } + } receiveValue: { _ in + + } + .store(in: &cancellable) + } +} + +extension MessagesLoader: MessagesLoading { + func start(on roomID: String) { + stop() + + self.roomID = roomID + + guard let room = database.room(id: roomID) else { return } + + if let updatedSince = room.updatedSince { + loadMessages(in: room, from: updatedSince) + syncMessages(in: room, from: updatedSince) + } else { + loadMessages(in: room, from: .now) + syncMessages(in: room, from: .now) + } + } + + func loadMore(from date: Date) { + guard let roomID, let room = database.room(id: roomID) else { return } + + loadMessages(in: room, from: date) + } + + func stop() { + timer?.invalidate() + cancellable.cancelAll() + } +} diff --git a/ios/RocketChat Watch App/Loaders/RoomsLoader.swift b/ios/RocketChat Watch App/Loaders/RoomsLoader.swift new file mode 100644 index 000000000..d4f37153c --- /dev/null +++ b/ios/RocketChat Watch App/Loaders/RoomsLoader.swift @@ -0,0 +1,79 @@ +import CoreData +import Combine +import Foundation + +protocol RoomsLoading { + func start(in url: URL) + func stop() +} + +final class RoomsLoader { + private var timer: Timer? + private var cancellable = CancelBag() + + private let client: RocketChatClientProtocol + private let database: Database + private let serversDB: ServersDatabase + + init(client: RocketChatClientProtocol, database: Database, serversDB: ServersDatabase) { + self.client = client + self.database = database + self.serversDB = serversDB + } + + private func scheduledLoadRooms(in server: Server) { + timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] _ in + self?.loadRooms(in: server) + } + } + + private func loadRooms(in server: Server) { + let newUpdatedSince = Date() + + let updatedSince = server.updatedSince + + Publishers.Zip( + client.getRooms(updatedSince: updatedSince), + client.getSubscriptions(updatedSince: updatedSince) + ) + .receive(on: DispatchQueue.main) + .sink { completion in + if case .failure(let error) = completion { + // TODO: LOGOUT + print(error) + } + } receiveValue: { (roomsResponse, subscriptionsResponse) in + let rooms = roomsResponse.update + let subscriptions = subscriptionsResponse.update + + for room in rooms { + let subscription = subscriptions.find(withRoomID: room._id) + + self.database.process(subscription: subscription, in: room) + } + + for subscription in subscriptions { + self.database.process(subscription: subscription) + } + + self.scheduledLoadRooms(in: server) + + server.updatedSince = newUpdatedSince + self.serversDB.save() + } + .store(in: &cancellable) + } +} + +extension RoomsLoader: RoomsLoading { + func start(in url: URL) { + guard let server = serversDB.server(url: url) else { return } + + loadRooms(in: server) + } + + func stop() { + timer?.invalidate() + cancellable.cancelAll() + } +} diff --git a/ios/RocketChat Watch App/Localizable.xcstrings b/ios/RocketChat Watch App/Localizable.xcstrings index 42aea340c..661924f80 100644 --- a/ios/RocketChat Watch App/Localizable.xcstrings +++ b/ios/RocketChat Watch App/Localizable.xcstrings @@ -1,6 +1,9 @@ { "sourceLanguage" : "en", "strings" : { + "Load More..." : { + + }, "Message" : { }, diff --git a/ios/RocketChat Watch App/NotificationController.swift b/ios/RocketChat Watch App/NotificationController.swift new file mode 100644 index 000000000..8bca64fd1 --- /dev/null +++ b/ios/RocketChat Watch App/NotificationController.swift @@ -0,0 +1,59 @@ +import SwiftUI +import UserNotifications +import WatchKit + +struct NotificationView: View { + let title: String? + let message: String? + + var body: some View { + VStack(alignment: .leading) { + Text(title ?? "") + .font(.caption) + .fontWeight(.bold) + .foregroundStyle(.primary) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity) + Text(message ?? "") + .font(.caption) + .foregroundStyle(.primary) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity) + } + } +} + +final class NotificationController: WKUserNotificationHostingController { + private var title: String? + private var message: String? + + override var body: NotificationView { + NotificationView(title: title, message: message) + } + + override func didReceive(_ notification: UNNotification) { + let notificationData = notification.request.content.userInfo as? [String: Any] + let aps = notificationData?["aps"] as? [String: Any] + let alert = aps?["alert"] as? [String: Any] + + title = alert?["title"] as? String + message = alert?["body"] as? String + } + + override func suggestionsForResponseToAction( + withIdentifier identifier: String, + for notification: UNNotification, + inputLanguage: String + ) -> [String] { + [ + "message-1", + "message-2", + "message-3", + "message-4", + "message-5", + "message-6", + "message-7", + "message-8" + ] + } +} diff --git a/ios/RocketChat Watch App/PushNotificationPayload.apns b/ios/RocketChat Watch App/PushNotificationPayload.apns new file mode 100644 index 000000000..6af1d8469 --- /dev/null +++ b/ios/RocketChat Watch App/PushNotificationPayload.apns @@ -0,0 +1,11 @@ +{ + "aps": { + "alert": { + "body": "Hey, how is everything going?", + "title": "Djorkaeff Pereira", + "subtitle": "Optional subtitle" + }, + "category": "MESSAGE" + }, + "Simulator Target Bundle": "chat.rocket.reactnative.watchkitapp" +} diff --git a/ios/RocketChat Watch App/RocketChatApp.swift b/ios/RocketChat Watch App/RocketChatApp.swift index c14b67a87..00ae4a83d 100644 --- a/ios/RocketChat Watch App/RocketChatApp.swift +++ b/ios/RocketChat Watch App/RocketChatApp.swift @@ -2,52 +2,69 @@ import SwiftUI @main struct RocketChat_Watch_AppApp: App { - private let store: DependencyStore - - @StateObject var router: RocketChatAppRouter - - init() { - let store = DependencyStore() - - self.store = store - self._router = StateObject(wrappedValue: RocketChatAppRouter(database: store.database)) - } - - @ViewBuilder - private var serverListView: some View { - ServerListView( - dependencies: .init( - connection: store.connection, - database: store.database, - router: router - ) - ) - } - - @ViewBuilder - private func roomListView(for server: Server) -> some View { - RoomListView( - dependencies: .init( - client: store.client(for: server), - database: store.database(for: server), - router: router, - server: server - ) - ) - } - - var body: some Scene { - WindowGroup { - NavigationStack { - switch router.route { - case .roomList(let server): - roomListView(for: server) - .environment(\.managedObjectContext, store.database(for: server).viewContext) - case .serverList: - serverListView - .environment(\.managedObjectContext, store.database.viewContext) - } - } - } - } + private let store: DependencyStore + + @WKExtensionDelegateAdaptor private var delegate: ExtensionDelegate + + @StateObject var router: RocketChatAppRouter + + init() { + let store = DependencyStore() + + self.store = store + self._router = StateObject(wrappedValue: RocketChatAppRouter(database: store.database)) + } + + @ViewBuilder + private var serverListView: some View { + ServerListView( + dependencies: .init( + connection: store.connection, + database: store.database, + router: router + ) + ) + } + + @ViewBuilder + private func roomListView(for server: Server) -> some View { + RoomListView( + client: store.client(for: server), + database: store.database(for: server), + messagesLoader: MessagesLoader( + client: store.client(for: server), + database: store.database(for: server), + serversDB: store.database + ), + messageSender: MessageSender( + client: store.client(for: server), + database: store.database(for: server), + server: server + ), + roomsLoader: RoomsLoader( + client: store.client(for: server), + database: store.database(for: server), + serversDB: store.database + ), + router: router, + server: server + ) + } + + var body: some Scene { + WindowGroup { + NavigationStack { + switch router.route { + case .roomList(let server): + roomListView(for: server) + .environment(\.managedObjectContext, store.database(for: server).viewContext) + case .serverList: + serverListView + .environment(\.managedObjectContext, store.database.viewContext) + } + } + } + + WKNotificationScene(controller: NotificationController.self, category: "MESSAGE") + } } diff --git a/ios/RocketChat Watch App/RocketChatAppRouter.swift b/ios/RocketChat Watch App/RocketChatAppRouter.swift index 1a476c836..be957c40e 100644 --- a/ios/RocketChat Watch App/RocketChatAppRouter.swift +++ b/ios/RocketChat Watch App/RocketChatAppRouter.swift @@ -1,37 +1,46 @@ import Foundation final class RocketChatAppRouter: ObservableObject { - @Storage("current_server") var currentServer: URL? - - @Published var route: Route = .serverList - - private let database: Database - - init(database: Database) { - self.database = database - loadRoute() - } - - private func loadRoute() { - if let currentServer, let server = database.server(url: currentServer) { - route = .roomList(server) - } else if database.servers().count == 1, let server = database.servers().first { - route = .roomList(server) - } else { - route = .serverList - } - } - - func route(to route: Route) { - DispatchQueue.main.async { - self.route = route - } - } + @Storage("current_server") var currentServer: URL? + + @Published var route: Route = .serverList { + didSet { + switch route { + case .roomList(let server): + currentServer = server.url + case .serverList: + break + } + } + } + + private let database: ServersDatabase + + init(database: ServersDatabase) { + self.database = database + loadRoute() + } + + private func loadRoute() { + if let currentServer, let server = database.server(url: currentServer) { + route = .roomList(server) + } else if database.servers().count == 1, let server = database.servers().first { + route = .roomList(server) + } else { + route = .serverList + } + } + + func route(to route: Route) { + DispatchQueue.main.async { + self.route = route + } + } } extension RocketChatAppRouter { - enum Route { - case roomList(Server) - case serverList - } + enum Route { + case roomList(Server) + case serverList + } } diff --git a/ios/RocketChat Watch App/Storage.swift b/ios/RocketChat Watch App/Storage.swift index 8930f84e8..353b604ba 100644 --- a/ios/RocketChat Watch App/Storage.swift +++ b/ios/RocketChat Watch App/Storage.swift @@ -2,27 +2,27 @@ import Foundation @propertyWrapper struct Storage { - private let key: String - private let defaultValue: T? - - init(_ key: String, defaultValue: T? = nil) { - self.key = key - self.defaultValue = defaultValue - } - - var wrappedValue: T? { - get { - guard let data = UserDefaults.standard.object(forKey: key) as? Data else { - return defaultValue - } - - let value = try? JSONDecoder().decode(T.self, from: data) - return value ?? defaultValue - } - set { - let data = try? JSONEncoder().encode(newValue) - - UserDefaults.standard.set(data, forKey: key) - } - } + private let key: String + private let defaultValue: T? + + init(_ key: String, defaultValue: T? = nil) { + self.key = key + self.defaultValue = defaultValue + } + + var wrappedValue: T? { + get { + guard let data = UserDefaults.standard.object(forKey: key) as? Data else { + return defaultValue + } + + let value = try? JSONDecoder().decode(T.self, from: data) + return value ?? defaultValue + } + set { + let data = try? JSONEncoder().encode(newValue) + + UserDefaults.standard.set(data, forKey: key) + } + } } diff --git a/ios/RocketChat Watch App/ViewModels/MessageComposerViewModel.swift b/ios/RocketChat Watch App/ViewModels/MessageComposerViewModel.swift deleted file mode 100644 index 55ba84202..000000000 --- a/ios/RocketChat Watch App/ViewModels/MessageComposerViewModel.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Combine -import Foundation - -final class MessageComposerViewModel: ObservableObject { - var isReadOnly: Bool { - room.isReadOnly - } - - private let room: Room - private let client: RocketChatClientProtocol - private let database: RocketChatDatabase - private let server: Server - - init(client: RocketChatClientProtocol, database: RocketChatDatabase, room: Room, server: Server) { - self.client = client - self.database = database - self.room = room - self.server = server - } - - func sendMessage(_ msg: String) { - guard let rid = room.id else { return } - - let messageID = database.createTempMessage(msg: msg, in: room, for: server.loggedUser) - - client.sendMessage(id: messageID, rid: rid, msg: msg) - .receive(on: DispatchQueue.main) - .subscribe(Subscribers.Sink { completion in - if case .failure(let error) = completion { - print(error) - } - } receiveValue: { [weak self] messageResponse in - guard let self else { - return - } - - let message = messageResponse.message - - database.process(updatedMessage: message, in: room) - }) - } -} diff --git a/ios/RocketChat Watch App/ViewModels/MessageListViewModel.swift b/ios/RocketChat Watch App/ViewModels/MessageListViewModel.swift deleted file mode 100644 index b68bc9900..000000000 --- a/ios/RocketChat Watch App/ViewModels/MessageListViewModel.swift +++ /dev/null @@ -1,126 +0,0 @@ -import Combine -import Foundation - -protocol MessageListViewModeling { - func composerViewModel() -> MessageComposerViewModel - func messageViewModel(for message: Message, and previousMessage: Message?) -> MessageViewModel - - func loadMessages(completionHandler: (() -> Void)?) - func markAsRead() - func stop() -} - -final class MessageListViewModel: ObservableObject { - @Published private var server: Server - - @Published private(set) var room: Room - @Published private(set) var lastMessageID: String? - - private let client: RocketChatClientProtocol - private let database: RocketChatDatabase - private let formatter: RoomFormatter - - private var timer: Timer? - - private var syncCancellable: AnyCancellable? - - var title: String { - formatter.title ?? "" - } - - init( - client: RocketChatClientProtocol, - database: RocketChatDatabase, - room: Room, - server: Server - ) { - self.client = client - self.database = database - self.room = room - self.server = server - self.formatter = RoomFormatter(room: room, server: server) - } - - deinit { - print("MessageListViewModel.deinit \(room.fname ?? "")") - } - - private func scheduledSync(in room: Room) -> Timer { - Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in - self?.syncMessages(in: room) - } - } - - private func syncMessages(in room: Room) { - guard let rid = room.id else { return } - - syncCancellable = client.syncMessages(rid: rid, updatedSince: room.updatedSince ?? Date()) - .receive(on: DispatchQueue.main) - .sink { completion in - if case .failure(let error) = completion { - print(error) - } - } receiveValue: { messagesResponse in - let messages = messagesResponse.result.updated - - for message in messages { - self.database.process(updatedMessage: message, in: room) - } - } - } - - private func loadMessages(in room: Room, latest: Date?) { - guard let rid = room.id else { return } - - room.updatedSince = latest - - client.getHistory(rid: rid, t: room.t ?? "", latest: latest ?? Date()) - .receive(on: DispatchQueue.main) - .subscribe(Subscribers.Sink { completion in - if case .failure(let error) = completion { - print(error) - } - } receiveValue: { messagesResponse in - let messages = messagesResponse.messages - - for message in messages { - self.database.process(updatedMessage: message, in: room) - } - }) - } -} - -extension MessageListViewModel: MessageListViewModeling { - func composerViewModel() -> MessageComposerViewModel { - MessageComposerViewModel(client: client, database: database, room: room, server: server) - } - - func messageViewModel(for message: Message, and previousMessage: Message?) -> MessageViewModel { - MessageViewModel(message: message, previousMessage: previousMessage, server: server) - } - - func loadMessages(completionHandler: (() -> Void)? = nil) { - loadMessages(in: room, latest: room.lastMessage?.ts) - } - - func markAsRead() { - guard (room.unread > 0 || room.alert), let rid = room.id else { - return - } - - client.sendRead(rid: rid) - .receive(on: DispatchQueue.main) - .subscribe(Subscribers.Sink { completion in - if case .failure(let error) = completion { - print(error) - } - } receiveValue: { _ in - self.database.markRead(in: rid) - }) - } - - func stop() { - syncCancellable?.cancel() - timer?.invalidate() - } -} diff --git a/ios/RocketChat Watch App/ViewModels/MessageViewModel.swift b/ios/RocketChat Watch App/ViewModels/MessageViewModel.swift index 938b66225..4e32f3b22 100644 --- a/ios/RocketChat Watch App/ViewModels/MessageViewModel.swift +++ b/ios/RocketChat Watch App/ViewModels/MessageViewModel.swift @@ -1,21 +1,21 @@ import Foundation final class MessageViewModel: ObservableObject { - @Published private(set) var server: Server? - @Published private(set) var message: Message - @Published private(set) var previousMessage: Message? - - let messageFormatter: MessageFormatter - - init(message: Message, previousMessage: Message? = nil, server: Server?) { - self.message = message - self.previousMessage = previousMessage - self.messageFormatter = MessageFormatter(message: message, previousMessage: previousMessage) - self.server = server - } - - var sender: String? { - server?.useRealName == true ? message.user?.name : message.user?.username - } - + @Published private(set) var server: Server? + @Published private(set) var message: Message + @Published private(set) var previousMessage: Message? + + let messageFormatter: MessageFormatter + + init(message: Message, previousMessage: Message? = nil, server: Server?) { + self.message = message + self.previousMessage = previousMessage + self.messageFormatter = MessageFormatter(message: message, previousMessage: previousMessage) + self.server = server + } + + var sender: String? { + server?.useRealName == true ? message.user?.name : message.user?.username + } + } diff --git a/ios/RocketChat Watch App/ViewModels/RoomListViewModel.swift b/ios/RocketChat Watch App/ViewModels/RoomListViewModel.swift deleted file mode 100644 index e724a5645..000000000 --- a/ios/RocketChat Watch App/ViewModels/RoomListViewModel.swift +++ /dev/null @@ -1,108 +0,0 @@ -import Combine -import CoreData -import Foundation - -protocol RoomListViewModeling { - func roomViewModel(for room: Room) -> RoomViewModel - func messageListViewModel(for room: Room) -> MessageListViewModel - - func loadRooms() - func logout() -} - -final class RoomListViewModel: ObservableObject { - struct Dependencies { - let client: RocketChatClientProtocol - let database: RocketChatDatabase - let router: RocketChatAppRouter - let server: Server - } - - var viewContext: NSManagedObjectContext { - dependencies.database.viewContext - } - - private let dependencies: Dependencies - - private var loadCancellable: AnyCancellable? - - init(dependencies: Dependencies) { - self.dependencies = dependencies - } - - private func scheduledLoadRooms() { - Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] _ in - self?.loadRooms() - } - } - - private func handleError(_ error: RocketChatError) { - switch error { - case .decoding(let error): - print(error) - case .unknown(let error): - print(error) - case .unauthorized: - logout() // TODO: Remove database and server entry - } - } -} - -// MARK: - RoomListViewModeling - -extension RoomListViewModel: RoomListViewModeling { - func roomViewModel(for room: Room) -> RoomViewModel { - RoomViewModel( - room: room, - server: dependencies.server - ) - } - - func messageListViewModel(for room: Room) -> MessageListViewModel { - MessageListViewModel( - client: dependencies.client, - database: dependencies.database, - room: room, - server: dependencies.server - ) - } - - func loadRooms() { - let newUpdatedSince = Date() - - let updatedSince = dependencies.server.updatedSince - - let client = dependencies.client - - loadCancellable = Publishers.Zip( - client.getRooms(updatedSince: updatedSince), - client.getSubscriptions(updatedSince: updatedSince) - ) - .receive(on: DispatchQueue.main) - .sink { completion in - if case .failure(let error) = completion { - self.handleError(error) - } - } receiveValue: { (roomsResponse, subscriptionsResponse) in - let rooms = roomsResponse.update - let subscriptions = subscriptionsResponse.update - - for room in rooms { - let subscription = subscriptions.find(withRoomID: room._id) - - self.dependencies.database.process(subscription: subscription, in: room) - } - - for subscription in subscriptions { - self.dependencies.database.process(subscription: subscription) - } - - self.scheduledLoadRooms() - self.dependencies.server.updatedSince = newUpdatedSince - } - } - - func logout() { - dependencies.router.route(to: .serverList) - } -} diff --git a/ios/RocketChat Watch App/ViewModels/RoomViewModel.swift b/ios/RocketChat Watch App/ViewModels/RoomViewModel.swift index 0aabc078e..7de3c23aa 100644 --- a/ios/RocketChat Watch App/ViewModels/RoomViewModel.swift +++ b/ios/RocketChat Watch App/ViewModels/RoomViewModel.swift @@ -1,110 +1,110 @@ import Foundation final class RoomViewModel: ObservableObject { - @Published var room: Room - @Published var server: Server - - let formatter: RoomFormatter - - init(room: Room, server: Server) { - self.room = room - self.server = server - self.formatter = RoomFormatter(room: room, server: server) - } - - var iconName: String? { - if room.prid != nil { - return "discussions" - } else if room.teamMain == true, room.t == "p" { - return "teams-private" - } else if room.teamMain == true { - return "teams" - } else if room.t == "p" { - return "channel-private" - } else if room.t == "c" { - return "channel-public" - } else if room.t == "d", formatter.isGroupChat { - return "message" - } - - return nil - } - - var lastMessage: String { - guard let user = room.lastMessage?.user else { - return "No Message" - } - - let isLastMessageSentByMe = user.username == server.loggedUser.username - let username = isLastMessageSentByMe ? "You" : ((server.useRealName ? user.name : user.username) ?? "") - let message = room.lastMessage?.msg ?? "No message" - - if room.lastMessage?.t == "jitsi_call_started" { - return "Call started by: \(username)" - } - - if room.lastMessage?.attachments?.allObjects.isEmpty == false { - return "\(username) sent an attachment" - } - - if room.lastMessage?.t == "e2e" { - return "Encrypted message" - } - - if room.lastMessage?.t == "videoconf" { - return "Call started" - } - - if room.t == "d", !isLastMessageSentByMe { - return message - } - - return "\(username): \(message)" - } - - var updatedAt: String? { - guard let ts = room.ts else { - return nil - } - - let calendar = Calendar.current - let dateFormatter = DateFormatter() - dateFormatter.locale = Locale.current - dateFormatter.timeZone = TimeZone.current - - if calendar.isDateInYesterday(ts) { - return "Yesterday" - } - - if calendar.isDateInToday(ts) { - dateFormatter.timeStyle = .short - dateFormatter.dateStyle = .none - - return dateFormatter.string(from: ts) - } - - if isInPreviousWeek(date: ts) { - dateFormatter.dateFormat = "EEEE" - - return dateFormatter.string(from: ts) - } - - dateFormatter.timeStyle = .none - dateFormatter.dateStyle = .short - - return dateFormatter.string(from: ts) - } - - private func isInPreviousWeek(date: Date) -> Bool { - let oneDay = 24 * 60 * 60 - let calendar = Calendar.current - let currentDate = Date() - let lastWeekDate = currentDate.addingTimeInterval(TimeInterval(-7 * oneDay)) - - return calendar.isDate( - date, - equalTo: lastWeekDate, - toGranularity: .weekOfYear - ) - } + @Published var room: Room + @Published var server: Server + + let formatter: RoomFormatter + + init(room: Room, server: Server) { + self.room = room + self.server = server + self.formatter = RoomFormatter(room: room, server: server) + } + + var iconName: String? { + if room.prid != nil { + return "discussions" + } else if room.teamMain == true, room.t == "p" { + return "teams-private" + } else if room.teamMain == true { + return "teams" + } else if room.t == "p" { + return "channel-private" + } else if room.t == "c" { + return "channel-public" + } else if room.t == "d", formatter.isGroupChat { + return "message" + } + + return nil + } + + var lastMessage: String { + guard let user = room.lastMessage?.user else { + return "No Message" + } + + let isLastMessageSentByMe = user.username == server.loggedUser.username + let username = isLastMessageSentByMe ? "You" : ((server.useRealName ? user.name : user.username) ?? "") + let message = room.lastMessage?.msg ?? "No message" + + if room.lastMessage?.t == "jitsi_call_started" { + return "Call started by: \(username)" + } + + if room.lastMessage?.attachments?.allObjects.isEmpty == false { + return "\(username) sent an attachment" + } + + if room.lastMessage?.t == "e2e" { + return "Encrypted message" + } + + if room.lastMessage?.t == "videoconf" { + return "Call started" + } + + if room.t == "d", !isLastMessageSentByMe { + return message + } + + return "\(username): \(message)" + } + + var updatedAt: String? { + guard let ts = room.ts else { + return nil + } + + let calendar = Calendar.current + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale.current + dateFormatter.timeZone = TimeZone.current + + if calendar.isDateInYesterday(ts) { + return "Yesterday" + } + + if calendar.isDateInToday(ts) { + dateFormatter.timeStyle = .short + dateFormatter.dateStyle = .none + + return dateFormatter.string(from: ts) + } + + if isInPreviousWeek(date: ts) { + dateFormatter.dateFormat = "EEEE" + + return dateFormatter.string(from: ts) + } + + dateFormatter.timeStyle = .none + dateFormatter.dateStyle = .short + + return dateFormatter.string(from: ts) + } + + private func isInPreviousWeek(date: Date) -> Bool { + let oneDay = 24 * 60 * 60 + let calendar = Calendar.current + let currentDate = Date() + let lastWeekDate = currentDate.addingTimeInterval(TimeInterval(-7 * oneDay)) + + return calendar.isDate( + date, + equalTo: lastWeekDate, + toGranularity: .weekOfYear + ) + } } diff --git a/ios/RocketChat Watch App/ViewModels/ServerListViewModel.swift b/ios/RocketChat Watch App/ViewModels/ServerListViewModel.swift index 0e006ce0b..91772277b 100644 --- a/ios/RocketChat Watch App/ViewModels/ServerListViewModel.swift +++ b/ios/RocketChat Watch App/ViewModels/ServerListViewModel.swift @@ -1,55 +1,55 @@ import Foundation enum ServerListState { - case loading - case loaded - case error(ConnectionError) + case loading + case loaded + case error(ConnectionError) } final class ServerListViewModel: ObservableObject { - struct Dependencies { - let connection: Connection - let database: Database - let router: RocketChatAppRouter - } - - private let dependencies: Dependencies - - @Published private(set) var state: ServerListState = .loading - - init(dependencies: Dependencies) { - self.dependencies = dependencies - } - - private func handleSuccess(message: WatchMessage) { - message.servers.forEach(dependencies.database.process(updatedServer:)) - state = .loaded - } - - private func handleFailure(error: Error) { - guard let connectionError = error as? ConnectionError else { - return - } - - state = .error(connectionError) - } - - func loadServers() { - dependencies.connection.sendMessage { [weak self] result in - guard let self else { - return - } - - switch result { - case .success(let message): - DispatchQueue.main.async { self.handleSuccess(message: message) } - case .failure(let error): - DispatchQueue.main.async { self.handleFailure(error: error) } - } - } - } - - func didTap(server: Server) { - dependencies.router.route(to: .roomList(server)) - } + struct Dependencies { + let connection: Connection + let database: ServersDatabase + let router: RocketChatAppRouter + } + + private let dependencies: Dependencies + + @Published private(set) var state: ServerListState = .loading + + init(dependencies: Dependencies) { + self.dependencies = dependencies + } + + private func handleSuccess(message: WatchMessage) { + message.servers.forEach(dependencies.database.process(updatedServer:)) + state = .loaded + } + + private func handleFailure(error: Error) { + guard let connectionError = error as? ConnectionError else { + return + } + + state = .error(connectionError) + } + + func loadServers() { + dependencies.connection.sendMessage { [weak self] result in + guard let self else { + return + } + + switch result { + case .success(let message): + DispatchQueue.main.async { self.handleSuccess(message: message) } + case .failure(let error): + DispatchQueue.main.async { self.handleFailure(error: error) } + } + } + } + + func didTap(server: Server) { + dependencies.router.route(to: .roomList(server)) + } } diff --git a/ios/RocketChat Watch App/Views/MessageComposerView.swift b/ios/RocketChat Watch App/Views/MessageComposerView.swift index d658e3db8..e67c1fbc7 100644 --- a/ios/RocketChat Watch App/Views/MessageComposerView.swift +++ b/ios/RocketChat Watch App/Views/MessageComposerView.swift @@ -1,38 +1,35 @@ import SwiftUI struct MessageComposerView: View { - @ObservedObject private var viewModel: MessageComposerViewModel - - init(viewModel: MessageComposerViewModel) { - self.viewModel = viewModel - } - - @State private var message = "" - - var body: some View { - if viewModel.isReadOnly { - HStack { - Spacer() - Text("This room is read only") - .font(.caption) - .fontWeight(.bold) - .foregroundStyle(.white) - .multilineTextAlignment(.center) - Spacer() - } - } else { - TextField("Message", text: $message) - .submitLabel(.send) - .onSubmit(send) - } - } - - func send() { - guard !message.isEmpty else { - return - } - - viewModel.sendMessage(message) - message = "" - } + @State private var message = "" + + let room: Room + let onSend: (String) -> Void + + var body: some View { + if room.isReadOnly { + HStack { + Spacer() + Text("This room is read only") + .font(.caption) + .fontWeight(.bold) + .foregroundStyle(.white) + .multilineTextAlignment(.center) + Spacer() + } + } else { + TextField("Message", text: $message) + .submitLabel(.send) + .onSubmit(send) + } + } + + func send() { + guard !message.isEmpty else { + return + } + + onSend(message) + message = "" + } } diff --git a/ios/RocketChat Watch App/Views/MessageListView.swift b/ios/RocketChat Watch App/Views/MessageListView.swift index 1ff4d44b9..19943c1c4 100644 --- a/ios/RocketChat Watch App/Views/MessageListView.swift +++ b/ios/RocketChat Watch App/Views/MessageListView.swift @@ -1,48 +1,76 @@ import SwiftUI struct MessageListView: View { - @StateObject private var viewModel: MessageListViewModel - - @FetchRequest private var messages: FetchedResults - - init(viewModel: MessageListViewModel) { - _viewModel = StateObject(wrappedValue: viewModel) - _messages = FetchRequest(fetchRequest: viewModel.room.messagesRequest, animation: .none) - } - - var body: some View { - ScrollViewReader { reader in - ScrollView { - VStack(alignment: .leading, spacing: 8) { - ForEach(messages.indices, id: \.self) { index in - let message = messages[index] - let previousMessage = messages.indices.contains(index - 1) ? messages[index - 1] : nil - - MessageView(viewModel: viewModel.messageViewModel(for: message, and: previousMessage)) - .id(message.id) - .transition(.move(edge: .bottom)) - } - - MessageComposerView(viewModel: viewModel.composerViewModel()) - .padding(.top) - } - } - .padding([.leading, .trailing]) - .navigationTitle(viewModel.title) - .navigationBarTitleDisplayMode(.inline) - .onAppear { - viewModel.loadMessages { - reader.scrollTo(messages.last?.id, anchor: .bottom) - } - - viewModel.markAsRead() - } - .onDisappear { - viewModel.stop() - } - .onReceive(messages.publisher) { _ in - viewModel.markAsRead() - } - } - } + private let messageComposer = "MESSAGE_COMPOSER_ID" + + private let client: RocketChatClientProtocol + private let database: Database + private let messagesLoader: MessagesLoading + private let messageSender: MessageSending + private let formatter: RoomFormatter + private let server: Server + private let room: Room + + @FetchRequest private var messages: FetchedResults + + init( + client: RocketChatClientProtocol, + database: Database, + messagesLoader: MessagesLoading, + messageSender: MessageSending, + room: Room, + server: Server + ) { + self.client = client + self.database = database + self.messagesLoader = messagesLoader + self.messageSender = messageSender + self.formatter = RoomFormatter(room: room, server: server) + self.room = room + self.server = server + _messages = FetchRequest(fetchRequest: room.messagesRequest, animation: .none) + } + + var body: some View { + ScrollViewReader { reader in + ScrollView { + VStack(alignment: .leading, spacing: 8) { + if room.hasMoreMessages { + Button("Load More...") { + guard let oldestMessage = room.firstMessage?.ts else { return } + + messagesLoader.loadMore(from: oldestMessage) + } + } + + ForEach(messages.indices, id: \.self) { index in + let message = messages[index] + let previousMessage = messages.indices.contains(index - 1) ? messages[index - 1] : nil + + MessageView(viewModel: .init(message: message, previousMessage: previousMessage, server: server)) + .transition(.move(edge: .bottom)) + } + + MessageComposerView(room: room) { + messageSender.sendMessage($0, in: room) + } + .id(messageComposer) + } + } + .padding([.leading, .trailing]) + .navigationTitle(formatter.title ?? "") + .navigationBarTitleDisplayMode(.inline) + .onAppear { + guard let roomID = room.id else { return } + + messagesLoader.start(on: roomID) + } + .onDisappear { + messagesLoader.stop() + } + .onReceive(messages.publisher) { _ in + reader.scrollTo(messageComposer, anchor: .bottom) + } + } + } } diff --git a/ios/RocketChat Watch App/Views/MessageView.swift b/ios/RocketChat Watch App/Views/MessageView.swift index 5cecb531a..ea649ce9c 100644 --- a/ios/RocketChat Watch App/Views/MessageView.swift +++ b/ios/RocketChat Watch App/Views/MessageView.swift @@ -1,59 +1,59 @@ import SwiftUI struct MessageView: View { - @ObservedObject private var viewModel: MessageViewModel - - init(viewModel: MessageViewModel) { - self.viewModel = viewModel - } - - var body: some View { - VStack(alignment: .leading) { - if viewModel.messageFormatter.hasDateSeparator() { - HStack(alignment: .center) { - VStack(alignment: .center) { - Divider() - .overlay(.gray) - } - Text(viewModel.messageFormatter.date() ?? "") - .lineLimit(1) - .font(.footnote) - .foregroundStyle(.gray) - .layoutPriority(1) - VStack(alignment: .center) { - Divider() - .overlay(.gray) - } - } - } - if viewModel.messageFormatter.isHeader() { - HStack(alignment: .center) { - Text(viewModel.sender ?? "") - .lineLimit(1) - .font(.caption) - .fontWeight(.bold) - .foregroundStyle(.primary) - Text(viewModel.messageFormatter.time() ?? "") - .lineLimit(1) - .font(.footnote) - .foregroundStyle(.secondary) - } - } - if let text = viewModel.messageFormatter.info() { - Text(text) - .font(.caption) - .foregroundStyle(.white) - .italic() - } else if let text = viewModel.message.msg { - Text(text) - .font(.caption) - .foregroundStyle(viewModel.message.status == "temp" ? .secondary : .primary) - } -// if let attachments = message.attachments?.allObjects as? Array { -// ForEach(attachments) { attachment in -// AttachmentView(attachment: attachment) -// } -// } - } - } + @ObservedObject private var viewModel: MessageViewModel + + init(viewModel: MessageViewModel) { + self.viewModel = viewModel + } + + var body: some View { + VStack(alignment: .leading) { + if viewModel.messageFormatter.hasDateSeparator() { + HStack(alignment: .center) { + VStack(alignment: .center) { + Divider() + .overlay(.secondary) + } + Text(viewModel.messageFormatter.date() ?? "") + .lineLimit(1) + .font(.footnote) + .foregroundStyle(.secondary) + .layoutPriority(1) + VStack(alignment: .center) { + Divider() + .overlay(.secondary) + } + } + } + if viewModel.messageFormatter.isHeader() { + HStack(alignment: .center) { + Text(viewModel.sender ?? "") + .lineLimit(1) + .font(.caption) + .fontWeight(.bold) + .foregroundStyle(.primary) + Text(viewModel.messageFormatter.time() ?? "") + .lineLimit(1) + .font(.footnote) + .foregroundStyle(.secondary) + } + } + if let text = viewModel.messageFormatter.info() { + Text(text) + .font(.caption) + .foregroundStyle(.primary) + .italic() + } else if let text = viewModel.message.msg { + Text(text) + .font(.caption) + .foregroundStyle(viewModel.message.status == "temp" ? .secondary : .primary) + } + // if let attachments = message.attachments?.allObjects as? Array { + // ForEach(attachments) { attachment in + // AttachmentView(attachment: attachment) + // } + // } + } + } } diff --git a/ios/RocketChat Watch App/Views/RoomListView.swift b/ios/RocketChat Watch App/Views/RoomListView.swift index 7911e20d4..59bba016e 100644 --- a/ios/RocketChat Watch App/Views/RoomListView.swift +++ b/ios/RocketChat Watch App/Views/RoomListView.swift @@ -1,38 +1,68 @@ import SwiftUI struct RoomListView: View { - @StateObject private var viewModel: RoomListViewModel - - @FetchRequest private var rooms: FetchedResults - - init(dependencies: RoomListViewModel.Dependencies) { - _viewModel = StateObject(wrappedValue: RoomListViewModel(dependencies: dependencies)) - _rooms = FetchRequest(fetchRequest: dependencies.server.roomsRequest) - } - - var body: some View { - List { - ForEach(rooms) { room in - NavigationLink(value: room) { - RoomView(viewModel: viewModel.roomViewModel(for: room)) - } - } - } - .onAppear { - viewModel.loadRooms() - } - .navigationTitle("Rooms") - .navigationBarTitleDisplayMode(.inline) - .navigationDestination(for: Room.self) { room in - MessageListView(viewModel: viewModel.messageListViewModel(for: room)) - .environment(\.managedObjectContext, viewModel.viewContext) - } - .toolbar { - ToolbarItem(placement: .automatic) { - Button("Servers") { - viewModel.logout() - } - } - } - } + private let client: RocketChatClientProtocol + private let database: Database + private let messagesLoader: MessagesLoading + private let messageSender: MessageSending + private let roomsLoader: RoomsLoading + private let router: RocketChatAppRouter + private let server: Server + + @FetchRequest private var rooms: FetchedResults + + init( + client: RocketChatClientProtocol, + database: Database, + messagesLoader: MessagesLoading, + messageSender: MessageSending, + roomsLoader: RoomsLoading, + router: RocketChatAppRouter, + server: Server + ) { + self.client = client + self.database = database + self.messagesLoader = messagesLoader + self.messageSender = messageSender + self.roomsLoader = roomsLoader + self.router = router + self.server = server + _rooms = FetchRequest(fetchRequest: server.roomsRequest) + } + + var body: some View { + List { + ForEach(rooms) { room in + NavigationLink(value: room) { + RoomView(viewModel: .init(room: room, server: server)) + } + } + } + .onAppear { + roomsLoader.start(in: server.url) + } + .onDisappear { + roomsLoader.stop() + } + .navigationTitle("Rooms") + .navigationBarTitleDisplayMode(.inline) + .navigationDestination(for: Room.self) { room in + MessageListView( + client: client, + database: database, + messagesLoader: messagesLoader, + messageSender: messageSender, + room: room, + server: server + ) + .environment(\.managedObjectContext, database.viewContext) + } + .toolbar { + ToolbarItem(placement: .automatic) { + Button("Servers") { + router.route(to: .serverList) + } + } + } + } } diff --git a/ios/RocketChat Watch App/Views/RoomView.swift b/ios/RocketChat Watch App/Views/RoomView.swift index 291bf650d..54395d24d 100644 --- a/ios/RocketChat Watch App/Views/RoomView.swift +++ b/ios/RocketChat Watch App/Views/RoomView.swift @@ -1,51 +1,51 @@ import SwiftUI struct RoomView: View { - @ObservedObject var viewModel: RoomViewModel - - private var isUnread: Bool { - viewModel.room.unread > 0 || viewModel.room.alert - } - - var body: some View { - VStack(alignment: .leading) { - HStack { - if let iconName = viewModel.iconName { - Image(iconName) - .resizable() - .frame(width: 16, height: 16) - .scaledToFit() - } - Text(viewModel.formatter.title ?? "") - .lineLimit(1) - .font(.caption) - .fontWeight(isUnread ? .bold : .medium) - .foregroundStyle(.primary) - Spacer() - Text(viewModel.updatedAt ?? "") - .lineLimit(1) - .font(.footnote) - .fontWeight(isUnread ? .bold : .medium) - .foregroundStyle(isUnread ? .blue : .primary) - } - HStack(alignment: .top) { - Text(viewModel.lastMessage) - .lineLimit(2) - .font(.caption2) - .foregroundStyle(isUnread ? .primary : .secondary) - Spacer() - if isUnread, viewModel.room.unread > 0 { - Text(String(viewModel.room.unread)) - .font(.footnote) - .fontWeight(.bold) - .padding(6) - .background( - Circle() - .fill(.blue) - ) - .foregroundColor(.primary) - } - } - } - } + @ObservedObject var viewModel: RoomViewModel + + private var isUnread: Bool { + viewModel.room.unread > 0 || viewModel.room.alert + } + + var body: some View { + VStack(alignment: .leading) { + HStack { + if let iconName = viewModel.iconName { + Image(iconName) + .resizable() + .frame(width: 16, height: 16) + .scaledToFit() + } + Text(viewModel.formatter.title ?? "") + .lineLimit(1) + .font(.caption) + .fontWeight(isUnread ? .bold : .medium) + .foregroundStyle(.primary) + Spacer() + Text(viewModel.updatedAt ?? "") + .lineLimit(1) + .font(.footnote) + .fontWeight(isUnread ? .bold : .medium) + .foregroundStyle(isUnread ? .blue : .primary) + } + HStack(alignment: .top) { + Text(viewModel.lastMessage) + .lineLimit(2) + .font(.caption2) + .foregroundStyle(isUnread ? .primary : .secondary) + Spacer() + if isUnread, viewModel.room.unread > 0 { + Text(String(viewModel.room.unread)) + .font(.footnote) + .fontWeight(.bold) + .padding(6) + .background( + Circle() + .fill(.blue) + ) + .foregroundColor(.primary) + } + } + } + } } diff --git a/ios/RocketChat Watch App/Views/ServerListView.swift b/ios/RocketChat Watch App/Views/ServerListView.swift index bf0886529..e1aa23df5 100644 --- a/ios/RocketChat Watch App/Views/ServerListView.swift +++ b/ios/RocketChat Watch App/Views/ServerListView.swift @@ -1,53 +1,53 @@ import SwiftUI struct ServerListView: View { - @StateObject var viewModel: ServerListViewModel - - @FetchRequest(entity: Server.entity(), sortDescriptors: [], animation: .default) - private var servers: FetchedResults - - init(dependencies: ServerListViewModel.Dependencies) { - _viewModel = StateObject(wrappedValue: ServerListViewModel(dependencies: dependencies)) - } - - @ViewBuilder - private func errorView(_ text: String) -> some View { - VStack(alignment: .center) { - Text(text) - Button("Try Again") { - viewModel.loadServers() - } - } - } - - var body: some View { - VStack { - switch viewModel.state { - case .loading: - ProgressView() - case .loaded where servers.count > 0: - List { - ForEach(servers) { server in - ServerView(server: server) - .onTapGesture { - viewModel.didTap(server: server) - } - } - } - case .loaded: - errorView("There are no servers connected.") - case .error(let connectionError): - switch connectionError { - case .needsUnlock: - errorView("You need to unlock your iPhone.") - case .decoding: - errorView("We can't read servers information.") - } - } - } - .navigationTitle("Servers") - .onAppear { - viewModel.loadServers() - } - } + @StateObject var viewModel: ServerListViewModel + + @FetchRequest(entity: Server.entity(), sortDescriptors: [], animation: .default) + private var servers: FetchedResults + + init(dependencies: ServerListViewModel.Dependencies) { + _viewModel = StateObject(wrappedValue: ServerListViewModel(dependencies: dependencies)) + } + + @ViewBuilder + private func errorView(_ text: String) -> some View { + VStack(alignment: .center) { + Text(text) + Button("Try Again") { + viewModel.loadServers() + } + } + } + + var body: some View { + VStack { + switch viewModel.state { + case .loading: + ProgressView() + case .loaded where servers.count > 0: + List { + ForEach(servers) { server in + ServerView(server: server) + .onTapGesture { + viewModel.didTap(server: server) + } + } + } + case .loaded: + errorView("There are no servers connected.") + case .error(let connectionError): + switch connectionError { + case .needsUnlock: + errorView("You need to unlock your iPhone.") + case .decoding: + errorView("We can't read servers information.") + } + } + } + .navigationTitle("Servers") + .onAppear { + viewModel.loadServers() + } + } } diff --git a/ios/RocketChat Watch App/Views/ServerView.swift b/ios/RocketChat Watch App/Views/ServerView.swift index f76e5e12e..ea86d33fc 100644 --- a/ios/RocketChat Watch App/Views/ServerView.swift +++ b/ios/RocketChat Watch App/Views/ServerView.swift @@ -1,17 +1,17 @@ import SwiftUI struct ServerView: View { - @ObservedObject var server: Server - - var body: some View { - VStack(alignment: .leading) { - Text(server.name) - .font(.caption) - .fontWeight(.bold) - .foregroundStyle(.primary) - Text(server.url.host ?? "") - .font(.caption) - .foregroundStyle(.primary) - } - } + @ObservedObject var server: Server + + var body: some View { + VStack(alignment: .leading) { + Text(server.name) + .font(.caption) + .fontWeight(.bold) + .foregroundStyle(.primary) + Text(server.url.host ?? "") + .font(.caption) + .foregroundStyle(.primary) + } + } } diff --git a/ios/RocketChat Watch App/WatchConnection.swift b/ios/RocketChat Watch App/WatchConnection.swift index 7e56808c8..4007636c3 100644 --- a/ios/RocketChat Watch App/WatchConnection.swift +++ b/ios/RocketChat Watch App/WatchConnection.swift @@ -2,67 +2,67 @@ import Foundation import WatchConnectivity enum ConnectionError: Error { - case needsUnlock - case decoding(Error) + case needsUnlock + case decoding(Error) } protocol Connection { - func sendMessage(completionHandler: @escaping (Result) -> Void) + func sendMessage(completionHandler: @escaping (Result) -> Void) } final class WatchConnection: NSObject { - private let session: WCSession - - init(session: WCSession) { - self.session = session - super.init() - session.delegate = self - session.activate() - } - - private func scheduledSendMessage(completionHandler: @escaping (Result) -> Void) { - Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { [weak self] _ in - self?.sendMessage(completionHandler: completionHandler) - } - } + private let session: WCSession + + init(session: WCSession) { + self.session = session + super.init() + session.delegate = self + session.activate() + } + + private func scheduledSendMessage(completionHandler: @escaping (Result) -> Void) { + Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { [weak self] _ in + self?.sendMessage(completionHandler: completionHandler) + } + } } // MARK: - WCSessionDelegate extension WatchConnection: WCSessionDelegate { - func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { - - } + func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { + + } } // MARK: - Connection extension WatchConnection: Connection { - func sendMessage(completionHandler: @escaping (Result) -> Void) { - guard session.activationState == .activated else { - scheduledSendMessage(completionHandler: completionHandler) - return - } - - guard !session.iOSDeviceNeedsUnlockAfterRebootForReachability else { - completionHandler(.failure(.needsUnlock)) - return - } - - guard session.isReachable else { - scheduledSendMessage(completionHandler: completionHandler) - return - } - - session.sendMessage([:]) { dictionary in - do { - let data = try JSONSerialization.data(withJSONObject: dictionary) - let message = try JSONDecoder().decode(WatchMessage.self, from: data) - - completionHandler(.success(message)) - } catch { - completionHandler(.failure(.decoding(error))) - } - } - } + func sendMessage(completionHandler: @escaping (Result) -> Void) { + guard session.activationState == .activated else { + scheduledSendMessage(completionHandler: completionHandler) + return + } + + guard !session.iOSDeviceNeedsUnlockAfterRebootForReachability else { + completionHandler(.failure(.needsUnlock)) + return + } + + guard session.isReachable else { + scheduledSendMessage(completionHandler: completionHandler) + return + } + + session.sendMessage([:]) { dictionary in + do { + let data = try JSONSerialization.data(withJSONObject: dictionary) + let message = try JSONDecoder().decode(WatchMessage.self, from: data) + + completionHandler(.success(message)) + } catch { + completionHandler(.failure(.decoding(error))) + } + } + } } diff --git a/ios/RocketChatRN.xcodeproj/project.pbxproj b/ios/RocketChatRN.xcodeproj/project.pbxproj index fda5b0295..18e7c18ef 100644 --- a/ios/RocketChatRN.xcodeproj/project.pbxproj +++ b/ios/RocketChatRN.xcodeproj/project.pbxproj @@ -35,7 +35,6 @@ 1E1EA81A2326CD5100E22452 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E1EA8192326CD5100E22452 /* libsqlite3.tbd */; }; 1E25743422CBA2CF005A877F /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7ACD4853222860DE00442C55 /* JavaScriptCore.framework */; }; 1E29A2CC2B5857F50093C03C /* RoomListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A2CB2B5857F50093C03C /* RoomListView.swift */; }; - 1E29A2CE2B5857FC0093C03C /* RoomListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A2CD2B5857FC0093C03C /* RoomListViewModel.swift */; }; 1E29A2D02B58582F0093C03C /* RoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A2CF2B58582F0093C03C /* RoomView.swift */; }; 1E29A2EF2B585B070093C03C /* RocketChatClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A2D22B585B070093C03C /* RocketChatClient.swift */; }; 1E29A2F02B585B070093C03C /* AttachmentResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A2D42B585B070093C03C /* AttachmentResponse.swift */; }; @@ -67,7 +66,6 @@ 1E29A30E2B58608C0093C03C /* LoggedUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A30D2B58608C0093C03C /* LoggedUser.swift */; }; 1E29A3102B5865B80093C03C /* RoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A30F2B5865B80093C03C /* RoomViewModel.swift */; }; 1E29A3122B5866090093C03C /* Room.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3112B5866090093C03C /* Room.swift */; }; - 1E29A3142B5868D80093C03C /* MessageListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3132B5868D80093C03C /* MessageListViewModel.swift */; }; 1E29A3162B5868DF0093C03C /* MessageListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3152B5868DF0093C03C /* MessageListView.swift */; }; 1E29A3182B5868E50093C03C /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3172B5868E50093C03C /* MessageView.swift */; }; 1E29A31A2B5868EE0093C03C /* MessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3192B5868EE0093C03C /* MessageViewModel.swift */; }; @@ -75,7 +73,6 @@ 1E29A3202B5871C80093C03C /* RoomFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A31F2B5871C80093C03C /* RoomFormatter.swift */; }; 1E29A3222B5871CE0093C03C /* MessageFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3212B5871CE0093C03C /* MessageFormatter.swift */; }; 1E29A3242B5874FF0093C03C /* MessageComposerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3232B5874FF0093C03C /* MessageComposerView.swift */; }; - 1E29A3262B58752D0093C03C /* MessageComposerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3252B58752D0093C03C /* MessageComposerViewModel.swift */; }; 1E2F615B25128F9A00871711 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2F615A25128F9A00871711 /* API.swift */; }; 1E2F615D25128FA300871711 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2F615C25128FA300871711 /* Response.swift */; }; 1E2F61642512955D00871711 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2F61632512955D00871711 /* HTTPMethod.swift */; }; @@ -86,6 +83,7 @@ 1E598AE42515057D002BDFBD /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E598AE32515057D002BDFBD /* Date+Extensions.swift */; }; 1E598AE725150660002BDFBD /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E598AE625150660002BDFBD /* Data+Extensions.swift */; }; 1E598AE925151A63002BDFBD /* SendMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E598AE825151A63002BDFBD /* SendMessage.swift */; }; + 1E6436242B59998A009F0CE1 /* ExtensionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E6436232B59998A009F0CE1 /* ExtensionDelegate.swift */; }; 1E67380424DC529B0009E081 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E67380324DC529B0009E081 /* String+Extensions.swift */; }; 1E680ED92512990700C9257A /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E680ED82512990700C9257A /* Request.swift */; }; 1E6CC61F2513DBF400965591 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7A006F13229C83B600803143 /* GoogleService-Info.plist */; }; @@ -111,6 +109,8 @@ 1E76CBD825152C870067298C /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E680ED82512990700C9257A /* Request.swift */; }; 1E76CBD925152C8C0067298C /* Push.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2F61652512958900871711 /* Push.swift */; }; 1E76CBDA25152C8E0067298C /* SendMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E598AE825151A63002BDFBD /* SendMessage.swift */; }; + 1E9A71672B599E6300477BA2 /* NotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9A71662B599E6300477BA2 /* NotificationController.swift */; }; + 1E9A71692B59B6E100477BA2 /* MessageSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9A71682B59B6E100477BA2 /* MessageSender.swift */; }; 1EB375892B55DBFB00AEC3D7 /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB375882B55DBFB00AEC3D7 /* Server.swift */; }; 1EB8EF722510F1EE00F352B7 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB8EF712510F1EE00F352B7 /* Storage.swift */; }; 1EC6ACB722CB9FC300A41C61 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1EC6ACB522CB9FC300A41C61 /* MainInterface.storyboard */; }; @@ -155,6 +155,9 @@ 1ED038C62B50A21800C007D4 /* WatchMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038C32B50A1F500C007D4 /* WatchMessage.swift */; }; 1ED038CA2B50A58400C007D4 /* WatchConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038C92B50A58400C007D4 /* WatchConnection.swift */; }; 1ED59D4C22CBA77D00C54289 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1ED59D4B22CBA77D00C54289 /* GoogleService-Info.plist */; }; + 1EDFD0FA2B589B8F002FEE5F /* MessagesLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EDFD0F92B589B8F002FEE5F /* MessagesLoader.swift */; }; + 1EDFD1062B58A66E002FEE5F /* CancelBag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EDFD1052B58A66E002FEE5F /* CancelBag.swift */; }; + 1EDFD1082B58AA77002FEE5F /* RoomsLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EDFD1072B58AA77002FEE5F /* RoomsLoader.swift */; }; 1EF5FBD1250C109E00614FEA /* Encryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF5FBD0250C109E00614FEA /* Encryption.swift */; }; 1EFEB5982493B6640072EDC0 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFEB5972493B6640072EDC0 /* NotificationService.swift */; }; 1EFEB59C2493B6640072EDC0 /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 1EFEB5952493B6640072EDC0 /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -338,7 +341,6 @@ 1E1EA8172326CD4B00E22452 /* libc.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libc.tbd; path = usr/lib/libc.tbd; sourceTree = SDKROOT; }; 1E1EA8192326CD5100E22452 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; 1E29A2CB2B5857F50093C03C /* RoomListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListView.swift; sourceTree = ""; }; - 1E29A2CD2B5857FC0093C03C /* RoomListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListViewModel.swift; sourceTree = ""; }; 1E29A2CF2B58582F0093C03C /* RoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomView.swift; sourceTree = ""; }; 1E29A2D22B585B070093C03C /* RocketChatClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RocketChatClient.swift; sourceTree = ""; }; 1E29A2D42B585B070093C03C /* AttachmentResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentResponse.swift; sourceTree = ""; }; @@ -370,7 +372,6 @@ 1E29A30D2B58608C0093C03C /* LoggedUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedUser.swift; sourceTree = ""; }; 1E29A30F2B5865B80093C03C /* RoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomViewModel.swift; sourceTree = ""; }; 1E29A3112B5866090093C03C /* Room.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Room.swift; sourceTree = ""; }; - 1E29A3132B5868D80093C03C /* MessageListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageListViewModel.swift; sourceTree = ""; }; 1E29A3152B5868DF0093C03C /* MessageListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageListView.swift; sourceTree = ""; }; 1E29A3172B5868E50093C03C /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; 1E29A3192B5868EE0093C03C /* MessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageViewModel.swift; sourceTree = ""; }; @@ -378,7 +379,6 @@ 1E29A31F2B5871C80093C03C /* RoomFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFormatter.swift; sourceTree = ""; }; 1E29A3212B5871CE0093C03C /* MessageFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageFormatter.swift; sourceTree = ""; }; 1E29A3232B5874FF0093C03C /* MessageComposerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposerView.swift; sourceTree = ""; }; - 1E29A3252B58752D0093C03C /* MessageComposerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposerViewModel.swift; sourceTree = ""; }; 1E2F615A25128F9A00871711 /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; 1E2F615C25128FA300871711 /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = ""; }; 1E2F61632512955D00871711 /* HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; @@ -389,9 +389,13 @@ 1E598AE32515057D002BDFBD /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = ""; }; 1E598AE625150660002BDFBD /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = ""; }; 1E598AE825151A63002BDFBD /* SendMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessage.swift; sourceTree = ""; }; + 1E6436232B59998A009F0CE1 /* ExtensionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionDelegate.swift; sourceTree = ""; }; 1E6737FF24DC52660009E081 /* NotificationService-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NotificationService-Bridging-Header.h"; sourceTree = ""; }; 1E67380324DC529B0009E081 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; 1E680ED82512990700C9257A /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; + 1E9A71652B599D3F00477BA2 /* PushNotificationPayload.apns */ = {isa = PBXFileReference; lastKnownFileType = text; path = PushNotificationPayload.apns; sourceTree = ""; }; + 1E9A71662B599E6300477BA2 /* NotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationController.swift; sourceTree = ""; }; + 1E9A71682B59B6E100477BA2 /* MessageSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSender.swift; sourceTree = ""; }; 1EB375882B55DBFB00AEC3D7 /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = ""; }; 1EB8EF712510F1EE00F352B7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; 1EC6ACB022CB9FC300A41C61 /* ShareRocketChatRN.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareRocketChatRN.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -425,6 +429,9 @@ 1ED038C32B50A1F500C007D4 /* WatchMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchMessage.swift; sourceTree = ""; }; 1ED038C92B50A58400C007D4 /* WatchConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConnection.swift; sourceTree = ""; }; 1ED59D4B22CBA77D00C54289 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = SOURCE_ROOT; }; + 1EDFD0F92B589B8F002FEE5F /* MessagesLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesLoader.swift; sourceTree = ""; }; + 1EDFD1052B58A66E002FEE5F /* CancelBag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelBag.swift; sourceTree = ""; }; + 1EDFD1072B58AA77002FEE5F /* RoomsLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomsLoader.swift; sourceTree = ""; }; 1EF5FBD0250C109E00614FEA /* Encryption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encryption.swift; sourceTree = ""; }; 1EFEB5952493B6640072EDC0 /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 1EFEB5972493B6640072EDC0 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; @@ -777,11 +784,8 @@ isa = PBXGroup; children = ( 1ED033B72B55B4BE004F4930 /* ServerListViewModel.swift */, - 1E29A2CD2B5857FC0093C03C /* RoomListViewModel.swift */, 1E29A30F2B5865B80093C03C /* RoomViewModel.swift */, - 1E29A3132B5868D80093C03C /* MessageListViewModel.swift */, 1E29A3192B5868EE0093C03C /* MessageViewModel.swift */, - 1E29A3252B58752D0093C03C /* MessageComposerViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -789,6 +793,8 @@ 1ED0388F2B507B4C00C007D4 /* RocketChat Watch App */ = { isa = PBXGroup; children = ( + 1EDFD0FB2B589FC4002FEE5F /* DependencyInjection */, + 1EDFD0F82B589B82002FEE5F /* Loaders */, 1E29A31E2B5871BE0093C03C /* Formatters */, 1E29A31B2B5871AC0093C03C /* Extensions */, 1E29A2D12B585B070093C03C /* Client */, @@ -802,7 +808,9 @@ 1ED038942B507B4D00C007D4 /* Assets.xcassets */, 1ED038962B507B4D00C007D4 /* Preview Content */, 1ED033C02B55C190004F4930 /* Localizable.xcstrings */, - 1ED033C72B55CE78004F4930 /* DependencyStore.swift */, + 1E6436232B59998A009F0CE1 /* ExtensionDelegate.swift */, + 1E9A71652B599D3F00477BA2 /* PushNotificationPayload.apns */, + 1E9A71662B599E6300477BA2 /* NotificationController.swift */, ); path = "RocketChat Watch App"; sourceTree = ""; @@ -834,6 +842,25 @@ path = Database; sourceTree = ""; }; + 1EDFD0F82B589B82002FEE5F /* Loaders */ = { + isa = PBXGroup; + children = ( + 1EDFD0F92B589B8F002FEE5F /* MessagesLoader.swift */, + 1EDFD1052B58A66E002FEE5F /* CancelBag.swift */, + 1EDFD1072B58AA77002FEE5F /* RoomsLoader.swift */, + 1E9A71682B59B6E100477BA2 /* MessageSender.swift */, + ); + path = Loaders; + sourceTree = ""; + }; + 1EDFD0FB2B589FC4002FEE5F /* DependencyInjection */ = { + isa = PBXGroup; + children = ( + 1ED033C72B55CE78004F4930 /* DependencyStore.swift */, + ); + path = DependencyInjection; + sourceTree = ""; + }; 1EFEB5962493B6640072EDC0 /* NotificationService */ = { isa = PBXGroup; children = ( @@ -904,9 +931,9 @@ 7AC2B09613AA7C3FEBAC9F57 /* Pods */, 7890E71355E6C0A3288089E7 /* ExpoModulesProviders */, ); - indentWidth = 2; + indentWidth = 4; sourceTree = ""; - tabWidth = 2; + tabWidth = 4; }; 83CBBA001A601CBA00E9B192 /* Products */ = { isa = PBXGroup; @@ -1809,7 +1836,6 @@ 1EB375892B55DBFB00AEC3D7 /* Server.swift in Sources */, 1E29A3162B5868DF0093C03C /* MessageListView.swift in Sources */, 1E29A2F42B585B070093C03C /* SubscriptionsResponse.swift in Sources */, - 1E29A3142B5868D80093C03C /* MessageListViewModel.swift in Sources */, 1E29A2F92B585B070093C03C /* SubscriptionsRequest.swift in Sources */, 1E29A2F22B585B070093C03C /* HistoryResponse.swift in Sources */, 1ED033BA2B55B5F6004F4930 /* ServerView.swift in Sources */, @@ -1821,21 +1847,26 @@ 1E29A3022B585B070093C03C /* DateCodingStrategy.swift in Sources */, 1ED033B62B55B4A5004F4930 /* ServerListView.swift in Sources */, 1E29A3202B5871C80093C03C /* RoomFormatter.swift in Sources */, + 1EDFD0FA2B589B8F002FEE5F /* MessagesLoader.swift in Sources */, 1E29A3102B5865B80093C03C /* RoomViewModel.swift in Sources */, 1E29A2FC2B585B070093C03C /* SendMessageRequest.swift in Sources */, - 1E29A3262B58752D0093C03C /* MessageComposerViewModel.swift in Sources */, 1E29A30C2B585D1D0093C03C /* String+Extensions.swift in Sources */, 1ED033CD2B55D671004F4930 /* RocketChatDatabase.swift in Sources */, 1E29A3122B5866090093C03C /* Room.swift in Sources */, 1E29A3032B585B070093C03C /* FailableDecodable.swift in Sources */, 1E29A2FE2B585B070093C03C /* ReadRequest.swift in Sources */, + 1E9A71692B59B6E100477BA2 /* MessageSender.swift in Sources */, 1E29A3062B585B070093C03C /* RocketChatServer.swift in Sources */, 1E29A3072B585B070093C03C /* RocketChatError.swift in Sources */, + 1E9A71672B599E6300477BA2 /* NotificationController.swift in Sources */, + 1EDFD1082B58AA77002FEE5F /* RoomsLoader.swift in Sources */, 1E29A2F12B585B070093C03C /* SendMessageResponse.swift in Sources */, 1E29A30E2B58608C0093C03C /* LoggedUser.swift in Sources */, 1E29A3182B5868E50093C03C /* MessageView.swift in Sources */, + 1EDFD1062B58A66E002FEE5F /* CancelBag.swift in Sources */, 1E29A2FF2B585B070093C03C /* TokenAdapter.swift in Sources */, 1E29A3052B585B070093C03C /* Request.swift in Sources */, + 1E6436242B59998A009F0CE1 /* ExtensionDelegate.swift in Sources */, 1E29A2EF2B585B070093C03C /* RocketChatClient.swift in Sources */, 1E29A2FB2B585B070093C03C /* MessagesRequest.swift in Sources */, 1E29A31D2B5871B60093C03C /* Date+Extensions.swift in Sources */, @@ -1845,7 +1876,6 @@ 1E29A2F82B585B070093C03C /* MessageResponse.swift in Sources */, 1E29A3042B585B070093C03C /* HTTPMethod.swift in Sources */, 1E29A3012B585B070093C03C /* RequestAdapter.swift in Sources */, - 1E29A2CE2B5857FC0093C03C /* RoomListViewModel.swift in Sources */, 1E29A2F52B585B070093C03C /* RoomsResponse.swift in Sources */, 1E29A2F32B585B070093C03C /* MessagesResponse.swift in Sources */, 1E29A2FA2B585B070093C03C /* HistoryRequest.swift in Sources */,