mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Added logout option, room list on home screen, made various methods asynchronous,
This commit is contained in:
parent
d90738e1e4
commit
b53efb4869
@ -23,7 +23,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
|||||||
init() {
|
init() {
|
||||||
splashViewController = SplashViewController()
|
splashViewController = SplashViewController()
|
||||||
mainNavigationController = UINavigationController(rootViewController: splashViewController)
|
mainNavigationController = UINavigationController(rootViewController: splashViewController)
|
||||||
mainNavigationController.setNavigationBarHidden(true, animated: false)
|
mainNavigationController.navigationBar.isHidden = true
|
||||||
window = UIWindow(frame: UIScreen.main.bounds)
|
window = UIWindow(frame: UIScreen.main.bounds)
|
||||||
window.rootViewController = mainNavigationController
|
window.rootViewController = mainNavigationController
|
||||||
|
|
||||||
@ -46,6 +46,10 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
|||||||
|
|
||||||
// MARK: - AuthenticationCoordinatorDelegate
|
// MARK: - AuthenticationCoordinatorDelegate
|
||||||
|
|
||||||
|
func authenticationCoordinatorDidStartLoading(_ authenticationCoordinator: AuthenticationCoordinator) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didFailWithError error: AuthenticationCoordinatorError) {
|
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didFailWithError error: AuthenticationCoordinatorError) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -55,7 +59,8 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func authenticationCoordinatorDidTearDownUserSession(_ authenticationCoordinator: AuthenticationCoordinator) {
|
func authenticationCoordinatorDidTearDownUserSession(_ authenticationCoordinator: AuthenticationCoordinator) {
|
||||||
|
mainNavigationController.setViewControllers([splashViewController], animated: false)
|
||||||
|
authenticationCoordinator.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
@ -68,6 +73,13 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
|||||||
let parameters = HomeScreenCoordinatorParameters(userSession: userSession)
|
let parameters = HomeScreenCoordinatorParameters(userSession: userSession)
|
||||||
let coordinator = HomeScreenCoordinator(parameters: parameters)
|
let coordinator = HomeScreenCoordinator(parameters: parameters)
|
||||||
|
|
||||||
|
coordinator.completion = { [weak self] result in
|
||||||
|
switch result {
|
||||||
|
case .logout:
|
||||||
|
self?.authenticationCoordinator.logout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
add(childCoordinator: coordinator)
|
add(childCoordinator: coordinator)
|
||||||
navigationRouter.setRootModule(coordinator)
|
navigationRouter.setRootModule(coordinator)
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,9 @@ enum AuthenticationCoordinatorError: Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protocol AuthenticationCoordinatorDelegate: AnyObject {
|
protocol AuthenticationCoordinatorDelegate: AnyObject {
|
||||||
|
|
||||||
|
func authenticationCoordinatorDidStartLoading(_ authenticationCoordinator: AuthenticationCoordinator)
|
||||||
|
|
||||||
func authenticationCoordinatorDidSetupUserSession(_ authenticationCoordinator: AuthenticationCoordinator)
|
func authenticationCoordinatorDidSetupUserSession(_ authenticationCoordinator: AuthenticationCoordinator)
|
||||||
|
|
||||||
func authenticationCoordinatorDidTearDownUserSession(_ authenticationCoordinator: AuthenticationCoordinator)
|
func authenticationCoordinatorDidTearDownUserSession(_ authenticationCoordinator: AuthenticationCoordinator)
|
||||||
@ -40,19 +43,46 @@ class AuthenticationCoordinator: Coordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
|
|
||||||
|
delegate?.authenticationCoordinatorDidStartLoading(self)
|
||||||
|
|
||||||
let availableRestoreTokens = keychainController.restoreTokens()
|
let availableRestoreTokens = keychainController.restoreTokens()
|
||||||
|
|
||||||
guard let usernameTokenTuple = availableRestoreTokens.first else {
|
guard let usernameTokenTuple = availableRestoreTokens.first else {
|
||||||
startNewLoginFlow()
|
startNewLoginFlow { result in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
self.delegate?.authenticationCoordinatorDidSetupUserSession(self)
|
||||||
|
case .failure(let error):
|
||||||
|
self.delegate?.authenticationCoordinator(self, didFailWithError: error)
|
||||||
|
MXLog.error("Failed logging in user with error: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
restorePreviousLogin(usernameTokenTuple)
|
restorePreviousLogin(usernameTokenTuple) { [weak self] result in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
self.delegate?.authenticationCoordinatorDidSetupUserSession(self)
|
||||||
|
case .failure(let error):
|
||||||
|
self.delegate?.authenticationCoordinator(self, didFailWithError: error)
|
||||||
|
MXLog.error("Failed restoring login with error: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func logout() {
|
||||||
|
keychainController.removeAllTokens()
|
||||||
|
userSession = nil
|
||||||
|
delegate?.authenticationCoordinatorDidTearDownUserSession(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func startNewLoginFlow() {
|
private func startNewLoginFlow(_ completion: @escaping (Result<(), AuthenticationCoordinatorError>) -> Void) {
|
||||||
let parameters = LoginScreenCoordinatorParameters()
|
let parameters = LoginScreenCoordinatorParameters()
|
||||||
let coordinator = LoginScreenCoordinator(parameters: parameters)
|
let coordinator = LoginScreenCoordinator(parameters: parameters)
|
||||||
|
|
||||||
@ -63,16 +93,18 @@ class AuthenticationCoordinator: Coordinator {
|
|||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .login(let result):
|
case .login(let result):
|
||||||
do {
|
self.login(username: result.username, password: result.password) { [weak self] result in
|
||||||
self.setupUserSessionForClient(try loginNewClient(basePath: self.baseDirectoryPathForUsername(result.username),
|
guard let self = self else { return }
|
||||||
username: result.username,
|
|
||||||
password: result.password))
|
switch result {
|
||||||
|
case .success:
|
||||||
|
completion(.success(()))
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
|
||||||
self.remove(childCoordinator: coordinator)
|
self.remove(childCoordinator: coordinator)
|
||||||
self.navigationRouter.dismissModule()
|
self.navigationRouter.dismissModule()
|
||||||
} catch {
|
|
||||||
self.delegate?.authenticationCoordinator(self, didFailWithError: .failedLoggingIn)
|
|
||||||
MXLog.error("Failed logging in user with error: \(error)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,13 +115,42 @@ class AuthenticationCoordinator: Coordinator {
|
|||||||
coordinator.start()
|
coordinator.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func restorePreviousLogin(_ usernameTokenTuple: (username: String, token: String)) {
|
private func login(username: String, password: String, completion: @escaping (Result<Void, AuthenticationCoordinatorError>) -> Void) {
|
||||||
do {
|
DispatchQueue.global(qos: .background).async { [weak self] in
|
||||||
setupUserSessionForClient(try loginWithToken(basePath: baseDirectoryPathForUsername(usernameTokenTuple.username),
|
guard let self = self else { return }
|
||||||
restoreToken: usernameTokenTuple.token))
|
|
||||||
} catch {
|
do {
|
||||||
delegate?.authenticationCoordinator(self, didFailWithError: .failedRestoringLogin)
|
self.setupUserSessionForClient(try loginNewClient(basePath: self.baseDirectoryPathForUsername(username),
|
||||||
MXLog.error("Failed restoring login with error: \(error)")
|
username: username,
|
||||||
|
password: password))
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(.success(()))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(.failure(.failedLoggingIn))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func restorePreviousLogin(_ usernameTokenTuple: (username: String, token: String), completion: @escaping (Result<Void, AuthenticationCoordinatorError>) -> Void) {
|
||||||
|
DispatchQueue.global(qos: .background).async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
self.setupUserSessionForClient(try loginWithToken(basePath: self.baseDirectoryPathForUsername(usernameTokenTuple.username),
|
||||||
|
restoreToken: usernameTokenTuple.token))
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(.success(()))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(.failure(.failedRestoringLogin))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +167,6 @@ class AuthenticationCoordinator: Coordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userSession = UserSession(client: client)
|
userSession = UserSession(client: client)
|
||||||
delegate?.authenticationCoordinatorDidSetupUserSession(self)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func baseDirectoryPathForUsername(_ username: String) -> String {
|
private func baseDirectoryPathForUsername(_ username: String) -> String {
|
||||||
|
@ -38,6 +38,7 @@ struct LoginScreen: View {
|
|||||||
.padding(.horizontal, 8.0)
|
.padding(.horizontal, 8.0)
|
||||||
.navigationTitle("Login")
|
.navigationTitle("Login")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.navigationViewStyle(StackNavigationViewStyle())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,33 +7,41 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import MatrixRustSDK
|
import MatrixRustSDK
|
||||||
|
import Combine
|
||||||
|
import UIKit
|
||||||
|
|
||||||
class UserSession {
|
enum UserSessionCallback {
|
||||||
|
case updatedData
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UserSessionError: Error {
|
||||||
|
case failedRetrievingAvatar
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserSession: ClientDelegate {
|
||||||
|
|
||||||
private let client: Client
|
private let client: Client
|
||||||
|
|
||||||
|
let callbacks = PassthroughSubject<UserSessionCallback, Never>()
|
||||||
|
|
||||||
init(client: Client) {
|
init(client: Client) {
|
||||||
self.client = client
|
self.client = client
|
||||||
|
|
||||||
if !client.hasFirstSynced() {
|
if !client.hasFirstSynced() {
|
||||||
MXLog.info("Started initial sync")
|
client.startSync(delegate: self)
|
||||||
client.startSync()
|
|
||||||
MXLog.info("Finished intial sync")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func roomList() -> [RoomModel] {
|
var userIdentifier: String {
|
||||||
client.conversations().compactMap { room in
|
do {
|
||||||
do {
|
return try client.userId()
|
||||||
return RoomModel(displayName: try room.displayName())
|
} catch {
|
||||||
} catch {
|
MXLog.error("Failed retrieving room info with error: \(error)")
|
||||||
MXLog.error("Failed retrieving room info with error: \(error)")
|
return "Unknown user identifier"
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var displayName: String? {
|
var userDisplayName: String? {
|
||||||
do {
|
do {
|
||||||
return try client.displayName()
|
return try client.displayName()
|
||||||
} catch {
|
} catch {
|
||||||
@ -41,4 +49,42 @@ class UserSession {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getUserAvatar(_ completion: @escaping (Result<UIImage?, Error>) -> Void) {
|
||||||
|
DispatchQueue.global(qos: .background).async {
|
||||||
|
do {
|
||||||
|
let avatarData = try self.client.avatar()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(.success(UIImage(data: Data(bytes: avatarData, count: avatarData.count))))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed retrieving room name with error: \(error)")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(.failure(UserSessionError.failedRetrievingAvatar))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRoomList(_ completion: @escaping ([RoomModel]) -> Void) {
|
||||||
|
DispatchQueue.global(qos: .background).async {
|
||||||
|
let conversations = self.client.conversations()
|
||||||
|
|
||||||
|
let rooms = conversations.map {
|
||||||
|
return RoomModel(room: $0)
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(rooms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: ClientDelegate
|
||||||
|
|
||||||
|
func didReceiveSyncUpdate() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.callbacks.send(.updatedData)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,11 +15,16 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
struct HomeScreenCoordinatorParameters {
|
struct HomeScreenCoordinatorParameters {
|
||||||
let userSession: UserSession
|
let userSession: UserSession
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum HomeScreenCoordinatorResult {
|
||||||
|
case logout
|
||||||
|
}
|
||||||
|
|
||||||
final class HomeScreenCoordinator: Coordinator, Presentable {
|
final class HomeScreenCoordinator: Coordinator, Presentable {
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
@ -27,30 +32,52 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
|
|||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
private let parameters: HomeScreenCoordinatorParameters
|
private let parameters: HomeScreenCoordinatorParameters
|
||||||
private let homeScreenHostingController: UIViewController
|
private let hostingController: UIViewController
|
||||||
private var homeScreenViewModel: HomeScreenViewModelProtocol
|
private var viewModel: HomeScreenViewModelProtocol
|
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
// MARK: Public
|
// MARK: Public
|
||||||
|
|
||||||
// Must be used only internally
|
// Must be used only internally
|
||||||
var childCoordinators: [Coordinator] = []
|
var childCoordinators: [Coordinator] = []
|
||||||
var completion: ((HomeScreenViewModelResult) -> Void)?
|
var completion: ((HomeScreenCoordinatorResult) -> Void)?
|
||||||
|
|
||||||
// MARK: - Setup
|
// MARK: - Setup
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
|
||||||
init(parameters: HomeScreenCoordinatorParameters) {
|
init(parameters: HomeScreenCoordinatorParameters) {
|
||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
|
|
||||||
let viewModel = HomeScreenViewModel(username: self.parameters.userSession.displayName ?? "💥")
|
let userDisplayName = self.parameters.userSession.userDisplayName ?? self.parameters.userSession.userIdentifier
|
||||||
let view = HomeScreen(context: viewModel.context)
|
viewModel = HomeScreenViewModel(userDisplayName: userDisplayName)
|
||||||
homeScreenViewModel = viewModel
|
|
||||||
homeScreenHostingController = UIHostingController(rootView: view)
|
|
||||||
|
|
||||||
homeScreenViewModel.completion = { [weak self] result in
|
let view = HomeScreen(context: viewModel.context)
|
||||||
|
hostingController = UIHostingController(rootView: view)
|
||||||
|
|
||||||
|
viewModel.completion = { [weak self] result in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.completion?(result)
|
|
||||||
|
switch result {
|
||||||
|
case .logout:
|
||||||
|
self.completion?(.logout)
|
||||||
|
case .loadUserAvatar:
|
||||||
|
self.parameters.userSession.getUserAvatar({ result in
|
||||||
|
switch result {
|
||||||
|
case .success(let avatar):
|
||||||
|
self.viewModel.updateWithUserAvatar(avatar)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parameters.userSession.callbacks.sink { [weak self] result in
|
||||||
|
switch result {
|
||||||
|
case .updatedData:
|
||||||
|
self?.updateRoomsList()
|
||||||
|
}
|
||||||
|
}.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
@ -59,6 +86,14 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func toPresentable() -> UIViewController {
|
func toPresentable() -> UIViewController {
|
||||||
return self.homeScreenHostingController
|
return self.hostingController
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
func updateRoomsList() {
|
||||||
|
parameters.userSession.getRoomList { [weak self] rooms in
|
||||||
|
self?.viewModel.updateWithRoomList(rooms)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,17 +15,43 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
enum HomeScreenViewModelResult {
|
enum HomeScreenViewModelResult {
|
||||||
case logout
|
case logout
|
||||||
}
|
case loadUserAvatar
|
||||||
|
|
||||||
// MARK: View
|
|
||||||
|
|
||||||
struct HomeScreenViewState: BindableState {
|
|
||||||
let username: String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum HomeScreenViewAction {
|
enum HomeScreenViewAction {
|
||||||
case logout
|
case logout
|
||||||
|
case loadUserAvatar
|
||||||
|
case loadRoomAvatar(roomId: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HomeScreenViewState: BindableState {
|
||||||
|
let userDisplayName: String
|
||||||
|
var userAvatar: UIImage?
|
||||||
|
|
||||||
|
var rooms: [HomeScreenRoom] = []
|
||||||
|
|
||||||
|
var directRooms: [HomeScreenRoom] {
|
||||||
|
rooms.filter { $0.isDirect }
|
||||||
|
}
|
||||||
|
|
||||||
|
var nondirectRooms: [HomeScreenRoom] {
|
||||||
|
rooms.filter { !$0.isDirect }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HomeScreenRoom: Identifiable {
|
||||||
|
let id: String
|
||||||
|
let displayName: String
|
||||||
|
|
||||||
|
let topic: String?
|
||||||
|
let lastMessage: String?
|
||||||
|
|
||||||
|
var avatar: UIImage?
|
||||||
|
|
||||||
|
let isDirect: Bool
|
||||||
|
let isEncrypted: Bool
|
||||||
}
|
}
|
||||||
|
@ -18,28 +18,70 @@ import SwiftUI
|
|||||||
|
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
typealias HomeScreenViewModelType = StateStoreViewModel<HomeScreenViewState,
|
typealias HomeScreenViewModelType = StateStoreViewModel<HomeScreenViewState,
|
||||||
Never,
|
Never,
|
||||||
HomeScreenViewAction>
|
HomeScreenViewAction>
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol {
|
class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol {
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
|
|
||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
|
private var roomList: [RoomModelProtocol]?
|
||||||
|
|
||||||
// MARK: Public
|
// MARK: Public
|
||||||
|
|
||||||
var completion: ((HomeScreenViewModelResult) -> Void)?
|
var completion: ((HomeScreenViewModelResult) -> Void)?
|
||||||
|
|
||||||
// MARK: - Setup
|
// MARK: - Setup
|
||||||
|
|
||||||
init(username: String) {
|
init(userDisplayName: String) {
|
||||||
super.init(initialViewState: HomeScreenViewState(username: username))
|
super.init(initialViewState: HomeScreenViewState(userDisplayName: userDisplayName))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
|
|
||||||
override func process(viewAction: HomeScreenViewAction) {
|
override func process(viewAction: HomeScreenViewAction) {
|
||||||
|
switch viewAction {
|
||||||
|
case .logout:
|
||||||
|
self.completion?(.logout)
|
||||||
|
case .loadRoomAvatar(let roomId):
|
||||||
|
guard let room = roomList?.filter({ $0.identifier == roomId }).first else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
room.getAvatar { [weak self] result in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success(let image):
|
||||||
|
guard let index = self.state.rooms.firstIndex(where: { $0.id == roomId }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state.rooms[index].avatar = image
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .loadUserAvatar:
|
||||||
|
self.completion?(.loadUserAvatar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateWithRoomList(_ roomList: [RoomModelProtocol]) {
|
||||||
|
self.roomList = roomList
|
||||||
|
state.rooms = roomList.map { roomModel in
|
||||||
|
HomeScreenRoom(id: roomModel.identifier,
|
||||||
|
displayName: roomModel.displayName,
|
||||||
|
topic: roomModel.topic,
|
||||||
|
lastMessage: roomModel.lastMessage,
|
||||||
|
isDirect: roomModel.isDirect,
|
||||||
|
isEncrypted: roomModel.isEncrypted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateWithUserAvatar(_ avatar: UIImage?) {
|
||||||
|
self.state.userAvatar = avatar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,14 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
protocol HomeScreenViewModelProtocol {
|
protocol HomeScreenViewModelProtocol {
|
||||||
|
|
||||||
var completion: ((HomeScreenViewModelResult) -> Void)? { get set }
|
var completion: ((HomeScreenViewModelResult) -> Void)? { get set }
|
||||||
@available(iOS 14, *)
|
|
||||||
var context: HomeScreenViewModelType.Context { get }
|
var context: HomeScreenViewModelType.Context { get }
|
||||||
|
|
||||||
|
func updateWithRoomList(_ roomList: [RoomModelProtocol])
|
||||||
|
|
||||||
|
func updateWithUserAvatar(_ avatar: UIImage?)
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
|
||||||
struct HomeScreen: View {
|
struct HomeScreen: View {
|
||||||
|
|
||||||
@ObservedObject var context: HomeScreenViewModel.Context
|
@ObservedObject var context: HomeScreenViewModel.Context
|
||||||
@ -24,18 +23,110 @@ struct HomeScreen: View {
|
|||||||
// MARK: Views
|
// MARK: Views
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
NavigationView {
|
||||||
Text("Hello, \(context.viewState.username)!")
|
VStack(spacing: 16.0) {
|
||||||
|
HStack {
|
||||||
|
if let avatar = context.viewState.userAvatar {
|
||||||
|
Image(uiImage: avatar)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 40, height: 40, alignment: .center)
|
||||||
|
} else {
|
||||||
|
let _ = context.send(viewAction: .loadUserAvatar)
|
||||||
|
}
|
||||||
|
Text("Hello, \(context.viewState.userDisplayName)!")
|
||||||
|
.font(.subheadline)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 32.0)
|
||||||
|
|
||||||
|
List {
|
||||||
|
Section("People") {
|
||||||
|
ForEach(context.viewState.directRooms) { room in
|
||||||
|
RoomCell(room: room, context: context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Rooms") {
|
||||||
|
ForEach(context.viewState.nondirectRooms) { room in
|
||||||
|
RoomCell(room: room, context: context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.headerProminence(.increased)
|
||||||
|
.listStyle(.plain)
|
||||||
|
}
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.navigationViewStyle(StackNavigationViewStyle())
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button("Logout") {
|
||||||
|
context.send(viewAction: .logout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct RoomCell: View {
|
||||||
|
|
||||||
|
let room: HomeScreenRoom
|
||||||
|
let context: HomeScreenViewModel.Context
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 16.0) {
|
||||||
|
if let avatar = room.avatar {
|
||||||
|
Image(uiImage: avatar)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
|
} else {
|
||||||
|
let _ = context.send(viewAction: .loadRoomAvatar(roomId: room.id))
|
||||||
|
Image(systemName: "person.3")
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4.0) {
|
||||||
|
Text(roomName(room))
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.regular)
|
||||||
|
|
||||||
|
if let roomTopic = room.topic, roomTopic.count > 0 {
|
||||||
|
Text(roomTopic)
|
||||||
|
.font(.footnote)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let lastMessage = room.lastMessage {
|
||||||
|
Text(lastMessage)
|
||||||
|
.font(.footnote)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(minHeight: 60.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func roomName(_ room: HomeScreenRoom) -> String {
|
||||||
|
room.displayName + (room.isEncrypted ? "🛡": "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Previews
|
// MARK: - Previews
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
|
||||||
struct HomeScreen_Previews: PreviewProvider {
|
struct HomeScreen_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let viewModel = HomeScreenViewModel(username: "Johnny Appleseed")
|
let viewModel = HomeScreenViewModel(userDisplayName: "Johnny Appleseed")
|
||||||
HomeScreen(context: viewModel.context)
|
|
||||||
|
let rooms = [MockRoomModel(displayName: "Alfa"),
|
||||||
|
MockRoomModel(displayName: "Beta"),
|
||||||
|
MockRoomModel(displayName: "Omega")]
|
||||||
|
|
||||||
|
viewModel.updateWithRoomList(rooms)
|
||||||
|
|
||||||
|
return HomeScreen(context: viewModel.context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
29
ElementX/Sources/Modules/Models/MockRoomModel.swift
Normal file
29
ElementX/Sources/Modules/Models/MockRoomModel.swift
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// MockRoomModel.swift
|
||||||
|
// ElementX
|
||||||
|
//
|
||||||
|
// Created by Stefan Ceriu on 17.02.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct MockRoomModel: RoomModelProtocol {
|
||||||
|
let identifier = UUID().uuidString
|
||||||
|
let name: String? = nil
|
||||||
|
let displayName: String
|
||||||
|
|
||||||
|
let topic: String? = nil
|
||||||
|
let lastMessage: String? = "Last message"
|
||||||
|
|
||||||
|
let avatarURL: URL? = nil
|
||||||
|
|
||||||
|
let isDirect = Bool.random()
|
||||||
|
let isSpace = Bool.random()
|
||||||
|
let isPublic = Bool.random()
|
||||||
|
let isEncrypted = Bool.random()
|
||||||
|
|
||||||
|
func getAvatar(_ completion: (Result<UIImage?, Error>) -> Void) {
|
||||||
|
completion(.success(UIImage(systemName: "wand.and.stars")))
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,86 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import MatrixRustSDK
|
||||||
|
|
||||||
struct RoomModel {
|
enum RoomModelError: Error {
|
||||||
let displayName: String
|
case failedRetrievingAvatar
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RoomModel: RoomModelProtocol {
|
||||||
|
|
||||||
|
private let room: Room
|
||||||
|
|
||||||
|
init(room: Room) {
|
||||||
|
self.room = room
|
||||||
|
}
|
||||||
|
|
||||||
|
var identifier: String {
|
||||||
|
return room.identifier()
|
||||||
|
}
|
||||||
|
|
||||||
|
var isDirect: Bool {
|
||||||
|
return room.isDirect()
|
||||||
|
}
|
||||||
|
|
||||||
|
var isPublic: Bool {
|
||||||
|
return room.isPublic()
|
||||||
|
}
|
||||||
|
|
||||||
|
var isSpace: Bool {
|
||||||
|
return room.isSpace()
|
||||||
|
}
|
||||||
|
|
||||||
|
var isEncrypted: Bool {
|
||||||
|
return room.isEncrypted()
|
||||||
|
}
|
||||||
|
|
||||||
|
var name: String? {
|
||||||
|
return room.name()
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayName: String {
|
||||||
|
do {
|
||||||
|
return try room.displayName()
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed retrieving room name with error: \(error)")
|
||||||
|
return "Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var topic: String? {
|
||||||
|
return room.topic()
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastMessage: String? {
|
||||||
|
guard let lastMessage = try? room.messages().last else {
|
||||||
|
return "Last message unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\(lastMessage.sender()): \(lastMessage.content())"
|
||||||
|
}
|
||||||
|
|
||||||
|
var avatarURL: URL? {
|
||||||
|
guard let urlString = room.avatarUrl() else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return URL(string: urlString)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAvatar(_ completion: @escaping (Result<UIImage?, Error>) -> Void) {
|
||||||
|
DispatchQueue.global(qos: .background).async {
|
||||||
|
do {
|
||||||
|
let avatarData = try room.avatar()
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(.success(UIImage(data: Data(bytes: avatarData, count: avatarData.count))))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(.failure(RoomModelError.failedRetrievingAvatar))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
26
ElementX/Sources/Modules/Models/RoomModelProtocol.swift
Normal file
26
ElementX/Sources/Modules/Models/RoomModelProtocol.swift
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// RoomModelProtocol.swift
|
||||||
|
// ElementX
|
||||||
|
//
|
||||||
|
// Created by Stefan Ceriu on 17.02.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol RoomModelProtocol {
|
||||||
|
var identifier: String { get }
|
||||||
|
var isDirect: Bool { get }
|
||||||
|
var isPublic: Bool { get }
|
||||||
|
var isSpace: Bool { get }
|
||||||
|
var isEncrypted: Bool { get }
|
||||||
|
|
||||||
|
var displayName: String { get }
|
||||||
|
var name: String? { get }
|
||||||
|
|
||||||
|
var topic: String? { get }
|
||||||
|
var lastMessage: String? { get }
|
||||||
|
|
||||||
|
var avatarURL: URL? { get }
|
||||||
|
|
||||||
|
func getAvatar(_ completion: @escaping (Result<UIImage?, Error>) -> Void)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user