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:
Mauro
2026-07-30 13:49:22 +02:00
committed by GitHub
parent 33f8ce2759
commit ae795df530
17 changed files with 243 additions and 110 deletions
@@ -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 {