Remove unnecessary ViewModel layer
This commit is contained in:
parent
7ced062950
commit
9668aed381
|
@ -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 {
|
|
||||||
fatalError("Can't find Core Data Model")
|
|
||||||
}
|
|
||||||
|
|
||||||
return managedObjectModel
|
func process(updatedServer: WatchMessage.Server)
|
||||||
}()
|
}
|
||||||
|
|
||||||
init() {
|
final class DefaultDatabase: ServersDatabase {
|
||||||
container = NSPersistentContainer(name: "default", managedObjectModel: Self.model)
|
private let container: NSPersistentContainer
|
||||||
|
|
||||||
container.loadPersistentStores { _, error in
|
var viewContext: NSManagedObjectContext {
|
||||||
if let error { fatalError("Can't load persistent stores: \(error)") }
|
container.viewContext
|
||||||
}
|
}
|
||||||
|
|
||||||
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
private static let model: NSManagedObjectModel = {
|
||||||
}
|
guard let url = Bundle.main.url(forResource: "Default", withExtension: "momd"),
|
||||||
|
let managedObjectModel = NSManagedObjectModel(contentsOf: url) else {
|
||||||
func save() {
|
fatalError("Can't find Core Data Model")
|
||||||
guard container.viewContext.hasChanges else {
|
}
|
||||||
return
|
|
||||||
}
|
return managedObjectModel
|
||||||
|
}()
|
||||||
try? container.viewContext.save()
|
|
||||||
}
|
init() {
|
||||||
|
container = NSPersistentContainer(name: "default", managedObjectModel: Self.model)
|
||||||
func server(url: URL) -> Server? {
|
|
||||||
let request = Server.fetchRequest()
|
container.loadPersistentStores { _, error in
|
||||||
request.predicate = NSPredicate(format: "url == %@", url.absoluteString)
|
if let error { fatalError("Can't load persistent stores: \(error)") }
|
||||||
|
}
|
||||||
return try? viewContext.fetch(request).first
|
|
||||||
}
|
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
||||||
|
}
|
||||||
func user(id: String) -> LoggedUser? {
|
|
||||||
let request = LoggedUser.fetchRequest()
|
func save() {
|
||||||
request.predicate = NSPredicate(format: "id == %@", id)
|
guard container.viewContext.hasChanges else {
|
||||||
|
return
|
||||||
return try? viewContext.fetch(request).first
|
}
|
||||||
}
|
|
||||||
|
try? container.viewContext.save()
|
||||||
func servers() -> [Server] {
|
}
|
||||||
let request = Server.fetchRequest()
|
|
||||||
|
func server(url: URL) -> Server? {
|
||||||
return (try? viewContext.fetch(request)) ?? []
|
let request = Server.fetchRequest()
|
||||||
}
|
request.predicate = NSPredicate(format: "url == %@", url.absoluteString)
|
||||||
|
|
||||||
func process(updatedServer: WatchMessage.Server) {
|
return try? viewContext.fetch(request).first
|
||||||
if let server = server(url: updatedServer.url) {
|
}
|
||||||
server.url = updatedServer.url
|
|
||||||
server.name = updatedServer.name
|
func user(id: String) -> LoggedUser? {
|
||||||
server.iconURL = updatedServer.iconURL
|
let request = LoggedUser.fetchRequest()
|
||||||
server.useRealName = updatedServer.useRealName
|
request.predicate = NSPredicate(format: "id == %@", id)
|
||||||
server.loggedUser = user(from: updatedServer.loggedUser)
|
|
||||||
} else {
|
return try? viewContext.fetch(request).first
|
||||||
Server(
|
}
|
||||||
context: viewContext,
|
|
||||||
iconURL: updatedServer.iconURL,
|
func servers() -> [Server] {
|
||||||
name: updatedServer.name,
|
let request = Server.fetchRequest()
|
||||||
url: updatedServer.url,
|
|
||||||
useRealName: updatedServer.useRealName,
|
return (try? viewContext.fetch(request)) ?? []
|
||||||
loggedUser: user(from: updatedServer.loggedUser)
|
}
|
||||||
)
|
|
||||||
}
|
func process(updatedServer: WatchMessage.Server) {
|
||||||
|
if let server = server(url: updatedServer.url) {
|
||||||
save()
|
server.url = updatedServer.url
|
||||||
}
|
server.name = updatedServer.name
|
||||||
|
server.iconURL = updatedServer.iconURL
|
||||||
private func user(from updatedUser: WatchMessage.Server.LoggedUser) -> LoggedUser {
|
server.useRealName = updatedServer.useRealName
|
||||||
if let user = user(id: updatedUser.id) {
|
server.loggedUser = user(from: updatedServer.loggedUser)
|
||||||
user.id = updatedUser.id
|
} else {
|
||||||
user.name = updatedUser.name
|
Server(
|
||||||
user.username = updatedUser.username
|
context: viewContext,
|
||||||
user.token = updatedUser.token
|
iconURL: updatedServer.iconURL,
|
||||||
|
name: updatedServer.name,
|
||||||
return user
|
url: updatedServer.url,
|
||||||
}
|
useRealName: updatedServer.useRealName,
|
||||||
|
loggedUser: user(from: updatedServer.loggedUser)
|
||||||
return LoggedUser(
|
)
|
||||||
context: viewContext,
|
}
|
||||||
id: updatedUser.id,
|
|
||||||
name: updatedUser.name,
|
save()
|
||||||
token: updatedUser.token,
|
}
|
||||||
username: updatedUser.username
|
|
||||||
)
|
private func user(from updatedUser: WatchMessage.Server.LoggedUser) -> LoggedUser {
|
||||||
}
|
if let user = user(id: updatedUser.id) {
|
||||||
|
user.id = updatedUser.id
|
||||||
|
user.name = updatedUser.name
|
||||||
|
user.username = updatedUser.username
|
||||||
|
user.token = updatedUser.token
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoggedUser(
|
||||||
|
context: viewContext,
|
||||||
|
id: updatedUser.id,
|
||||||
|
name: updatedUser.name,
|
||||||
|
token: updatedUser.token,
|
||||||
|
username: updatedUser.username
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,44 +3,44 @@ 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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,50 +3,50 @@ 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 {
|
||||||
|
@ -54,12 +54,12 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
typealias CancelBag = Set<AnyCancellable>
|
||||||
|
|
||||||
|
extension CancelBag {
|
||||||
|
mutating func cancelAll() {
|
||||||
|
forEach { $0.cancel() }
|
||||||
|
removeAll()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
{
|
{
|
||||||
"sourceLanguage" : "en",
|
"sourceLanguage" : "en",
|
||||||
"strings" : {
|
"strings" : {
|
||||||
|
"Load More..." : {
|
||||||
|
|
||||||
|
},
|
||||||
"Message" : {
|
"Message" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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()
|
|
||||||
|
|
||||||
self.store = store
|
init() {
|
||||||
self._router = StateObject(wrappedValue: RocketChatAppRouter(database: store.database))
|
let store = DependencyStore()
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
self.store = store
|
||||||
private var serverListView: some View {
|
self._router = StateObject(wrappedValue: RocketChatAppRouter(database: store.database))
|
||||||
ServerListView(
|
}
|
||||||
dependencies: .init(
|
|
||||||
connection: store.connection,
|
|
||||||
database: store.database,
|
|
||||||
router: router
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func roomListView(for server: Server) -> some View {
|
private var serverListView: some View {
|
||||||
RoomListView(
|
ServerListView(
|
||||||
dependencies: .init(
|
dependencies: .init(
|
||||||
client: store.client(for: server),
|
connection: store.connection,
|
||||||
database: store.database(for: server),
|
database: store.database,
|
||||||
router: router,
|
router: router
|
||||||
server: server
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var body: some Scene {
|
@ViewBuilder
|
||||||
WindowGroup {
|
private func roomListView(for server: Server) -> some View {
|
||||||
NavigationStack {
|
RoomListView(
|
||||||
switch router.route {
|
client: store.client(for: server),
|
||||||
case .roomList(let server):
|
database: store.database(for: server),
|
||||||
roomListView(for: server)
|
messagesLoader: MessagesLoader(
|
||||||
.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)
|
),
|
||||||
}
|
messageSender: MessageSender(
|
||||||
}
|
client: store.client(for: server),
|
||||||
}
|
database: store.database(for: server),
|
||||||
}
|
server: server
|
||||||
|
),
|
||||||
|
roomsLoader: RoomsLoader(
|
||||||
|
client: store.client(for: server),
|
||||||
|
database: store.database(for: server),
|
||||||
|
serversDB: store.database
|
||||||
|
),
|
||||||
|
router: router,
|
||||||
|
server: server
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
NavigationStack {
|
||||||
|
switch router.route {
|
||||||
|
case .roomList(let server):
|
||||||
|
roomListView(for: server)
|
||||||
|
.environment(\.managedObjectContext, store.database(for: server).viewContext)
|
||||||
|
case .serverList:
|
||||||
|
serverListView
|
||||||
|
.environment(\.managedObjectContext, store.database.viewContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WKNotificationScene(controller: NotificationController.self, category: "MESSAGE")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
switch route {
|
||||||
|
case .roomList(let server):
|
||||||
|
currentServer = server.url
|
||||||
|
case .serverList:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private let database: Database
|
private let database: ServersDatabase
|
||||||
|
|
||||||
init(database: Database) {
|
init(database: ServersDatabase) {
|
||||||
self.database = database
|
self.database = database
|
||||||
loadRoute()
|
loadRoute()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadRoute() {
|
private func loadRoute() {
|
||||||
if let currentServer, let server = database.server(url: currentServer) {
|
if let currentServer, let server = database.server(url: currentServer) {
|
||||||
route = .roomList(server)
|
route = .roomList(server)
|
||||||
} else if database.servers().count == 1, let server = database.servers().first {
|
} else if database.servers().count == 1, let server = database.servers().first {
|
||||||
route = .roomList(server)
|
route = .roomList(server)
|
||||||
} else {
|
} else {
|
||||||
route = .serverList
|
route = .serverList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func route(to route: Route) {
|
func route(to route: Route) {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.route = route
|
self.route = route
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RocketChatAppRouter {
|
extension RocketChatAppRouter {
|
||||||
enum Route {
|
enum Route {
|
||||||
case roomList(Server)
|
case roomList(Server)
|
||||||
case serverList
|
case serverList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
|
||||||
|
|
||||||
@State private var message = ""
|
var body: some View {
|
||||||
|
if room.isReadOnly {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text("This room is read only")
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TextField("Message", text: $message)
|
||||||
|
.submitLabel(.send)
|
||||||
|
.onSubmit(send)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
func send() {
|
||||||
if viewModel.isReadOnly {
|
guard !message.isEmpty else {
|
||||||
HStack {
|
return
|
||||||
Spacer()
|
}
|
||||||
Text("This room is read only")
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TextField("Message", text: $message)
|
|
||||||
.submitLabel(.send)
|
|
||||||
.onSubmit(send)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func send() {
|
onSend(message)
|
||||||
guard !message.isEmpty else {
|
message = ""
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.sendMessage(message)
|
|
||||||
message = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
private let messagesLoader: MessagesLoading
|
||||||
|
private let messageSender: MessageSending
|
||||||
|
private let formatter: RoomFormatter
|
||||||
|
private let server: Server
|
||||||
|
private let room: Room
|
||||||
|
|
||||||
init(viewModel: MessageListViewModel) {
|
@FetchRequest<Message> private var messages: FetchedResults<Message>
|
||||||
_viewModel = StateObject(wrappedValue: viewModel)
|
|
||||||
_messages = FetchRequest(fetchRequest: viewModel.room.messagesRequest, animation: .none)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
init(
|
||||||
ScrollViewReader { reader in
|
client: RocketChatClientProtocol,
|
||||||
ScrollView {
|
database: Database,
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
messagesLoader: MessagesLoading,
|
||||||
ForEach(messages.indices, id: \.self) { index in
|
messageSender: MessageSending,
|
||||||
let message = messages[index]
|
room: Room,
|
||||||
let previousMessage = messages.indices.contains(index - 1) ? messages[index - 1] : nil
|
server: Server
|
||||||
|
) {
|
||||||
|
self.client = client
|
||||||
|
self.database = database
|
||||||
|
self.messagesLoader = messagesLoader
|
||||||
|
self.messageSender = messageSender
|
||||||
|
self.formatter = RoomFormatter(room: room, server: server)
|
||||||
|
self.room = room
|
||||||
|
self.server = server
|
||||||
|
_messages = FetchRequest(fetchRequest: room.messagesRequest, animation: .none)
|
||||||
|
}
|
||||||
|
|
||||||
MessageView(viewModel: viewModel.messageViewModel(for: message, and: previousMessage))
|
var body: some View {
|
||||||
.id(message.id)
|
ScrollViewReader { reader in
|
||||||
.transition(.move(edge: .bottom))
|
ScrollView {
|
||||||
}
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
if room.hasMoreMessages {
|
||||||
|
Button("Load More...") {
|
||||||
|
guard let oldestMessage = room.firstMessage?.ts else { return }
|
||||||
|
|
||||||
MessageComposerView(viewModel: viewModel.composerViewModel())
|
messagesLoader.loadMore(from: oldestMessage)
|
||||||
.padding(.top)
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.padding([.leading, .trailing])
|
|
||||||
.navigationTitle(viewModel.title)
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.onAppear {
|
|
||||||
viewModel.loadMessages {
|
|
||||||
reader.scrollTo(messages.last?.id, anchor: .bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.markAsRead()
|
ForEach(messages.indices, id: \.self) { index in
|
||||||
}
|
let message = messages[index]
|
||||||
.onDisappear {
|
let previousMessage = messages.indices.contains(index - 1) ? messages[index - 1] : nil
|
||||||
viewModel.stop()
|
|
||||||
}
|
MessageView(viewModel: .init(message: message, previousMessage: previousMessage, server: server))
|
||||||
.onReceive(messages.publisher) { _ in
|
.transition(.move(edge: .bottom))
|
||||||
viewModel.markAsRead()
|
}
|
||||||
}
|
|
||||||
}
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
private let messagesLoader: MessagesLoading
|
||||||
|
private let messageSender: MessageSending
|
||||||
|
private let roomsLoader: RoomsLoading
|
||||||
|
private let router: RocketChatAppRouter
|
||||||
|
private let server: Server
|
||||||
|
|
||||||
@FetchRequest<Room> private var rooms: FetchedResults<Room>
|
@FetchRequest<Room> private var rooms: FetchedResults<Room>
|
||||||
|
|
||||||
init(dependencies: RoomListViewModel.Dependencies) {
|
init(
|
||||||
_viewModel = StateObject(wrappedValue: RoomListViewModel(dependencies: dependencies))
|
client: RocketChatClientProtocol,
|
||||||
_rooms = FetchRequest(fetchRequest: dependencies.server.roomsRequest)
|
database: Database,
|
||||||
}
|
messagesLoader: MessagesLoading,
|
||||||
|
messageSender: MessageSending,
|
||||||
|
roomsLoader: RoomsLoading,
|
||||||
|
router: RocketChatAppRouter,
|
||||||
|
server: Server
|
||||||
|
) {
|
||||||
|
self.client = client
|
||||||
|
self.database = database
|
||||||
|
self.messagesLoader = messagesLoader
|
||||||
|
self.messageSender = messageSender
|
||||||
|
self.roomsLoader = roomsLoader
|
||||||
|
self.router = router
|
||||||
|
self.server = server
|
||||||
|
_rooms = FetchRequest(fetchRequest: server.roomsRequest)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
ForEach(rooms) { room in
|
ForEach(rooms) { room in
|
||||||
NavigationLink(value: room) {
|
NavigationLink(value: room) {
|
||||||
RoomView(viewModel: viewModel.roomViewModel(for: room))
|
RoomView(viewModel: .init(room: room, server: server))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.loadRooms()
|
roomsLoader.start(in: server.url)
|
||||||
}
|
}
|
||||||
.navigationTitle("Rooms")
|
.onDisappear {
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
roomsLoader.stop()
|
||||||
.navigationDestination(for: Room.self) { room in
|
}
|
||||||
MessageListView(viewModel: viewModel.messageListViewModel(for: room))
|
.navigationTitle("Rooms")
|
||||||
.environment(\.managedObjectContext, viewModel.viewContext)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
}
|
.navigationDestination(for: Room.self) { room in
|
||||||
.toolbar {
|
MessageListView(
|
||||||
ToolbarItem(placement: .automatic) {
|
client: client,
|
||||||
Button("Servers") {
|
database: database,
|
||||||
viewModel.logout()
|
messagesLoader: messagesLoader,
|
||||||
}
|
messageSender: messageSender,
|
||||||
}
|
room: room,
|
||||||
}
|
server: server
|
||||||
}
|
)
|
||||||
|
.environment(\.managedObjectContext, database.viewContext)
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .automatic) {
|
||||||
|
Button("Servers") {
|
||||||
|
router.route(to: .serverList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 */,
|
||||||
|
|
Loading…
Reference in New Issue