Remove unnecessary ViewModel layer

This commit is contained in:
Djorkaeff Alexandre 2024-01-18 17:10:30 -03:00
parent 7ced062950
commit 9668aed381
37 changed files with 1685 additions and 1448 deletions

View File

@ -1,97 +1,109 @@
import CoreData import CoreData
import Foundation import Foundation
final class Database { protocol ServersDatabase {
private let container: NSPersistentContainer var viewContext: NSManagedObjectContext { get }
var viewContext: NSManagedObjectContext { func server(url: URL) -> Server?
container.viewContext func user(id: String) -> LoggedUser?
} func servers() -> [Server]
private static let model: NSManagedObjectModel = { func save()
guard let url = Bundle.main.url(forResource: "Default", withExtension: "momd"),
let managedObjectModel = NSManagedObjectModel(contentsOf: url) else { func process(updatedServer: WatchMessage.Server)
fatalError("Can't find Core Data Model") }
}
final class DefaultDatabase: ServersDatabase {
return managedObjectModel private let container: NSPersistentContainer
}()
var viewContext: NSManagedObjectContext {
init() { container.viewContext
container = NSPersistentContainer(name: "default", managedObjectModel: Self.model) }
container.loadPersistentStores { _, error in private static let model: NSManagedObjectModel = {
if let error { fatalError("Can't load persistent stores: \(error)") } guard let url = Bundle.main.url(forResource: "Default", withExtension: "momd"),
} let managedObjectModel = NSManagedObjectModel(contentsOf: url) else {
fatalError("Can't find Core Data Model")
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy }
}
return managedObjectModel
func save() { }()
guard container.viewContext.hasChanges else {
return init() {
} container = NSPersistentContainer(name: "default", managedObjectModel: Self.model)
try? container.viewContext.save() container.loadPersistentStores { _, error in
} if let error { fatalError("Can't load persistent stores: \(error)") }
}
func server(url: URL) -> Server? {
let request = Server.fetchRequest() container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
request.predicate = NSPredicate(format: "url == %@", url.absoluteString) }
return try? viewContext.fetch(request).first func save() {
} guard container.viewContext.hasChanges else {
return
func user(id: String) -> LoggedUser? { }
let request = LoggedUser.fetchRequest()
request.predicate = NSPredicate(format: "id == %@", id) try? container.viewContext.save()
}
return try? viewContext.fetch(request).first
} func server(url: URL) -> Server? {
let request = Server.fetchRequest()
func servers() -> [Server] { request.predicate = NSPredicate(format: "url == %@", url.absoluteString)
let request = Server.fetchRequest()
return try? viewContext.fetch(request).first
return (try? viewContext.fetch(request)) ?? [] }
}
func user(id: String) -> LoggedUser? {
func process(updatedServer: WatchMessage.Server) { let request = LoggedUser.fetchRequest()
if let server = server(url: updatedServer.url) { request.predicate = NSPredicate(format: "id == %@", id)
server.url = updatedServer.url
server.name = updatedServer.name return try? viewContext.fetch(request).first
server.iconURL = updatedServer.iconURL }
server.useRealName = updatedServer.useRealName
server.loggedUser = user(from: updatedServer.loggedUser) func servers() -> [Server] {
} else { let request = Server.fetchRequest()
Server(
context: viewContext, return (try? viewContext.fetch(request)) ?? []
iconURL: updatedServer.iconURL, }
name: updatedServer.name,
url: updatedServer.url, func process(updatedServer: WatchMessage.Server) {
useRealName: updatedServer.useRealName, if let server = server(url: updatedServer.url) {
loggedUser: user(from: updatedServer.loggedUser) server.url = updatedServer.url
) server.name = updatedServer.name
} server.iconURL = updatedServer.iconURL
server.useRealName = updatedServer.useRealName
save() server.loggedUser = user(from: updatedServer.loggedUser)
} } else {
Server(
private func user(from updatedUser: WatchMessage.Server.LoggedUser) -> LoggedUser { context: viewContext,
if let user = user(id: updatedUser.id) { iconURL: updatedServer.iconURL,
user.id = updatedUser.id name: updatedServer.name,
user.name = updatedUser.name url: updatedServer.url,
user.username = updatedUser.username useRealName: updatedServer.useRealName,
user.token = updatedUser.token loggedUser: user(from: updatedServer.loggedUser)
)
return user }
}
save()
return LoggedUser( }
context: viewContext,
id: updatedUser.id, private func user(from updatedUser: WatchMessage.Server.LoggedUser) -> LoggedUser {
name: updatedUser.name, if let user = user(id: updatedUser.id) {
token: updatedUser.token, user.id = updatedUser.id
username: updatedUser.username 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
)
}
} }

View File

@ -2,47 +2,47 @@ import CoreData
@objc @objc
public final class LoggedUser: NSManagedObject { public final class LoggedUser: NSManagedObject {
@nonobjc public class func fetchRequest() -> NSFetchRequest<LoggedUser> { @nonobjc public class func fetchRequest() -> NSFetchRequest<LoggedUser> {
NSFetchRequest<LoggedUser>(entityName: "LoggedUser") NSFetchRequest<LoggedUser>(entityName: "LoggedUser")
} }
@NSManaged public var id: String @NSManaged public var id: String
@NSManaged public var name: String @NSManaged public var name: String
@NSManaged public var token: String @NSManaged public var token: String
@NSManaged public var username: String @NSManaged public var username: String
@available(*, unavailable) @available(*, unavailable)
init() { init() {
fatalError() fatalError()
} }
@available(*, unavailable) @available(*, unavailable)
init(context: NSManagedObjectContext) { init(context: NSManagedObjectContext) {
fatalError() fatalError()
} }
public override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) { public override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context) super.init(entity: entity, insertInto: context)
} }
@discardableResult @discardableResult
public init( public init(
context: NSManagedObjectContext, context: NSManagedObjectContext,
id: String, id: String,
name: String, name: String,
token: String, token: String,
username: String username: String
) { ) {
let entity = NSEntityDescription.entity(forEntityName: "LoggedUser", in: context)! let entity = NSEntityDescription.entity(forEntityName: "LoggedUser", in: context)!
super.init(entity: entity, insertInto: context) super.init(entity: entity, insertInto: context)
self.id = id self.id = id
self.name = name self.name = name
self.token = token self.token = token
self.username = username self.username = username
} }
} }
extension LoggedUser: Identifiable { extension LoggedUser: Identifiable {
} }

View File

@ -1,22 +1,32 @@
import CoreData import CoreData
extension Room { extension Room {
var messagesRequest: NSFetchRequest<Message> { var messagesRequest: NSFetchRequest<Message> {
let request = Message.fetchRequest() let request = Message.fetchRequest()
request.predicate = NSPredicate(format: "room == %@", self) request.predicate = NSPredicate(format: "room == %@", self)
request.sortDescriptors = [NSSortDescriptor(keyPath: \Message.ts, ascending: true)] request.sortDescriptors = [NSSortDescriptor(keyPath: \Message.ts, ascending: true)]
return request return request
} }
var lastMessage: Message? { var lastMessage: Message? {
let request = Message.fetchRequest() let request = Message.fetchRequest()
request.predicate = NSPredicate(format: "room == %@", self) request.predicate = NSPredicate(format: "room == %@", self)
request.sortDescriptors = [NSSortDescriptor(keyPath: \Message.ts, ascending: false)] request.sortDescriptors = [NSSortDescriptor(keyPath: \Message.ts, ascending: false)]
request.fetchLimit = 1 request.fetchLimit = 1
return try? managedObjectContext?.fetch(request).first 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
}
} }

View File

@ -2,64 +2,64 @@ import CoreData
@objc @objc
public final class Server: NSManagedObject { public final class Server: NSManagedObject {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Server> { @nonobjc public class func fetchRequest() -> NSFetchRequest<Server> {
NSFetchRequest<Server>(entityName: "Server") NSFetchRequest<Server>(entityName: "Server")
} }
@NSManaged public var iconURL: URL @NSManaged public var iconURL: URL
@NSManaged public var name: String @NSManaged public var name: String
@NSManaged public var updatedSince: Date? @NSManaged public var updatedSince: Date?
@NSManaged public var url: URL @NSManaged public var url: URL
@NSManaged public var useRealName: Bool @NSManaged public var useRealName: Bool
@NSManaged public var loggedUser: LoggedUser @NSManaged public var loggedUser: LoggedUser
@available(*, unavailable) @available(*, unavailable)
init() { init() {
fatalError() fatalError()
} }
@available(*, unavailable) @available(*, unavailable)
init(context: NSManagedObjectContext) { init(context: NSManagedObjectContext) {
fatalError() fatalError()
} }
public override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) { public override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context) super.init(entity: entity, insertInto: context)
} }
@discardableResult @discardableResult
public init( public init(
context: NSManagedObjectContext, context: NSManagedObjectContext,
iconURL: URL, iconURL: URL,
name: String, name: String,
updatedSince: Date? = nil, updatedSince: Date? = nil,
url: URL, url: URL,
useRealName: Bool, useRealName: Bool,
loggedUser: LoggedUser loggedUser: LoggedUser
) { ) {
let entity = NSEntityDescription.entity(forEntityName: "Server", in: context)! let entity = NSEntityDescription.entity(forEntityName: "Server", in: context)!
super.init(entity: entity, insertInto: context) super.init(entity: entity, insertInto: context)
self.iconURL = iconURL self.iconURL = iconURL
self.name = name self.name = name
self.updatedSince = updatedSince self.updatedSince = updatedSince
self.url = url self.url = url
self.useRealName = useRealName self.useRealName = useRealName
self.loggedUser = loggedUser self.loggedUser = loggedUser
} }
} }
extension Server: Identifiable { extension Server: Identifiable {
} }
extension Server { extension Server {
var roomsRequest: NSFetchRequest<Room> { var roomsRequest: NSFetchRequest<Room> {
let request = Room.fetchRequest() let request = Room.fetchRequest()
request.predicate = NSPredicate(format: "archived == false") request.predicate = NSPredicate(format: "archived == false")
request.sortDescriptors = [NSSortDescriptor(keyPath: \Room.ts, ascending: false)] request.sortDescriptors = [NSSortDescriptor(keyPath: \Room.ts, ascending: false)]
return request return request
} }
} }

View File

@ -24,6 +24,7 @@
<attribute name="broadcast" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="broadcast" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="encrypted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="encrypted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="fname" optional="YES" attributeType="String"/> <attribute name="fname" optional="YES" attributeType="String"/>
<attribute name="hasMoreMessages" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="String"/> <attribute name="id" optional="YES" attributeType="String"/>
<attribute name="isReadOnly" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="isReadOnly" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/> <attribute name="name" optional="YES" attributeType="String"/>

View File

@ -1,216 +1,219 @@
import CoreData import CoreData
final class RocketChatDatabase { protocol Database {
private let container: NSPersistentContainer var viewContext: NSManagedObjectContext { get }
var viewContext: NSManagedObjectContext { func room(id: String) -> Room?
container.viewContext func message(id: String) -> Message?
} func createTempMessage(msg: String, in room: Room, for loggedUser: LoggedUser) -> String
private static let model: NSManagedObjectModel = { func process(subscription: SubscriptionsResponse.Subscription)
guard let url = Bundle.main.url(forResource: "RocketChat", withExtension: "momd"), func process(subscription: SubscriptionsResponse.Subscription?, in updatedRoom: RoomsResponse.Room)
let managedObjectModel = NSManagedObjectModel(contentsOf: url) else { func process(updatedMessage: MessageResponse, in room: Room)
fatalError("Can't find Core Data Model") }
}
final class RocketChatDatabase: Database {
return managedObjectModel private let container: NSPersistentContainer
}()
var viewContext: NSManagedObjectContext {
init(name: String) { container.viewContext
container = NSPersistentContainer(name: name, managedObjectModel: Self.model) }
container.loadPersistentStores { _, error in private static let model: NSManagedObjectModel = {
if let error { fatalError("Can't load persistent stores: \(error)") } guard let url = Bundle.main.url(forResource: "RocketChat", withExtension: "momd"),
} let managedObjectModel = NSManagedObjectModel(contentsOf: url) else {
fatalError("Can't find Core Data Model")
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy }
}
return managedObjectModel
func save() { }()
guard container.viewContext.hasChanges else {
return init(name: String) {
} container = NSPersistentContainer(name: name, managedObjectModel: Self.model)
try? container.viewContext.save() container.loadPersistentStores { _, error in
} if let error { fatalError("Can't load persistent stores: \(error)") }
}
func createUser(id: String) -> User {
let user = User(context: viewContext) container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
user.id = id }
return user private func save() {
} guard container.viewContext.hasChanges else {
return
func createRoom(id: String) -> Room { }
let room = Room(context: viewContext)
room.id = id try? container.viewContext.save()
}
return room
} func createUser(id: String) -> User {
let user = User(context: viewContext)
func createMessage(id: String) -> Message { user.id = id
let message = Message(context: viewContext)
message.id = id return user
message.ts = Date() }
return message func createRoom(id: String) -> Room {
} let room = Room(context: viewContext)
room.id = id
func createAttachment(url: String) -> Attachment {
let attachment = Attachment(context: viewContext) return room
attachment.imageURL = URL(string: url) }
return attachment func createMessage(id: String) -> Message {
} let message = Message(context: viewContext)
message.id = id
func createTempMessage(msg: String, in room: Room, for loggedUser: LoggedUser) -> String { message.ts = Date()
let id = String.random(17)
let message = message(id: id) ?? createMessage(id: id) return message
}
message.id = id
message.ts = Date() func createAttachment(url: String) -> Attachment {
message.room = room let attachment = Attachment(context: viewContext)
message.status = "temp" // TODO: attachment.imageURL = URL(string: url)
message.msg = msg
return attachment
let user = user(id: loggedUser.id) ?? createUser(id: loggedUser.id) }
user.username = loggedUser.username
user.name = loggedUser.name func createTempMessage(msg: String, in room: Room, for loggedUser: LoggedUser) -> String {
message.user = user let id = String.random(17)
let message = message(id: id) ?? createMessage(id: id)
return id
} message.id = id
message.ts = Date()
func user(id: String) -> User? { message.room = room
let user = User(context: viewContext) message.status = "temp" // TODO:
user.id = id message.msg = msg
return user let user = user(id: loggedUser.id) ?? createUser(id: loggedUser.id)
} user.username = loggedUser.username
user.name = loggedUser.name
func room(id: String) -> Room? { message.user = user
let request = Room.fetchRequest()
request.predicate = NSPredicate(format: "id == %@", id) return id
}
return try? viewContext.fetch(request).first
} func user(id: String) -> User? {
let user = User(context: viewContext)
func message(id: String) -> Message? { user.id = id
let request = Message.fetchRequest()
request.predicate = NSPredicate(format: "id == %@", id) return user
}
return try? viewContext.fetch(request).first
} func room(id: String) -> Room? {
let request = Room.fetchRequest()
func attachment(url: String) -> Attachment? { request.predicate = NSPredicate(format: "id == %@", id)
let request = Attachment.fetchRequest()
request.predicate = NSPredicate(format: "imageURL == %@", url) return try? viewContext.fetch(request).first
}
return try? viewContext.fetch(request).first
} func message(id: String) -> Message? {
let request = Message.fetchRequest()
func rooms(ids: [String]) -> [Room] { request.predicate = NSPredicate(format: "id == %@", id)
let request = Room.fetchRequest()
request.predicate = NSPredicate(format: "ANY id IN %@", ids) return try? viewContext.fetch(request).first
}
return (try? viewContext.fetch(request)) ?? []
} func attachment(url: String) -> Attachment? {
let request = Attachment.fetchRequest()
func process(updatedMessage: MessageResponse, in room: Room) { request.predicate = NSPredicate(format: "imageURL == %@", url)
let message = message(id: updatedMessage._id) ?? createMessage(id: updatedMessage._id)
return try? viewContext.fetch(request).first
let user = user(id: updatedMessage.u._id) ?? createUser(id: updatedMessage.u._id) }
user.name = updatedMessage.u.name
user.username = updatedMessage.u.username func rooms(ids: [String]) -> [Room] {
let request = Room.fetchRequest()
message.status = "received" // TODO: request.predicate = NSPredicate(format: "ANY id IN %@", ids)
message.id = updatedMessage._id
message.msg = updatedMessage.msg return (try? viewContext.fetch(request)) ?? []
message.room = room }
message.ts = updatedMessage.ts
message.user = user func process(updatedMessage: MessageResponse, in room: Room) {
message.t = updatedMessage.t let message = message(id: updatedMessage._id) ?? createMessage(id: updatedMessage._id)
message.groupable = updatedMessage.groupable ?? true
let user = user(id: updatedMessage.u._id) ?? createUser(id: updatedMessage.u._id)
updatedMessage.attachments?.forEach { attachment in user.name = updatedMessage.u.name
process(updatedAttachment: attachment, in: message) user.username = updatedMessage.u.username
}
message.status = "received" // TODO:
save() message.id = updatedMessage._id
} message.msg = updatedMessage.msg
message.room = room
func process(updatedAttachment: AttachmentResponse, in message: Message) { message.ts = updatedMessage.ts
guard let url = updatedAttachment.imageURL?.absoluteString else { message.user = user
return message.t = updatedMessage.t
} message.groupable = updatedMessage.groupable ?? true
let attachment = attachment(url: url) ?? createAttachment(url: url) updatedMessage.attachments?.forEach { attachment in
process(updatedAttachment: attachment, in: message)
attachment.msg = updatedAttachment.description }
attachment.message = message
attachment.width = updatedAttachment.dimensions?.width ?? 0 save()
attachment.height = updatedAttachment.dimensions?.height ?? 0 }
}
func process(updatedAttachment: AttachmentResponse, in message: Message) {
func process(subscription: SubscriptionsResponse.Subscription?, in updatedRoom: RoomsResponse.Room) { guard let url = updatedAttachment.imageURL?.absoluteString else {
let room = room(id: updatedRoom._id) ?? createRoom(id: updatedRoom._id) return
}
room.name = updatedRoom.name
room.fname = updatedRoom.fname let attachment = attachment(url: url) ?? createAttachment(url: url)
room.updatedAt = updatedRoom._updatedAt
room.t = updatedRoom.t attachment.msg = updatedAttachment.description
room.usernames = updatedRoom.usernames attachment.message = message
room.uids = updatedRoom.uids attachment.width = updatedAttachment.dimensions?.width ?? 0
room.prid = updatedRoom.prid attachment.height = updatedAttachment.dimensions?.height ?? 0
room.isReadOnly = updatedRoom.ro ?? false }
room.encrypted = updatedRoom.encrypted ?? false
room.teamMain = updatedRoom.teamMain ?? false func process(subscription: SubscriptionsResponse.Subscription?, in updatedRoom: RoomsResponse.Room) {
room.archived = updatedRoom.archived ?? false let room = room(id: updatedRoom._id) ?? createRoom(id: updatedRoom._id)
room.broadcast = updatedRoom.broadcast ?? false
room.name = updatedRoom.name
if let subscription { room.fname = updatedRoom.fname
room.alert = subscription.alert room.updatedAt = updatedRoom._updatedAt
room.name = room.name ?? subscription.name room.t = updatedRoom.t
room.fname = room.fname ?? subscription.fname room.usernames = updatedRoom.usernames
room.unread = Int32(subscription.unread) room.uids = updatedRoom.uids
} room.prid = updatedRoom.prid
room.isReadOnly = updatedRoom.ro ?? false
if let lastMessage = updatedRoom.lastMessage?.value { room.encrypted = updatedRoom.encrypted ?? false
process(updatedMessage: lastMessage, in: room) room.teamMain = updatedRoom.teamMain ?? false
} room.archived = updatedRoom.archived ?? false
room.broadcast = updatedRoom.broadcast ?? false
let lastRoomUpdate = updatedRoom.lm ?? updatedRoom.ts ?? updatedRoom._updatedAt
if let subscription {
if let lr = subscription?.lr, let lastRoomUpdate { room.alert = subscription.alert
room.ts = max(lr, lastRoomUpdate) room.name = room.name ?? subscription.name
} else { room.fname = room.fname ?? subscription.fname
room.ts = lastRoomUpdate room.unread = Int32(subscription.unread)
} }
save() if let lastMessage = updatedRoom.lastMessage?.value {
} process(updatedMessage: lastMessage, in: room)
}
func process(subscription: SubscriptionsResponse.Subscription) {
let room = room(id: subscription.rid) ?? createRoom(id: subscription.rid) let lastRoomUpdate = updatedRoom.lm ?? updatedRoom.ts ?? updatedRoom._updatedAt
room.alert = subscription.alert if let lr = subscription?.lr, let lastRoomUpdate {
room.name = room.name ?? subscription.name room.ts = max(lr, lastRoomUpdate)
room.fname = room.fname ?? subscription.fname } else {
room.unread = Int32(subscription.unread) room.ts = lastRoomUpdate
}
if let lr = subscription.lr, let lastRoomUpdate = room.ts {
room.ts = max(lr, lastRoomUpdate) save()
} }
save() func process(subscription: SubscriptionsResponse.Subscription) {
} let room = room(id: subscription.rid) ?? createRoom(id: subscription.rid)
func markRead(in roomID: String) { room.alert = subscription.alert
let room = room(id: roomID) ?? createRoom(id: roomID) room.name = room.name ?? subscription.name
room.fname = room.fname ?? subscription.fname
room.alert = false room.unread = Int32(subscription.unread)
room.unread = 0
if let lr = subscription.lr, let lastRoomUpdate = room.ts {
save() room.ts = max(lr, lastRoomUpdate)
} }
save()
}
} }

View File

@ -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<RocketChatDatabase>?
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<T: AnyObject> {
weak var value: T?
init(value: T) {
self.value = value
}
}

View File

@ -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<RocketChatDatabase>?
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<T: AnyObject> {
weak var value: T?
init(value: T) {
self.value = value
}
}

View File

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

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
extension Date { extension Date {
static func - (lhs: Date, rhs: Date) -> TimeInterval { static func - (lhs: Date, rhs: Date) -> TimeInterval {
return lhs.timeIntervalSinceReferenceDate - rhs.timeIntervalSinceReferenceDate return lhs.timeIntervalSinceReferenceDate - rhs.timeIntervalSinceReferenceDate
} }
} }

View File

@ -1,74 +1,74 @@
import Foundation import Foundation
final class MessageFormatter { final class MessageFormatter {
private let message: Message private let message: Message
private let previousMessage: Message? private let previousMessage: Message?
init(message: Message, previousMessage: Message?) { init(message: Message, previousMessage: Message?) {
self.message = message self.message = message
self.previousMessage = previousMessage self.previousMessage = previousMessage
} }
func hasDateSeparator() -> Bool { func hasDateSeparator() -> Bool {
if let previousMessage, if let previousMessage,
let previousMessageTS = previousMessage.ts, let previousMessageTS = previousMessage.ts,
let messageTS = message.ts, let messageTS = message.ts,
Calendar.current.isDate(previousMessageTS, inSameDayAs: messageTS) { Calendar.current.isDate(previousMessageTS, inSameDayAs: messageTS) {
return false return false
} }
return true return true
} }
func isHeader() -> Bool { func isHeader() -> Bool {
if let previousMessage, if let previousMessage,
let previousMessageTS = previousMessage.ts, let previousMessageTS = previousMessage.ts,
let messageTS = message.ts, let messageTS = message.ts,
Calendar.current.isDate(previousMessageTS, inSameDayAs: messageTS), Calendar.current.isDate(previousMessageTS, inSameDayAs: messageTS),
previousMessage.user?.username == message.user?.username, previousMessage.user?.username == message.user?.username,
!(previousMessage.groupable == false || message.groupable == false || message.room?.broadcast == true), !(previousMessage.groupable == false || message.groupable == false || message.room?.broadcast == true),
messageTS - previousMessageTS < 300, messageTS - previousMessageTS < 300,
message.t != "rm", message.t != "rm",
previousMessage.t != "rm" { previousMessage.t != "rm" {
return false return false
} }
return true return true
} }
func info() -> String? { func info() -> String? {
switch message.t { switch message.t {
case "rm": case "rm":
return "Message Removed" return "Message Removed"
case "e2e": case "e2e":
return "Encrypted message" return "Encrypted message"
default: default:
return nil return nil
} }
} }
func date() -> String? { func date() -> String? {
guard let ts = message.ts else { return nil } guard let ts = message.ts else { return nil }
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.locale = Locale.current dateFormatter.locale = Locale.current
dateFormatter.timeZone = TimeZone.current dateFormatter.timeZone = TimeZone.current
dateFormatter.timeStyle = .none dateFormatter.timeStyle = .none
dateFormatter.dateStyle = .long dateFormatter.dateStyle = .long
return dateFormatter.string(from: ts) return dateFormatter.string(from: ts)
} }
func time() -> String? { func time() -> String? {
guard let ts = message.ts else { return nil } guard let ts = message.ts else { return nil }
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.locale = Locale.current dateFormatter.locale = Locale.current
dateFormatter.timeZone = TimeZone.current dateFormatter.timeZone = TimeZone.current
dateFormatter.timeStyle = .short dateFormatter.timeStyle = .short
dateFormatter.dateStyle = .none dateFormatter.dateStyle = .none
return dateFormatter.string(from: ts) return dateFormatter.string(from: ts)
} }
} }

View File

@ -1,46 +1,46 @@
import Foundation import Foundation
final class RoomFormatter { final class RoomFormatter {
private let room: Room private let room: Room
private let server: Server private let server: Server
init(room: Room, server: Server) { init(room: Room, server: Server) {
self.room = room self.room = room
self.server = server self.server = server
} }
var title: String? { var title: String? {
if isGroupChat, (room.name == nil || room.name?.isEmpty == true), let usernames = room.usernames { if isGroupChat, (room.name == nil || room.name?.isEmpty == true), let usernames = room.usernames {
return usernames return usernames
.filter { $0 == server.loggedUser.username } .filter { $0 == server.loggedUser.username }
.sorted() .sorted()
.joined(separator: ", ") .joined(separator: ", ")
} }
if room.t != "d" { if room.t != "d" {
if let fname = room.fname { if let fname = room.fname {
return fname return fname
} else if let name = room.name { } else if let name = room.name {
return name return name
} }
} }
if room.prid != nil || server.useRealName { if room.prid != nil || server.useRealName {
return room.fname return room.fname
} }
return room.name return room.name
} }
var isGroupChat: Bool { var isGroupChat: Bool {
if let uids = room.uids, uids.count > 2 { if let uids = room.uids, uids.count > 2 {
return true return true
} }
if let usernames = room.usernames, usernames.count > 2 { if let usernames = room.usernames, usernames.count > 2 {
return true return true
} }
return false return false
} }
} }

View File

@ -0,0 +1,10 @@
import Combine
typealias CancelBag = Set<AnyCancellable>
extension CancelBag {
mutating func cancelAll() {
forEach { $0.cancel() }
removeAll()
}
}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,9 @@
{ {
"sourceLanguage" : "en", "sourceLanguage" : "en",
"strings" : { "strings" : {
"Load More..." : {
},
"Message" : { "Message" : {
}, },

View File

@ -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<NotificationView> {
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"
]
}
}

View File

@ -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"
}

View File

@ -2,52 +2,69 @@ import SwiftUI
@main @main
struct RocketChat_Watch_AppApp: App { struct RocketChat_Watch_AppApp: App {
private let store: DependencyStore private let store: DependencyStore
@StateObject var router: RocketChatAppRouter @WKExtensionDelegateAdaptor private var delegate: ExtensionDelegate
init() { @StateObject var router: RocketChatAppRouter
let store = DependencyStore()
init() {
self.store = store let store = DependencyStore()
self._router = StateObject(wrappedValue: RocketChatAppRouter(database: store.database))
} self.store = store
self._router = StateObject(wrappedValue: RocketChatAppRouter(database: store.database))
@ViewBuilder }
private var serverListView: some View {
ServerListView( @ViewBuilder
dependencies: .init( private var serverListView: some View {
connection: store.connection, ServerListView(
database: store.database, dependencies: .init(
router: router connection: store.connection,
) database: store.database,
) router: router
} )
)
@ViewBuilder }
private func roomListView(for server: Server) -> some View {
RoomListView( @ViewBuilder
dependencies: .init( private func roomListView(for server: Server) -> some View {
client: store.client(for: server), RoomListView(
database: store.database(for: server), client: store.client(for: server),
router: router, database: store.database(for: server),
server: server messagesLoader: MessagesLoader(
) client: store.client(for: server),
) database: store.database(for: server),
} serversDB: store.database
),
var body: some Scene { messageSender: MessageSender(
WindowGroup { client: store.client(for: server),
NavigationStack { database: store.database(for: server),
switch router.route { server: server
case .roomList(let server): ),
roomListView(for: server) roomsLoader: RoomsLoader(
.environment(\.managedObjectContext, store.database(for: server).viewContext) client: store.client(for: server),
case .serverList: database: store.database(for: server),
serverListView serversDB: store.database
.environment(\.managedObjectContext, store.database.viewContext) ),
} 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")
}
} }

View File

@ -1,37 +1,46 @@
import Foundation import Foundation
final class RocketChatAppRouter: ObservableObject { final class RocketChatAppRouter: ObservableObject {
@Storage("current_server") var currentServer: URL? @Storage("current_server") var currentServer: URL?
@Published var route: Route = .serverList @Published var route: Route = .serverList {
didSet {
private let database: Database switch route {
case .roomList(let server):
init(database: Database) { currentServer = server.url
self.database = database case .serverList:
loadRoute() break
} }
}
private func loadRoute() { }
if let currentServer, let server = database.server(url: currentServer) {
route = .roomList(server) private let database: ServersDatabase
} else if database.servers().count == 1, let server = database.servers().first {
route = .roomList(server) init(database: ServersDatabase) {
} else { self.database = database
route = .serverList loadRoute()
} }
}
private func loadRoute() {
func route(to route: Route) { if let currentServer, let server = database.server(url: currentServer) {
DispatchQueue.main.async { route = .roomList(server)
self.route = route } 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 { extension RocketChatAppRouter {
enum Route { enum Route {
case roomList(Server) case roomList(Server)
case serverList case serverList
} }
} }

View File

@ -2,27 +2,27 @@ import Foundation
@propertyWrapper @propertyWrapper
struct Storage<T: Codable> { struct Storage<T: Codable> {
private let key: String private let key: String
private let defaultValue: T? private let defaultValue: T?
init(_ key: String, defaultValue: T? = nil) { init(_ key: String, defaultValue: T? = nil) {
self.key = key self.key = key
self.defaultValue = defaultValue self.defaultValue = defaultValue
} }
var wrappedValue: T? { var wrappedValue: T? {
get { get {
guard let data = UserDefaults.standard.object(forKey: key) as? Data else { guard let data = UserDefaults.standard.object(forKey: key) as? Data else {
return defaultValue return defaultValue
} }
let value = try? JSONDecoder().decode(T.self, from: data) let value = try? JSONDecoder().decode(T.self, from: data)
return value ?? defaultValue return value ?? defaultValue
} }
set { set {
let data = try? JSONEncoder().encode(newValue) let data = try? JSONEncoder().encode(newValue)
UserDefaults.standard.set(data, forKey: key) UserDefaults.standard.set(data, forKey: key)
} }
} }
} }

View File

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

View File

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

View File

@ -1,21 +1,21 @@
import Foundation import Foundation
final class MessageViewModel: ObservableObject { final class MessageViewModel: ObservableObject {
@Published private(set) var server: Server? @Published private(set) var server: Server?
@Published private(set) var message: Message @Published private(set) var message: Message
@Published private(set) var previousMessage: Message? @Published private(set) var previousMessage: Message?
let messageFormatter: MessageFormatter let messageFormatter: MessageFormatter
init(message: Message, previousMessage: Message? = nil, server: Server?) { init(message: Message, previousMessage: Message? = nil, server: Server?) {
self.message = message self.message = message
self.previousMessage = previousMessage self.previousMessage = previousMessage
self.messageFormatter = MessageFormatter(message: message, previousMessage: previousMessage) self.messageFormatter = MessageFormatter(message: message, previousMessage: previousMessage)
self.server = server self.server = server
} }
var sender: String? { var sender: String? {
server?.useRealName == true ? message.user?.name : message.user?.username server?.useRealName == true ? message.user?.name : message.user?.username
} }
} }

View File

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

View File

@ -1,110 +1,110 @@
import Foundation import Foundation
final class RoomViewModel: ObservableObject { final class RoomViewModel: ObservableObject {
@Published var room: Room @Published var room: Room
@Published var server: Server @Published var server: Server
let formatter: RoomFormatter let formatter: RoomFormatter
init(room: Room, server: Server) { init(room: Room, server: Server) {
self.room = room self.room = room
self.server = server self.server = server
self.formatter = RoomFormatter(room: room, server: server) self.formatter = RoomFormatter(room: room, server: server)
} }
var iconName: String? { var iconName: String? {
if room.prid != nil { if room.prid != nil {
return "discussions" return "discussions"
} else if room.teamMain == true, room.t == "p" { } else if room.teamMain == true, room.t == "p" {
return "teams-private" return "teams-private"
} else if room.teamMain == true { } else if room.teamMain == true {
return "teams" return "teams"
} else if room.t == "p" { } else if room.t == "p" {
return "channel-private" return "channel-private"
} else if room.t == "c" { } else if room.t == "c" {
return "channel-public" return "channel-public"
} else if room.t == "d", formatter.isGroupChat { } else if room.t == "d", formatter.isGroupChat {
return "message" return "message"
} }
return nil return nil
} }
var lastMessage: String { var lastMessage: String {
guard let user = room.lastMessage?.user else { guard let user = room.lastMessage?.user else {
return "No Message" return "No Message"
} }
let isLastMessageSentByMe = user.username == server.loggedUser.username let isLastMessageSentByMe = user.username == server.loggedUser.username
let username = isLastMessageSentByMe ? "You" : ((server.useRealName ? user.name : user.username) ?? "") let username = isLastMessageSentByMe ? "You" : ((server.useRealName ? user.name : user.username) ?? "")
let message = room.lastMessage?.msg ?? "No message" let message = room.lastMessage?.msg ?? "No message"
if room.lastMessage?.t == "jitsi_call_started" { if room.lastMessage?.t == "jitsi_call_started" {
return "Call started by: \(username)" return "Call started by: \(username)"
} }
if room.lastMessage?.attachments?.allObjects.isEmpty == false { if room.lastMessage?.attachments?.allObjects.isEmpty == false {
return "\(username) sent an attachment" return "\(username) sent an attachment"
} }
if room.lastMessage?.t == "e2e" { if room.lastMessage?.t == "e2e" {
return "Encrypted message" return "Encrypted message"
} }
if room.lastMessage?.t == "videoconf" { if room.lastMessage?.t == "videoconf" {
return "Call started" return "Call started"
} }
if room.t == "d", !isLastMessageSentByMe { if room.t == "d", !isLastMessageSentByMe {
return message return message
} }
return "\(username): \(message)" return "\(username): \(message)"
} }
var updatedAt: String? { var updatedAt: String? {
guard let ts = room.ts else { guard let ts = room.ts else {
return nil return nil
} }
let calendar = Calendar.current let calendar = Calendar.current
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.locale = Locale.current dateFormatter.locale = Locale.current
dateFormatter.timeZone = TimeZone.current dateFormatter.timeZone = TimeZone.current
if calendar.isDateInYesterday(ts) { if calendar.isDateInYesterday(ts) {
return "Yesterday" return "Yesterday"
} }
if calendar.isDateInToday(ts) { if calendar.isDateInToday(ts) {
dateFormatter.timeStyle = .short dateFormatter.timeStyle = .short
dateFormatter.dateStyle = .none dateFormatter.dateStyle = .none
return dateFormatter.string(from: ts) return dateFormatter.string(from: ts)
} }
if isInPreviousWeek(date: ts) { if isInPreviousWeek(date: ts) {
dateFormatter.dateFormat = "EEEE" dateFormatter.dateFormat = "EEEE"
return dateFormatter.string(from: ts) return dateFormatter.string(from: ts)
} }
dateFormatter.timeStyle = .none dateFormatter.timeStyle = .none
dateFormatter.dateStyle = .short dateFormatter.dateStyle = .short
return dateFormatter.string(from: ts) return dateFormatter.string(from: ts)
} }
private func isInPreviousWeek(date: Date) -> Bool { private func isInPreviousWeek(date: Date) -> Bool {
let oneDay = 24 * 60 * 60 let oneDay = 24 * 60 * 60
let calendar = Calendar.current let calendar = Calendar.current
let currentDate = Date() let currentDate = Date()
let lastWeekDate = currentDate.addingTimeInterval(TimeInterval(-7 * oneDay)) let lastWeekDate = currentDate.addingTimeInterval(TimeInterval(-7 * oneDay))
return calendar.isDate( return calendar.isDate(
date, date,
equalTo: lastWeekDate, equalTo: lastWeekDate,
toGranularity: .weekOfYear toGranularity: .weekOfYear
) )
} }
} }

View File

@ -1,55 +1,55 @@
import Foundation import Foundation
enum ServerListState { enum ServerListState {
case loading case loading
case loaded case loaded
case error(ConnectionError) case error(ConnectionError)
} }
final class ServerListViewModel: ObservableObject { final class ServerListViewModel: ObservableObject {
struct Dependencies { struct Dependencies {
let connection: Connection let connection: Connection
let database: Database let database: ServersDatabase
let router: RocketChatAppRouter let router: RocketChatAppRouter
} }
private let dependencies: Dependencies private let dependencies: Dependencies
@Published private(set) var state: ServerListState = .loading @Published private(set) var state: ServerListState = .loading
init(dependencies: Dependencies) { init(dependencies: Dependencies) {
self.dependencies = dependencies self.dependencies = dependencies
} }
private func handleSuccess(message: WatchMessage) { private func handleSuccess(message: WatchMessage) {
message.servers.forEach(dependencies.database.process(updatedServer:)) message.servers.forEach(dependencies.database.process(updatedServer:))
state = .loaded state = .loaded
} }
private func handleFailure(error: Error) { private func handleFailure(error: Error) {
guard let connectionError = error as? ConnectionError else { guard let connectionError = error as? ConnectionError else {
return return
} }
state = .error(connectionError) state = .error(connectionError)
} }
func loadServers() { func loadServers() {
dependencies.connection.sendMessage { [weak self] result in dependencies.connection.sendMessage { [weak self] result in
guard let self else { guard let self else {
return return
} }
switch result { switch result {
case .success(let message): case .success(let message):
DispatchQueue.main.async { self.handleSuccess(message: message) } DispatchQueue.main.async { self.handleSuccess(message: message) }
case .failure(let error): case .failure(let error):
DispatchQueue.main.async { self.handleFailure(error: error) } DispatchQueue.main.async { self.handleFailure(error: error) }
} }
} }
} }
func didTap(server: Server) { func didTap(server: Server) {
dependencies.router.route(to: .roomList(server)) dependencies.router.route(to: .roomList(server))
} }
} }

View File

@ -1,38 +1,35 @@
import SwiftUI import SwiftUI
struct MessageComposerView: View { struct MessageComposerView: View {
@ObservedObject private var viewModel: MessageComposerViewModel @State private var message = ""
init(viewModel: MessageComposerViewModel) { let room: Room
self.viewModel = viewModel let onSend: (String) -> Void
}
var body: some View {
@State private var message = "" if room.isReadOnly {
HStack {
var body: some View { Spacer()
if viewModel.isReadOnly { Text("This room is read only")
HStack { .font(.caption)
Spacer() .fontWeight(.bold)
Text("This room is read only") .foregroundStyle(.white)
.font(.caption) .multilineTextAlignment(.center)
.fontWeight(.bold) Spacer()
.foregroundStyle(.white) }
.multilineTextAlignment(.center) } else {
Spacer() TextField("Message", text: $message)
} .submitLabel(.send)
} else { .onSubmit(send)
TextField("Message", text: $message) }
.submitLabel(.send) }
.onSubmit(send)
} func send() {
} guard !message.isEmpty else {
return
func send() { }
guard !message.isEmpty else {
return onSend(message)
} message = ""
}
viewModel.sendMessage(message)
message = ""
}
} }

View File

@ -1,48 +1,76 @@
import SwiftUI import SwiftUI
struct MessageListView: View { struct MessageListView: View {
@StateObject private var viewModel: MessageListViewModel private let messageComposer = "MESSAGE_COMPOSER_ID"
@FetchRequest<Message> private var messages: FetchedResults<Message> private let client: RocketChatClientProtocol
private let database: Database
init(viewModel: MessageListViewModel) { private let messagesLoader: MessagesLoading
_viewModel = StateObject(wrappedValue: viewModel) private let messageSender: MessageSending
_messages = FetchRequest(fetchRequest: viewModel.room.messagesRequest, animation: .none) private let formatter: RoomFormatter
} private let server: Server
private let room: Room
var body: some View {
ScrollViewReader { reader in @FetchRequest<Message> private var messages: FetchedResults<Message>
ScrollView {
VStack(alignment: .leading, spacing: 8) { init(
ForEach(messages.indices, id: \.self) { index in client: RocketChatClientProtocol,
let message = messages[index] database: Database,
let previousMessage = messages.indices.contains(index - 1) ? messages[index - 1] : nil messagesLoader: MessagesLoading,
messageSender: MessageSending,
MessageView(viewModel: viewModel.messageViewModel(for: message, and: previousMessage)) room: Room,
.id(message.id) server: Server
.transition(.move(edge: .bottom)) ) {
} self.client = client
self.database = database
MessageComposerView(viewModel: viewModel.composerViewModel()) self.messagesLoader = messagesLoader
.padding(.top) self.messageSender = messageSender
} self.formatter = RoomFormatter(room: room, server: server)
} self.room = room
.padding([.leading, .trailing]) self.server = server
.navigationTitle(viewModel.title) _messages = FetchRequest(fetchRequest: room.messagesRequest, animation: .none)
.navigationBarTitleDisplayMode(.inline) }
.onAppear {
viewModel.loadMessages { var body: some View {
reader.scrollTo(messages.last?.id, anchor: .bottom) ScrollViewReader { reader in
} ScrollView {
VStack(alignment: .leading, spacing: 8) {
viewModel.markAsRead() if room.hasMoreMessages {
} Button("Load More...") {
.onDisappear { guard let oldestMessage = room.firstMessage?.ts else { return }
viewModel.stop()
} messagesLoader.loadMore(from: oldestMessage)
.onReceive(messages.publisher) { _ in }
viewModel.markAsRead() }
}
} 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)
}
}
}
} }

View File

@ -1,59 +1,59 @@
import SwiftUI import SwiftUI
struct MessageView: View { struct MessageView: View {
@ObservedObject private var viewModel: MessageViewModel @ObservedObject private var viewModel: MessageViewModel
init(viewModel: MessageViewModel) { init(viewModel: MessageViewModel) {
self.viewModel = viewModel self.viewModel = viewModel
} }
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if viewModel.messageFormatter.hasDateSeparator() { if viewModel.messageFormatter.hasDateSeparator() {
HStack(alignment: .center) { HStack(alignment: .center) {
VStack(alignment: .center) { VStack(alignment: .center) {
Divider() Divider()
.overlay(.gray) .overlay(.secondary)
} }
Text(viewModel.messageFormatter.date() ?? "") Text(viewModel.messageFormatter.date() ?? "")
.lineLimit(1) .lineLimit(1)
.font(.footnote) .font(.footnote)
.foregroundStyle(.gray) .foregroundStyle(.secondary)
.layoutPriority(1) .layoutPriority(1)
VStack(alignment: .center) { VStack(alignment: .center) {
Divider() Divider()
.overlay(.gray) .overlay(.secondary)
} }
} }
} }
if viewModel.messageFormatter.isHeader() { if viewModel.messageFormatter.isHeader() {
HStack(alignment: .center) { HStack(alignment: .center) {
Text(viewModel.sender ?? "") Text(viewModel.sender ?? "")
.lineLimit(1) .lineLimit(1)
.font(.caption) .font(.caption)
.fontWeight(.bold) .fontWeight(.bold)
.foregroundStyle(.primary) .foregroundStyle(.primary)
Text(viewModel.messageFormatter.time() ?? "") Text(viewModel.messageFormatter.time() ?? "")
.lineLimit(1) .lineLimit(1)
.font(.footnote) .font(.footnote)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} }
if let text = viewModel.messageFormatter.info() { if let text = viewModel.messageFormatter.info() {
Text(text) Text(text)
.font(.caption) .font(.caption)
.foregroundStyle(.white) .foregroundStyle(.primary)
.italic() .italic()
} else if let text = viewModel.message.msg { } else if let text = viewModel.message.msg {
Text(text) Text(text)
.font(.caption) .font(.caption)
.foregroundStyle(viewModel.message.status == "temp" ? .secondary : .primary) .foregroundStyle(viewModel.message.status == "temp" ? .secondary : .primary)
} }
// if let attachments = message.attachments?.allObjects as? Array<Attachment> { // if let attachments = message.attachments?.allObjects as? Array<Attachment> {
// ForEach(attachments) { attachment in // ForEach(attachments) { attachment in
// AttachmentView(attachment: attachment) // AttachmentView(attachment: attachment)
// } // }
// } // }
} }
} }
} }

View File

@ -1,38 +1,68 @@
import SwiftUI import SwiftUI
struct RoomListView: View { struct RoomListView: View {
@StateObject private var viewModel: RoomListViewModel private let client: RocketChatClientProtocol
private let database: Database
@FetchRequest<Room> private var rooms: FetchedResults<Room> private let messagesLoader: MessagesLoading
private let messageSender: MessageSending
init(dependencies: RoomListViewModel.Dependencies) { private let roomsLoader: RoomsLoading
_viewModel = StateObject(wrappedValue: RoomListViewModel(dependencies: dependencies)) private let router: RocketChatAppRouter
_rooms = FetchRequest(fetchRequest: dependencies.server.roomsRequest) private let server: Server
}
@FetchRequest<Room> private var rooms: FetchedResults<Room>
var body: some View {
List { init(
ForEach(rooms) { room in client: RocketChatClientProtocol,
NavigationLink(value: room) { database: Database,
RoomView(viewModel: viewModel.roomViewModel(for: room)) messagesLoader: MessagesLoading,
} messageSender: MessageSending,
} roomsLoader: RoomsLoading,
} router: RocketChatAppRouter,
.onAppear { server: Server
viewModel.loadRooms() ) {
} self.client = client
.navigationTitle("Rooms") self.database = database
.navigationBarTitleDisplayMode(.inline) self.messagesLoader = messagesLoader
.navigationDestination(for: Room.self) { room in self.messageSender = messageSender
MessageListView(viewModel: viewModel.messageListViewModel(for: room)) self.roomsLoader = roomsLoader
.environment(\.managedObjectContext, viewModel.viewContext) self.router = router
} self.server = server
.toolbar { _rooms = FetchRequest(fetchRequest: server.roomsRequest)
ToolbarItem(placement: .automatic) { }
Button("Servers") {
viewModel.logout() 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)
}
}
}
}
} }

View File

@ -1,51 +1,51 @@
import SwiftUI import SwiftUI
struct RoomView: View { struct RoomView: View {
@ObservedObject var viewModel: RoomViewModel @ObservedObject var viewModel: RoomViewModel
private var isUnread: Bool { private var isUnread: Bool {
viewModel.room.unread > 0 || viewModel.room.alert viewModel.room.unread > 0 || viewModel.room.alert
} }
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack { HStack {
if let iconName = viewModel.iconName { if let iconName = viewModel.iconName {
Image(iconName) Image(iconName)
.resizable() .resizable()
.frame(width: 16, height: 16) .frame(width: 16, height: 16)
.scaledToFit() .scaledToFit()
} }
Text(viewModel.formatter.title ?? "") Text(viewModel.formatter.title ?? "")
.lineLimit(1) .lineLimit(1)
.font(.caption) .font(.caption)
.fontWeight(isUnread ? .bold : .medium) .fontWeight(isUnread ? .bold : .medium)
.foregroundStyle(.primary) .foregroundStyle(.primary)
Spacer() Spacer()
Text(viewModel.updatedAt ?? "") Text(viewModel.updatedAt ?? "")
.lineLimit(1) .lineLimit(1)
.font(.footnote) .font(.footnote)
.fontWeight(isUnread ? .bold : .medium) .fontWeight(isUnread ? .bold : .medium)
.foregroundStyle(isUnread ? .blue : .primary) .foregroundStyle(isUnread ? .blue : .primary)
} }
HStack(alignment: .top) { HStack(alignment: .top) {
Text(viewModel.lastMessage) Text(viewModel.lastMessage)
.lineLimit(2) .lineLimit(2)
.font(.caption2) .font(.caption2)
.foregroundStyle(isUnread ? .primary : .secondary) .foregroundStyle(isUnread ? .primary : .secondary)
Spacer() Spacer()
if isUnread, viewModel.room.unread > 0 { if isUnread, viewModel.room.unread > 0 {
Text(String(viewModel.room.unread)) Text(String(viewModel.room.unread))
.font(.footnote) .font(.footnote)
.fontWeight(.bold) .fontWeight(.bold)
.padding(6) .padding(6)
.background( .background(
Circle() Circle()
.fill(.blue) .fill(.blue)
) )
.foregroundColor(.primary) .foregroundColor(.primary)
} }
} }
} }
} }
} }

View File

@ -1,53 +1,53 @@
import SwiftUI import SwiftUI
struct ServerListView: View { struct ServerListView: View {
@StateObject var viewModel: ServerListViewModel @StateObject var viewModel: ServerListViewModel
@FetchRequest(entity: Server.entity(), sortDescriptors: [], animation: .default) @FetchRequest(entity: Server.entity(), sortDescriptors: [], animation: .default)
private var servers: FetchedResults<Server> private var servers: FetchedResults<Server>
init(dependencies: ServerListViewModel.Dependencies) { init(dependencies: ServerListViewModel.Dependencies) {
_viewModel = StateObject(wrappedValue: ServerListViewModel(dependencies: dependencies)) _viewModel = StateObject(wrappedValue: ServerListViewModel(dependencies: dependencies))
} }
@ViewBuilder @ViewBuilder
private func errorView(_ text: String) -> some View { private func errorView(_ text: String) -> some View {
VStack(alignment: .center) { VStack(alignment: .center) {
Text(text) Text(text)
Button("Try Again") { Button("Try Again") {
viewModel.loadServers() viewModel.loadServers()
} }
} }
} }
var body: some View { var body: some View {
VStack { VStack {
switch viewModel.state { switch viewModel.state {
case .loading: case .loading:
ProgressView() ProgressView()
case .loaded where servers.count > 0: case .loaded where servers.count > 0:
List { List {
ForEach(servers) { server in ForEach(servers) { server in
ServerView(server: server) ServerView(server: server)
.onTapGesture { .onTapGesture {
viewModel.didTap(server: server) viewModel.didTap(server: server)
} }
} }
} }
case .loaded: case .loaded:
errorView("There are no servers connected.") errorView("There are no servers connected.")
case .error(let connectionError): case .error(let connectionError):
switch connectionError { switch connectionError {
case .needsUnlock: case .needsUnlock:
errorView("You need to unlock your iPhone.") errorView("You need to unlock your iPhone.")
case .decoding: case .decoding:
errorView("We can't read servers information.") errorView("We can't read servers information.")
} }
} }
} }
.navigationTitle("Servers") .navigationTitle("Servers")
.onAppear { .onAppear {
viewModel.loadServers() viewModel.loadServers()
} }
} }
} }

View File

@ -1,17 +1,17 @@
import SwiftUI import SwiftUI
struct ServerView: View { struct ServerView: View {
@ObservedObject var server: Server @ObservedObject var server: Server
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(server.name) Text(server.name)
.font(.caption) .font(.caption)
.fontWeight(.bold) .fontWeight(.bold)
.foregroundStyle(.primary) .foregroundStyle(.primary)
Text(server.url.host ?? "") Text(server.url.host ?? "")
.font(.caption) .font(.caption)
.foregroundStyle(.primary) .foregroundStyle(.primary)
} }
} }
} }

View File

@ -2,67 +2,67 @@ import Foundation
import WatchConnectivity import WatchConnectivity
enum ConnectionError: Error { enum ConnectionError: Error {
case needsUnlock case needsUnlock
case decoding(Error) case decoding(Error)
} }
protocol Connection { protocol Connection {
func sendMessage(completionHandler: @escaping (Result<WatchMessage, ConnectionError>) -> Void) func sendMessage(completionHandler: @escaping (Result<WatchMessage, ConnectionError>) -> Void)
} }
final class WatchConnection: NSObject { final class WatchConnection: NSObject {
private let session: WCSession private let session: WCSession
init(session: WCSession) { init(session: WCSession) {
self.session = session self.session = session
super.init() super.init()
session.delegate = self session.delegate = self
session.activate() session.activate()
} }
private func scheduledSendMessage(completionHandler: @escaping (Result<WatchMessage, ConnectionError>) -> Void) { private func scheduledSendMessage(completionHandler: @escaping (Result<WatchMessage, ConnectionError>) -> Void) {
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { [weak self] _ in Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { [weak self] _ in
self?.sendMessage(completionHandler: completionHandler) self?.sendMessage(completionHandler: completionHandler)
} }
} }
} }
// MARK: - WCSessionDelegate // MARK: - WCSessionDelegate
extension WatchConnection: WCSessionDelegate { extension WatchConnection: WCSessionDelegate {
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
} }
} }
// MARK: - Connection // MARK: - Connection
extension WatchConnection: Connection { extension WatchConnection: Connection {
func sendMessage(completionHandler: @escaping (Result<WatchMessage, ConnectionError>) -> Void) { func sendMessage(completionHandler: @escaping (Result<WatchMessage, ConnectionError>) -> Void) {
guard session.activationState == .activated else { guard session.activationState == .activated else {
scheduledSendMessage(completionHandler: completionHandler) scheduledSendMessage(completionHandler: completionHandler)
return return
} }
guard !session.iOSDeviceNeedsUnlockAfterRebootForReachability else { guard !session.iOSDeviceNeedsUnlockAfterRebootForReachability else {
completionHandler(.failure(.needsUnlock)) completionHandler(.failure(.needsUnlock))
return return
} }
guard session.isReachable else { guard session.isReachable else {
scheduledSendMessage(completionHandler: completionHandler) scheduledSendMessage(completionHandler: completionHandler)
return return
} }
session.sendMessage([:]) { dictionary in session.sendMessage([:]) { dictionary in
do { do {
let data = try JSONSerialization.data(withJSONObject: dictionary) let data = try JSONSerialization.data(withJSONObject: dictionary)
let message = try JSONDecoder().decode(WatchMessage.self, from: data) let message = try JSONDecoder().decode(WatchMessage.self, from: data)
completionHandler(.success(message)) completionHandler(.success(message))
} catch { } catch {
completionHandler(.failure(.decoding(error))) completionHandler(.failure(.decoding(error)))
} }
} }
} }
} }

View File

@ -35,7 +35,6 @@
1E1EA81A2326CD5100E22452 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E1EA8192326CD5100E22452 /* libsqlite3.tbd */; }; 1E1EA81A2326CD5100E22452 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E1EA8192326CD5100E22452 /* libsqlite3.tbd */; };
1E25743422CBA2CF005A877F /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7ACD4853222860DE00442C55 /* JavaScriptCore.framework */; }; 1E25743422CBA2CF005A877F /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7ACD4853222860DE00442C55 /* JavaScriptCore.framework */; };
1E29A2CC2B5857F50093C03C /* RoomListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A2CB2B5857F50093C03C /* RoomListView.swift */; }; 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 */; }; 1E29A2D02B58582F0093C03C /* RoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A2CF2B58582F0093C03C /* RoomView.swift */; };
1E29A2EF2B585B070093C03C /* RocketChatClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A2D22B585B070093C03C /* RocketChatClient.swift */; }; 1E29A2EF2B585B070093C03C /* RocketChatClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A2D22B585B070093C03C /* RocketChatClient.swift */; };
1E29A2F02B585B070093C03C /* AttachmentResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A2D42B585B070093C03C /* AttachmentResponse.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 */; }; 1E29A30E2B58608C0093C03C /* LoggedUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A30D2B58608C0093C03C /* LoggedUser.swift */; };
1E29A3102B5865B80093C03C /* RoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A30F2B5865B80093C03C /* RoomViewModel.swift */; }; 1E29A3102B5865B80093C03C /* RoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A30F2B5865B80093C03C /* RoomViewModel.swift */; };
1E29A3122B5866090093C03C /* Room.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3112B5866090093C03C /* Room.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 */; }; 1E29A3162B5868DF0093C03C /* MessageListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3152B5868DF0093C03C /* MessageListView.swift */; };
1E29A3182B5868E50093C03C /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3172B5868E50093C03C /* MessageView.swift */; }; 1E29A3182B5868E50093C03C /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3172B5868E50093C03C /* MessageView.swift */; };
1E29A31A2B5868EE0093C03C /* MessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3192B5868EE0093C03C /* MessageViewModel.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 */; }; 1E29A3202B5871C80093C03C /* RoomFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A31F2B5871C80093C03C /* RoomFormatter.swift */; };
1E29A3222B5871CE0093C03C /* MessageFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3212B5871CE0093C03C /* MessageFormatter.swift */; }; 1E29A3222B5871CE0093C03C /* MessageFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3212B5871CE0093C03C /* MessageFormatter.swift */; };
1E29A3242B5874FF0093C03C /* MessageComposerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3232B5874FF0093C03C /* MessageComposerView.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 */; }; 1E2F615B25128F9A00871711 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2F615A25128F9A00871711 /* API.swift */; };
1E2F615D25128FA300871711 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2F615C25128FA300871711 /* Response.swift */; }; 1E2F615D25128FA300871711 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2F615C25128FA300871711 /* Response.swift */; };
1E2F61642512955D00871711 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2F61632512955D00871711 /* HTTPMethod.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 */; }; 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 */; }; 1E598AE725150660002BDFBD /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E598AE625150660002BDFBD /* Data+Extensions.swift */; };
1E598AE925151A63002BDFBD /* SendMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E598AE825151A63002BDFBD /* SendMessage.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 */; }; 1E67380424DC529B0009E081 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E67380324DC529B0009E081 /* String+Extensions.swift */; };
1E680ED92512990700C9257A /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E680ED82512990700C9257A /* Request.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 */; }; 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 */; }; 1E76CBD825152C870067298C /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E680ED82512990700C9257A /* Request.swift */; };
1E76CBD925152C8C0067298C /* Push.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2F61652512958900871711 /* Push.swift */; }; 1E76CBD925152C8C0067298C /* Push.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2F61652512958900871711 /* Push.swift */; };
1E76CBDA25152C8E0067298C /* SendMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E598AE825151A63002BDFBD /* SendMessage.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 */; }; 1EB375892B55DBFB00AEC3D7 /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB375882B55DBFB00AEC3D7 /* Server.swift */; };
1EB8EF722510F1EE00F352B7 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB8EF712510F1EE00F352B7 /* Storage.swift */; }; 1EB8EF722510F1EE00F352B7 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB8EF712510F1EE00F352B7 /* Storage.swift */; };
1EC6ACB722CB9FC300A41C61 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1EC6ACB522CB9FC300A41C61 /* MainInterface.storyboard */; }; 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 */; }; 1ED038C62B50A21800C007D4 /* WatchMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038C32B50A1F500C007D4 /* WatchMessage.swift */; };
1ED038CA2B50A58400C007D4 /* WatchConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038C92B50A58400C007D4 /* WatchConnection.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 */; }; 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 */; }; 1EF5FBD1250C109E00614FEA /* Encryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF5FBD0250C109E00614FEA /* Encryption.swift */; };
1EFEB5982493B6640072EDC0 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFEB5972493B6640072EDC0 /* NotificationService.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, ); }; }; 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; }; 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; }; 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 = "<group>"; }; 1E29A2CB2B5857F50093C03C /* RoomListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListView.swift; sourceTree = "<group>"; };
1E29A2CD2B5857FC0093C03C /* RoomListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListViewModel.swift; sourceTree = "<group>"; };
1E29A2CF2B58582F0093C03C /* RoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomView.swift; sourceTree = "<group>"; }; 1E29A2CF2B58582F0093C03C /* RoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomView.swift; sourceTree = "<group>"; };
1E29A2D22B585B070093C03C /* RocketChatClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RocketChatClient.swift; sourceTree = "<group>"; }; 1E29A2D22B585B070093C03C /* RocketChatClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RocketChatClient.swift; sourceTree = "<group>"; };
1E29A2D42B585B070093C03C /* AttachmentResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentResponse.swift; sourceTree = "<group>"; }; 1E29A2D42B585B070093C03C /* AttachmentResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentResponse.swift; sourceTree = "<group>"; };
@ -370,7 +372,6 @@
1E29A30D2B58608C0093C03C /* LoggedUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedUser.swift; sourceTree = "<group>"; }; 1E29A30D2B58608C0093C03C /* LoggedUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedUser.swift; sourceTree = "<group>"; };
1E29A30F2B5865B80093C03C /* RoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomViewModel.swift; sourceTree = "<group>"; }; 1E29A30F2B5865B80093C03C /* RoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomViewModel.swift; sourceTree = "<group>"; };
1E29A3112B5866090093C03C /* Room.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Room.swift; sourceTree = "<group>"; }; 1E29A3112B5866090093C03C /* Room.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Room.swift; sourceTree = "<group>"; };
1E29A3132B5868D80093C03C /* MessageListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageListViewModel.swift; sourceTree = "<group>"; };
1E29A3152B5868DF0093C03C /* MessageListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageListView.swift; sourceTree = "<group>"; }; 1E29A3152B5868DF0093C03C /* MessageListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageListView.swift; sourceTree = "<group>"; };
1E29A3172B5868E50093C03C /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = "<group>"; }; 1E29A3172B5868E50093C03C /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = "<group>"; };
1E29A3192B5868EE0093C03C /* MessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageViewModel.swift; sourceTree = "<group>"; }; 1E29A3192B5868EE0093C03C /* MessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageViewModel.swift; sourceTree = "<group>"; };
@ -378,7 +379,6 @@
1E29A31F2B5871C80093C03C /* RoomFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFormatter.swift; sourceTree = "<group>"; }; 1E29A31F2B5871C80093C03C /* RoomFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFormatter.swift; sourceTree = "<group>"; };
1E29A3212B5871CE0093C03C /* MessageFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageFormatter.swift; sourceTree = "<group>"; }; 1E29A3212B5871CE0093C03C /* MessageFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageFormatter.swift; sourceTree = "<group>"; };
1E29A3232B5874FF0093C03C /* MessageComposerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposerView.swift; sourceTree = "<group>"; }; 1E29A3232B5874FF0093C03C /* MessageComposerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposerView.swift; sourceTree = "<group>"; };
1E29A3252B58752D0093C03C /* MessageComposerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposerViewModel.swift; sourceTree = "<group>"; };
1E2F615A25128F9A00871711 /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = "<group>"; }; 1E2F615A25128F9A00871711 /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = "<group>"; };
1E2F615C25128FA300871711 /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = "<group>"; }; 1E2F615C25128FA300871711 /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = "<group>"; };
1E2F61632512955D00871711 /* HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = "<group>"; }; 1E2F61632512955D00871711 /* HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = "<group>"; };
@ -389,9 +389,13 @@
1E598AE32515057D002BDFBD /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = "<group>"; }; 1E598AE32515057D002BDFBD /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = "<group>"; };
1E598AE625150660002BDFBD /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; }; 1E598AE625150660002BDFBD /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
1E598AE825151A63002BDFBD /* SendMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessage.swift; sourceTree = "<group>"; }; 1E598AE825151A63002BDFBD /* SendMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessage.swift; sourceTree = "<group>"; };
1E6436232B59998A009F0CE1 /* ExtensionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionDelegate.swift; sourceTree = "<group>"; };
1E6737FF24DC52660009E081 /* NotificationService-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NotificationService-Bridging-Header.h"; sourceTree = "<group>"; }; 1E6737FF24DC52660009E081 /* NotificationService-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NotificationService-Bridging-Header.h"; sourceTree = "<group>"; };
1E67380324DC529B0009E081 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; }; 1E67380324DC529B0009E081 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
1E680ED82512990700C9257A /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = "<group>"; }; 1E680ED82512990700C9257A /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = "<group>"; };
1E9A71652B599D3F00477BA2 /* PushNotificationPayload.apns */ = {isa = PBXFileReference; lastKnownFileType = text; path = PushNotificationPayload.apns; sourceTree = "<group>"; };
1E9A71662B599E6300477BA2 /* NotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationController.swift; sourceTree = "<group>"; };
1E9A71682B59B6E100477BA2 /* MessageSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSender.swift; sourceTree = "<group>"; };
1EB375882B55DBFB00AEC3D7 /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; }; 1EB375882B55DBFB00AEC3D7 /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; };
1EB8EF712510F1EE00F352B7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; }; 1EB8EF712510F1EE00F352B7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
1EC6ACB022CB9FC300A41C61 /* ShareRocketChatRN.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareRocketChatRN.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; 1ED038C32B50A1F500C007D4 /* WatchMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchMessage.swift; sourceTree = "<group>"; };
1ED038C92B50A58400C007D4 /* WatchConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConnection.swift; sourceTree = "<group>"; }; 1ED038C92B50A58400C007D4 /* WatchConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConnection.swift; sourceTree = "<group>"; };
1ED59D4B22CBA77D00C54289 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = SOURCE_ROOT; }; 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 = "<group>"; };
1EDFD1052B58A66E002FEE5F /* CancelBag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelBag.swift; sourceTree = "<group>"; };
1EDFD1072B58AA77002FEE5F /* RoomsLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomsLoader.swift; sourceTree = "<group>"; };
1EF5FBD0250C109E00614FEA /* Encryption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encryption.swift; sourceTree = "<group>"; }; 1EF5FBD0250C109E00614FEA /* Encryption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encryption.swift; sourceTree = "<group>"; };
1EFEB5952493B6640072EDC0 /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; 1EFEB5972493B6640072EDC0 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
@ -777,11 +784,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
1ED033B72B55B4BE004F4930 /* ServerListViewModel.swift */, 1ED033B72B55B4BE004F4930 /* ServerListViewModel.swift */,
1E29A2CD2B5857FC0093C03C /* RoomListViewModel.swift */,
1E29A30F2B5865B80093C03C /* RoomViewModel.swift */, 1E29A30F2B5865B80093C03C /* RoomViewModel.swift */,
1E29A3132B5868D80093C03C /* MessageListViewModel.swift */,
1E29A3192B5868EE0093C03C /* MessageViewModel.swift */, 1E29A3192B5868EE0093C03C /* MessageViewModel.swift */,
1E29A3252B58752D0093C03C /* MessageComposerViewModel.swift */,
); );
path = ViewModels; path = ViewModels;
sourceTree = "<group>"; sourceTree = "<group>";
@ -789,6 +793,8 @@
1ED0388F2B507B4C00C007D4 /* RocketChat Watch App */ = { 1ED0388F2B507B4C00C007D4 /* RocketChat Watch App */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
1EDFD0FB2B589FC4002FEE5F /* DependencyInjection */,
1EDFD0F82B589B82002FEE5F /* Loaders */,
1E29A31E2B5871BE0093C03C /* Formatters */, 1E29A31E2B5871BE0093C03C /* Formatters */,
1E29A31B2B5871AC0093C03C /* Extensions */, 1E29A31B2B5871AC0093C03C /* Extensions */,
1E29A2D12B585B070093C03C /* Client */, 1E29A2D12B585B070093C03C /* Client */,
@ -802,7 +808,9 @@
1ED038942B507B4D00C007D4 /* Assets.xcassets */, 1ED038942B507B4D00C007D4 /* Assets.xcassets */,
1ED038962B507B4D00C007D4 /* Preview Content */, 1ED038962B507B4D00C007D4 /* Preview Content */,
1ED033C02B55C190004F4930 /* Localizable.xcstrings */, 1ED033C02B55C190004F4930 /* Localizable.xcstrings */,
1ED033C72B55CE78004F4930 /* DependencyStore.swift */, 1E6436232B59998A009F0CE1 /* ExtensionDelegate.swift */,
1E9A71652B599D3F00477BA2 /* PushNotificationPayload.apns */,
1E9A71662B599E6300477BA2 /* NotificationController.swift */,
); );
path = "RocketChat Watch App"; path = "RocketChat Watch App";
sourceTree = "<group>"; sourceTree = "<group>";
@ -834,6 +842,25 @@
path = Database; path = Database;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
1EDFD0F82B589B82002FEE5F /* Loaders */ = {
isa = PBXGroup;
children = (
1EDFD0F92B589B8F002FEE5F /* MessagesLoader.swift */,
1EDFD1052B58A66E002FEE5F /* CancelBag.swift */,
1EDFD1072B58AA77002FEE5F /* RoomsLoader.swift */,
1E9A71682B59B6E100477BA2 /* MessageSender.swift */,
);
path = Loaders;
sourceTree = "<group>";
};
1EDFD0FB2B589FC4002FEE5F /* DependencyInjection */ = {
isa = PBXGroup;
children = (
1ED033C72B55CE78004F4930 /* DependencyStore.swift */,
);
path = DependencyInjection;
sourceTree = "<group>";
};
1EFEB5962493B6640072EDC0 /* NotificationService */ = { 1EFEB5962493B6640072EDC0 /* NotificationService */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -904,9 +931,9 @@
7AC2B09613AA7C3FEBAC9F57 /* Pods */, 7AC2B09613AA7C3FEBAC9F57 /* Pods */,
7890E71355E6C0A3288089E7 /* ExpoModulesProviders */, 7890E71355E6C0A3288089E7 /* ExpoModulesProviders */,
); );
indentWidth = 2; indentWidth = 4;
sourceTree = "<group>"; sourceTree = "<group>";
tabWidth = 2; tabWidth = 4;
}; };
83CBBA001A601CBA00E9B192 /* Products */ = { 83CBBA001A601CBA00E9B192 /* Products */ = {
isa = PBXGroup; isa = PBXGroup;
@ -1809,7 +1836,6 @@
1EB375892B55DBFB00AEC3D7 /* Server.swift in Sources */, 1EB375892B55DBFB00AEC3D7 /* Server.swift in Sources */,
1E29A3162B5868DF0093C03C /* MessageListView.swift in Sources */, 1E29A3162B5868DF0093C03C /* MessageListView.swift in Sources */,
1E29A2F42B585B070093C03C /* SubscriptionsResponse.swift in Sources */, 1E29A2F42B585B070093C03C /* SubscriptionsResponse.swift in Sources */,
1E29A3142B5868D80093C03C /* MessageListViewModel.swift in Sources */,
1E29A2F92B585B070093C03C /* SubscriptionsRequest.swift in Sources */, 1E29A2F92B585B070093C03C /* SubscriptionsRequest.swift in Sources */,
1E29A2F22B585B070093C03C /* HistoryResponse.swift in Sources */, 1E29A2F22B585B070093C03C /* HistoryResponse.swift in Sources */,
1ED033BA2B55B5F6004F4930 /* ServerView.swift in Sources */, 1ED033BA2B55B5F6004F4930 /* ServerView.swift in Sources */,
@ -1821,21 +1847,26 @@
1E29A3022B585B070093C03C /* DateCodingStrategy.swift in Sources */, 1E29A3022B585B070093C03C /* DateCodingStrategy.swift in Sources */,
1ED033B62B55B4A5004F4930 /* ServerListView.swift in Sources */, 1ED033B62B55B4A5004F4930 /* ServerListView.swift in Sources */,
1E29A3202B5871C80093C03C /* RoomFormatter.swift in Sources */, 1E29A3202B5871C80093C03C /* RoomFormatter.swift in Sources */,
1EDFD0FA2B589B8F002FEE5F /* MessagesLoader.swift in Sources */,
1E29A3102B5865B80093C03C /* RoomViewModel.swift in Sources */, 1E29A3102B5865B80093C03C /* RoomViewModel.swift in Sources */,
1E29A2FC2B585B070093C03C /* SendMessageRequest.swift in Sources */, 1E29A2FC2B585B070093C03C /* SendMessageRequest.swift in Sources */,
1E29A3262B58752D0093C03C /* MessageComposerViewModel.swift in Sources */,
1E29A30C2B585D1D0093C03C /* String+Extensions.swift in Sources */, 1E29A30C2B585D1D0093C03C /* String+Extensions.swift in Sources */,
1ED033CD2B55D671004F4930 /* RocketChatDatabase.swift in Sources */, 1ED033CD2B55D671004F4930 /* RocketChatDatabase.swift in Sources */,
1E29A3122B5866090093C03C /* Room.swift in Sources */, 1E29A3122B5866090093C03C /* Room.swift in Sources */,
1E29A3032B585B070093C03C /* FailableDecodable.swift in Sources */, 1E29A3032B585B070093C03C /* FailableDecodable.swift in Sources */,
1E29A2FE2B585B070093C03C /* ReadRequest.swift in Sources */, 1E29A2FE2B585B070093C03C /* ReadRequest.swift in Sources */,
1E9A71692B59B6E100477BA2 /* MessageSender.swift in Sources */,
1E29A3062B585B070093C03C /* RocketChatServer.swift in Sources */, 1E29A3062B585B070093C03C /* RocketChatServer.swift in Sources */,
1E29A3072B585B070093C03C /* RocketChatError.swift in Sources */, 1E29A3072B585B070093C03C /* RocketChatError.swift in Sources */,
1E9A71672B599E6300477BA2 /* NotificationController.swift in Sources */,
1EDFD1082B58AA77002FEE5F /* RoomsLoader.swift in Sources */,
1E29A2F12B585B070093C03C /* SendMessageResponse.swift in Sources */, 1E29A2F12B585B070093C03C /* SendMessageResponse.swift in Sources */,
1E29A30E2B58608C0093C03C /* LoggedUser.swift in Sources */, 1E29A30E2B58608C0093C03C /* LoggedUser.swift in Sources */,
1E29A3182B5868E50093C03C /* MessageView.swift in Sources */, 1E29A3182B5868E50093C03C /* MessageView.swift in Sources */,
1EDFD1062B58A66E002FEE5F /* CancelBag.swift in Sources */,
1E29A2FF2B585B070093C03C /* TokenAdapter.swift in Sources */, 1E29A2FF2B585B070093C03C /* TokenAdapter.swift in Sources */,
1E29A3052B585B070093C03C /* Request.swift in Sources */, 1E29A3052B585B070093C03C /* Request.swift in Sources */,
1E6436242B59998A009F0CE1 /* ExtensionDelegate.swift in Sources */,
1E29A2EF2B585B070093C03C /* RocketChatClient.swift in Sources */, 1E29A2EF2B585B070093C03C /* RocketChatClient.swift in Sources */,
1E29A2FB2B585B070093C03C /* MessagesRequest.swift in Sources */, 1E29A2FB2B585B070093C03C /* MessagesRequest.swift in Sources */,
1E29A31D2B5871B60093C03C /* Date+Extensions.swift in Sources */, 1E29A31D2B5871B60093C03C /* Date+Extensions.swift in Sources */,
@ -1845,7 +1876,6 @@
1E29A2F82B585B070093C03C /* MessageResponse.swift in Sources */, 1E29A2F82B585B070093C03C /* MessageResponse.swift in Sources */,
1E29A3042B585B070093C03C /* HTTPMethod.swift in Sources */, 1E29A3042B585B070093C03C /* HTTPMethod.swift in Sources */,
1E29A3012B585B070093C03C /* RequestAdapter.swift in Sources */, 1E29A3012B585B070093C03C /* RequestAdapter.swift in Sources */,
1E29A2CE2B5857FC0093C03C /* RoomListViewModel.swift in Sources */,
1E29A2F52B585B070093C03C /* RoomsResponse.swift in Sources */, 1E29A2F52B585B070093C03C /* RoomsResponse.swift in Sources */,
1E29A2F32B585B070093C03C /* MessagesResponse.swift in Sources */, 1E29A2F32B585B070093C03C /* MessagesResponse.swift in Sources */,
1E29A2FA2B585B070093C03C /* HistoryRequest.swift in Sources */, 1E29A2FA2B585B070093C03C /* HistoryRequest.swift in Sources */,