From 7ced062950d5da9c3b655130bfe670175977f067 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Wed, 17 Jan 2024 18:07:53 -0300 Subject: [PATCH] Add MessageListView & MessageView --- .../Database/Models/Room.swift | 9 ++ .../Database/Models/Server.swift | 11 ++ .../Extensions/Date+Extensions.swift | 7 + .../Formatters/MessageFormatter.swift | 74 ++++++++++ .../Formatters/RoomFormatter.swift | 46 +++++++ .../Localizable.xcstrings | 6 + .../ViewModels/MessageComposerViewModel.swift | 42 ++++++ .../ViewModels/MessageListViewModel.swift | 126 ++++++++++++++++++ .../ViewModels/MessageViewModel.swift | 21 +++ .../ViewModels/RoomListViewModel.swift | 24 +++- .../ViewModels/RoomViewModel.swift | 40 +----- .../Views/MessageComposerView.swift | 38 ++++++ .../Views/MessageListView.swift | 48 +++++++ .../Views/MessageView.swift | 59 ++++++++ .../Views/RoomListView.swift | 21 ++- ios/RocketChat Watch App/Views/RoomView.swift | 2 +- .../Views/ServerListView.swift | 1 - ios/RocketChatRN.xcodeproj/project.pbxproj | 54 +++++++- 18 files changed, 576 insertions(+), 53 deletions(-) create mode 100644 ios/RocketChat Watch App/Extensions/Date+Extensions.swift create mode 100644 ios/RocketChat Watch App/Formatters/MessageFormatter.swift create mode 100644 ios/RocketChat Watch App/Formatters/RoomFormatter.swift create mode 100644 ios/RocketChat Watch App/ViewModels/MessageComposerViewModel.swift create mode 100644 ios/RocketChat Watch App/ViewModels/MessageListViewModel.swift create mode 100644 ios/RocketChat Watch App/ViewModels/MessageViewModel.swift create mode 100644 ios/RocketChat Watch App/Views/MessageComposerView.swift create mode 100644 ios/RocketChat Watch App/Views/MessageListView.swift create mode 100644 ios/RocketChat Watch App/Views/MessageView.swift diff --git a/ios/RocketChat Watch App/Database/Models/Room.swift b/ios/RocketChat Watch App/Database/Models/Room.swift index aac3eb16b..a1a6efa55 100644 --- a/ios/RocketChat Watch App/Database/Models/Room.swift +++ b/ios/RocketChat Watch App/Database/Models/Room.swift @@ -1,6 +1,15 @@ import CoreData extension Room { + var messagesRequest: NSFetchRequest { + let request = Message.fetchRequest() + + request.predicate = NSPredicate(format: "room == %@", self) + request.sortDescriptors = [NSSortDescriptor(keyPath: \Message.ts, ascending: true)] + + return request + } + var lastMessage: Message? { let request = Message.fetchRequest() diff --git a/ios/RocketChat Watch App/Database/Models/Server.swift b/ios/RocketChat Watch App/Database/Models/Server.swift index 84be1dfba..f1b19fb17 100644 --- a/ios/RocketChat Watch App/Database/Models/Server.swift +++ b/ios/RocketChat Watch App/Database/Models/Server.swift @@ -52,3 +52,14 @@ public final class Server: NSManagedObject { extension Server: Identifiable { } + +extension Server { + var roomsRequest: NSFetchRequest { + let request = Room.fetchRequest() + + request.predicate = NSPredicate(format: "archived == false") + request.sortDescriptors = [NSSortDescriptor(keyPath: \Room.ts, ascending: false)] + + return request + } +} diff --git a/ios/RocketChat Watch App/Extensions/Date+Extensions.swift b/ios/RocketChat Watch App/Extensions/Date+Extensions.swift new file mode 100644 index 000000000..cc5c5cb59 --- /dev/null +++ b/ios/RocketChat Watch App/Extensions/Date+Extensions.swift @@ -0,0 +1,7 @@ +import Foundation + +extension Date { + static func - (lhs: Date, rhs: Date) -> TimeInterval { + return lhs.timeIntervalSinceReferenceDate - rhs.timeIntervalSinceReferenceDate + } +} diff --git a/ios/RocketChat Watch App/Formatters/MessageFormatter.swift b/ios/RocketChat Watch App/Formatters/MessageFormatter.swift new file mode 100644 index 000000000..63ce03d22 --- /dev/null +++ b/ios/RocketChat Watch App/Formatters/MessageFormatter.swift @@ -0,0 +1,74 @@ +import Foundation + +final class MessageFormatter { + private let message: Message + private let previousMessage: Message? + + init(message: Message, previousMessage: Message?) { + self.message = message + self.previousMessage = previousMessage + } + + func hasDateSeparator() -> Bool { + if let previousMessage, + let previousMessageTS = previousMessage.ts, + let messageTS = message.ts, + Calendar.current.isDate(previousMessageTS, inSameDayAs: messageTS) { + return false + } + return true + } + + func isHeader() -> Bool { + if let previousMessage, + let previousMessageTS = previousMessage.ts, + let messageTS = message.ts, + Calendar.current.isDate(previousMessageTS, inSameDayAs: messageTS), + previousMessage.user?.username == message.user?.username, + !(previousMessage.groupable == false || message.groupable == false || message.room?.broadcast == true), + messageTS - previousMessageTS < 300, + message.t != "rm", + previousMessage.t != "rm" { + return false + } + + return true + } + + func info() -> String? { + switch message.t { + case "rm": + return "Message Removed" + case "e2e": + return "Encrypted message" + default: + return nil + } + } + + func date() -> String? { + guard let ts = message.ts else { return nil } + + let dateFormatter = DateFormatter() + + dateFormatter.locale = Locale.current + dateFormatter.timeZone = TimeZone.current + dateFormatter.timeStyle = .none + dateFormatter.dateStyle = .long + + return dateFormatter.string(from: ts) + } + + func time() -> String? { + guard let ts = message.ts else { return nil } + + let dateFormatter = DateFormatter() + + dateFormatter.locale = Locale.current + dateFormatter.timeZone = TimeZone.current + dateFormatter.timeStyle = .short + dateFormatter.dateStyle = .none + + return dateFormatter.string(from: ts) + } +} diff --git a/ios/RocketChat Watch App/Formatters/RoomFormatter.swift b/ios/RocketChat Watch App/Formatters/RoomFormatter.swift new file mode 100644 index 000000000..fc50a117d --- /dev/null +++ b/ios/RocketChat Watch App/Formatters/RoomFormatter.swift @@ -0,0 +1,46 @@ +import Foundation + +final class RoomFormatter { + private let room: Room + private let server: Server + + init(room: Room, server: Server) { + self.room = room + self.server = server + } + + var title: String? { + if isGroupChat, (room.name == nil || room.name?.isEmpty == true), let usernames = room.usernames { + return usernames + .filter { $0 == server.loggedUser.username } + .sorted() + .joined(separator: ", ") + } + + if room.t != "d" { + if let fname = room.fname { + return fname + } else if let name = room.name { + return name + } + } + + if room.prid != nil || server.useRealName { + return room.fname + } + + return room.name + } + + var isGroupChat: Bool { + if let uids = room.uids, uids.count > 2 { + return true + } + + if let usernames = room.usernames, usernames.count > 2 { + return true + } + + return false + } +} diff --git a/ios/RocketChat Watch App/Localizable.xcstrings b/ios/RocketChat Watch App/Localizable.xcstrings index 41e4458d7..42aea340c 100644 --- a/ios/RocketChat Watch App/Localizable.xcstrings +++ b/ios/RocketChat Watch App/Localizable.xcstrings @@ -1,12 +1,18 @@ { "sourceLanguage" : "en", "strings" : { + "Message" : { + + }, "Rooms" : { }, "Servers" : { "comment" : "View title for ServerList.", "extractionState" : "manual" + }, + "This room is read only" : { + }, "Try Again" : { diff --git a/ios/RocketChat Watch App/ViewModels/MessageComposerViewModel.swift b/ios/RocketChat Watch App/ViewModels/MessageComposerViewModel.swift new file mode 100644 index 000000000..55ba84202 --- /dev/null +++ b/ios/RocketChat Watch App/ViewModels/MessageComposerViewModel.swift @@ -0,0 +1,42 @@ +import Combine +import Foundation + +final class MessageComposerViewModel: ObservableObject { + var isReadOnly: Bool { + room.isReadOnly + } + + private let room: Room + private let client: RocketChatClientProtocol + private let database: RocketChatDatabase + private let server: Server + + init(client: RocketChatClientProtocol, database: RocketChatDatabase, room: Room, server: Server) { + self.client = client + self.database = database + self.room = room + self.server = server + } + + func sendMessage(_ msg: String) { + guard let rid = room.id else { return } + + let messageID = database.createTempMessage(msg: msg, in: room, for: server.loggedUser) + + client.sendMessage(id: messageID, rid: rid, msg: msg) + .receive(on: DispatchQueue.main) + .subscribe(Subscribers.Sink { completion in + if case .failure(let error) = completion { + print(error) + } + } receiveValue: { [weak self] messageResponse in + guard let self else { + return + } + + let message = messageResponse.message + + database.process(updatedMessage: message, in: room) + }) + } +} diff --git a/ios/RocketChat Watch App/ViewModels/MessageListViewModel.swift b/ios/RocketChat Watch App/ViewModels/MessageListViewModel.swift new file mode 100644 index 000000000..b68bc9900 --- /dev/null +++ b/ios/RocketChat Watch App/ViewModels/MessageListViewModel.swift @@ -0,0 +1,126 @@ +import Combine +import Foundation + +protocol MessageListViewModeling { + func composerViewModel() -> MessageComposerViewModel + func messageViewModel(for message: Message, and previousMessage: Message?) -> MessageViewModel + + func loadMessages(completionHandler: (() -> Void)?) + func markAsRead() + func stop() +} + +final class MessageListViewModel: ObservableObject { + @Published private var server: Server + + @Published private(set) var room: Room + @Published private(set) var lastMessageID: String? + + private let client: RocketChatClientProtocol + private let database: RocketChatDatabase + private let formatter: RoomFormatter + + private var timer: Timer? + + private var syncCancellable: AnyCancellable? + + var title: String { + formatter.title ?? "" + } + + init( + client: RocketChatClientProtocol, + database: RocketChatDatabase, + room: Room, + server: Server + ) { + self.client = client + self.database = database + self.room = room + self.server = server + self.formatter = RoomFormatter(room: room, server: server) + } + + deinit { + print("MessageListViewModel.deinit \(room.fname ?? "")") + } + + private func scheduledSync(in room: Room) -> Timer { + Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in + self?.syncMessages(in: room) + } + } + + private func syncMessages(in room: Room) { + guard let rid = room.id else { return } + + syncCancellable = client.syncMessages(rid: rid, updatedSince: room.updatedSince ?? Date()) + .receive(on: DispatchQueue.main) + .sink { completion in + if case .failure(let error) = completion { + print(error) + } + } receiveValue: { messagesResponse in + let messages = messagesResponse.result.updated + + for message in messages { + self.database.process(updatedMessage: message, in: room) + } + } + } + + private func loadMessages(in room: Room, latest: Date?) { + guard let rid = room.id else { return } + + room.updatedSince = latest + + client.getHistory(rid: rid, t: room.t ?? "", latest: latest ?? Date()) + .receive(on: DispatchQueue.main) + .subscribe(Subscribers.Sink { completion in + if case .failure(let error) = completion { + print(error) + } + } receiveValue: { messagesResponse in + let messages = messagesResponse.messages + + for message in messages { + self.database.process(updatedMessage: message, in: room) + } + }) + } +} + +extension MessageListViewModel: MessageListViewModeling { + func composerViewModel() -> MessageComposerViewModel { + MessageComposerViewModel(client: client, database: database, room: room, server: server) + } + + func messageViewModel(for message: Message, and previousMessage: Message?) -> MessageViewModel { + MessageViewModel(message: message, previousMessage: previousMessage, server: server) + } + + func loadMessages(completionHandler: (() -> Void)? = nil) { + loadMessages(in: room, latest: room.lastMessage?.ts) + } + + func markAsRead() { + guard (room.unread > 0 || room.alert), let rid = room.id else { + return + } + + client.sendRead(rid: rid) + .receive(on: DispatchQueue.main) + .subscribe(Subscribers.Sink { completion in + if case .failure(let error) = completion { + print(error) + } + } receiveValue: { _ in + self.database.markRead(in: rid) + }) + } + + func stop() { + syncCancellable?.cancel() + timer?.invalidate() + } +} diff --git a/ios/RocketChat Watch App/ViewModels/MessageViewModel.swift b/ios/RocketChat Watch App/ViewModels/MessageViewModel.swift new file mode 100644 index 000000000..938b66225 --- /dev/null +++ b/ios/RocketChat Watch App/ViewModels/MessageViewModel.swift @@ -0,0 +1,21 @@ +import Foundation + +final class MessageViewModel: ObservableObject { + @Published private(set) var server: Server? + @Published private(set) var message: Message + @Published private(set) var previousMessage: Message? + + let messageFormatter: MessageFormatter + + init(message: Message, previousMessage: Message? = nil, server: Server?) { + self.message = message + self.previousMessage = previousMessage + self.messageFormatter = MessageFormatter(message: message, previousMessage: previousMessage) + self.server = server + } + + var sender: String? { + server?.useRealName == true ? message.user?.name : message.user?.username + } + +} diff --git a/ios/RocketChat Watch App/ViewModels/RoomListViewModel.swift b/ios/RocketChat Watch App/ViewModels/RoomListViewModel.swift index 9a93dcbd9..e724a5645 100644 --- a/ios/RocketChat Watch App/ViewModels/RoomListViewModel.swift +++ b/ios/RocketChat Watch App/ViewModels/RoomListViewModel.swift @@ -1,8 +1,10 @@ import Combine +import CoreData import Foundation protocol RoomListViewModeling { - func viewModel(for room: Room) -> RoomViewModel + func roomViewModel(for room: Room) -> RoomViewModel + func messageListViewModel(for room: Room) -> MessageListViewModel func loadRooms() func logout() @@ -16,6 +18,10 @@ final class RoomListViewModel: ObservableObject { let server: Server } + var viewContext: NSManagedObjectContext { + dependencies.database.viewContext + } + private let dependencies: Dependencies private var loadCancellable: AnyCancellable? @@ -45,8 +51,20 @@ final class RoomListViewModel: ObservableObject { // MARK: - RoomListViewModeling extension RoomListViewModel: RoomListViewModeling { - func viewModel(for room: Room) -> RoomViewModel { - RoomViewModel(room: room, server: dependencies.server) + 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() { diff --git a/ios/RocketChat Watch App/ViewModels/RoomViewModel.swift b/ios/RocketChat Watch App/ViewModels/RoomViewModel.swift index 4188ccc80..0aabc078e 100644 --- a/ios/RocketChat Watch App/ViewModels/RoomViewModel.swift +++ b/ios/RocketChat Watch App/ViewModels/RoomViewModel.swift @@ -4,32 +4,12 @@ final class RoomViewModel: ObservableObject { @Published var room: Room @Published var server: Server + let formatter: RoomFormatter + init(room: Room, server: Server) { self.room = room self.server = server - } - - var title: String? { - if isGroupChat, (room.name == nil || room.name?.isEmpty == true), let usernames = room.usernames { - return usernames - .filter { $0 == server.loggedUser.username } - .sorted() - .joined(separator: ", ") - } - - if room.t != "d" { - if let fname = room.fname { - return fname - } else if let name = room.name { - return name - } - } - - if room.prid != nil || server.useRealName { - return room.fname - } - - return room.name + self.formatter = RoomFormatter(room: room, server: server) } var iconName: String? { @@ -43,7 +23,7 @@ final class RoomViewModel: ObservableObject { return "channel-private" } else if room.t == "c" { return "channel-public" - } else if room.t == "d", isGroupChat { + } else if room.t == "d", formatter.isGroupChat { return "message" } @@ -82,18 +62,6 @@ final class RoomViewModel: ObservableObject { return "\(username): \(message)" } - var isGroupChat: Bool { - if let uids = room.uids, uids.count > 2 { - return true - } - - if let usernames = room.usernames, usernames.count > 2 { - return true - } - - return false - } - var updatedAt: String? { guard let ts = room.ts else { return nil diff --git a/ios/RocketChat Watch App/Views/MessageComposerView.swift b/ios/RocketChat Watch App/Views/MessageComposerView.swift new file mode 100644 index 000000000..d658e3db8 --- /dev/null +++ b/ios/RocketChat Watch App/Views/MessageComposerView.swift @@ -0,0 +1,38 @@ +import SwiftUI + +struct MessageComposerView: View { + @ObservedObject private var viewModel: MessageComposerViewModel + + init(viewModel: MessageComposerViewModel) { + self.viewModel = viewModel + } + + @State private var message = "" + + var body: some View { + if viewModel.isReadOnly { + HStack { + Spacer() + Text("This room is read only") + .font(.caption) + .fontWeight(.bold) + .foregroundStyle(.white) + .multilineTextAlignment(.center) + Spacer() + } + } else { + TextField("Message", text: $message) + .submitLabel(.send) + .onSubmit(send) + } + } + + func send() { + guard !message.isEmpty else { + return + } + + viewModel.sendMessage(message) + message = "" + } +} diff --git a/ios/RocketChat Watch App/Views/MessageListView.swift b/ios/RocketChat Watch App/Views/MessageListView.swift new file mode 100644 index 000000000..1ff4d44b9 --- /dev/null +++ b/ios/RocketChat Watch App/Views/MessageListView.swift @@ -0,0 +1,48 @@ +import SwiftUI + +struct MessageListView: View { + @StateObject private var viewModel: MessageListViewModel + + @FetchRequest private var messages: FetchedResults + + init(viewModel: MessageListViewModel) { + _viewModel = StateObject(wrappedValue: viewModel) + _messages = FetchRequest(fetchRequest: viewModel.room.messagesRequest, animation: .none) + } + + var body: some View { + ScrollViewReader { reader in + ScrollView { + VStack(alignment: .leading, spacing: 8) { + ForEach(messages.indices, id: \.self) { index in + let message = messages[index] + let previousMessage = messages.indices.contains(index - 1) ? messages[index - 1] : nil + + MessageView(viewModel: viewModel.messageViewModel(for: message, and: previousMessage)) + .id(message.id) + .transition(.move(edge: .bottom)) + } + + MessageComposerView(viewModel: viewModel.composerViewModel()) + .padding(.top) + } + } + .padding([.leading, .trailing]) + .navigationTitle(viewModel.title) + .navigationBarTitleDisplayMode(.inline) + .onAppear { + viewModel.loadMessages { + reader.scrollTo(messages.last?.id, anchor: .bottom) + } + + viewModel.markAsRead() + } + .onDisappear { + viewModel.stop() + } + .onReceive(messages.publisher) { _ in + viewModel.markAsRead() + } + } + } +} diff --git a/ios/RocketChat Watch App/Views/MessageView.swift b/ios/RocketChat Watch App/Views/MessageView.swift new file mode 100644 index 000000000..5cecb531a --- /dev/null +++ b/ios/RocketChat Watch App/Views/MessageView.swift @@ -0,0 +1,59 @@ +import SwiftUI + +struct MessageView: View { + @ObservedObject private var viewModel: MessageViewModel + + init(viewModel: MessageViewModel) { + self.viewModel = viewModel + } + + var body: some View { + VStack(alignment: .leading) { + if viewModel.messageFormatter.hasDateSeparator() { + HStack(alignment: .center) { + VStack(alignment: .center) { + Divider() + .overlay(.gray) + } + Text(viewModel.messageFormatter.date() ?? "") + .lineLimit(1) + .font(.footnote) + .foregroundStyle(.gray) + .layoutPriority(1) + VStack(alignment: .center) { + Divider() + .overlay(.gray) + } + } + } + if viewModel.messageFormatter.isHeader() { + HStack(alignment: .center) { + Text(viewModel.sender ?? "") + .lineLimit(1) + .font(.caption) + .fontWeight(.bold) + .foregroundStyle(.primary) + Text(viewModel.messageFormatter.time() ?? "") + .lineLimit(1) + .font(.footnote) + .foregroundStyle(.secondary) + } + } + if let text = viewModel.messageFormatter.info() { + Text(text) + .font(.caption) + .foregroundStyle(.white) + .italic() + } else if let text = viewModel.message.msg { + Text(text) + .font(.caption) + .foregroundStyle(viewModel.message.status == "temp" ? .secondary : .primary) + } +// if let attachments = message.attachments?.allObjects as? Array { +// ForEach(attachments) { attachment in +// AttachmentView(attachment: attachment) +// } +// } + } + } +} diff --git a/ios/RocketChat Watch App/Views/RoomListView.swift b/ios/RocketChat Watch App/Views/RoomListView.swift index 8f45d6000..7911e20d4 100644 --- a/ios/RocketChat Watch App/Views/RoomListView.swift +++ b/ios/RocketChat Watch App/Views/RoomListView.swift @@ -1,26 +1,21 @@ import SwiftUI struct RoomListView: View { - @StateObject var viewModel: RoomListViewModel + @StateObject private var viewModel: RoomListViewModel - @FetchRequest( - entity: Room.entity(), - sortDescriptors: [ - NSSortDescriptor(keyPath: \Room.ts, ascending: false) - ], - predicate: NSPredicate(format: "archived == false"), - animation: .default - ) - private var rooms: FetchedResults + @FetchRequest private var rooms: FetchedResults init(dependencies: RoomListViewModel.Dependencies) { _viewModel = StateObject(wrappedValue: RoomListViewModel(dependencies: dependencies)) + _rooms = FetchRequest(fetchRequest: dependencies.server.roomsRequest) } var body: some View { List { ForEach(rooms) { room in - RoomView(viewModel: viewModel.viewModel(for: room)) + NavigationLink(value: room) { + RoomView(viewModel: viewModel.roomViewModel(for: room)) + } } } .onAppear { @@ -28,6 +23,10 @@ struct RoomListView: View { } .navigationTitle("Rooms") .navigationBarTitleDisplayMode(.inline) + .navigationDestination(for: Room.self) { room in + MessageListView(viewModel: viewModel.messageListViewModel(for: room)) + .environment(\.managedObjectContext, viewModel.viewContext) + } .toolbar { ToolbarItem(placement: .automatic) { Button("Servers") { diff --git a/ios/RocketChat Watch App/Views/RoomView.swift b/ios/RocketChat Watch App/Views/RoomView.swift index 86c9cf37e..291bf650d 100644 --- a/ios/RocketChat Watch App/Views/RoomView.swift +++ b/ios/RocketChat Watch App/Views/RoomView.swift @@ -16,7 +16,7 @@ struct RoomView: View { .frame(width: 16, height: 16) .scaledToFit() } - Text(viewModel.title ?? "") + Text(viewModel.formatter.title ?? "") .lineLimit(1) .font(.caption) .fontWeight(isUnread ? .bold : .medium) diff --git a/ios/RocketChat Watch App/Views/ServerListView.swift b/ios/RocketChat Watch App/Views/ServerListView.swift index 428137cfb..bf0886529 100644 --- a/ios/RocketChat Watch App/Views/ServerListView.swift +++ b/ios/RocketChat Watch App/Views/ServerListView.swift @@ -46,7 +46,6 @@ struct ServerListView: View { } } .navigationTitle("Servers") - .padding() .onAppear { viewModel.loadServers() } diff --git a/ios/RocketChatRN.xcodeproj/project.pbxproj b/ios/RocketChatRN.xcodeproj/project.pbxproj index b1a43ac5f..fda5b0295 100644 --- a/ios/RocketChatRN.xcodeproj/project.pbxproj +++ b/ios/RocketChatRN.xcodeproj/project.pbxproj @@ -67,6 +67,15 @@ 1E29A30E2B58608C0093C03C /* LoggedUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A30D2B58608C0093C03C /* LoggedUser.swift */; }; 1E29A3102B5865B80093C03C /* RoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A30F2B5865B80093C03C /* RoomViewModel.swift */; }; 1E29A3122B5866090093C03C /* Room.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3112B5866090093C03C /* Room.swift */; }; + 1E29A3142B5868D80093C03C /* MessageListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3132B5868D80093C03C /* MessageListViewModel.swift */; }; + 1E29A3162B5868DF0093C03C /* MessageListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3152B5868DF0093C03C /* MessageListView.swift */; }; + 1E29A3182B5868E50093C03C /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3172B5868E50093C03C /* MessageView.swift */; }; + 1E29A31A2B5868EE0093C03C /* MessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3192B5868EE0093C03C /* MessageViewModel.swift */; }; + 1E29A31D2B5871B60093C03C /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A31C2B5871B60093C03C /* Date+Extensions.swift */; }; + 1E29A3202B5871C80093C03C /* RoomFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A31F2B5871C80093C03C /* RoomFormatter.swift */; }; + 1E29A3222B5871CE0093C03C /* MessageFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3212B5871CE0093C03C /* MessageFormatter.swift */; }; + 1E29A3242B5874FF0093C03C /* MessageComposerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3232B5874FF0093C03C /* MessageComposerView.swift */; }; + 1E29A3262B58752D0093C03C /* MessageComposerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E29A3252B58752D0093C03C /* MessageComposerViewModel.swift */; }; 1E2F615B25128F9A00871711 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2F615A25128F9A00871711 /* API.swift */; }; 1E2F615D25128FA300871711 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2F615C25128FA300871711 /* Response.swift */; }; 1E2F61642512955D00871711 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2F61632512955D00871711 /* HTTPMethod.swift */; }; @@ -361,6 +370,15 @@ 1E29A30D2B58608C0093C03C /* LoggedUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedUser.swift; sourceTree = ""; }; 1E29A30F2B5865B80093C03C /* RoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomViewModel.swift; sourceTree = ""; }; 1E29A3112B5866090093C03C /* Room.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Room.swift; sourceTree = ""; }; + 1E29A3132B5868D80093C03C /* MessageListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageListViewModel.swift; sourceTree = ""; }; + 1E29A3152B5868DF0093C03C /* MessageListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageListView.swift; sourceTree = ""; }; + 1E29A3172B5868E50093C03C /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; + 1E29A3192B5868EE0093C03C /* MessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageViewModel.swift; sourceTree = ""; }; + 1E29A31C2B5871B60093C03C /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = ""; }; + 1E29A31F2B5871C80093C03C /* RoomFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFormatter.swift; sourceTree = ""; }; + 1E29A3212B5871CE0093C03C /* MessageFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageFormatter.swift; sourceTree = ""; }; + 1E29A3232B5874FF0093C03C /* MessageComposerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposerView.swift; sourceTree = ""; }; + 1E29A3252B58752D0093C03C /* MessageComposerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposerViewModel.swift; sourceTree = ""; }; 1E2F615A25128F9A00871711 /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; 1E2F615C25128FA300871711 /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = ""; }; 1E2F61632512955D00871711 /* HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; @@ -628,6 +646,23 @@ path = Extensions; sourceTree = ""; }; + 1E29A31B2B5871AC0093C03C /* Extensions */ = { + isa = PBXGroup; + children = ( + 1E29A31C2B5871B60093C03C /* Date+Extensions.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 1E29A31E2B5871BE0093C03C /* Formatters */ = { + isa = PBXGroup; + children = ( + 1E29A31F2B5871C80093C03C /* RoomFormatter.swift */, + 1E29A3212B5871CE0093C03C /* MessageFormatter.swift */, + ); + path = Formatters; + sourceTree = ""; + }; 1E2F61622512954500871711 /* Requests */ = { isa = PBXGroup; children = ( @@ -731,6 +766,9 @@ 1ED033B92B55B5F6004F4930 /* ServerView.swift */, 1E29A2CB2B5857F50093C03C /* RoomListView.swift */, 1E29A2CF2B58582F0093C03C /* RoomView.swift */, + 1E29A3152B5868DF0093C03C /* MessageListView.swift */, + 1E29A3172B5868E50093C03C /* MessageView.swift */, + 1E29A3232B5874FF0093C03C /* MessageComposerView.swift */, ); path = Views; sourceTree = ""; @@ -741,6 +779,9 @@ 1ED033B72B55B4BE004F4930 /* ServerListViewModel.swift */, 1E29A2CD2B5857FC0093C03C /* RoomListViewModel.swift */, 1E29A30F2B5865B80093C03C /* RoomViewModel.swift */, + 1E29A3132B5868D80093C03C /* MessageListViewModel.swift */, + 1E29A3192B5868EE0093C03C /* MessageViewModel.swift */, + 1E29A3252B58752D0093C03C /* MessageComposerViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -748,6 +789,8 @@ 1ED0388F2B507B4C00C007D4 /* RocketChat Watch App */ = { isa = PBXGroup; children = ( + 1E29A31E2B5871BE0093C03C /* Formatters */, + 1E29A31B2B5871AC0093C03C /* Extensions */, 1E29A2D12B585B070093C03C /* Client */, 1ED033B42B55B495004F4930 /* ViewModels */, 1ED033B12B55B47F004F4930 /* Views */, @@ -1488,7 +1531,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "SOURCE_MAP=\"$TMPDIR/$(md5 -qs \"$CONFIGURATION_BUILD_DIR\")-main.jsbundle.map\" ../node_modules/@bugsnag/react-native/bugsnag-react-native-xcode.sh\n"; + shellScript = "# SOURCE_MAP=\"$TMPDIR/$(md5 -qs \"$CONFIGURATION_BUILD_DIR\")-main.jsbundle.map\" ../node_modules/@bugsnag/react-native/bugsnag-react-native-xcode.sh\n"; }; 7F13D807CA5B7E43CE899DB3 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; @@ -1762,19 +1805,25 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1E29A3242B5874FF0093C03C /* MessageComposerView.swift in Sources */, 1EB375892B55DBFB00AEC3D7 /* Server.swift in Sources */, + 1E29A3162B5868DF0093C03C /* MessageListView.swift in Sources */, 1E29A2F42B585B070093C03C /* SubscriptionsResponse.swift in Sources */, + 1E29A3142B5868D80093C03C /* MessageListViewModel.swift in Sources */, 1E29A2F92B585B070093C03C /* SubscriptionsRequest.swift in Sources */, 1E29A2F22B585B070093C03C /* HistoryResponse.swift in Sources */, 1ED033BA2B55B5F6004F4930 /* ServerView.swift in Sources */, 1E29A2D02B58582F0093C03C /* RoomView.swift in Sources */, 1E29A2FD2B585B070093C03C /* RoomsRequest.swift in Sources */, 1ED038CA2B50A58400C007D4 /* WatchConnection.swift in Sources */, + 1E29A3222B5871CE0093C03C /* MessageFormatter.swift in Sources */, 1E29A3002B585B070093C03C /* JSONAdapter.swift in Sources */, 1E29A3022B585B070093C03C /* DateCodingStrategy.swift in Sources */, 1ED033B62B55B4A5004F4930 /* ServerListView.swift in Sources */, + 1E29A3202B5871C80093C03C /* RoomFormatter.swift in Sources */, 1E29A3102B5865B80093C03C /* RoomViewModel.swift in Sources */, 1E29A2FC2B585B070093C03C /* SendMessageRequest.swift in Sources */, + 1E29A3262B58752D0093C03C /* MessageComposerViewModel.swift in Sources */, 1E29A30C2B585D1D0093C03C /* String+Extensions.swift in Sources */, 1ED033CD2B55D671004F4930 /* RocketChatDatabase.swift in Sources */, 1E29A3122B5866090093C03C /* Room.swift in Sources */, @@ -1784,10 +1833,12 @@ 1E29A3072B585B070093C03C /* RocketChatError.swift in Sources */, 1E29A2F12B585B070093C03C /* SendMessageResponse.swift in Sources */, 1E29A30E2B58608C0093C03C /* LoggedUser.swift in Sources */, + 1E29A3182B5868E50093C03C /* MessageView.swift in Sources */, 1E29A2FF2B585B070093C03C /* TokenAdapter.swift in Sources */, 1E29A3052B585B070093C03C /* Request.swift in Sources */, 1E29A2EF2B585B070093C03C /* RocketChatClient.swift in Sources */, 1E29A2FB2B585B070093C03C /* MessagesRequest.swift in Sources */, + 1E29A31D2B5871B60093C03C /* Date+Extensions.swift in Sources */, 1E29A2F62B585B070093C03C /* UserResponse.swift in Sources */, 1ED033AE2B55B1CC004F4930 /* Default.xcdatamodeld in Sources */, 1ED033BF2B55BF94004F4930 /* Storage.swift in Sources */, @@ -1802,6 +1853,7 @@ 1E29A2F02B585B070093C03C /* AttachmentResponse.swift in Sources */, 1ED038912B507B4C00C007D4 /* RocketChatApp.swift in Sources */, 1E29A2CC2B5857F50093C03C /* RoomListView.swift in Sources */, + 1E29A31A2B5868EE0093C03C /* MessageViewModel.swift in Sources */, 1ED033B82B55B4BE004F4930 /* ServerListViewModel.swift in Sources */, 1ED033C42B55C65C004F4930 /* RocketChatAppRouter.swift in Sources */, 1ED033B02B55B25A004F4930 /* Database.swift in Sources */,