Media gallery - part 1(#3588)

* Introduce a `MediaEventsTimelineFlowCoordinator`
* Update SDK API and architecture
* Add a feature flag, add translations
* Move the media events timeline presentation under the room flow coordinator state machine
* Rename `TimelineViewState.timelineViewState` of type `TimelineState` to `timelineState`
* Enabled SwiftLint's `trailing_closure` rule and fix the warnings.
This commit is contained in:
Stefan Ceriu
2024-12-06 16:58:14 +02:00
committed by GitHub
parent a9e4837b62
commit caaa89af62
83 changed files with 1247 additions and 279 deletions
@@ -23,6 +23,12 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
var paginationState: PaginationState = .initial {
didSet {
callbacks.send(.paginationState(paginationState))
}
}
var timelineItems: [RoomTimelineItemProtocol] = RoomTimelineItemFixtures.default
var timelineItemsTimestamp: [TimelineItemIdentifier: Date] = [:]
@@ -30,9 +36,18 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
init(timelineKind: TimelineKind = .live, listenForSignals: Bool = false) {
self.timelineKind = timelineKind
callbacks.send(.paginationState(PaginationState(backward: .idle, forward: .timelineEndReached)))
paginationState = PaginationState(backward: .idle, forward: .timelineEndReached)
callbacks.send(.isLive(true))
switch timelineKind {
case .media:
timelineItems = (0..<5).reduce([]) { partialResult, _ in
partialResult + RoomTimelineItemFixtures.mediaChunk
}
default:
break
}
guard listenForSignals else { return }
do {
@@ -56,7 +71,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
}
func paginateBackwards(requestSize: UInt16) async -> Result<Void, RoomTimelineControllerError> {
callbacks.send(.paginationState(PaginationState(backward: .paginating, forward: .timelineEndReached)))
paginationState = PaginationState(backward: .paginating, forward: .timelineEndReached)
if client == nil {
try? await simulateBackPagination()
@@ -170,8 +185,8 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
/// Prepends the next chunk of items to the `timelineItems` array.
private func simulateBackPagination() async throws {
defer {
callbacks.send(.paginationState(PaginationState(backward: backPaginationResponses.isEmpty ? .timelineEndReached : .idle,
forward: .timelineEndReached)))
paginationState = PaginationState(backward: backPaginationResponses.isEmpty ? .timelineEndReached : .idle,
forward: .timelineEndReached)
}
guard !backPaginationResponses.isEmpty else { return }
@@ -30,6 +30,12 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
private(set) var timelineItems = [RoomTimelineItemProtocol]()
private(set) var paginationState: PaginationState = .initial {
didSet {
callbacks.send(.paginationState(paginationState))
}
}
var roomID: String {
roomProxy.id
@@ -64,7 +70,8 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
Task {
callbacks.send(.paginationState(PaginationState(backward: .paginating, forward: .paginating)))
paginationState = PaginationState(backward: .paginating, forward: .paginating)
switch await focusOnEvent(initialFocussedEventID, timelineSize: 100) {
case .success:
break
@@ -368,7 +375,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
isSwitchingTimelines = true
// Inform the world that the initial items are loading from the store
callbacks.send(.paginationState(.init(backward: .paginating, forward: .paginating)))
paginationState = PaginationState(backward: .paginating, forward: .paginating)
callbacks.send(.isLive(activeTimelineProvider.kind == .live))
updateTimelineItemsCancellable = activeTimelineProvider
@@ -446,7 +453,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
callbacks.send(.updatedTimelineItems(timelineItems: newTimelineItems, isSwitchingTimelines: isNewTimeline))
callbacks.send(.paginationState(paginationState))
self.paginationState = paginationState
}
private func buildTimelineItem(for itemProxy: TimelineItemProxy) -> RoomTimelineItemProtocol? {
@@ -6,6 +6,7 @@
//
import Foundation
import MatrixRustSDK
struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol {
func buildRoomTimelineController(roomProxy: JoinedRoomProxyProtocol,
@@ -20,12 +21,13 @@ struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol {
appSettings: ServiceLocator.shared.settings)
}
func buildRoomPinnedTimelineController(roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> RoomTimelineControllerProtocol? {
func buildPinnedEventsRoomTimelineController(roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> RoomTimelineControllerProtocol? {
guard let pinnedEventsTimeline = await roomProxy.pinnedEventsTimeline else {
return nil
}
return RoomTimelineController(roomProxy: roomProxy,
timelineProxy: pinnedEventsTimeline,
initialFocussedEventID: nil,
@@ -33,4 +35,21 @@ struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol {
mediaProvider: mediaProvider,
appSettings: ServiceLocator.shared.settings)
}
func buildMessageFilteredRoomTimelineController(allowedMessageTypes: [RoomMessageEventMessageType],
roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> Result<RoomTimelineControllerProtocol, RoomTimelineFactoryControllerError> {
switch await roomProxy.messageFilteredTimeline(allowedMessageTypes: allowedMessageTypes) {
case .success(let timelineProxy):
return .success(RoomTimelineController(roomProxy: roomProxy,
timelineProxy: timelineProxy,
initialFocussedEventID: nil,
timelineItemFactory: timelineItemFactory,
mediaProvider: mediaProvider,
appSettings: ServiceLocator.shared.settings))
case .failure(let error):
return .failure(.roomProxyError(error))
}
}
}
@@ -6,6 +6,11 @@
//
import Foundation
import MatrixRustSDK
enum RoomTimelineFactoryControllerError: Error {
case roomProxyError(RoomProxyError)
}
@MainActor
protocol RoomTimelineControllerFactoryProtocol {
@@ -13,9 +18,15 @@ protocol RoomTimelineControllerFactoryProtocol {
initialFocussedEventID: String?,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) -> RoomTimelineControllerProtocol
func buildRoomPinnedTimelineController(roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> RoomTimelineControllerProtocol?
func buildPinnedEventsRoomTimelineController(roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> RoomTimelineControllerProtocol?
func buildMessageFilteredRoomTimelineController(allowedMessageTypes: [RoomMessageEventMessageType],
roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> Result<RoomTimelineControllerProtocol, RoomTimelineFactoryControllerError>
}
// sourcery: AutoMockable
@@ -31,7 +31,12 @@ protocol RoomTimelineControllerProtocol {
var roomID: String { get }
var timelineKind: TimelineKind { get }
/// The currently known items, use only for setting up the intial state.
var timelineItems: [RoomTimelineItemProtocol] { get }
/// The current pagination state, use only for setting up the intial state
var paginationState: PaginationState { get }
var callbacks: PassthroughSubject<RoomTimelineControllerCallback, Never> { get }
func processItemAppearance(_ itemID: TimelineItemIdentifier) async
@@ -42,7 +42,7 @@ extension AggregatedReaction {
/// Whether to highlight the reaction, indicating that the current user sent this reaction.
var isHighlighted: Bool {
senders.contains(where: { $0.id == accountOwnerID })
senders.contains { $0.id == accountOwnerID }
}
/// The key to be displayed on screen. See `maxDisplayChars`.
@@ -13,7 +13,7 @@ import MatrixRustSDK
/// Its value is consistent only per timeline instance, it should **not** be used to identify an item across timeline instances.
/// - eventOrTransactionID: Contains the 2 possible identifiers of an event, either it has a remote event id or
/// a local transaction id, never both or none.
enum TimelineItemIdentifier: Hashable {
enum TimelineItemIdentifier: Hashable, Sendable {
case event(uniqueID: TimelineUniqueId, eventOrTransactionID: EventOrTransactionId)
case virtual(uniqueID: TimelineUniqueId)
@@ -61,9 +61,10 @@ struct VoiceMessageRoomPlaybackView: View {
if let url = playerState.fileURL {
WaveformView(audioURL: url,
configuration: .init(style: .striped(.init(color: .black, width: waveformLineWidth, spacing: waveformLinePadding)),
verticalScalingFactor: 1.0),
placeholder: { estimatedWaveformView })
.progressMask(progress: playerState.progress)
verticalScalingFactor: 1.0)) {
estimatedWaveformView
}
.progressMask(progress: playerState.progress)
} else {
estimatedWaveformView
}
@@ -92,7 +92,7 @@ final class TimelineProxy: TimelineProxyProtocol {
switch kind {
case .live:
return await paginateBackwardsOnLive(requestSize: requestSize)
case .detached:
case .detached, .media:
return await focussedPaginate(.backwards, requestSize: requestSize)
case .pinned:
return .success(())
@@ -319,7 +319,6 @@ final class TimelineProxy: TimelineProxyProtocol {
return .success(())
}
// swiftlint:disable:next function_parameter_count
func sendVideo(url: URL,
thumbnailURL: URL,
videoInfo: VideoInfo,
@@ -580,7 +579,7 @@ final class TimelineProxy: TimelineProxyProtocol {
MXLog.error("Failed to subscribe to back pagination status with error: \(error)")
}
forwardPaginationStatusSubject.send(.timelineEndReached)
case .detached:
case .detached, .media:
// Detached timelines don't support observation, set the initial state ourself.
backPaginationStatusSubject.send(.idle)
forwardPaginationStatusSubject.send(.idle)
@@ -13,6 +13,7 @@ enum TimelineKind {
case live
case detached
case pinned
case media
}
enum TimelineProxyError: Error {