mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +00:00
Allow previews in the timeline to include filtered galleries (#5943)
* remove the usage of the timeline kind override in favour of getting it properly from the timeline view model * allow the previews to include gallery items filtered by the allowed types when started outside the gallery sss asa * fix for actions not being forwarded to the timeline right * pr suggestions
This commit is contained in:
@@ -107,6 +107,14 @@ class TimelineInteractionHandler {
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
func handleTimelineItemMenuAction(_ action: TimelineItemMenuAction, itemID: TimelineItemIdentifier) {
|
||||
// Redacting needs the event alone, so it works even when the item isn't part of this timeline,
|
||||
// such as one held by a media preview that was built from a different one.
|
||||
if case .redact = action {
|
||||
guard case let .event(_, eventOrTransactionID) = itemID else { fatalError() }
|
||||
Task { await timelineController.redact(eventOrTransactionID) }
|
||||
return
|
||||
}
|
||||
|
||||
guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID),
|
||||
let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else {
|
||||
return
|
||||
@@ -156,8 +164,7 @@ class TimelineInteractionHandler {
|
||||
UIPasteboard.general.url = permalinkURL
|
||||
}
|
||||
case .redact:
|
||||
guard case let .event(_, eventOrTransactionID) = itemID else { fatalError() }
|
||||
Task { await timelineController.redact(eventOrTransactionID) }
|
||||
break // Handled above, before the timeline item is looked up.
|
||||
case .reply:
|
||||
guard let eventID = eventTimelineItem.id.eventID else { return }
|
||||
|
||||
@@ -560,29 +567,23 @@ class TimelineInteractionHandler {
|
||||
timestamp: item.timestamp,
|
||||
timeoutDate: item.content.timeoutDate)
|
||||
return .displayLiveLocation(sender: item.sender, initialLiveLocationShare: initialLiveLocationShare)
|
||||
// Galleries are included so that their attachments can be browsed as individual media.
|
||||
case let item as ImageRoomTimelineItem:
|
||||
return await mediaPreviewAction(for: item, messageTypes: [.image, .video])
|
||||
return await mediaPreviewAction(for: item, messageTypes: [.image, .video, .gallery])
|
||||
case let item as VideoRoomTimelineItem:
|
||||
return await mediaPreviewAction(for: item, messageTypes: [.image, .video])
|
||||
return await mediaPreviewAction(for: item, messageTypes: [.image, .video, .gallery])
|
||||
case let item as AudioRoomTimelineItem:
|
||||
return await mediaPreviewAction(for: item, messageTypes: [.audio, .file])
|
||||
return await mediaPreviewAction(for: item, messageTypes: [.audio, .file, .gallery])
|
||||
case let item as FileRoomTimelineItem:
|
||||
return await mediaPreviewAction(for: item, messageTypes: [.audio, .file])
|
||||
return await mediaPreviewAction(for: item, messageTypes: [.audio, .file, .gallery])
|
||||
case let item as GalleryRoomTimelineItem:
|
||||
// Only galleries are needed as the preview is scoped to the attachments of the tapped one.
|
||||
return await mediaPreviewAction(for: item, messageTypes: [.gallery])
|
||||
default:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a media preview scoped to a single gallery's attachments. The preview pages
|
||||
/// only between the items of that one gallery — siblings in the wider timeline aren't
|
||||
/// reachable from this entry point (use the regular media tap for that).
|
||||
func processGalleryItemTap(itemID: TimelineItemIdentifier, index: Int) -> TimelineControllerAction {
|
||||
guard let galleryItem = timelineController.timelineItems.firstUsingStableID(itemID) as? GalleryRoomTimelineItem else {
|
||||
return .none
|
||||
}
|
||||
return .displayGalleryPreview(galleryItem: galleryItem, initialIndex: index)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func buildReplyInfo(for item: EventBasedTimelineItemProtocol) -> ReplyInfo {
|
||||
@@ -613,7 +614,10 @@ class TimelineInteractionHandler {
|
||||
case .pinned:
|
||||
newTimelineFocus = .pinned
|
||||
newTimelinePresentation = .pinnedEventsScreen
|
||||
case .media, .thread:
|
||||
case .thread(let rootEventID):
|
||||
newTimelineFocus = .thread(eventID: rootEventID)
|
||||
newTimelinePresentation = .roomScreenThread
|
||||
case .media:
|
||||
break // We don't need to create a new timeline as it is already filtered.
|
||||
}
|
||||
|
||||
@@ -644,11 +648,21 @@ class TimelineInteractionHandler {
|
||||
linkMetadataProvider: linkMetadataProvider,
|
||||
timelineControllerFactory: timelineControllerFactory)
|
||||
|
||||
return .displayMediaPreview(item: item, timelineViewModel: .new(timelineViewModel))
|
||||
return previewAction(for: item, timelineViewModel: .new(timelineViewModel))
|
||||
} else {
|
||||
return .displayMediaPreview(item: item, timelineViewModel: .active)
|
||||
return previewAction(for: item, timelineViewModel: .active)
|
||||
}
|
||||
}
|
||||
|
||||
/// A gallery is previewed scoped to its own attachments rather than the wider timeline's media.
|
||||
private func previewAction(for item: EventBasedMessageTimelineItemProtocol,
|
||||
timelineViewModel: TimelineControllerAction.TimelineViewModelKind) -> TimelineControllerAction {
|
||||
guard let galleryItem = item as? GalleryRoomTimelineItem else {
|
||||
return .displayMediaPreview(item: item, timelineViewModel: timelineViewModel)
|
||||
}
|
||||
|
||||
return .displayGalleryPreview(galleryItem: galleryItem, timelineViewModel: timelineViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
private struct ReplyInfo {
|
||||
|
||||
@@ -104,6 +104,8 @@ enum TimelineComposerAction {
|
||||
|
||||
struct TimelineViewState: BindableState {
|
||||
let timelineKind: TimelineKind
|
||||
/// The gallery attachments the timeline includes, or `nil` when it isn't filtered.
|
||||
let allowedGalleryItemTypes: [TimelineAllowedGalleryItemType]?
|
||||
var roomID: String
|
||||
var members: [String: RoomMemberState] = [:]
|
||||
var typingMembers: [String] = []
|
||||
|
||||
@@ -92,6 +92,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
true
|
||||
}
|
||||
super.init(initialViewState: TimelineViewState(timelineKind: timelineController.timelineKind,
|
||||
allowedGalleryItemTypes: timelineController.allowedGalleryItemTypes,
|
||||
roomID: roomProxy.id,
|
||||
isDM: roomProxy.infoPublisher.value.isDM,
|
||||
timelineState: TimelineState(focussedEvent: focussedEventID.map { .init(eventID: $0, appearance: .immediate) }),
|
||||
@@ -160,7 +161,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
case .mediaTapped(let id):
|
||||
Task { await handleMediaTapped(with: id) }
|
||||
case .galleryItemTapped(let id, let index):
|
||||
handleGalleryItemTapped(itemID: id, index: index)
|
||||
Task { await handleMediaTapped(with: id, galleryIndex: index) }
|
||||
case .itemSendInfoTapped(let itemID):
|
||||
handleItemSendInfoTapped(itemID: itemID)
|
||||
case .toggleReaction(let emoji, let itemID):
|
||||
@@ -700,7 +701,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
await timelineController.sendReadReceipt(for: lastVisibleItemID)
|
||||
}
|
||||
|
||||
private func handleMediaTapped(with itemID: TimelineItemIdentifier) async {
|
||||
private func handleMediaTapped(with itemID: TimelineItemIdentifier, galleryIndex: Int? = nil) async {
|
||||
state.showLoading = true
|
||||
let action = await timelineInteractionHandler.processItemTap(itemID)
|
||||
|
||||
@@ -710,8 +711,13 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
|
||||
let mediaPreviewViewModel = makeMediaPreviewViewModel(item: item, timelineViewModelKind: timelineViewModelKind)
|
||||
actionsSubject.send(.displayMediaPreview(mediaPreviewViewModel))
|
||||
case .displayGalleryPreview:
|
||||
fatalError("Galleries open through the dedicated galleryItemTapped path, which carries the tapped index.")
|
||||
case .displayGalleryPreview(let galleryItem, let timelineViewModelKind):
|
||||
actionsSubject.send(.composer(action: .removeFocus))
|
||||
|
||||
let mediaPreviewViewModel = makeGalleryPreviewViewModel(galleryItem: galleryItem,
|
||||
timelineViewModelKind: timelineViewModelKind,
|
||||
initialIndex: galleryIndex ?? 0)
|
||||
actionsSubject.send(.displayMediaPreview(mediaPreviewViewModel))
|
||||
case .displayLocation(let location):
|
||||
actionsSubject.send(.displayLocation(location))
|
||||
case .displayLiveLocation(let sender, let initialLiveLocationShare):
|
||||
@@ -722,18 +728,6 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
state.showLoading = false
|
||||
}
|
||||
|
||||
private func handleGalleryItemTapped(itemID: TimelineItemIdentifier, index: Int) {
|
||||
if case let .displayGalleryPreview(galleryItem, initialIndex) = timelineInteractionHandler.processGalleryItemTap(itemID: itemID, index: index) {
|
||||
displayGalleryPreview(galleryItem: galleryItem, initialIndex: initialIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private func displayGalleryPreview(galleryItem: GalleryRoomTimelineItem, initialIndex: Int) {
|
||||
actionsSubject.send(.composer(action: .removeFocus))
|
||||
let mediaPreviewViewModel = makeGalleryPreviewViewModel(galleryItem: galleryItem, initialIndex: initialIndex)
|
||||
actionsSubject.send(.displayMediaPreview(mediaPreviewViewModel))
|
||||
}
|
||||
|
||||
private func handleItemSendInfoTapped(itemID: TimelineItemIdentifier) {
|
||||
guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID) else {
|
||||
MXLog.warning("Couldn't find timeline item.")
|
||||
@@ -836,30 +830,33 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
|
||||
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: userSession.mediaProvider,
|
||||
photoLibraryManager: PhotoLibraryManager(),
|
||||
userIndicatorController: userIndicatorController,
|
||||
appMediator: appMediator)
|
||||
}
|
||||
|
||||
private func makeGalleryPreviewViewModel(galleryItem: GalleryRoomTimelineItem,
|
||||
initialIndex: Int) -> TimelineMediaPreviewViewModel {
|
||||
TimelineMediaPreviewViewModel(galleryItem: galleryItem,
|
||||
initialIndex: initialIndex,
|
||||
timelineViewModel: self,
|
||||
TimelineMediaPreviewViewModel(initialItem: item,
|
||||
timelineViewModel: timelineViewModel(for: timelineViewModelKind),
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
photoLibraryManager: PhotoLibraryManager(),
|
||||
userIndicatorController: userIndicatorController,
|
||||
appMediator: appMediator)
|
||||
}
|
||||
|
||||
private func makeGalleryPreviewViewModel(galleryItem: GalleryRoomTimelineItem,
|
||||
timelineViewModelKind: TimelineControllerAction.TimelineViewModelKind,
|
||||
initialIndex: Int) -> TimelineMediaPreviewViewModel {
|
||||
TimelineMediaPreviewViewModel(galleryItem: galleryItem,
|
||||
initialIndex: initialIndex,
|
||||
timelineViewModel: timelineViewModel(for: timelineViewModelKind),
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
photoLibraryManager: PhotoLibraryManager(),
|
||||
userIndicatorController: userIndicatorController,
|
||||
appMediator: appMediator)
|
||||
}
|
||||
|
||||
private func timelineViewModel(for kind: TimelineControllerAction.TimelineViewModelKind) -> TimelineViewModel {
|
||||
switch kind {
|
||||
case .active: self
|
||||
case .new(let newViewModel): newViewModel
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Timeline Item Building
|
||||
|
||||
private func buildTimelineViews(timelineItems: [RoomTimelineItemProtocol], isSwitchingTimelines: Bool = false) {
|
||||
|
||||
@@ -52,7 +52,7 @@ struct GalleryItemTileView: View {
|
||||
private var content: some View {
|
||||
if let source = item.thumbnailSource ?? item.mediaSource {
|
||||
LoadableImage(mediaSource: source,
|
||||
mediaType: .timelineItem(uniqueID: item.id.timelineItemUniqueID),
|
||||
mediaType: .timelineItem(uniqueID: item.id.timelineItemID.uniqueID),
|
||||
blurhash: item.blurhash,
|
||||
size: item.size,
|
||||
mediaProvider: mediaProvider) { imageView in
|
||||
|
||||
@@ -110,7 +110,7 @@ private struct GalleryListRow: View {
|
||||
private var icon: some View {
|
||||
if item.hasThumbnail, let source = item.thumbnailSource ?? item.mediaSource {
|
||||
LoadableImage(mediaSource: source,
|
||||
mediaType: .timelineItem(uniqueID: item.id.timelineItemUniqueID),
|
||||
mediaType: .timelineItem(uniqueID: item.id.timelineItemID.uniqueID),
|
||||
blurhash: item.blurhash,
|
||||
size: item.size,
|
||||
mediaProvider: mediaProvider) { imageView in
|
||||
|
||||
Reference in New Issue
Block a user