Files
letro-ios/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift
Doug 9f847454e7 Switch to Xcode 14 and handle the UICollectionView-backed List. (#229)
* Fix Timeline on Xcode 14/iOS 16

Raise requirement to iOS 16+
Reduce pagination jumping.
Sonarcloud fixes.
Fix verification test.
Adopt if let optional { syntax.

* Remove unused ScrollViewReader

The ScrollViewReader didn't appear to change the behaviour.

* Fix warnings on Run Scripts.

Run script build phase 'SwiftLint' will be run during every build because it does not specify any outputs. To address this warning, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase.
2022-10-17 09:56:17 +01:00

215 lines
7.8 KiB
Swift

//
// 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 SwiftUI
typealias RoomScreenViewModelType = StateStoreViewModel<RoomScreenViewState, RoomScreenViewAction>
class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol {
private enum Constants {
static let backPaginationPageSize: UInt = 50
}
private let timelineController: RoomTimelineControllerProtocol
private let timelineViewFactory: RoomTimelineViewFactoryProtocol
private let mediaProvider: MediaProviderProtocol
// MARK: - Setup
init(timelineController: RoomTimelineControllerProtocol,
timelineViewFactory: RoomTimelineViewFactoryProtocol,
mediaProvider: MediaProviderProtocol,
roomName: String?,
roomAvatarUrl: String? = nil) {
self.timelineController = timelineController
self.timelineViewFactory = timelineViewFactory
self.mediaProvider = mediaProvider
super.init(initialViewState: RoomScreenViewState(roomId: timelineController.roomId,
roomTitle: roomName ?? "Unknown room 💥",
roomAvatar: nil,
bindings: .init(composerText: "", composerFocused: false)))
timelineController.callbacks
.receive(on: DispatchQueue.main)
.sink { [weak self] callback in
guard let self else { return }
switch callback {
case .updatedTimelineItems:
self.buildTimelineViews()
case .updatedTimelineItem(let itemId):
guard let timelineItem = self.timelineController.timelineItems.first(where: { $0.id == itemId }),
let viewIndex = self.state.items.firstIndex(where: { $0.id == itemId }) else {
return
}
self.state.items[viewIndex] = timelineViewFactory.buildTimelineViewFor(timelineItem: timelineItem)
}
}
.store(in: &cancellables)
state.contextMenuBuilder = buildContexMenuForItemId(_:)
buildTimelineViews()
if let roomAvatarUrl {
Task {
if case let .success(avatar) = await mediaProvider.loadImageFromURLString(roomAvatarUrl,
avatarSize: .room(on: .timeline)) {
state.roomAvatar = avatar
}
}
}
}
// MARK: - Public
override func process(viewAction: RoomScreenViewAction) async {
switch viewAction {
case .loadPreviousPage:
state.isBackPaginating = true
switch await timelineController.paginateBackwards(Constants.backPaginationPageSize) {
default:
state.isBackPaginating = false
}
case .itemAppeared(let id):
await timelineController.processItemAppearance(id)
case .itemDisappeared(let id):
await timelineController.processItemDisappearance(id)
case .linkClicked(let url):
MXLog.warning("Link clicked: \(url)")
case .sendMessage:
await sendCurrentMessage()
case .sendReaction(let key, _):
#warning("Reaction implementation awaiting SDK support.")
MXLog.warning("React with \(key) failed. Not implemented.")
case .cancelReply:
state.composerMode = .default
}
}
func stop() {
cancellables.forEach { $0.cancel() }
cancellables.removeAll()
state.contextMenuBuilder = nil
}
// MARK: - Private
private func buildTimelineViews() {
let stateItems = timelineController.timelineItems.map { item in
timelineViewFactory.buildTimelineViewFor(timelineItem: item)
}
state.items = stateItems
}
private func sendCurrentMessage() async {
guard !state.bindings.composerText.isEmpty else {
fatalError("This message should never be empty")
}
let currentMessage = state.bindings.composerText
let currentComposerState = state.composerMode
state.bindings.composerText = ""
state.composerMode = .default
switch currentComposerState {
case .reply(let itemId, _):
await timelineController.sendReply(currentMessage, to: itemId)
default:
await timelineController.sendMessage(currentMessage)
}
}
private func displayError(_ type: RoomScreenErrorType) {
switch type {
case .alert(let message):
state.bindings.alertInfo = AlertInfo(id: type,
title: ElementL10n.dialogTitleError,
message: message)
}
}
// MARK: ContextMenus
private func buildContexMenuForItemId(_ itemId: String) -> TimelineItemContextMenu {
TimelineItemContextMenu(contextMenuActions: contextMenuActionsForItemId(itemId)) { [weak self] action in
self?.processContentMenuAction(action, itemId: itemId)
}
}
private func contextMenuActionsForItemId(_ itemId: String) -> [TimelineItemContextMenuAction] {
guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemId }),
timelineItem is EventBasedTimelineItemProtocol else {
return []
}
var actions: [TimelineItemContextMenuAction] = [
.copy, .quote, .copyPermalink, .reply
]
if let item = timelineItem as? EventBasedTimelineItemProtocol, item.isOutgoing {
actions.append(.redact)
}
return actions
}
private func processContentMenuAction(_ action: TimelineItemContextMenuAction, itemId: String) {
guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemId }),
let item = timelineItem as? EventBasedTimelineItemProtocol else {
return
}
switch action {
case .copy:
UIPasteboard.general.string = item.text
case .quote:
state.bindings.composerFocused = true
state.bindings.composerText = "> \(item.text)"
case .copyPermalink:
do {
let permalink = try PermalinkBuilder.permalinkTo(eventIdentifier: item.id, roomIdentifier: timelineController.roomId)
UIPasteboard.general.url = permalink
} catch {
displayError(.alert(ElementL10n.roomTimelinePermalinkCreationFailure))
}
case .redact:
redact(itemId)
case .reply:
state.bindings.composerFocused = true
state.composerMode = .reply(id: item.id, displayName: item.senderDisplayName ?? item.senderId)
}
switch action {
case .reply:
break
default:
state.composerMode = .default
}
}
private func redact(_ eventID: String) {
Task {
await timelineController.redact(eventID)
}
}
}