Files
element-x-ios/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift
T
Stefan Ceriu f040964d3f Allow the user to forward the same message to multiple rooms at once
This will send the item to the rooms in order they were chosen and only deep link
into the target room if single, otherwise it will just dismiss the forwarding screen
after sending.
2026-05-29 14:27:42 +03:00

148 lines
5.5 KiB
Swift

//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Combine
import SwiftUI
typealias MessageForwardingScreenViewModelType = StateStoreViewModel<MessageForwardingScreenViewState, MessageForwardingScreenViewAction>
class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, MessageForwardingScreenViewModelProtocol {
private let forwardingItem: MessageForwardingItem
private let clientProxy: ClientProxyProtocol
private let roomSummaryProvider: RoomSummaryProviderProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private var actionsSubject: PassthroughSubject<MessageForwardingScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<MessageForwardingScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(forwardingItem: MessageForwardingItem,
userSession: UserSessionProtocol,
roomSummaryProvider: RoomSummaryProviderProtocol,
userIndicatorController: UserIndicatorControllerProtocol) {
self.forwardingItem = forwardingItem
clientProxy = userSession.clientProxy
self.roomSummaryProvider = roomSummaryProvider
self.userIndicatorController = userIndicatorController
super.init(initialViewState: MessageForwardingScreenViewState(), mediaProvider: userSession.mediaProvider)
roomSummaryProvider.roomListPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.updateRooms()
}
.store(in: &cancellables)
context.$viewState
.map(\.bindings.searchQuery)
.removeDuplicates()
.sink { [weak self] searchQuery in
if searchQuery.isEmpty {
self?.roomSummaryProvider.setFilter(.all(filters: []))
} else {
self?.roomSummaryProvider.setFilter(.search(query: searchQuery))
}
}
.store(in: &cancellables)
updateRooms()
}
override func process(viewAction: MessageForwardingScreenViewAction) {
switch viewAction {
case .cancel:
actionsSubject.send(.dismiss)
case .send:
Task { await forward() }
case .selectRoom(let roomID):
if let index = state.selectedRoomIDs.firstIndex(of: roomID) {
state.selectedRoomIDs.remove(at: index)
} else {
state.selectedRoomIDs.append(roomID)
}
case .reachedTop:
updateVisibleRange(edge: .top)
case .reachedBottom:
updateVisibleRange(edge: .bottom)
}
}
func stop() {
// This is a shared provider so we should reset the filtering when we are done with the view
roomSummaryProvider.setFilter(.all(filters: []))
}
// MARK: - Private
private func updateRooms() {
var rooms = [MessageForwardingRoom]()
for summary in roomSummaryProvider.roomListPublisher.value {
if summary.id == forwardingItem.roomID {
continue
}
rooms.append(.init(id: summary.id,
title: summary.name,
description: summary.roomListDescription,
avatar: summary.avatar))
}
state.rooms = rooms
}
/// The actual range values don't matter as long as they contain the lower
/// or upper bounds. updateVisibleRange is a hybrid API that powers both
/// sliding sync visible range update and list paginations
/// For lists other than the home screen one we don't care about visible ranges,
/// we just need the respective bounds to be there to trigger a next page load or
/// a reset to just one page
private func updateVisibleRange(edge: UIRectEdge) {
switch edge {
case .top:
roomSummaryProvider.updateVisibleRange(0..<0)
case .bottom:
let roomCount = roomSummaryProvider.roomListPublisher.value.count
roomSummaryProvider.updateVisibleRange(roomCount..<roomCount)
default:
break
}
}
private func forward() async {
guard !state.selectedRoomIDs.isEmpty else {
fatalError()
}
var succeededRoomIdentifiers = [String]()
for roomID in state.selectedRoomIDs {
guard case let .joined(targetRoomProxy) = await clientProxy.roomForIdentifier(roomID) else {
MXLog.error("Failed retrieving room to forward to with id: \(roomID)")
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
continue
}
if case .failure(let error) = await targetRoomProxy.timeline.sendMessageEventContent(forwardingItem.content) {
MXLog.error("Failed forwarding message with error: \(error)")
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
continue
}
succeededRoomIdentifiers.append(roomID)
}
if !succeededRoomIdentifiers.isEmpty {
actionsSubject.send(.sent(roomIDs: succeededRoomIdentifiers))
}
}
}