Refactor ServerListView

This commit is contained in:
Djorkaeff Alexandre 2024-01-19 21:00:06 -03:00
parent 91968be958
commit 16b2a123be
15 changed files with 287 additions and 208 deletions

View File

@ -5,25 +5,17 @@ protocol AppRouting {
}
final class AppRouter: ObservableObject {
@Storage(.currentServer) private var currentURL: URL?
@Published private(set) var route: Route = .serverList
@Published private(set) var route: Route = .loading
}
extension AppRouter: AppRouting {
func route(to route: Route) {
switch route {
case .roomList(let server):
currentURL = server.url
case .serverList:
break
}
self.route = route
}
}
enum Route {
enum Route: Equatable {
case loading
case serverList
case roomList(Server)
}

View File

@ -3,8 +3,7 @@ import SwiftUI
struct AppView: View {
@Dependency private var database: Database
@Dependency private var serversDB: ServersDatabase
@Storage(.currentServer) private var currentURL: URL?
@Dependency private var stateProvider: StateProviding
@StateObject private var router: AppRouter
@ -15,12 +14,22 @@ struct AppView: View {
var body: some View {
NavigationStack {
switch router.route {
case .roomList(let server):
RoomListView(server: server)
.environment(\.managedObjectContext, database.viewContext)
case .serverList:
ServerListView(viewModel: ServerListViewModel())
.environment(\.managedObjectContext, serversDB.viewContext)
case .loading:
ProgressView()
case .roomList(let server):
RoomListView(server: server)
.environment(\.managedObjectContext, database.viewContext)
case .serverList:
ServerListView()
.environment(\.managedObjectContext, serversDB.viewContext)
}
}
.onChange(of: router.route) { newValue in
switch newValue {
case .roomList(let server):
stateProvider.update(to: .loggedIn(server))
case .serverList, .loading:
stateProvider.update(to: .loggedOut)
}
}
.onAppear {
@ -29,11 +38,10 @@ struct AppView: View {
}
private func loadRoute() {
if let currentURL, let server = serversDB.server(url: currentURL) {
switch stateProvider.state {
case .loggedIn(let server):
router.route(to: .roomList(server))
} else if serversDB.servers().count == 1, let server = serversDB.servers().first {
router.route(to: .roomList(server))
} else {
case .loggedOut:
router.route(to: .serverList)
}
}

View File

@ -15,7 +15,7 @@ final class RocketChatClient: NSObject {
@Dependency private var serverProvider: ServerProviding
private var server: Server {
serverProvider.current()
serverProvider.server
}
private lazy var session = URLSession(

View File

@ -29,7 +29,7 @@ final class RocketChatDatabase: Database {
}()
private lazy var container: NSPersistentContainer = {
let name = serverProvider.current().url.host ?? "default"
let name = serverProvider.server.url.host ?? "default"
let container = NSPersistentContainer(name: name, managedObjectModel: Self.model)

View File

@ -15,7 +15,7 @@ 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: serverProvider.current().loggedUser)
let messageID = database.createTempMessage(msg: msg, in: room, for: serverProvider.server.loggedUser)
client.sendMessage(id: messageID, rid: rid, msg: msg)
.receive(on: DispatchQueue.main)

View File

@ -0,0 +1,97 @@
import Combine
import Foundation
import WatchConnectivity
enum ServersLoadingError: Error, Equatable {
case unactive
case unreachable
case locked
case undecodable(Error)
static func == (lhs: ServersLoadingError, rhs: ServersLoadingError) -> Bool {
switch (lhs, rhs) {
case (.unactive, .unactive), (.unreachable, .unreachable), (.locked, .locked), (.undecodable, .undecodable):
return true
default:
return false
}
}
}
protocol ServersLoading {
func loadServers() -> AnyPublisher<Void, ServersLoadingError>
}
final class ServersLoader: NSObject {
@Dependency private var database: ServersDatabase
private let session: WCSession
init(session: WCSession) {
self.session = session
super.init()
session.delegate = self
session.activate()
}
private func sendMessage(completionHandler: @escaping (Result<WatchMessage, ServersLoadingError>) -> Void) {
print("sendMessage")
guard session.activationState == .activated else {
completionHandler(.failure(.unactive))
return
}
guard !session.iOSDeviceNeedsUnlockAfterRebootForReachability else {
completionHandler(.failure(.locked))
return
}
guard session.isReachable else {
completionHandler(.failure(.unreachable))
return
}
session.sendMessage([:]) { dictionary in
do {
let data = try JSONSerialization.data(withJSONObject: dictionary)
let message = try JSONDecoder().decode(WatchMessage.self, from: data)
completionHandler(.success(message))
} catch {
completionHandler(.failure(.undecodable(error)))
}
}
}
}
// MARK: - WCSessionDelegate
extension ServersLoader: WCSessionDelegate {
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
}
}
// MARK: - ServersLoading
extension ServersLoader: ServersLoading {
func loadServers() -> AnyPublisher<Void, ServersLoadingError> {
Future<Void, ServersLoadingError> { [self] promise in
sendMessage { result in
switch result {
case .success(let message):
for server in message.servers {
self.database.process(updatedServer: server)
}
promise(.success(()))
case .failure(let error):
promise(.failure(error))
}
}
}
.retryWithDelay(retries: 3, delay: 1, scheduler: DispatchQueue.global())
.eraseToAnyPublisher()
}
}

View File

@ -0,0 +1,20 @@
import Foundation
protocol ServerProviding {
var server: Server { get }
}
final class ServerProvider {
@Dependency private var stateProvider: StateProviding
}
extension ServerProvider: ServerProviding {
var server: Server {
switch stateProvider.state {
case .loggedIn(let server):
return server
case .loggedOut:
fatalError("Attempt to get server while logged out.")
}
}
}

View File

@ -0,0 +1,37 @@
import Foundation
enum AppState {
case loggedIn(Server)
case loggedOut
}
protocol StateProviding {
var state: AppState { get }
func update(to state: AppState)
}
final class StateProvider: StateProviding {
@Storage(.currentServer) private var currentURL: URL?
@Dependency private var database: ServersDatabase
var state: AppState {
if let currentURL, let server = database.server(url: currentURL) {
return .loggedIn(server)
} else if database.servers().count == 1, let server = database.servers().first {
return .loggedIn(server)
} else {
return .loggedOut
}
}
func update(to state: AppState) {
switch state {
case .loggedIn(let server):
currentURL = server.url
case .loggedOut:
break
}
}
}

View File

@ -9,15 +9,16 @@ struct RocketChat_Watch_AppApp: App {
}
private func registerDependencies() {
Store.register(AppRouting.self, factory: router)
Store.register(ServersDatabase.self, factory: DefaultDatabase())
Store.register(StateProviding.self, factory: StateProvider())
Store.register(ServerProviding.self, factory: ServerProvider())
Store.register(Connection.self, factory: WatchConnection(session: .default))
Store.register(ServersLoading.self, factory: ServersLoader(session: .default))
Store.register(RocketChatClientProtocol.self, factory: RocketChatClient())
Store.register(Database.self, factory: RocketChatDatabase())
Store.register(MessagesLoading.self, factory: MessagesLoader())
Store.register(MessageSending.self, factory: MessageSender())
Store.register(RoomsLoading.self, factory: RoomsLoader())
Store.register(AppRouting.self, factory: router)
}
var body: some Scene {

View File

@ -1,20 +0,0 @@
import Foundation
protocol ServerProviding {
func current() -> Server
}
final class ServerProvider {
@Storage(.currentServer) private var currentURL: URL?
@Dependency private var database: ServersDatabase
}
extension ServerProvider: ServerProviding {
func current() -> Server {
if let currentURL, let server = database.server(url: currentURL) {
return server
} else {
fatalError("Attempt to get server before it was not set.")
}
}
}

View File

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

View File

@ -0,0 +1,20 @@
import SwiftUI
struct RetryView: View {
private let label: String
private let action: () -> Void
init(_ label: String, action: @escaping () -> Void) {
self.label = label
self.action = action
}
var body: some View {
VStack {
Text(label)
.multilineTextAlignment(.center)
Button("Try Again", action: action)
}
.padding()
}
}

View File

@ -1,53 +1,80 @@
import Combine
import CoreData
import SwiftUI
struct ServerListView: View {
@StateObject var viewModel: ServerListViewModel
@Dependency private var router: AppRouting
@Dependency private var serversLoader: ServersLoading
@FetchRequest(entity: Server.entity(), sortDescriptors: [], animation: .default)
private var servers: FetchedResults<Server>
@State private var state: ViewState = .loading
init(viewModel: ServerListViewModel) {
_viewModel = StateObject(wrappedValue: viewModel)
@FetchRequest<Server> private var servers: FetchedResults<Server>
init() {
let fetchRequest = Server.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Server.updatedSince, ascending: true)]
_servers = FetchRequest(fetchRequest: fetchRequest)
}
@ViewBuilder
private func errorView(_ text: String) -> some View {
VStack(alignment: .center) {
Text(text)
Button("Try Again") {
viewModel.loadServers()
private var serverList: some View {
List {
ForEach(servers) { server in
ServerView(server: server)
.onTapGesture {
router.route(to: .roomList(server))
}
}
}
}
var body: some View {
VStack {
switch viewModel.state {
case .loading:
ProgressView()
case .loaded where servers.count > 0:
List {
ForEach(servers) { server in
ServerView(server: server)
.onTapGesture {
viewModel.didTap(server: server)
}
}
}
case .loaded:
errorView("There are no servers connected.")
case .error(let connectionError):
switch connectionError {
case .needsUnlock:
errorView("You need to unlock your iPhone.")
case .decoding:
errorView("We can't read servers information.")
}
switch state {
case .loading:
ProgressView()
case .loaded:
serverList
case .loaded where servers.isEmpty:
RetryView("No Connected servers.", action: loadServers)
case .error(let error) where error == .locked:
RetryView("Please unlock your iPhone.", action: loadServers)
case .error(let error) where error == .unactive:
RetryView("Could not connect to your iPhone.", action: loadServers)
case .error(let error) where error == .unreachable:
RetryView("Could not reach your iPhone.", action: loadServers)
case .error(let error) where error == .undecodable(error):
RetryView("Could not read servers from iPhone.", action: loadServers)
default:
RetryView("Unexpected error.", action: loadServers)
}
}
.navigationTitle("Servers")
.onAppear {
viewModel.loadServers()
loadServers()
}
}
private func loadServers() {
state = .loading
serversLoader.loadServers()
.receive(on: DispatchQueue.main)
.subscribe(Subscribers.Sink { completion in
if case .failure(let error) = completion {
state = .error(error)
}
} receiveValue: { _ in
state = .loaded
})
}
}
extension ServerListView {
enum ViewState {
case loading
case loaded
case error(ServersLoadingError)
}
}

View File

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

View File

@ -82,6 +82,8 @@
1E4AFC1B2B5AFC6A00E2AA7D /* Publisher+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4AFC1A2B5AFC6A00E2AA7D /* Publisher+Extensions.swift */; };
1E4AFC1F2B5B0D0500E2AA7D /* ServerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4AFC1E2B5B0D0500E2AA7D /* ServerProvider.swift */; };
1E4AFC212B5B1AA000E2AA7D /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4AFC202B5B1AA000E2AA7D /* AppView.swift */; };
1E4AFC252B5B1DA300E2AA7D /* StateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4AFC242B5B1DA300E2AA7D /* StateProvider.swift */; };
1E4AFC272B5B23C600E2AA7D /* RetryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4AFC262B5B23C600E2AA7D /* RetryView.swift */; };
1E51D962251263CD00DC95DE /* MessageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E51D961251263CD00DC95DE /* MessageType.swift */; };
1E51D965251263D600DC95DE /* NotificationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E51D964251263D600DC95DE /* NotificationType.swift */; };
1E598AE42515057D002BDFBD /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E598AE32515057D002BDFBD /* Date+Extensions.swift */; };
@ -127,11 +129,10 @@
1ED033AE2B55B1CC004F4930 /* Default.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1ED033AC2B55B1CC004F4930 /* Default.xcdatamodeld */; };
1ED033B02B55B25A004F4930 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED033AF2B55B25A004F4930 /* Database.swift */; };
1ED033B62B55B4A5004F4930 /* ServerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED033B52B55B4A5004F4930 /* ServerListView.swift */; };
1ED033B82B55B4BE004F4930 /* ServerListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED033B72B55B4BE004F4930 /* ServerListViewModel.swift */; };
1ED033BA2B55B5F6004F4930 /* ServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED033B92B55B5F6004F4930 /* ServerView.swift */; };
1ED033BF2B55BF94004F4930 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED033BE2B55BF94004F4930 /* Storage.swift */; };
1ED033C12B55C190004F4930 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 1ED033C02B55C190004F4930 /* Localizable.xcstrings */; };
1ED033C42B55C65C004F4930 /* RocketChatAppRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED033C32B55C65C004F4930 /* RocketChatAppRouter.swift */; };
1ED033C42B55C65C004F4930 /* AppRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED033C32B55C65C004F4930 /* AppRouter.swift */; };
1ED033CB2B55D4F0004F4930 /* RocketChat.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1ED033C92B55D4F0004F4930 /* RocketChat.xcdatamodeld */; };
1ED033CD2B55D671004F4930 /* RocketChatDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED033CC2B55D671004F4930 /* RocketChatDatabase.swift */; };
1ED038912B507B4C00C007D4 /* RocketChatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038902B507B4C00C007D4 /* RocketChatApp.swift */; };
@ -159,7 +160,7 @@
1ED038C42B50A1F500C007D4 /* WatchMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038C32B50A1F500C007D4 /* WatchMessage.swift */; };
1ED038C52B50A1F500C007D4 /* 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 /* ServersLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038C92B50A58400C007D4 /* ServersLoader.swift */; };
1ED59D4C22CBA77D00C54289 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1ED59D4B22CBA77D00C54289 /* GoogleService-Info.plist */; };
1EDFD0FA2B589B8F002FEE5F /* MessagesLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EDFD0F92B589B8F002FEE5F /* MessagesLoader.swift */; };
1EDFD1062B58A66E002FEE5F /* CancelBag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EDFD1052B58A66E002FEE5F /* CancelBag.swift */; };
@ -394,6 +395,8 @@
1E4AFC1A2B5AFC6A00E2AA7D /* Publisher+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+Extensions.swift"; sourceTree = "<group>"; };
1E4AFC1E2B5B0D0500E2AA7D /* ServerProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerProvider.swift; sourceTree = "<group>"; };
1E4AFC202B5B1AA000E2AA7D /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = "<group>"; };
1E4AFC242B5B1DA300E2AA7D /* StateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateProvider.swift; sourceTree = "<group>"; };
1E4AFC262B5B23C600E2AA7D /* RetryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryView.swift; sourceTree = "<group>"; };
1E51D961251263CD00DC95DE /* MessageType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageType.swift; sourceTree = "<group>"; };
1E51D964251263D600DC95DE /* NotificationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationType.swift; sourceTree = "<group>"; };
1E598AE32515057D002BDFBD /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = "<group>"; };
@ -418,11 +421,10 @@
1ED033AD2B55B1CC004F4930 /* Default.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Default.xcdatamodel; sourceTree = "<group>"; };
1ED033AF2B55B25A004F4930 /* Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = "<group>"; };
1ED033B52B55B4A5004F4930 /* ServerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListView.swift; sourceTree = "<group>"; };
1ED033B72B55B4BE004F4930 /* ServerListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListViewModel.swift; sourceTree = "<group>"; };
1ED033B92B55B5F6004F4930 /* ServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerView.swift; sourceTree = "<group>"; };
1ED033BE2B55BF94004F4930 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
1ED033C02B55C190004F4930 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
1ED033C32B55C65C004F4930 /* RocketChatAppRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RocketChatAppRouter.swift; sourceTree = "<group>"; };
1ED033C32B55C65C004F4930 /* AppRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouter.swift; sourceTree = "<group>"; };
1ED033CA2B55D4F0004F4930 /* RocketChat.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = RocketChat.xcdatamodel; sourceTree = "<group>"; };
1ED033CC2B55D671004F4930 /* RocketChatDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RocketChatDatabase.swift; sourceTree = "<group>"; };
1ED0388E2B507B4B00C007D4 /* Rocket.Chat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Rocket.Chat.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -437,7 +439,7 @@
1ED038BD2B50A1D400C007D4 /* DBServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBServer.swift; sourceTree = "<group>"; };
1ED038C02B50A1E400C007D4 /* DBUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBUser.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 /* ServersLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersLoader.swift; sourceTree = "<group>"; };
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>"; };
@ -690,6 +692,15 @@
path = Requests;
sourceTree = "<group>";
};
1E4AFC232B5B1D9C00E2AA7D /* Providers */ = {
isa = PBXGroup;
children = (
1E4AFC1E2B5B0D0500E2AA7D /* ServerProvider.swift */,
1E4AFC242B5B1DA300E2AA7D /* StateProvider.swift */,
);
path = Providers;
sourceTree = "<group>";
};
1E76CBC425152A7F0067298C /* Shared */ = {
isa = PBXGroup;
children = (
@ -790,6 +801,7 @@
1E29A3172B5868E50093C03C /* MessageView.swift */,
1E29A3232B5874FF0093C03C /* MessageComposerView.swift */,
1E9A716E2B59CBCA00477BA2 /* AttachmentView.swift */,
1E4AFC262B5B23C600E2AA7D /* RetryView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -797,7 +809,6 @@
1ED033B42B55B495004F4930 /* ViewModels */ = {
isa = PBXGroup;
children = (
1ED033B72B55B4BE004F4930 /* ServerListViewModel.swift */,
1E29A30F2B5865B80093C03C /* RoomViewModel.swift */,
1E29A3192B5868EE0093C03C /* MessageViewModel.swift */,
);
@ -807,6 +818,7 @@
1ED0388F2B507B4C00C007D4 /* RocketChat Watch App */ = {
isa = PBXGroup;
children = (
1E4AFC232B5B1D9C00E2AA7D /* Providers */,
1EDFD0FB2B589FC4002FEE5F /* DependencyInjection */,
1EDFD0F82B589B82002FEE5F /* Loaders */,
1E29A31E2B5871BE0093C03C /* Formatters */,
@ -816,14 +828,12 @@
1ED033B12B55B47F004F4930 /* Views */,
1ED033AB2B55B1C2004F4930 /* Database */,
1ED038902B507B4C00C007D4 /* RocketChatApp.swift */,
1ED033C32B55C65C004F4930 /* RocketChatAppRouter.swift */,
1ED033C32B55C65C004F4930 /* AppRouter.swift */,
1E4AFC202B5B1AA000E2AA7D /* AppView.swift */,
1ED038C92B50A58400C007D4 /* WatchConnection.swift */,
1ED033BE2B55BF94004F4930 /* Storage.swift */,
1ED038942B507B4D00C007D4 /* Assets.xcassets */,
1ED038962B507B4D00C007D4 /* Preview Content */,
1ED033C02B55C190004F4930 /* Localizable.xcstrings */,
1E4AFC1E2B5B0D0500E2AA7D /* ServerProvider.swift */,
);
path = "RocketChat Watch App";
sourceTree = "<group>";
@ -858,6 +868,7 @@
1EDFD0F82B589B82002FEE5F /* Loaders */ = {
isa = PBXGroup;
children = (
1ED038C92B50A58400C007D4 /* ServersLoader.swift */,
1EDFD0F92B589B8F002FEE5F /* MessagesLoader.swift */,
1EDFD1052B58A66E002FEE5F /* CancelBag.swift */,
1EDFD1072B58AA77002FEE5F /* RoomsLoader.swift */,
@ -1858,12 +1869,13 @@
1ED033BA2B55B5F6004F4930 /* ServerView.swift in Sources */,
1E29A2D02B58582F0093C03C /* RoomView.swift in Sources */,
1E29A2FD2B585B070093C03C /* RoomsRequest.swift in Sources */,
1ED038CA2B50A58400C007D4 /* WatchConnection.swift in Sources */,
1ED038CA2B50A58400C007D4 /* ServersLoader.swift in Sources */,
1E29A3222B5871CE0093C03C /* MessageFormatter.swift in Sources */,
1E9A716F2B59CBCA00477BA2 /* AttachmentView.swift in Sources */,
1E29A3002B585B070093C03C /* JSONAdapter.swift in Sources */,
1E29A3022B585B070093C03C /* DateCodingStrategy.swift in Sources */,
1E4AFC1B2B5AFC6A00E2AA7D /* Publisher+Extensions.swift in Sources */,
1E4AFC272B5B23C600E2AA7D /* RetryView.swift in Sources */,
1ED033B62B55B4A5004F4930 /* ServerListView.swift in Sources */,
1E29A3202B5871C80093C03C /* RoomFormatter.swift in Sources */,
1EDFD0FA2B589B8F002FEE5F /* MessagesLoader.swift in Sources */,
@ -1903,8 +1915,8 @@
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 */,
1ED033C42B55C65C004F4930 /* AppRouter.swift in Sources */,
1E4AFC252B5B1DA300E2AA7D /* StateProvider.swift in Sources */,
1ED033B02B55B25A004F4930 /* Database.swift in Sources */,
1E9A71712B59CC1300477BA2 /* Attachment.swift in Sources */,
1E29A30A2B585B370093C03C /* Data+Extensions.swift in Sources */,