mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
use the appropriate timeline kind (to be refactored)
This commit is contained in:
@@ -59,7 +59,7 @@ class MediaEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
|
||||
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userSession.clientProxy.userID))
|
||||
|
||||
guard case let .success(mediaTimelineController) = await flowParameters.timelineControllerFactory.buildMessageFilteredTimelineController(focus: .live,
|
||||
allowedMessageTypes: [.image, .video],
|
||||
allowedMessageTypes: [.image, .video, .gallery],
|
||||
presentation: .mediaFilesScreen,
|
||||
roomProxy: roomProxy,
|
||||
timelineItemFactory: timelineItemFactory,
|
||||
@@ -69,7 +69,7 @@ class MediaEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
|
||||
guard case let .success(filesTimelineController) = await flowParameters.timelineControllerFactory.buildMessageFilteredTimelineController(focus: .live,
|
||||
allowedMessageTypes: [.file, .audio],
|
||||
allowedMessageTypes: [.file, .audio, .gallery],
|
||||
presentation: .mediaFilesScreen,
|
||||
roomProxy: roomProxy,
|
||||
timelineItemFactory: timelineItemFactory,
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ enum MediaEventsTimelineScreenViewModelAction {
|
||||
case viewInRoomTimeline(TimelineItemIdentifier)
|
||||
}
|
||||
|
||||
enum MediaEventsTimelineScreenMode {
|
||||
nonisolated enum MediaEventsTimelineScreenMode {
|
||||
case media
|
||||
case files
|
||||
}
|
||||
|
||||
+66
-7
@@ -20,6 +20,15 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
||||
|
||||
private var isOldestItemVisible = false
|
||||
|
||||
/// The gallery attachment that a listed item was built from, keyed by that item's unique ID.
|
||||
/// Interactions are forwarded using the gallery's own identifier, which is the one the timeline knows.
|
||||
private struct GalleryItemLocation {
|
||||
let parentID: TimelineItemIdentifier
|
||||
let mediaIndex: Int
|
||||
}
|
||||
|
||||
private var galleryItemLookup = [TimelineItemIdentifier.UniqueID: GalleryItemLocation]()
|
||||
|
||||
private var activeTimelineViewModel: TimelineViewModelProtocol {
|
||||
switch state.screenMode {
|
||||
case .media:
|
||||
@@ -123,9 +132,15 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
||||
case .oldestItemDidDisappear:
|
||||
isOldestItemVisible = false
|
||||
case .tappedItem(let item):
|
||||
activeTimelineViewModel.context.send(viewAction: .mediaTapped(itemID: item.identifier))
|
||||
if let location = galleryItemLookup[item.id] {
|
||||
activeTimelineViewModel.context.send(viewAction: .galleryItemTapped(itemID: location.parentID, index: location.mediaIndex))
|
||||
} else {
|
||||
activeTimelineViewModel.context.send(viewAction: .mediaTapped(itemID: item.identifier))
|
||||
}
|
||||
case .longPressedItem(let item):
|
||||
activeTimelineViewModel.context.send(viewAction: .displayTimelineItemMenu(itemID: item.identifier))
|
||||
// The menu acts on the gallery as a whole, as its actions can't apply to a single attachment.
|
||||
let itemID = galleryItemLookup[item.id]?.parentID ?? item.identifier
|
||||
activeTimelineViewModel.context.send(viewAction: .displayTimelineItemMenu(itemID: itemID))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,17 +192,25 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
||||
private func updateWithTimelineViewState(_ timelineViewState: TimelineViewState) {
|
||||
var newGroups = [MediaEventsTimelineGroup]()
|
||||
var currentItems = [RoomTimelineItemViewState]()
|
||||
var newGalleryItemLookup = [TimelineItemIdentifier.UniqueID: GalleryItemLocation]()
|
||||
|
||||
timelineViewState.timelineState.itemViewStates.filter { itemViewState in
|
||||
timelineViewState.timelineState.itemViewStates.flatMap { itemViewState -> [RoomTimelineItemViewState] in
|
||||
switch itemViewState.type {
|
||||
case .image, .video:
|
||||
state.screenMode == .media
|
||||
return state.screenMode == .media ? [itemViewState] : []
|
||||
case .audio, .file, .voice:
|
||||
state.screenMode == .files
|
||||
return state.screenMode == .files ? [itemViewState] : []
|
||||
case .separator:
|
||||
true
|
||||
return [itemViewState]
|
||||
case .gallery(let galleryItem):
|
||||
// A gallery's attachments are displayed flattened, listed individually.
|
||||
let flattenedItems = galleryItem.itemsAsIndividualMessages(for: state.screenMode)
|
||||
for (mediaIndex, item) in flattenedItems {
|
||||
newGalleryItemLookup[item.id.uniqueID] = .init(parentID: galleryItem.id, mediaIndex: mediaIndex)
|
||||
}
|
||||
return flattenedItems.map { .init(item: $0.item, groupStyle: .single) }
|
||||
default:
|
||||
false
|
||||
return []
|
||||
}
|
||||
}.reversed().forEach { item in
|
||||
if case .separator(let item) = item.type {
|
||||
@@ -212,6 +235,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
||||
}
|
||||
|
||||
state.groups = newGroups
|
||||
galleryItemLookup = newGalleryItemLookup
|
||||
|
||||
state.isBackPaginating = timelineViewState.timelineState.paginationState.backward == .paginating
|
||||
state.shouldShowEmptyState = newGroups.isEmpty && timelineViewState.timelineState.paginationState.backward == .endReached
|
||||
@@ -259,3 +283,38 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension GalleryRoomTimelineItem {
|
||||
/// Represents the gallery's attachments as though each had been sent as an individual message, so
|
||||
/// that the screen can list them alongside the room's other media, keeping only those that belong
|
||||
/// in the given mode. Each one keeps the gallery's event ID but is given its own unique ID so that
|
||||
/// the attachments remain distinct within the list.
|
||||
func itemsAsIndividualMessages(for screenMode: MediaEventsTimelineScreenMode) -> [(mediaIndex: Int, item: EventBasedMessageTimelineItemProtocol)] {
|
||||
guard let eventOrTransactionID = id.eventOrTransactionID else { return [] }
|
||||
|
||||
return content.items.enumerated().compactMap { mediaIndex, galleryItem in
|
||||
let itemID = TimelineItemIdentifier.event(uniqueID: .init("\(id.uniqueID.value)-\(mediaIndex)"),
|
||||
eventOrTransactionID: eventOrTransactionID)
|
||||
|
||||
let item: EventBasedMessageTimelineItemProtocol? = switch galleryItem {
|
||||
case .image(_, let content) where screenMode == .media:
|
||||
ImageRoomTimelineItem(id: itemID, timestamp: timestamp, isOutgoing: isOutgoing, isEditable: isEditable,
|
||||
canBeRepliedTo: canBeRepliedTo, sender: sender, content: content, properties: properties)
|
||||
case .video(_, let content) where screenMode == .media:
|
||||
VideoRoomTimelineItem(id: itemID, timestamp: timestamp, isOutgoing: isOutgoing, isEditable: isEditable,
|
||||
canBeRepliedTo: canBeRepliedTo, sender: sender, content: content, properties: properties)
|
||||
case .audio(_, let content) where screenMode == .files:
|
||||
AudioRoomTimelineItem(id: itemID, timestamp: timestamp, isOutgoing: isOutgoing, isEditable: isEditable,
|
||||
canBeRepliedTo: canBeRepliedTo, sender: sender, content: content, properties: properties)
|
||||
case .file(_, let content) where screenMode == .files:
|
||||
FileRoomTimelineItem(id: itemID, timestamp: timestamp, isOutgoing: isOutgoing, isEditable: isEditable,
|
||||
canBeRepliedTo: canBeRepliedTo, sender: sender, content: content, properties: properties)
|
||||
default:
|
||||
nil // Attachments of an unknown type, or that belong in the other mode.
|
||||
}
|
||||
|
||||
guard let item else { return nil }
|
||||
return (mediaIndex, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +165,15 @@ enum TimelineFixtures {
|
||||
VideoRoomTimelineItem(isOutgoing: false, caption: "Woah, it was incredible!"),
|
||||
VideoRoomTimelineItem(isOutgoing: true),
|
||||
VoiceMessageRoomTimelineItem(isOutgoing: false),
|
||||
VoiceMessageRoomTimelineItem(isOutgoing: true)
|
||||
VoiceMessageRoomTimelineItem(isOutgoing: true),
|
||||
GalleryRoomTimelineItem(isOutgoing: false,
|
||||
items: [.mockImage(index: 0), .mockVideo(index: 1), .mockImage(index: 2)]),
|
||||
GalleryRoomTimelineItem(isOutgoing: true,
|
||||
items: [.mockImage(index: 0),
|
||||
.mockVideo(index: 1),
|
||||
.mockFile(index: 2, source: try? .init(url: .mockMXCFile, mimeType: nil)),
|
||||
.mockAudio(index: 3, source: try? .init(url: .mockMXCAudio, mimeType: nil))],
|
||||
caption: "Everything from the trip!")
|
||||
]
|
||||
}
|
||||
|
||||
@@ -358,6 +366,18 @@ private extension FileRoomTimelineItem {
|
||||
}
|
||||
}
|
||||
|
||||
private extension GalleryRoomTimelineItem {
|
||||
init(isOutgoing: Bool, items: [GalleryItem], caption: String? = nil) {
|
||||
self.init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: isOutgoing,
|
||||
canBeRepliedTo: true,
|
||||
sender: .init(id: isOutgoing ? "@alice:matrix.org" : "@bob:matrix.org"),
|
||||
content: .init(body: "Gallery", caption: caption, items: items))
|
||||
}
|
||||
}
|
||||
|
||||
private extension ImageRoomTimelineItem {
|
||||
init(isOutgoing: Bool, caption: String? = nil) {
|
||||
self.init(id: .randomEvent,
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:356f63709601d46ee1668e18dd2409233f7be96b9a88f7d9a18b0a9cab2e8469
|
||||
size 156418
|
||||
oid sha256:1ec3f3c099e9ad530b71aeba2715f76f45ff1b4ed7c2459df3da5c49f2e09400
|
||||
size 159454
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:746eed66f8225346510d36ba0a7b4a2fa7b6a11758dc6cd4cdbef6662ce2f2b3
|
||||
size 164016
|
||||
oid sha256:eee23832fec2633137eb1c9d581f7c14ae5f8c1a179d8f265bfa68793ee219b3
|
||||
size 167421
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4a38781ee98ea6056d3b5514f82f37f14da5bedeb914a8b065806f5a7ee95cfd
|
||||
size 100047
|
||||
oid sha256:522ceeb4fd72b4dcee8889505a83e193c6c5cd0e334ed1a11062c323d6a0bd66
|
||||
size 101582
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:11c5f04a623ec21cf870c6101313d24f749f535b55d4f8221413fdb407b6405c
|
||||
size 108263
|
||||
oid sha256:491099140a46ab7c3a3fbd9623226202c6accdc1952a4b690941b7d06f20ba91
|
||||
size 109177
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8570e21cc95a6121da7f1805c78dedd78ea9df082f60c0561bf767d0f4a0bfa3
|
||||
size 746399
|
||||
oid sha256:46ad69d119cecd7c343cd6b108a9c7839d1aa7ec743d4e1122d51e2fb2cf8e35
|
||||
size 1089783
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:884c2061bab04d274b44296c24118b634be4c52c47c099b474870df565109a54
|
||||
size 751705
|
||||
oid sha256:f7310ef85ba8ddc2ff56541ff60dca46fd765f996b11bd55206202f4043ea124
|
||||
size 1095236
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b1fe7781fd4623024860b9d98e7f1810a8f0ac9ae3bb58a53a76352029016f67
|
||||
size 822595
|
||||
oid sha256:9b4a12cc3b586ca0812f786a851243d4ac975f939f0109a29a49cfc99805d6b1
|
||||
size 1293697
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f918cd8bc7462364c993b55e3af42c305b01e94e27a4edd44ec0e44acbf8e19c
|
||||
size 827220
|
||||
oid sha256:56b812e3d7c7af233cdcf9a4b0cd9de29cc826d94ed416b678f39a4038431f97
|
||||
size 1295622
|
||||
|
||||
@@ -340,6 +340,7 @@ struct TimelineMediaPreviewDataSourceTests {
|
||||
TimelineFixtures.mediaChunk
|
||||
.compactMap { $0 as? EventBasedMessageTimelineItemProtocol }
|
||||
.filter(\.supportsMediaCaption) // Voice messages can't be previewed (and don't support captions).
|
||||
.filter { !($0 is GalleryRoomTimelineItem) } // Galleries are previewed through their own data source.
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
|
||||
Reference in New Issue
Block a user