mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
c41a38159b
* Initial plan * Offer only joined rooms in destination pickers Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> * Clean whitespace in share picker test Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> * Restrict the shared picker room list to joined rooms The alternate room summary provider, which backs the forward, share and Add rooms pickers and global search, now always includes the SDK's joined filter in its base filter. This replaces the per-call joinedOnly flags, so Add rooms no longer offers invited rooms or spaces and no screen can forget the flag. A filter set before the provider has its room list is now applied once the list is set, instead of being reset to the default. The share picker hit this on a cold launch from the share extension. The forward and share pickers also hide upgraded rooms, matching the Suggestions section. The SDK has no filter for these. Also registers the share picker's test file in the project, fixes the blank-line indentation SwiftFormat flagged, and re-records the picker snapshots without the upgraded room. * Check room membership for forwarding suggestions and search breadcrumbs RoomSummary now has isJoined, built from the room's membership. The forwarding suggestions and the search breadcrumbs used a missing joinRequestType to mean joined, which is also the case for rooms that have been left or banned from. It only worked because the room list's base filter already drops those. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> Co-authored-by: Mauro Romito <mauro.romito@element.io>
163 lines
6.5 KiB
Swift
163 lines
6.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 static let maxSuggestedRoomCount = 5
|
|
|
|
private let forwardingPayload: MessageForwardingPayload
|
|
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(forwardingPayload: MessageForwardingPayload,
|
|
userSession: UserSessionProtocol,
|
|
roomSummaryProvider: RoomSummaryProviderProtocol,
|
|
userIndicatorController: UserIndicatorControllerProtocol) {
|
|
self.forwardingPayload = forwardingPayload
|
|
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()
|
|
|
|
Task { await loadSuggestedRooms() }
|
|
}
|
|
|
|
override func process(viewAction: MessageForwardingScreenViewAction) {
|
|
switch viewAction {
|
|
case .cancel:
|
|
actionsSubject.send(.dismiss)
|
|
case .send:
|
|
Task { await forward() }
|
|
case .selectRoom(let roomID):
|
|
if state.selectedRoomIDs.contains(roomID) == false, state.selectedRoomIDs.count < state.maxRoomSelectionCount {
|
|
state.selectedRoomIDs.insert(roomID)
|
|
} else {
|
|
state.selectedRoomIDs.remove(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() {
|
|
state.rooms = roomSummaryProvider.roomListPublisher.value
|
|
.filter { $0.id != forwardingPayload.roomID && !$0.isTombstoned }
|
|
.map(MessageForwardingRoom.init(summary:))
|
|
}
|
|
|
|
/// Built from the room summaries: building room proxies is slow and would shift the list down when the section appears.
|
|
private func loadSuggestedRooms() async {
|
|
guard case let .success(roomIDs) = await clientProxy.recentlyVisitedRoomIDs() else { return }
|
|
|
|
state.suggestedRooms = roomIDs
|
|
.filter { $0 != forwardingPayload.roomID }
|
|
.compactMap(clientProxy.roomSummaryForIdentifier)
|
|
.filter { $0.isJoined && !$0.isSpace && !$0.isTombstoned }
|
|
.prefix(Self.maxSuggestedRoomCount)
|
|
.map(MessageForwardingRoom.init(summary:))
|
|
}
|
|
|
|
/// 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]()
|
|
var hasFailures = false
|
|
|
|
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)")
|
|
hasFailures = true
|
|
continue
|
|
}
|
|
|
|
// The contents are already in timeline order and the send queue preserves it.
|
|
for content in forwardingPayload.contents {
|
|
if case .failure(let error) = await targetRoomProxy.timeline.sendMessageEventContent(content) {
|
|
MXLog.error("Failed forwarding message with error: \(error)")
|
|
hasFailures = true
|
|
}
|
|
}
|
|
|
|
succeededRoomIdentifiers.append(roomID)
|
|
}
|
|
|
|
if hasFailures {
|
|
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
|
|
} else if succeededRoomIdentifiers.count > 1 {
|
|
// The flow opens the room when there is a single one, otherwise the user stays here and needs to know it worked.
|
|
userIndicatorController.submitIndicator(UserIndicator(title: L10n.screenRoomMessagesForwarded(forwardingPayload.contents.count)))
|
|
}
|
|
|
|
if !succeededRoomIdentifiers.isEmpty {
|
|
actionsSubject.send(.sent(roomIDs: succeededRoomIdentifiers))
|
|
}
|
|
}
|
|
}
|