Open a full-screen preview when tapping a gallery item

Tapping a gallery tile/row opens the existing media preview scoped to that gallery's attachments, paging between them. Introduces a typed MediaPreviewItemID (replacing the stringly-typed preview identifier) and a gallery-scoped TimelineMediaPreviewDataSource; the tap routes through TimelineInteractionHandler/TimelineViewModel. Non-previewable (.other) items are skipped without shifting the tapped index.

Co-authored-by: svetoslav.mitov <svetoslav.mitov@sportinno.com>

just use id instead of previewID

removed horrible multiline ternary

pr suggestions

remove redundant code

remove empty caption display + gallery uses the appropriate media timeline actions
This commit is contained in:
Mauro Romito
2026-07-27 19:14:23 +02:00
committed by Mauro
parent b4f397ed6a
commit 7ea9aeb174
15 changed files with 343 additions and 112 deletions
@@ -159,6 +159,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
Task { await timelineController.processItemDisappearance(id) }
case .mediaTapped(let id):
Task { await handleMediaTapped(with: id) }
case .galleryItemTapped(let id, let index):
handleGalleryItemTapped(itemID: id, index: index)
case .itemSendInfoTapped(let itemID):
handleItemSendInfoTapped(itemID: itemID)
case .toggleReaction(let emoji, let itemID):
@@ -708,6 +710,8 @@ 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 .displayLocation(let location):
actionsSubject.send(.displayLocation(location))
case .displayLiveLocation(let sender, let initialLiveLocationShare):
@@ -718,6 +722,18 @@ 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.")
@@ -833,6 +849,17 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
appMediator: appMediator)
}
private func makeGalleryPreviewViewModel(galleryItem: GalleryRoomTimelineItem,
initialIndex: Int) -> TimelineMediaPreviewViewModel {
TimelineMediaPreviewViewModel(galleryItem: galleryItem,
initialIndex: initialIndex,
timelineViewModel: self,
mediaProvider: userSession.mediaProvider,
photoLibraryManager: PhotoLibraryManager(),
userIndicatorController: userIndicatorController,
appMediator: appMediator)
}
// MARK: - Timeline Item Building
private func buildTimelineViews(timelineItems: [RoomTimelineItemProtocol], isSwitchingTimelines: Bool = false) {