Add MessageListView & MessageView

This commit is contained in:
Djorkaeff Alexandre 2024-01-17 18:07:53 -03:00
parent 077686d0fd
commit 7ced062950
18 changed files with 576 additions and 53 deletions

View File

@ -1,6 +1,15 @@
import CoreData
extension Room {
var messagesRequest: NSFetchRequest<Message> {
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()

View File

@ -52,3 +52,14 @@ public final class Server: NSManagedObject {
extension Server: Identifiable {
}
extension Server {
var roomsRequest: NSFetchRequest<Room> {
let request = Room.fetchRequest()
request.predicate = NSPredicate(format: "archived == false")
request.sortDescriptors = [NSSortDescriptor(keyPath: \Room.ts, ascending: false)]
return request
}
}

View File

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

View File

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

View File

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

View File

@ -1,12 +1,18 @@
{
"sourceLanguage" : "en",
"strings" : {
"Message" : {
},
"Rooms" : {
},
"Servers" : {
"comment" : "View title for ServerList.",
"extractionState" : "manual"
},
"This room is read only" : {
},
"Try Again" : {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,48 @@
import SwiftUI
struct MessageListView: View {
@StateObject private var viewModel: MessageListViewModel
@FetchRequest<Message> private var messages: FetchedResults<Message>
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()
}
}
}
}

View File

@ -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<Attachment> {
// ForEach(attachments) { attachment in
// AttachmentView(attachment: attachment)
// }
// }
}
}
}

View File

@ -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<Room>
@FetchRequest<Room> private var rooms: FetchedResults<Room>
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") {

View File

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

View File

@ -46,7 +46,6 @@ struct ServerListView: View {
}
}
.navigationTitle("Servers")
.padding()
.onAppear {
viewModel.loadServers()
}

View File

@ -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 = "<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>"; };
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>"; };
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>"; };
1E29A31C2B5871B60093C03C /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.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>"; };
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>"; };
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>"; };
@ -628,6 +646,23 @@
path = Extensions;
sourceTree = "<group>";
};
1E29A31B2B5871AC0093C03C /* Extensions */ = {
isa = PBXGroup;
children = (
1E29A31C2B5871B60093C03C /* Date+Extensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
1E29A31E2B5871BE0093C03C /* Formatters */ = {
isa = PBXGroup;
children = (
1E29A31F2B5871C80093C03C /* RoomFormatter.swift */,
1E29A3212B5871CE0093C03C /* MessageFormatter.swift */,
);
path = Formatters;
sourceTree = "<group>";
};
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 = "<group>";
@ -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 = "<group>";
@ -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 */,