// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Combine import SwiftUI typealias HomeScreenViewModelType = StateStoreViewModel class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol { private let attributedStringBuilder: AttributedStringBuilderProtocol private var roomUpdateListeners = Set() private var roomsUpdateTask: Task? { willSet { roomsUpdateTask?.cancel() } } private var roomSummaries: [RoomSummaryProtocol]? { didSet { state.isLoadingRooms = (roomSummaries?.count ?? 0 == 0) } } var callback: ((HomeScreenViewModelAction) -> Void)? // MARK: - Setup init(initialDisplayName: String, attributedStringBuilder: AttributedStringBuilderProtocol) { self.attributedStringBuilder = attributedStringBuilder super.init(initialViewState: HomeScreenViewState(userDisplayName: initialDisplayName, isLoadingRooms: true)) } // MARK: - Public override func process(viewAction: HomeScreenViewAction) async { switch viewAction { case .loadRoomData(let roomIdentifier): loadRoomDataForIdentifier(roomIdentifier) case .selectRoom(let roomIdentifier): callback?(.selectRoom(roomIdentifier: roomIdentifier)) case .tapUserAvatar: callback?(.tapUserAvatar) case .verifySession: callback?(.verifySession) } } func updateWithRoomSummaries(_ roomSummaries: [RoomSummaryProtocol]) { roomsUpdateTask = Task { await updateWithRoomSummaries(roomSummaries) } } private func updateWithRoomSummaries(_ roomSummaries: [RoomSummaryProtocol]) async { var rooms = [HomeScreenRoom]() for summary in roomSummaries { if Task.isCancelled { return } rooms.append(await buildOrUpdateRoomForSummary(summary)) } if Task.isCancelled { return } state.rooms = rooms self.roomSummaries = roomSummaries roomUpdateListeners.removeAll() roomSummaries.forEach { roomSummary in roomSummary.callbacks .receive(on: DispatchQueue.main) .sink { [weak self] callback in guard let self = self else { return } Task { if let index = self.state.rooms.firstIndex(where: { $0.id == roomSummary.id }) { switch callback { case .updatedLastMessage: var room = self.state.rooms[index] room.lastMessage = await self.lastMessageFromEventBrief(roomSummary.lastMessage) self.state.rooms[index] = room default: self.state.rooms[index] = await self.buildOrUpdateRoomForSummary(roomSummary) } } } } .store(in: &roomUpdateListeners) } } func updateWithUserAvatar(_ avatar: UIImage) { state.userAvatar = avatar } func updateWithUserDisplayName(_ displayName: String) { state.userDisplayName = displayName } func showSessionVerificationBanner() { state.showSessionVerificationBanner = true } func hideSessionVerificationBanner() { state.showSessionVerificationBanner = false } // MARK: - Private private func loadRoomDataForIdentifier(_ roomIdentifier: String) { guard let roomSummary = roomSummaries?.first(where: { $0.id == roomIdentifier }) else { MXLog.error("Invalid room identifier") return } Task { await roomSummary.loadDetails() } } private func buildOrUpdateRoomForSummary(_ roomSummary: RoomSummaryProtocol) async -> HomeScreenRoom { guard var room = state.rooms.first(where: { $0.id == roomSummary.id }) else { return HomeScreenRoom(id: roomSummary.id, displayName: roomSummary.displayName, topic: roomSummary.topic, lastMessage: await lastMessageFromEventBrief(roomSummary.lastMessage), avatar: roomSummary.avatar, isDirect: roomSummary.isDirect, isEncrypted: roomSummary.isEncrypted, isSpace: roomSummary.isSpace, isTombstoned: roomSummary.isTombstoned) } room.avatar = roomSummary.avatar room.displayName = roomSummary.displayName room.topic = roomSummary.topic return room } private func lastMessageFromEventBrief(_ eventBrief: EventBrief?) async -> String? { guard let eventBrief = eventBrief else { return nil } let senderDisplayName = senderDisplayNameForBrief(eventBrief) if let htmlBody = eventBrief.htmlBody, let lastMessageAttributedString = await attributedStringBuilder.fromHTML(htmlBody) { return "\(senderDisplayName): \(String(lastMessageAttributedString.characters))" } else { return "\(senderDisplayName): \(eventBrief.body)" } } private func senderDisplayNameForBrief(_ brief: EventBrief) -> String { brief.senderDisplayName ?? brief.senderId } }