mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Use the new preview screen when tapping media on the room and pinned events screens. (#3736)
* Use the new TimelineMediaPreview modifier on the room and pinned timeline screens. * Use the same presentation logic for all timeline media previews. * Fix a bug with the detection of the timeline end. * Send pagination requests from the media preview screen. * Add SwiftLint to the Danger workflow (it is no longer installed on the runner). * Put SwiftLint back on all of the GitHub runners too. * Set the function_parameter_count lint rule to 10. * Make sure to clean-up any previews when the coordinator is done. * Handle the viewInRoomTimeline action more appropriately.
This commit is contained in:
@@ -502,7 +502,7 @@ class TimelineInteractionHandler {
|
||||
}
|
||||
|
||||
func processItemTap(_ itemID: TimelineItemIdentifier) async -> TimelineControllerAction {
|
||||
guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID) else {
|
||||
guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID) as? EventBasedMessageTimelineItemProtocol else {
|
||||
return .none
|
||||
}
|
||||
|
||||
@@ -510,8 +510,14 @@ class TimelineInteractionHandler {
|
||||
case let item as LocationRoomTimelineItem:
|
||||
guard let geoURI = item.content.geoURI else { return .none }
|
||||
return .displayLocation(body: item.content.body, geoURI: geoURI, description: item.content.description)
|
||||
case is ImageRoomTimelineItem,
|
||||
is VideoRoomTimelineItem:
|
||||
return await mediaPreviewAction(for: timelineItem, messageTypes: [.image, .video])
|
||||
case is AudioRoomTimelineItem,
|
||||
is FileRoomTimelineItem:
|
||||
return await mediaPreviewAction(for: timelineItem, messageTypes: [.audio, .file])
|
||||
default:
|
||||
return await displayMediaActionIfPossible(timelineItem: timelineItem)
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,39 +534,57 @@ class TimelineInteractionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private func displayMediaActionIfPossible(timelineItem: RoomTimelineItemProtocol) async -> TimelineControllerAction {
|
||||
var source: MediaSourceProxy?
|
||||
var filename: String
|
||||
var caption: String?
|
||||
|
||||
switch timelineItem {
|
||||
case let item as ImageRoomTimelineItem:
|
||||
source = item.content.imageInfo.source
|
||||
filename = item.content.filename
|
||||
caption = item.content.caption
|
||||
case let item as VideoRoomTimelineItem:
|
||||
source = item.content.videoInfo.source
|
||||
filename = item.content.filename
|
||||
caption = item.content.caption
|
||||
case let item as FileRoomTimelineItem:
|
||||
source = item.content.source
|
||||
filename = item.content.filename
|
||||
caption = item.content.caption
|
||||
case let item as AudioRoomTimelineItem:
|
||||
// For now we are just displaying audio messages with the File preview until we create a timeline player for them.
|
||||
source = item.content.source
|
||||
filename = item.content.filename
|
||||
caption = item.content.caption
|
||||
default:
|
||||
return .none
|
||||
private func mediaPreviewAction(for item: EventBasedMessageTimelineItemProtocol, messageTypes: [TimelineAllowedMessageType]) async -> TimelineControllerAction {
|
||||
var newTimelineFocus: TimelineFocus?
|
||||
var newTimelinePresentation: TimelineKind.MediaPresentation?
|
||||
switch timelineController.timelineKind {
|
||||
case .live:
|
||||
newTimelineFocus = .live
|
||||
newTimelinePresentation = .roomScreenLive
|
||||
case .detached:
|
||||
guard case let .event(_, eventOrTransactionID: .eventID(eventID)) = item.id else {
|
||||
MXLog.error("Unexpected event type on a detached timeline.")
|
||||
return .none
|
||||
}
|
||||
newTimelineFocus = .eventID(eventID)
|
||||
newTimelinePresentation = .roomScreenDetached
|
||||
case .pinned:
|
||||
newTimelineFocus = .pinned
|
||||
newTimelinePresentation = .pinnedEventsScreen
|
||||
case .media:
|
||||
break // We don't need to create a new timeline as it is already filtered.
|
||||
}
|
||||
|
||||
guard let source else { return .none }
|
||||
switch await mediaProvider.loadFileFromSource(source, filename: filename) {
|
||||
case .success(let file):
|
||||
return .displayMediaFile(file: file, title: caption ?? filename)
|
||||
case .failure:
|
||||
return .none
|
||||
|
||||
if let newTimelineFocus, let newTimelinePresentation {
|
||||
let timelineItemFactory = RoomTimelineItemFactory(userID: roomProxy.ownUserID,
|
||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||
stateEventStringBuilder: RoomStateEventStringBuilder(userID: roomProxy.ownUserID))
|
||||
|
||||
guard case let .success(timelineController) = await timelineControllerFactory.buildMessageFilteredTimelineController(focus: newTimelineFocus,
|
||||
allowedMessageTypes: messageTypes,
|
||||
presentation: newTimelinePresentation,
|
||||
roomProxy: roomProxy,
|
||||
timelineItemFactory: timelineItemFactory,
|
||||
mediaProvider: mediaProvider) else {
|
||||
MXLog.error("Failed presenting media timeline")
|
||||
return .none
|
||||
}
|
||||
|
||||
let timelineViewModel = TimelineViewModel(roomProxy: roomProxy,
|
||||
timelineController: timelineController,
|
||||
mediaProvider: mediaProvider,
|
||||
mediaPlayerProvider: mediaPlayerProvider,
|
||||
voiceMessageMediaManager: voiceMessageMediaManager,
|
||||
userIndicatorController: userIndicatorController,
|
||||
appMediator: appMediator,
|
||||
appSettings: appSettings,
|
||||
analyticsService: analyticsService,
|
||||
emojiProvider: emojiProvider,
|
||||
timelineControllerFactory: timelineControllerFactory)
|
||||
|
||||
return .displayMediaPreview(item: item, timelineViewModel: .new(timelineViewModel))
|
||||
} else {
|
||||
return .displayMediaPreview(item: item, timelineViewModel: .active)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ enum TimelineViewModelAction {
|
||||
case displayMediaUploadPreviewScreen(url: URL)
|
||||
case tappedOnSenderDetails(userID: String)
|
||||
case displayMessageForwarding(forwardingItem: MessageForwardingItem)
|
||||
case displayMediaPreview(TimelineMediaPreviewViewModel)
|
||||
case displayLocation(body: String, geoURI: GeoURI, description: String?)
|
||||
case displayResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy)
|
||||
case composer(action: TimelineComposerAction)
|
||||
@@ -123,9 +124,6 @@ struct TimelineViewStateBindings {
|
||||
/// Key is itemID, value is the collapsed state.
|
||||
var reactionsCollapsed: [TimelineItemIdentifier: Bool]
|
||||
|
||||
/// A media item that will be previewed with QuickLook.
|
||||
var mediaPreviewItem: MediaPreviewItem?
|
||||
|
||||
var alertInfo: AlertInfo<RoomScreenAlertInfoType>?
|
||||
|
||||
var debugInfo: TimelineItemDebugInfo?
|
||||
|
||||
@@ -23,6 +23,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
|
||||
private let roomProxy: JoinedRoomProxyProtocol
|
||||
private let timelineController: TimelineControllerProtocol
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let mediaPlayerProvider: MediaPlayerProviderProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let appMediator: AppMediatorProtocol
|
||||
@@ -56,6 +57,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
emojiProvider: EmojiProviderProtocol,
|
||||
timelineControllerFactory: TimelineControllerFactoryProtocol) {
|
||||
self.timelineController = timelineController
|
||||
self.mediaProvider = mediaProvider
|
||||
self.mediaPlayerProvider = mediaPlayerProvider
|
||||
self.roomProxy = roomProxy
|
||||
self.appSettings = appSettings
|
||||
@@ -123,11 +125,6 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func stop() {
|
||||
// Work around QLPreviewController dismissal issues, see the InteractiveQuickLookModifier.
|
||||
state.bindings.mediaPreviewItem = nil
|
||||
}
|
||||
|
||||
override func process(viewAction: TimelineViewAction) {
|
||||
switch viewAction {
|
||||
case .itemAppeared(let id):
|
||||
@@ -542,11 +539,13 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
private func handleMediaTapped(with itemID: TimelineItemIdentifier) async {
|
||||
state.showLoading = true
|
||||
let action = await timelineInteractionHandler.processItemTap(itemID)
|
||||
|
||||
|
||||
switch action {
|
||||
case .displayMediaFile(let file, let title):
|
||||
case .displayMediaPreview(let item, let timelineViewModelKind):
|
||||
actionsSubject.send(.composer(action: .removeFocus)) // Hide the keyboard otherwise a big white space is sometimes shown when dismissing the preview.
|
||||
state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: title)
|
||||
|
||||
let mediaPreviewViewModel = makeMediaPreviewViewModel(item: item, timelineViewModelKind: timelineViewModelKind)
|
||||
actionsSubject.send(.displayMediaPreview(mediaPreviewViewModel))
|
||||
case .displayLocation(let body, let geoURI, let description):
|
||||
actionsSubject.send(.displayLocation(body: body, geoURI: geoURI, description: description))
|
||||
case .none:
|
||||
@@ -655,6 +654,21 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
analyticsService.trackComposer(inThread: false, isEditing: isEdit, isReply: isReply, startsThread: nil)
|
||||
}
|
||||
|
||||
private func makeMediaPreviewViewModel(item: EventBasedMessageTimelineItemProtocol,
|
||||
timelineViewModelKind: TimelineControllerAction.TimelineViewModelKind) -> TimelineMediaPreviewViewModel {
|
||||
let timelineViewModel = switch timelineViewModelKind {
|
||||
case .active: self
|
||||
case .new(let newViewModel): newViewModel
|
||||
}
|
||||
|
||||
return TimelineMediaPreviewViewModel(initialItem: item,
|
||||
timelineViewModel: timelineViewModel,
|
||||
mediaProvider: mediaProvider,
|
||||
photoLibraryManager: PhotoLibraryManager(),
|
||||
userIndicatorController: userIndicatorController,
|
||||
appMediator: appMediator)
|
||||
}
|
||||
|
||||
// MARK: - Timeline Item Building
|
||||
|
||||
private func buildTimelineViews(timelineItems: [RoomTimelineItemProtocol], isSwitchingTimelines: Bool = false) {
|
||||
|
||||
@@ -13,8 +13,8 @@ import SwiftUI
|
||||
protocol TimelineViewModelProtocol {
|
||||
var actions: AnyPublisher<TimelineViewModelAction, Never> { get }
|
||||
var context: TimelineViewModel.Context { get }
|
||||
|
||||
func process(composerAction: ComposerToolbarViewModelAction)
|
||||
/// Updates the timeline to show and highlight the item with the corresponding event ID.
|
||||
func focusOnEvent(eventID: String) async
|
||||
func stop()
|
||||
}
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ struct TimelineItemMenuActionProvider {
|
||||
var actions: [TimelineItemMenuAction] = []
|
||||
var secondaryActions: [TimelineItemMenuAction] = []
|
||||
|
||||
if timelineKind == .pinned || timelineKind == .media(.mediaFilesScreen) {
|
||||
if timelineKind == .pinned || timelineKind == .media(.mediaFilesScreen) || timelineKind == .media(.pinnedEventsScreen) {
|
||||
actions.append(.viewInRoomTimeline)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user