mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 13:37:11 +00:00
Use the new preview screen when tapping media on the room and pinned events screens. (#3736)
* Use the new TimelineMediaPreview modifier on the room and pinned timeline screens. * Use the same presentation logic for all timeline media previews. * Fix a bug with the detection of the timeline end. * Send pagination requests from the media preview screen. * Add SwiftLint to the Danger workflow (it is no longer installed on the runner). * Put SwiftLint back on all of the GitHub runners too. * Set the function_parameter_count lint rule to 10. * Make sure to clean-up any previews when the coordinator is done. * Handle the viewInRoomTimeline action more appropriately.
This commit is contained in:
parent
921d1c627d
commit
cfaa1b455a
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
- name: Setup environment
|
||||
run:
|
||||
brew install danger/tap/danger-swift
|
||||
brew install danger/tap/danger-swift swiftlint
|
||||
|
||||
- name: Danger
|
||||
run:
|
||||
|
@ -41,6 +41,10 @@ function_body_length:
|
||||
warning: 100
|
||||
error: 100
|
||||
|
||||
function_parameter_count:
|
||||
warning: 10
|
||||
error: 10
|
||||
|
||||
cyclomatic_complexity:
|
||||
ignores_case_statements: true
|
||||
|
||||
|
@ -64,7 +64,8 @@ class MediaEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
|
||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userSession.clientProxy.userID))
|
||||
|
||||
guard case let .success(mediaTimelineController) = await timelineControllerFactory.buildMessageFilteredTimelineController(allowedMessageTypes: [.image, .video],
|
||||
guard case let .success(mediaTimelineController) = await timelineControllerFactory.buildMessageFilteredTimelineController(focus: .live,
|
||||
allowedMessageTypes: [.image, .video],
|
||||
presentation: .mediaFilesScreen,
|
||||
roomProxy: roomProxy,
|
||||
timelineItemFactory: timelineItemFactory,
|
||||
@ -73,7 +74,8 @@ class MediaEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
guard case let .success(filesTimelineController) = await timelineControllerFactory.buildMessageFilteredTimelineController(allowedMessageTypes: [.file, .audio],
|
||||
guard case let .success(filesTimelineController) = await timelineControllerFactory.buildMessageFilteredTimelineController(focus: .live,
|
||||
allowedMessageTypes: [.file, .audio],
|
||||
presentation: .mediaFilesScreen,
|
||||
roomProxy: roomProxy,
|
||||
timelineItemFactory: timelineItemFactory,
|
||||
|
@ -6158,15 +6158,15 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
|
||||
}
|
||||
//MARK: - messageFilteredTimeline
|
||||
|
||||
var messageFilteredTimelineAllowedMessageTypesPresentationUnderlyingCallsCount = 0
|
||||
var messageFilteredTimelineAllowedMessageTypesPresentationCallsCount: Int {
|
||||
var messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingCallsCount = 0
|
||||
var messageFilteredTimelineFocusAllowedMessageTypesPresentationCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return messageFilteredTimelineAllowedMessageTypesPresentationUnderlyingCallsCount
|
||||
return messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = messageFilteredTimelineAllowedMessageTypesPresentationUnderlyingCallsCount
|
||||
returnValue = messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
@ -6174,29 +6174,29 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
messageFilteredTimelineAllowedMessageTypesPresentationUnderlyingCallsCount = newValue
|
||||
messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
messageFilteredTimelineAllowedMessageTypesPresentationUnderlyingCallsCount = newValue
|
||||
messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var messageFilteredTimelineAllowedMessageTypesPresentationCalled: Bool {
|
||||
return messageFilteredTimelineAllowedMessageTypesPresentationCallsCount > 0
|
||||
var messageFilteredTimelineFocusAllowedMessageTypesPresentationCalled: Bool {
|
||||
return messageFilteredTimelineFocusAllowedMessageTypesPresentationCallsCount > 0
|
||||
}
|
||||
var messageFilteredTimelineAllowedMessageTypesPresentationReceivedArguments: (allowedMessageTypes: [RoomMessageEventMessageType], presentation: TimelineKind.MediaPresentation)?
|
||||
var messageFilteredTimelineAllowedMessageTypesPresentationReceivedInvocations: [(allowedMessageTypes: [RoomMessageEventMessageType], presentation: TimelineKind.MediaPresentation)] = []
|
||||
var messageFilteredTimelineFocusAllowedMessageTypesPresentationReceivedArguments: (focus: TimelineFocus, allowedMessageTypes: [TimelineAllowedMessageType], presentation: TimelineKind.MediaPresentation)?
|
||||
var messageFilteredTimelineFocusAllowedMessageTypesPresentationReceivedInvocations: [(focus: TimelineFocus, allowedMessageTypes: [TimelineAllowedMessageType], presentation: TimelineKind.MediaPresentation)] = []
|
||||
|
||||
var messageFilteredTimelineAllowedMessageTypesPresentationUnderlyingReturnValue: Result<TimelineProxyProtocol, RoomProxyError>!
|
||||
var messageFilteredTimelineAllowedMessageTypesPresentationReturnValue: Result<TimelineProxyProtocol, RoomProxyError>! {
|
||||
var messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingReturnValue: Result<TimelineProxyProtocol, RoomProxyError>!
|
||||
var messageFilteredTimelineFocusAllowedMessageTypesPresentationReturnValue: Result<TimelineProxyProtocol, RoomProxyError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return messageFilteredTimelineAllowedMessageTypesPresentationUnderlyingReturnValue
|
||||
return messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<TimelineProxyProtocol, RoomProxyError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = messageFilteredTimelineAllowedMessageTypesPresentationUnderlyingReturnValue
|
||||
returnValue = messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
@ -6204,26 +6204,26 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
messageFilteredTimelineAllowedMessageTypesPresentationUnderlyingReturnValue = newValue
|
||||
messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
messageFilteredTimelineAllowedMessageTypesPresentationUnderlyingReturnValue = newValue
|
||||
messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var messageFilteredTimelineAllowedMessageTypesPresentationClosure: (([RoomMessageEventMessageType], TimelineKind.MediaPresentation) async -> Result<TimelineProxyProtocol, RoomProxyError>)?
|
||||
var messageFilteredTimelineFocusAllowedMessageTypesPresentationClosure: ((TimelineFocus, [TimelineAllowedMessageType], TimelineKind.MediaPresentation) async -> Result<TimelineProxyProtocol, RoomProxyError>)?
|
||||
|
||||
func messageFilteredTimeline(allowedMessageTypes: [RoomMessageEventMessageType], presentation: TimelineKind.MediaPresentation) async -> Result<TimelineProxyProtocol, RoomProxyError> {
|
||||
messageFilteredTimelineAllowedMessageTypesPresentationCallsCount += 1
|
||||
messageFilteredTimelineAllowedMessageTypesPresentationReceivedArguments = (allowedMessageTypes: allowedMessageTypes, presentation: presentation)
|
||||
func messageFilteredTimeline(focus: TimelineFocus, allowedMessageTypes: [TimelineAllowedMessageType], presentation: TimelineKind.MediaPresentation) async -> Result<TimelineProxyProtocol, RoomProxyError> {
|
||||
messageFilteredTimelineFocusAllowedMessageTypesPresentationCallsCount += 1
|
||||
messageFilteredTimelineFocusAllowedMessageTypesPresentationReceivedArguments = (focus: focus, allowedMessageTypes: allowedMessageTypes, presentation: presentation)
|
||||
DispatchQueue.main.async {
|
||||
self.messageFilteredTimelineAllowedMessageTypesPresentationReceivedInvocations.append((allowedMessageTypes: allowedMessageTypes, presentation: presentation))
|
||||
self.messageFilteredTimelineFocusAllowedMessageTypesPresentationReceivedInvocations.append((focus: focus, allowedMessageTypes: allowedMessageTypes, presentation: presentation))
|
||||
}
|
||||
if let messageFilteredTimelineAllowedMessageTypesPresentationClosure = messageFilteredTimelineAllowedMessageTypesPresentationClosure {
|
||||
return await messageFilteredTimelineAllowedMessageTypesPresentationClosure(allowedMessageTypes, presentation)
|
||||
if let messageFilteredTimelineFocusAllowedMessageTypesPresentationClosure = messageFilteredTimelineFocusAllowedMessageTypesPresentationClosure {
|
||||
return await messageFilteredTimelineFocusAllowedMessageTypesPresentationClosure(focus, allowedMessageTypes, presentation)
|
||||
} else {
|
||||
return messageFilteredTimelineAllowedMessageTypesPresentationReturnValue
|
||||
return messageFilteredTimelineFocusAllowedMessageTypesPresentationReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - enableEncryption
|
||||
@ -14488,15 +14488,15 @@ class TimelineControllerFactoryMock: TimelineControllerFactoryProtocol, @uncheck
|
||||
}
|
||||
//MARK: - buildMessageFilteredTimelineController
|
||||
|
||||
var buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = 0
|
||||
var buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderCallsCount: Int {
|
||||
var buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = 0
|
||||
var buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount
|
||||
return buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount
|
||||
returnValue = buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
@ -14504,29 +14504,29 @@ class TimelineControllerFactoryMock: TimelineControllerFactoryProtocol, @uncheck
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue
|
||||
buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue
|
||||
buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderCalled: Bool {
|
||||
return buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderCallsCount > 0
|
||||
var buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderCalled: Bool {
|
||||
return buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderCallsCount > 0
|
||||
}
|
||||
var buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReceivedArguments: (allowedMessageTypes: [RoomMessageEventMessageType], presentation: TimelineKind.MediaPresentation, roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)?
|
||||
var buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReceivedInvocations: [(allowedMessageTypes: [RoomMessageEventMessageType], presentation: TimelineKind.MediaPresentation, roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)] = []
|
||||
var buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReceivedArguments: (focus: TimelineFocus, allowedMessageTypes: [TimelineAllowedMessageType], presentation: TimelineKind.MediaPresentation, roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)?
|
||||
var buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReceivedInvocations: [(focus: TimelineFocus, allowedMessageTypes: [TimelineAllowedMessageType], presentation: TimelineKind.MediaPresentation, roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)] = []
|
||||
|
||||
var buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue: Result<TimelineControllerProtocol, TimelineFactoryControllerError>!
|
||||
var buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReturnValue: Result<TimelineControllerProtocol, TimelineFactoryControllerError>! {
|
||||
var buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue: Result<TimelineControllerProtocol, TimelineFactoryControllerError>!
|
||||
var buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReturnValue: Result<TimelineControllerProtocol, TimelineFactoryControllerError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue
|
||||
return buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<TimelineControllerProtocol, TimelineFactoryControllerError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue
|
||||
returnValue = buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
@ -14534,26 +14534,26 @@ class TimelineControllerFactoryMock: TimelineControllerFactoryProtocol, @uncheck
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue
|
||||
buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue
|
||||
buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderClosure: (([RoomMessageEventMessageType], TimelineKind.MediaPresentation, JoinedRoomProxyProtocol, RoomTimelineItemFactoryProtocol, MediaProviderProtocol) async -> Result<TimelineControllerProtocol, TimelineFactoryControllerError>)?
|
||||
var buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderClosure: ((TimelineFocus, [TimelineAllowedMessageType], TimelineKind.MediaPresentation, JoinedRoomProxyProtocol, RoomTimelineItemFactoryProtocol, MediaProviderProtocol) async -> Result<TimelineControllerProtocol, TimelineFactoryControllerError>)?
|
||||
|
||||
func buildMessageFilteredTimelineController(allowedMessageTypes: [RoomMessageEventMessageType], presentation: TimelineKind.MediaPresentation, roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol) async -> Result<TimelineControllerProtocol, TimelineFactoryControllerError> {
|
||||
buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderCallsCount += 1
|
||||
buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReceivedArguments = (allowedMessageTypes: allowedMessageTypes, presentation: presentation, roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider)
|
||||
func buildMessageFilteredTimelineController(focus: TimelineFocus, allowedMessageTypes: [TimelineAllowedMessageType], presentation: TimelineKind.MediaPresentation, roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol) async -> Result<TimelineControllerProtocol, TimelineFactoryControllerError> {
|
||||
buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderCallsCount += 1
|
||||
buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReceivedArguments = (focus: focus, allowedMessageTypes: allowedMessageTypes, presentation: presentation, roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider)
|
||||
DispatchQueue.main.async {
|
||||
self.buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReceivedInvocations.append((allowedMessageTypes: allowedMessageTypes, presentation: presentation, roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider))
|
||||
self.buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReceivedInvocations.append((focus: focus, allowedMessageTypes: allowedMessageTypes, presentation: presentation, roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider))
|
||||
}
|
||||
if let buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderClosure = buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderClosure {
|
||||
return await buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderClosure(allowedMessageTypes, presentation, roomProxy, timelineItemFactory, mediaProvider)
|
||||
if let buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderClosure = buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderClosure {
|
||||
return await buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderClosure(focus, allowedMessageTypes, presentation, roomProxy, timelineItemFactory, mediaProvider)
|
||||
} else {
|
||||
return buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReturnValue
|
||||
return buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReturnValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,9 +41,16 @@ class TimelineMediaPreviewDataSource: NSObject, QLPreviewControllerDataSource {
|
||||
previewItems = itemViewStates.compactMap(TimelineMediaPreviewItem.Media.init)
|
||||
self.initialItem = initialItem
|
||||
|
||||
let initialItemArrayIndex = previewItems.firstIndex { $0.id == initialItem.id } ?? 0
|
||||
if let initialItemArrayIndex = previewItems.firstIndex(where: { $0.id == initialItem.id.eventOrTransactionID }) {
|
||||
initialItemIndex = initialItemArrayIndex + initialPadding
|
||||
currentItem = .media(previewItems[initialItemArrayIndex])
|
||||
} else {
|
||||
// The timeline hasn't loaded the initial item yet, so replace the whatever was loaded with
|
||||
// the item the user wants to preview.
|
||||
initialItemIndex = initialPadding
|
||||
previewItems = [.init(timelineItem: initialItem)]
|
||||
currentItem = .media(previewItems[0])
|
||||
}
|
||||
|
||||
backwardPadding = initialPadding
|
||||
forwardPadding = initialPadding
|
||||
@ -83,10 +90,18 @@ class TimelineMediaPreviewDataSource: NSObject, QLPreviewControllerDataSource {
|
||||
hasPaginated = true
|
||||
}
|
||||
} else {
|
||||
// Do nothing! Not ideal but if we reload the data source the current item will
|
||||
// also be, reloaded resetting any interaction the user has made with it. If we
|
||||
// ignore the pagination, then the next time they swipe they'll land on a different
|
||||
// When the timeline is loading items from the store and the initial item is the only
|
||||
// preview in the array, we don't want to wipe it out, so if the existing items aren't
|
||||
// found within the new items then let's ignore the update for now. This comes with a
|
||||
// tradeoff that when a media gets redacted, no more previews will be added to the viewer.
|
||||
//
|
||||
// Note for the future if anyone wants to fix the redaction issue: Reloading the data source,
|
||||
// will also reload the current item resetting any interaction the user has made with it.
|
||||
// If you ignore the pagination, then the next time they swipe they'll land on a different
|
||||
// media but this is probably less jarring overall. I hate QLPreviewController!
|
||||
|
||||
MXLog.info("Ignoring update: unable to find existing preview items range.")
|
||||
return
|
||||
}
|
||||
|
||||
previewItems = newItems
|
||||
@ -109,9 +124,9 @@ class TimelineMediaPreviewDataSource: NSObject, QLPreviewControllerDataSource {
|
||||
let arrayIndex = index - backwardPadding
|
||||
|
||||
if index < firstPreviewItemIndex {
|
||||
return paginationState.backward == .timelineEndReached ? TimelineMediaPreviewItem.Loading.timelineStart : .paginating
|
||||
return paginationState.backward == .timelineEndReached ? TimelineMediaPreviewItem.Loading.timelineStart : .paginatingBackwards
|
||||
} else if index > lastPreviewItemIndex {
|
||||
return paginationState.forward == .timelineEndReached ? TimelineMediaPreviewItem.Loading.timelineEnd : .paginating
|
||||
return paginationState.forward == .timelineEndReached ? TimelineMediaPreviewItem.Loading.timelineEnd : .paginatingForwards
|
||||
} else {
|
||||
return previewItems[arrayIndex]
|
||||
}
|
||||
@ -151,7 +166,15 @@ enum TimelineMediaPreviewItem: Equatable {
|
||||
|
||||
// MARK: Identifiable
|
||||
|
||||
var id: TimelineItemIdentifier { timelineItem.id }
|
||||
/// The timeline item's event or transaction ID.
|
||||
///
|
||||
/// We're identifying items by this to ensure that all matching is made using only this part of the identifier. This is
|
||||
/// because the unique ID will be different across timelines so when the initial item comes from a regular timeline and
|
||||
/// we build a filtered timeline to fetch the other media items, it is impossible to match by the `TimelineItemIdentifier`.
|
||||
var id: TimelineItemIdentifier.EventOrTransactionID {
|
||||
guard let id = timelineItem.id.eventOrTransactionID else { fatalError("Virtual items cannot be previewed.") }
|
||||
return id
|
||||
}
|
||||
|
||||
// MARK: QLPreviewItem
|
||||
|
||||
@ -274,11 +297,12 @@ enum TimelineMediaPreviewItem: Equatable {
|
||||
}
|
||||
|
||||
class Loading: NSObject, QLPreviewItem {
|
||||
static let paginating = Loading(state: .paginating)
|
||||
static let paginatingBackwards = Loading(state: .paginating(.backwards))
|
||||
static let paginatingForwards = Loading(state: .paginating(.forwards))
|
||||
static let timelineStart = Loading(state: .timelineStart)
|
||||
static let timelineEnd = Loading(state: .timelineEnd)
|
||||
|
||||
enum State { case paginating, timelineStart, timelineEnd }
|
||||
enum State { case paginating(PaginationDirection), timelineStart, timelineEnd }
|
||||
let state: State
|
||||
|
||||
let previewItemURL: URL? = nil
|
||||
|
@ -14,7 +14,7 @@ enum TimelineMediaPreviewViewModelAction: Equatable {
|
||||
}
|
||||
|
||||
enum TimelineMediaPreviewDriverAction {
|
||||
case itemLoaded(TimelineItemIdentifier)
|
||||
case itemLoaded(TimelineItemIdentifier.EventOrTransactionID)
|
||||
case showItemDetails(TimelineMediaPreviewItem.Media)
|
||||
case exportFile(TimelineMediaPreviewFileExportPicker.File)
|
||||
case authorizationRequired(appMediator: AppMediatorProtocol)
|
||||
|
@ -63,7 +63,11 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
|
||||
timelineViewModel.context.$viewState.map(\.timelineState.paginationState)
|
||||
.removeDuplicates()
|
||||
.weakAssign(to: \.state.dataSource.paginationState, on: self)
|
||||
.sink { [weak self] paginationState in
|
||||
guard let self else { return }
|
||||
state.dataSource.paginationState = paginationState
|
||||
paginateIfNeeded()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
@ -77,7 +81,7 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
switch action {
|
||||
case .viewInRoomTimeline:
|
||||
state.previewControllerDriver.send(.dismissDetailsSheet)
|
||||
actionsSubject.send(.viewInRoomTimeline(item.id))
|
||||
actionsSubject.send(.viewInRoomTimeline(item.timelineItem.id))
|
||||
case .save:
|
||||
Task { await saveCurrentItem() }
|
||||
case .redact:
|
||||
@ -111,6 +115,23 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
mediaItem.downloadError = error
|
||||
}
|
||||
}
|
||||
} else {
|
||||
paginateIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
private func paginateIfNeeded() {
|
||||
switch state.currentItem {
|
||||
case .loading(.paginatingBackwards):
|
||||
if state.dataSource.paginationState.backward == .idle {
|
||||
timelineViewModel.context.send(viewAction: .paginateBackwards)
|
||||
}
|
||||
case .loading(.paginatingForwards):
|
||||
if state.dataSource.paginationState.forward == .idle {
|
||||
timelineViewModel.context.send(viewAction: .paginateForwards)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,7 +187,7 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
}
|
||||
|
||||
private func redactItem(_ item: TimelineMediaPreviewItem.Media) {
|
||||
timelineViewModel.context.send(viewAction: .handleTimelineItemMenuAction(itemID: item.id, action: .redact))
|
||||
timelineViewModel.context.send(viewAction: .handleTimelineItemMenuAction(itemID: item.timelineItem.id, action: .redact))
|
||||
state.bindings.redactConfirmationItem = nil
|
||||
state.previewControllerDriver.send(.dismissDetailsSheet)
|
||||
actionsSubject.send(.dismiss)
|
||||
|
@ -182,7 +182,7 @@ class TimelineMediaPreviewController: QLPreviewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func handleFileLoaded(itemID: TimelineItemIdentifier) {
|
||||
private func handleFileLoaded(itemID: TimelineItemIdentifier.EventOrTransactionID) {
|
||||
guard (currentPreviewItem as? TimelineMediaPreviewItem.Media)?.id == itemID else { return }
|
||||
refreshCurrentPreviewItem()
|
||||
}
|
||||
@ -301,7 +301,8 @@ private struct DownloadIndicatorView: View {
|
||||
private var shouldShowDownloadIndicator: Bool {
|
||||
switch currentItem {
|
||||
case .media(let mediaItem): mediaItem.fileHandle == nil
|
||||
case .loading(let loadingItem): loadingItem.state == .paginating
|
||||
case .loading(.paginatingBackwards), .loading(.paginatingForwards): true
|
||||
case .loading: false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -222,7 +222,7 @@ struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePrevie
|
||||
thumbnailInfo: .mockThumbnail,
|
||||
contentType: contentType))
|
||||
|
||||
let timelineKind = TimelineKind.media(isPresentedOnRoomScreen ? .roomScreen : .mediaFilesScreen)
|
||||
let timelineKind = TimelineKind.media(isPresentedOnRoomScreen ? .roomScreenLive : .mediaFilesScreen)
|
||||
let timelineController = MockTimelineController(timelineKind: timelineKind)
|
||||
timelineController.timelineItems = [item]
|
||||
|
||||
|
@ -63,7 +63,7 @@ struct TimelineMediaPreviewRedactConfirmationView: View {
|
||||
.scaledFrame(size: 40)
|
||||
.background {
|
||||
LoadableImage(mediaSource: mediaSource,
|
||||
mediaType: .timelineItem(uniqueID: item.id.uniqueID),
|
||||
mediaType: .generic,
|
||||
blurhash: item.blurhash,
|
||||
mediaProvider: context.mediaProvider) {
|
||||
Color.compound.bgSubtleSecondary
|
||||
|
@ -79,6 +79,10 @@ final class MediaEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func stop() {
|
||||
viewModel.stop()
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(MediaEventsTimelineScreen(context: viewModel.context))
|
||||
}
|
||||
|
@ -64,6 +64,19 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
mediaTimelineViewModel.actions.sink { [weak self] action in
|
||||
switch action {
|
||||
case .displayMediaPreview(let mediaPreviewViewModel):
|
||||
self?.displayMediaPreview(mediaPreviewViewModel)
|
||||
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker,
|
||||
.displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen,
|
||||
.tappedOnSenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure,
|
||||
.composer, .hasScrolled, .viewInRoomTimeline:
|
||||
break
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
filesTimelineViewModel.context.$viewState.sink { [weak self] timelineViewState in
|
||||
guard let self, state.bindings.screenMode == .files else {
|
||||
return
|
||||
@ -73,6 +86,19 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
filesTimelineViewModel.actions.sink { [weak self] action in
|
||||
switch action {
|
||||
case .displayMediaPreview(let mediaPreviewViewModel):
|
||||
self?.displayMediaPreview(mediaPreviewViewModel)
|
||||
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker,
|
||||
.displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen,
|
||||
.tappedOnSenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure,
|
||||
.composer, .hasScrolled, .viewInRoomTimeline:
|
||||
break
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
updateWithTimelineViewState(activeTimelineViewModel.context.viewState)
|
||||
}
|
||||
|
||||
@ -90,10 +116,15 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
||||
case .oldestItemDidDisappear:
|
||||
isOldestItemVisible = false
|
||||
case .tappedItem(let item):
|
||||
handleItemTapped(item)
|
||||
activeTimelineViewModel.context.send(viewAction: .mediaTapped(itemID: item.identifier))
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
// Work around QLPreviewController dismissal issues, see the InteractiveQuickLookModifier.
|
||||
state.bindings.mediaPreviewViewModel = nil
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func updateWithTimelineViewState(_ timelineViewState: TimelineViewState) {
|
||||
@ -146,26 +177,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
||||
}
|
||||
}
|
||||
|
||||
private func handleItemTapped(_ item: RoomTimelineItemViewState) {
|
||||
let item: EventBasedMessageTimelineItemProtocol? = switch item.type {
|
||||
case .audio(let audioItem): audioItem
|
||||
case .file(let fileItem): fileItem
|
||||
case .image(let imageItem): imageItem
|
||||
case .video(let videoItem): videoItem
|
||||
default: nil
|
||||
}
|
||||
|
||||
guard let item else {
|
||||
MXLog.error("Unexpected item type tapped.")
|
||||
return
|
||||
}
|
||||
|
||||
let viewModel = TimelineMediaPreviewViewModel(initialItem: item,
|
||||
timelineViewModel: activeTimelineViewModel,
|
||||
mediaProvider: mediaProvider,
|
||||
photoLibraryManager: PhotoLibraryManager(),
|
||||
userIndicatorController: userIndicatorController,
|
||||
appMediator: appMediator)
|
||||
private func displayMediaPreview(_ viewModel: TimelineMediaPreviewViewModel) {
|
||||
viewModel.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
|
@ -11,4 +11,6 @@ import Combine
|
||||
protocol MediaEventsTimelineScreenViewModelProtocol {
|
||||
var actionsPublisher: AnyPublisher<MediaEventsTimelineScreenViewModelAction, Never> { get }
|
||||
var context: MediaEventsTimelineScreenViewModelType.Context { get }
|
||||
|
||||
func stop()
|
||||
}
|
||||
|
@ -62,6 +62,9 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .viewInRoomTimeline(let itemID):
|
||||
guard let eventID = itemID.eventID else { fatalError("A pinned event must have an event ID.") }
|
||||
actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID))
|
||||
case .dismiss:
|
||||
self.actionsSubject.send(.dismiss)
|
||||
}
|
||||
@ -77,6 +80,8 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
actionsSubject.send(.displayUser(userID: userID))
|
||||
case .displayMessageForwarding(let forwardingItem):
|
||||
actionsSubject.send(.displayMessageForwarding(forwardingItem: forwardingItem))
|
||||
case .displayMediaPreview(let mediaPreviewViewModel):
|
||||
viewModel.displayMediaPreview(mediaPreviewViewModel)
|
||||
case .displayLocation(_, let geoURI, let description):
|
||||
actionsSubject.send(.presentLocationViewer(geoURI: geoURI, description: description))
|
||||
case .viewInRoomTimeline(let eventID):
|
||||
@ -92,6 +97,10 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func stop() {
|
||||
viewModel.stop()
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(PinnedEventsTimelineScreen(context: viewModel.context, timelineContext: timelineViewModel.context))
|
||||
}
|
||||
|
@ -8,10 +8,18 @@
|
||||
import Foundation
|
||||
|
||||
enum PinnedEventsTimelineScreenViewModelAction {
|
||||
case viewInRoomTimeline(itemID: TimelineItemIdentifier)
|
||||
case dismiss
|
||||
}
|
||||
|
||||
struct PinnedEventsTimelineScreenViewState: BindableState { }
|
||||
struct PinnedEventsTimelineScreenViewState: BindableState {
|
||||
var bindings = PinnedEventsTimelineScreenViewStateBindings()
|
||||
}
|
||||
|
||||
struct PinnedEventsTimelineScreenViewStateBindings {
|
||||
/// The view model used to present a QuickLook media preview.
|
||||
var mediaPreviewViewModel: TimelineMediaPreviewViewModel?
|
||||
}
|
||||
|
||||
enum PinnedEventsTimelineScreenViewAction {
|
||||
case close
|
||||
|
@ -34,4 +34,23 @@ class PinnedEventsTimelineScreenViewModel: PinnedEventsTimelineScreenViewModelTy
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
// Work around QLPreviewController dismissal issues, see the InteractiveQuickLookModifier.
|
||||
state.bindings.mediaPreviewViewModel = nil
|
||||
}
|
||||
|
||||
func displayMediaPreview(_ mediaPreviewViewModel: TimelineMediaPreviewViewModel) {
|
||||
mediaPreviewViewModel.actions.sink { [weak self] action in
|
||||
switch action {
|
||||
case .viewInRoomTimeline(let itemID):
|
||||
self?.actionsSubject.send(.viewInRoomTimeline(itemID: itemID))
|
||||
case .dismiss:
|
||||
self?.state.bindings.mediaPreviewViewModel = nil
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
state.bindings.mediaPreviewViewModel = mediaPreviewViewModel
|
||||
}
|
||||
}
|
||||
|
@ -11,4 +11,8 @@ import Combine
|
||||
protocol PinnedEventsTimelineScreenViewModelProtocol {
|
||||
var actionsPublisher: AnyPublisher<PinnedEventsTimelineScreenViewModelAction, Never> { get }
|
||||
var context: PinnedEventsTimelineScreenViewModelType.Context { get }
|
||||
|
||||
func stop()
|
||||
|
||||
func displayMediaPreview(_ mediaPreviewViewModel: TimelineMediaPreviewViewModel)
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ struct PinnedEventsTimelineScreen: View {
|
||||
.toolbar { toolbar }
|
||||
.background(.compound.bgCanvasDefault)
|
||||
.interactiveDismissDisabled()
|
||||
.interactiveQuickLook(item: $timelineContext.mediaPreviewItem)
|
||||
.timelineMediaPreview(viewModel: $context.mediaPreviewViewModel)
|
||||
.sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) }
|
||||
.sheet(item: $timelineContext.actionMenuInfo) { info in
|
||||
let actions = TimelineItemMenuActionProvider(timelineItem: info.item,
|
||||
|
@ -126,6 +126,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
actionsSubject.send(.presentMediaUploadPicker(.photoLibrary))
|
||||
case .displayDocumentPicker:
|
||||
actionsSubject.send(.presentMediaUploadPicker(.documents))
|
||||
case .displayMediaPreview(let mediaPreviewViewModel):
|
||||
roomViewModel.displayMediaPreview(mediaPreviewViewModel)
|
||||
case .displayLocationPicker:
|
||||
actionsSubject.send(.presentLocationPicker)
|
||||
case .displayPollForm(let mode):
|
||||
@ -199,7 +201,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
func stop() {
|
||||
composerViewModel.saveDraft()
|
||||
timelineViewModel.stop()
|
||||
roomViewModel.stop()
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
|
@ -67,7 +67,10 @@ struct RoomScreenViewState: BindableState {
|
||||
var bindings: RoomScreenViewStateBindings
|
||||
}
|
||||
|
||||
struct RoomScreenViewStateBindings { }
|
||||
struct RoomScreenViewStateBindings {
|
||||
/// The view model used to present a QuickLook media preview.
|
||||
var mediaPreviewViewModel: TimelineMediaPreviewViewModel?
|
||||
}
|
||||
|
||||
enum RoomScreenFooterViewAction {
|
||||
case resolvePinViolation(userID: String)
|
||||
|
@ -115,6 +115,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
// Work around QLPreviewController dismissal issues, see the InteractiveQuickLookModifier.
|
||||
state.bindings.mediaPreviewViewModel = nil
|
||||
}
|
||||
|
||||
func timelineHasScrolled(direction: ScrollDirection) {
|
||||
state.lastScrollDirection = direction
|
||||
}
|
||||
@ -123,6 +128,20 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
state.pinnedEventsBannerState.setSelectedPinnedEventID(eventID)
|
||||
}
|
||||
|
||||
func displayMediaPreview(_ mediaPreviewViewModel: TimelineMediaPreviewViewModel) {
|
||||
mediaPreviewViewModel.actions.sink { [weak self] action in
|
||||
switch action {
|
||||
case .viewInRoomTimeline:
|
||||
fatalError("viewInRoomTimeline should not be visible on a room preview.")
|
||||
case .dismiss:
|
||||
self?.state.bindings.mediaPreviewViewModel = nil
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
state.bindings.mediaPreviewViewModel = mediaPreviewViewModel
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setupSubscriptions(ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>) {
|
||||
|
@ -12,6 +12,9 @@ protocol RoomScreenViewModelProtocol {
|
||||
var actions: AnyPublisher<RoomScreenViewModelAction, Never> { get }
|
||||
var context: RoomScreenViewModel.Context { get }
|
||||
|
||||
func stop()
|
||||
|
||||
func timelineHasScrolled(direction: ScrollDirection)
|
||||
func setSelectedPinnedEventID(_ eventID: String)
|
||||
func displayMediaPreview(_ mediaPreviewViewModel: TimelineMediaPreviewViewModel)
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ struct RoomScreen: View {
|
||||
ReadReceiptsSummaryView(orderedReadReceipts: $0.orderedReceipts)
|
||||
.environmentObject(timelineContext)
|
||||
}
|
||||
.interactiveQuickLook(item: $timelineContext.mediaPreviewItem)
|
||||
.timelineMediaPreview(viewModel: $roomContext.mediaPreviewViewModel)
|
||||
.track(screen: .Room)
|
||||
.onDrop(of: ["public.item", "public.file-url"], isTargeted: $dragOver) { providers -> Bool in
|
||||
guard let provider = providers.first,
|
||||
|
@ -502,7 +502,7 @@ class TimelineInteractionHandler {
|
||||
}
|
||||
|
||||
func processItemTap(_ itemID: TimelineItemIdentifier) async -> TimelineControllerAction {
|
||||
guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID) else {
|
||||
guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID) as? EventBasedMessageTimelineItemProtocol else {
|
||||
return .none
|
||||
}
|
||||
|
||||
@ -510,8 +510,14 @@ class TimelineInteractionHandler {
|
||||
case let item as LocationRoomTimelineItem:
|
||||
guard let geoURI = item.content.geoURI else { return .none }
|
||||
return .displayLocation(body: item.content.body, geoURI: geoURI, description: item.content.description)
|
||||
case is ImageRoomTimelineItem,
|
||||
is VideoRoomTimelineItem:
|
||||
return await mediaPreviewAction(for: timelineItem, messageTypes: [.image, .video])
|
||||
case is AudioRoomTimelineItem,
|
||||
is FileRoomTimelineItem:
|
||||
return await mediaPreviewAction(for: timelineItem, messageTypes: [.audio, .file])
|
||||
default:
|
||||
return await displayMediaActionIfPossible(timelineItem: timelineItem)
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
@ -528,39 +534,57 @@ class TimelineInteractionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private func displayMediaActionIfPossible(timelineItem: RoomTimelineItemProtocol) async -> TimelineControllerAction {
|
||||
var source: MediaSourceProxy?
|
||||
var filename: String
|
||||
var caption: String?
|
||||
private func mediaPreviewAction(for item: EventBasedMessageTimelineItemProtocol, messageTypes: [TimelineAllowedMessageType]) async -> TimelineControllerAction {
|
||||
var newTimelineFocus: TimelineFocus?
|
||||
var newTimelinePresentation: TimelineKind.MediaPresentation?
|
||||
switch timelineController.timelineKind {
|
||||
case .live:
|
||||
newTimelineFocus = .live
|
||||
newTimelinePresentation = .roomScreenLive
|
||||
case .detached:
|
||||
guard case let .event(_, eventOrTransactionID: .eventID(eventID)) = item.id else {
|
||||
MXLog.error("Unexpected event type on a detached timeline.")
|
||||
return .none
|
||||
}
|
||||
newTimelineFocus = .eventID(eventID)
|
||||
newTimelinePresentation = .roomScreenDetached
|
||||
case .pinned:
|
||||
newTimelineFocus = .pinned
|
||||
newTimelinePresentation = .pinnedEventsScreen
|
||||
case .media:
|
||||
break // We don't need to create a new timeline as it is already filtered.
|
||||
}
|
||||
|
||||
switch timelineItem {
|
||||
case let item as ImageRoomTimelineItem:
|
||||
source = item.content.imageInfo.source
|
||||
filename = item.content.filename
|
||||
caption = item.content.caption
|
||||
case let item as VideoRoomTimelineItem:
|
||||
source = item.content.videoInfo.source
|
||||
filename = item.content.filename
|
||||
caption = item.content.caption
|
||||
case let item as FileRoomTimelineItem:
|
||||
source = item.content.source
|
||||
filename = item.content.filename
|
||||
caption = item.content.caption
|
||||
case let item as AudioRoomTimelineItem:
|
||||
// For now we are just displaying audio messages with the File preview until we create a timeline player for them.
|
||||
source = item.content.source
|
||||
filename = item.content.filename
|
||||
caption = item.content.caption
|
||||
default:
|
||||
if let newTimelineFocus, let newTimelinePresentation {
|
||||
let timelineItemFactory = RoomTimelineItemFactory(userID: roomProxy.ownUserID,
|
||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||
stateEventStringBuilder: RoomStateEventStringBuilder(userID: roomProxy.ownUserID))
|
||||
|
||||
guard case let .success(timelineController) = await timelineControllerFactory.buildMessageFilteredTimelineController(focus: newTimelineFocus,
|
||||
allowedMessageTypes: messageTypes,
|
||||
presentation: newTimelinePresentation,
|
||||
roomProxy: roomProxy,
|
||||
timelineItemFactory: timelineItemFactory,
|
||||
mediaProvider: mediaProvider) else {
|
||||
MXLog.error("Failed presenting media timeline")
|
||||
return .none
|
||||
}
|
||||
|
||||
guard let source else { return .none }
|
||||
switch await mediaProvider.loadFileFromSource(source, filename: filename) {
|
||||
case .success(let file):
|
||||
return .displayMediaFile(file: file, title: caption ?? filename)
|
||||
case .failure:
|
||||
return .none
|
||||
let timelineViewModel = TimelineViewModel(roomProxy: roomProxy,
|
||||
timelineController: timelineController,
|
||||
mediaProvider: mediaProvider,
|
||||
mediaPlayerProvider: mediaPlayerProvider,
|
||||
voiceMessageMediaManager: voiceMessageMediaManager,
|
||||
userIndicatorController: userIndicatorController,
|
||||
appMediator: appMediator,
|
||||
appSettings: appSettings,
|
||||
analyticsService: analyticsService,
|
||||
emojiProvider: emojiProvider,
|
||||
timelineControllerFactory: timelineControllerFactory)
|
||||
|
||||
return .displayMediaPreview(item: item, timelineViewModel: .new(timelineViewModel))
|
||||
} else {
|
||||
return .displayMediaPreview(item: item, timelineViewModel: .active)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ enum TimelineViewModelAction {
|
||||
case displayMediaUploadPreviewScreen(url: URL)
|
||||
case tappedOnSenderDetails(userID: String)
|
||||
case displayMessageForwarding(forwardingItem: MessageForwardingItem)
|
||||
case displayMediaPreview(TimelineMediaPreviewViewModel)
|
||||
case displayLocation(body: String, geoURI: GeoURI, description: String?)
|
||||
case displayResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy)
|
||||
case composer(action: TimelineComposerAction)
|
||||
@ -123,9 +124,6 @@ struct TimelineViewStateBindings {
|
||||
/// Key is itemID, value is the collapsed state.
|
||||
var reactionsCollapsed: [TimelineItemIdentifier: Bool]
|
||||
|
||||
/// A media item that will be previewed with QuickLook.
|
||||
var mediaPreviewItem: MediaPreviewItem?
|
||||
|
||||
var alertInfo: AlertInfo<RoomScreenAlertInfoType>?
|
||||
|
||||
var debugInfo: TimelineItemDebugInfo?
|
||||
|
@ -23,6 +23,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
|
||||
private let roomProxy: JoinedRoomProxyProtocol
|
||||
private let timelineController: TimelineControllerProtocol
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let mediaPlayerProvider: MediaPlayerProviderProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let appMediator: AppMediatorProtocol
|
||||
@ -56,6 +57,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
emojiProvider: EmojiProviderProtocol,
|
||||
timelineControllerFactory: TimelineControllerFactoryProtocol) {
|
||||
self.timelineController = timelineController
|
||||
self.mediaProvider = mediaProvider
|
||||
self.mediaPlayerProvider = mediaPlayerProvider
|
||||
self.roomProxy = roomProxy
|
||||
self.appSettings = appSettings
|
||||
@ -123,11 +125,6 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func stop() {
|
||||
// Work around QLPreviewController dismissal issues, see the InteractiveQuickLookModifier.
|
||||
state.bindings.mediaPreviewItem = nil
|
||||
}
|
||||
|
||||
override func process(viewAction: TimelineViewAction) {
|
||||
switch viewAction {
|
||||
case .itemAppeared(let id):
|
||||
@ -544,9 +541,11 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
let action = await timelineInteractionHandler.processItemTap(itemID)
|
||||
|
||||
switch action {
|
||||
case .displayMediaFile(let file, let title):
|
||||
case .displayMediaPreview(let item, let timelineViewModelKind):
|
||||
actionsSubject.send(.composer(action: .removeFocus)) // Hide the keyboard otherwise a big white space is sometimes shown when dismissing the preview.
|
||||
state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: title)
|
||||
|
||||
let mediaPreviewViewModel = makeMediaPreviewViewModel(item: item, timelineViewModelKind: timelineViewModelKind)
|
||||
actionsSubject.send(.displayMediaPreview(mediaPreviewViewModel))
|
||||
case .displayLocation(let body, let geoURI, let description):
|
||||
actionsSubject.send(.displayLocation(body: body, geoURI: geoURI, description: description))
|
||||
case .none:
|
||||
@ -655,6 +654,21 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
analyticsService.trackComposer(inThread: false, isEditing: isEdit, isReply: isReply, startsThread: nil)
|
||||
}
|
||||
|
||||
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: mediaProvider,
|
||||
photoLibraryManager: PhotoLibraryManager(),
|
||||
userIndicatorController: userIndicatorController,
|
||||
appMediator: appMediator)
|
||||
}
|
||||
|
||||
// MARK: - Timeline Item Building
|
||||
|
||||
private func buildTimelineViews(timelineItems: [RoomTimelineItemProtocol], isSwitchingTimelines: Bool = false) {
|
||||
|
@ -13,8 +13,8 @@ import SwiftUI
|
||||
protocol TimelineViewModelProtocol {
|
||||
var actions: AnyPublisher<TimelineViewModelAction, Never> { get }
|
||||
var context: TimelineViewModel.Context { get }
|
||||
|
||||
func process(composerAction: ComposerToolbarViewModelAction)
|
||||
/// Updates the timeline to show and highlight the item with the corresponding event ID.
|
||||
func focusOnEvent(eventID: String) async
|
||||
func stop()
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ struct TimelineItemMenuActionProvider {
|
||||
var actions: [TimelineItemMenuAction] = []
|
||||
var secondaryActions: [TimelineItemMenuAction] = []
|
||||
|
||||
if timelineKind == .pinned || timelineKind == .media(.mediaFilesScreen) {
|
||||
if timelineKind == .pinned || timelineKind == .media(.mediaFilesScreen) || timelineKind == .media(.pinnedEventsScreen) {
|
||||
actions.append(.viewInRoomTimeline)
|
||||
}
|
||||
|
||||
|
@ -375,7 +375,6 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_parameter_count
|
||||
func createRoom(name: String,
|
||||
topic: String?,
|
||||
isRoomPrivate: Bool,
|
||||
|
@ -117,7 +117,6 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||
|
||||
func createDirectRoom(with userID: String, expectedRoomName: String?) async -> Result<String, ClientProxyError>
|
||||
|
||||
// swiftlint:disable:next function_parameter_count
|
||||
func createRoom(name: String,
|
||||
topic: String?,
|
||||
isRoomPrivate: Bool,
|
||||
|
@ -182,11 +182,27 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func messageFilteredTimeline(allowedMessageTypes: [RoomMessageEventMessageType],
|
||||
func messageFilteredTimeline(focus: TimelineFocus,
|
||||
allowedMessageTypes: [TimelineAllowedMessageType],
|
||||
presentation: TimelineKind.MediaPresentation) async -> Result<any TimelineProxyProtocol, RoomProxyError> {
|
||||
do {
|
||||
let sdkTimeline = try await room.timelineWithConfiguration(configuration: .init(focus: .live,
|
||||
allowedMessageTypes: .only(types: allowedMessageTypes),
|
||||
let rustFocus: MatrixRustSDK.TimelineFocus = switch focus {
|
||||
case .live: .live
|
||||
case .eventID(let eventID): .event(eventId: eventID, numContextEvents: 100)
|
||||
case .pinned: .pinnedEvents(maxEventsToLoad: 100, maxConcurrentRequests: 10)
|
||||
}
|
||||
|
||||
let rustMessageTypes: [MatrixRustSDK.RoomMessageEventMessageType] = allowedMessageTypes.map {
|
||||
switch $0 {
|
||||
case .audio: .audio
|
||||
case .file: .file
|
||||
case .image: .image
|
||||
case .video: .video
|
||||
}
|
||||
}
|
||||
|
||||
let sdkTimeline = try await room.timelineWithConfiguration(configuration: .init(focus: rustFocus,
|
||||
allowedMessageTypes: .only(types: rustMessageTypes),
|
||||
internalIdPrefix: nil,
|
||||
dateDividerMode: .monthly))
|
||||
|
||||
|
@ -78,7 +78,8 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
|
||||
|
||||
func timelineFocusedOnEvent(eventID: String, numberOfEvents: UInt16) async -> Result<TimelineProxyProtocol, RoomProxyError>
|
||||
|
||||
func messageFilteredTimeline(allowedMessageTypes: [RoomMessageEventMessageType],
|
||||
func messageFilteredTimeline(focus: TimelineFocus,
|
||||
allowedMessageTypes: [TimelineAllowedMessageType],
|
||||
presentation: TimelineKind.MediaPresentation) async -> Result<TimelineProxyProtocol, RoomProxyError>
|
||||
|
||||
func enableEncryption() async -> Result<Void, RoomProxyError>
|
||||
|
@ -75,7 +75,10 @@ class MockTimelineController: TimelineControllerProtocol {
|
||||
callbacks.send(.isLive(true))
|
||||
}
|
||||
|
||||
private(set) var paginateBackwardsCallCount = 0
|
||||
func paginateBackwards(requestSize: UInt16) async -> Result<Void, TimelineControllerError> {
|
||||
paginateBackwardsCallCount += 1
|
||||
|
||||
paginationState = PaginationState(backward: .paginating, forward: .timelineEndReached)
|
||||
|
||||
if client == nil {
|
||||
@ -85,9 +88,10 @@ class MockTimelineController: TimelineControllerProtocol {
|
||||
return .success(())
|
||||
}
|
||||
|
||||
private(set) var paginateForwardsCallCount = 0
|
||||
func paginateForwards(requestSize: UInt16) async -> Result<Void, TimelineControllerError> {
|
||||
// try? await simulateForwardPagination()
|
||||
.success(())
|
||||
paginateForwardsCallCount += 1
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func sendReadReceipt(for itemID: TimelineItemIdentifier) async {
|
||||
|
@ -42,7 +42,7 @@ class TimelineController: TimelineControllerProtocol {
|
||||
}
|
||||
|
||||
var timelineKind: TimelineKind {
|
||||
liveTimelineProvider.kind
|
||||
activeTimelineProvider.kind
|
||||
}
|
||||
|
||||
init(roomProxy: JoinedRoomProxyProtocol,
|
||||
|
@ -36,12 +36,13 @@ struct TimelineControllerFactory: TimelineControllerFactoryProtocol {
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
}
|
||||
|
||||
func buildMessageFilteredTimelineController(allowedMessageTypes: [RoomMessageEventMessageType],
|
||||
func buildMessageFilteredTimelineController(focus: TimelineFocus,
|
||||
allowedMessageTypes: [TimelineAllowedMessageType],
|
||||
presentation: TimelineKind.MediaPresentation,
|
||||
roomProxy: JoinedRoomProxyProtocol,
|
||||
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
||||
mediaProvider: MediaProviderProtocol) async -> Result<TimelineControllerProtocol, TimelineFactoryControllerError> {
|
||||
switch await roomProxy.messageFilteredTimeline(allowedMessageTypes: allowedMessageTypes, presentation: presentation) {
|
||||
switch await roomProxy.messageFilteredTimeline(focus: focus, allowedMessageTypes: allowedMessageTypes, presentation: presentation) {
|
||||
case .success(let timelineProxy):
|
||||
return .success(TimelineController(roomProxy: roomProxy,
|
||||
timelineProxy: timelineProxy,
|
||||
|
@ -23,7 +23,8 @@ protocol TimelineControllerFactoryProtocol {
|
||||
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
||||
mediaProvider: MediaProviderProtocol) async -> TimelineControllerProtocol?
|
||||
|
||||
func buildMessageFilteredTimelineController(allowedMessageTypes: [RoomMessageEventMessageType],
|
||||
func buildMessageFilteredTimelineController(focus: TimelineFocus,
|
||||
allowedMessageTypes: [TimelineAllowedMessageType],
|
||||
presentation: TimelineKind.MediaPresentation,
|
||||
roomProxy: JoinedRoomProxyProtocol,
|
||||
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
||||
|
@ -16,7 +16,14 @@ enum TimelineControllerCallback {
|
||||
}
|
||||
|
||||
enum TimelineControllerAction {
|
||||
case displayMediaFile(file: MediaFileHandleProxy, title: String?)
|
||||
enum TimelineViewModelKind {
|
||||
/// Use the active timeline view model.
|
||||
case active
|
||||
/// Use the newly generated view model provided.
|
||||
case new(TimelineViewModel)
|
||||
}
|
||||
|
||||
case displayMediaPreview(item: EventBasedMessageTimelineItemProtocol, timelineViewModel: TimelineViewModelKind)
|
||||
case displayLocation(body: String, geoURI: GeoURI, description: String?)
|
||||
case none
|
||||
}
|
||||
|
@ -76,7 +76,6 @@ struct RoomStateEventStringBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_parameter_count
|
||||
func buildProfileChangeString(displayName: String?, previousDisplayName: String?,
|
||||
avatarURLString: String?, previousAvatarURLString: String?,
|
||||
member: String, memberIsYou: Bool) -> String? {
|
||||
|
@ -380,7 +380,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_parameter_count
|
||||
private func buildPollTimelineItem(_ question: String,
|
||||
_ pollKind: PollKind,
|
||||
_ maxSelections: UInt64,
|
||||
@ -645,7 +644,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
return buildStateTimelineItem(for: eventItemProxy, text: text, isOutgoing: isOutgoing)
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_parameter_count
|
||||
private func buildStateProfileChangeTimelineItem(for eventItemProxy: EventTimelineItemProxy,
|
||||
displayName: String?,
|
||||
previousDisplayName: String?,
|
||||
|
@ -591,8 +591,8 @@ final class TimelineProxy: TimelineProxyProtocol {
|
||||
backPaginationStatusSubject.send(.idle)
|
||||
forwardPaginationStatusSubject.send(.idle)
|
||||
case .media(let presentation):
|
||||
backPaginationStatusSubject.send(.idle)
|
||||
forwardPaginationStatusSubject.send(presentation == .mediaFilesScreen ? .timelineEndReached : .idle)
|
||||
backPaginationStatusSubject.send(presentation == .pinnedEventsScreen ? .timelineEndReached : .idle)
|
||||
forwardPaginationStatusSubject.send(presentation == .roomScreenDetached ? .idle : .timelineEndReached)
|
||||
case .pinned:
|
||||
backPaginationStatusSubject.send(.timelineEndReached)
|
||||
forwardPaginationStatusSubject.send(.timelineEndReached)
|
||||
|
@ -14,10 +14,20 @@ enum TimelineKind: Equatable {
|
||||
case detached
|
||||
case pinned
|
||||
|
||||
enum MediaPresentation { case roomScreen, mediaFilesScreen }
|
||||
enum MediaPresentation { case roomScreenLive, roomScreenDetached, pinnedEventsScreen, mediaFilesScreen }
|
||||
case media(MediaPresentation)
|
||||
}
|
||||
|
||||
enum TimelineFocus {
|
||||
case live
|
||||
case eventID(String)
|
||||
case pinned
|
||||
}
|
||||
|
||||
enum TimelineAllowedMessageType {
|
||||
case audio, file, image, video
|
||||
}
|
||||
|
||||
enum TimelineProxyError: Error {
|
||||
case sdkError(Error)
|
||||
|
||||
|
@ -14,7 +14,6 @@ import XCTest
|
||||
class HomeScreenRoomTests: XCTestCase {
|
||||
var roomSummary: RoomSummary!
|
||||
|
||||
// swiftlint:disable:next function_parameter_count
|
||||
func setupRoomSummary(isMarkedUnread: Bool,
|
||||
unreadMessagesCount: UInt,
|
||||
unreadMentionsCount: UInt,
|
||||
|
@ -23,7 +23,7 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
initialMediaViewStates = initialMediaItems.map { RoomTimelineItemViewState(item: $0, groupStyle: .single) }
|
||||
}
|
||||
|
||||
func testInitialItems() -> TimelineMediaPreviewDataSource {
|
||||
func testInitialItems() throws -> TimelineMediaPreviewDataSource {
|
||||
// Given a data source built with the initial items.
|
||||
let dataSource = TimelineMediaPreviewDataSource(itemViewStates: initialMediaViewStates,
|
||||
initialItem: initialMediaItems[initialItemIndex],
|
||||
@ -32,12 +32,13 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
|
||||
// When the preview controller displays the data.
|
||||
let previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
let displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media
|
||||
let displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media,
|
||||
"A preview item should be found.")
|
||||
|
||||
// Then the preview controller should be showing the initial item and the data source should reflect this.
|
||||
XCTAssertEqual(dataSource.initialItemIndex, initialItemIndex + initialPadding, "The initial item index should be padded for the preview controller.")
|
||||
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should be the initial item.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id, "The current item should also be the initial item.")
|
||||
XCTAssertEqual(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should be the initial item.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should also be the initial item.")
|
||||
|
||||
XCTAssertEqual(dataSource.previewItems.count, initialMediaViewStates.count, "The initial count of preview items should be correct.")
|
||||
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The initial item count should be padded for the preview controller.")
|
||||
@ -45,17 +46,15 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func testCurrentUpdateItem() {
|
||||
func testCurrentUpdateItem() throws {
|
||||
// Given a data source built with the initial items.
|
||||
let dataSource = TimelineMediaPreviewDataSource(itemViewStates: initialMediaViewStates,
|
||||
initialItem: initialMediaItems[initialItemIndex],
|
||||
paginationState: .initial)
|
||||
|
||||
// When a different item is displayed.
|
||||
guard let previewItem = dataSource.previewController(previewController, previewItemAt: 1 + initialPadding) as? TimelineMediaPreviewItem.Media else {
|
||||
XCTFail("A preview item should be found.")
|
||||
return
|
||||
}
|
||||
let previewItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: 1 + initialPadding) as? TimelineMediaPreviewItem.Media,
|
||||
"A preview item should be found.")
|
||||
dataSource.updateCurrentItem(.media(previewItem))
|
||||
|
||||
// Then the data source should reflect the change of item.
|
||||
@ -74,7 +73,7 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
|
||||
func testUpdatedItems() async throws {
|
||||
// Given a data source built with the initial items.
|
||||
let dataSource = testInitialItems()
|
||||
let dataSource = try testInitialItems()
|
||||
|
||||
// When one of the items changes but no pagination has occurred.
|
||||
let deferred = deferFailure(dataSource.previewItemsPaginationPublisher, timeout: 1) { _ in true }
|
||||
@ -84,9 +83,9 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
let previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
let displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media
|
||||
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id, "The current item should not change.")
|
||||
let displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media)
|
||||
XCTAssertEqual(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should not change.")
|
||||
|
||||
XCTAssertEqual(dataSource.previewItems.count, initialMediaViewStates.count, "The number of items should not change.")
|
||||
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The padded number of items should not change.")
|
||||
@ -94,7 +93,7 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
|
||||
func testPagination() async throws {
|
||||
// Given a data source built with the initial items.
|
||||
let dataSource = testInitialItems()
|
||||
let dataSource = try testInitialItems()
|
||||
|
||||
// When more items are loaded in a back pagination.
|
||||
var deferred = deferFulfillment(dataSource.previewItemsPaginationPublisher) { _ in true }
|
||||
@ -107,9 +106,9 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
||||
|
||||
var previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
var displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media
|
||||
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id, "The current item should not change.")
|
||||
var displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media)
|
||||
XCTAssertEqual(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should not change.")
|
||||
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
||||
|
||||
// When more items are loaded in a forward pagination or sync.
|
||||
@ -123,16 +122,16 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
||||
|
||||
previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media
|
||||
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id, "The current item should not change.")
|
||||
displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media)
|
||||
XCTAssertEqual(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should not change.")
|
||||
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
||||
}
|
||||
|
||||
func testPaginationLimits() async throws {
|
||||
// Given a data source with a small amount of padding remaining.
|
||||
initialPadding = 2
|
||||
let dataSource = testInitialItems()
|
||||
let dataSource = try testInitialItems()
|
||||
|
||||
// When paginating backwards by more than the available padding.
|
||||
var deferred = deferFulfillment(dataSource.previewItemsPaginationPublisher) { _ in true }
|
||||
@ -146,9 +145,9 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
||||
|
||||
var previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
var displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media
|
||||
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id, "The current item should not change.")
|
||||
var displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media)
|
||||
XCTAssertEqual(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should not change.")
|
||||
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
||||
|
||||
// When paginating forwards by more than the available padding.
|
||||
@ -162,12 +161,92 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
||||
|
||||
previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media
|
||||
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id, "The current item should not change.")
|
||||
displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media)
|
||||
XCTAssertEqual(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should not change.")
|
||||
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
||||
}
|
||||
|
||||
func testEmptyTimeline() async throws {
|
||||
// Given a data source built with no timeline items loaded.
|
||||
let initialItem = initialMediaItems[initialItemIndex]
|
||||
let dataSource = TimelineMediaPreviewDataSource(itemViewStates: [],
|
||||
initialItem: initialItem,
|
||||
initialPadding: initialPadding,
|
||||
paginationState: .initial)
|
||||
|
||||
// When the preview controller displays the data.
|
||||
var previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
var displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media,
|
||||
"A preview item should be found.")
|
||||
|
||||
// Then the preview controller should always show the initial item.
|
||||
XCTAssertEqual(dataSource.previewItems.count, 1, "The initial item should be in the preview items array.")
|
||||
XCTAssertEqual(previewItemCount, 1 + (2 * initialPadding), "The initial item count should be padded for the preview controller.")
|
||||
XCTAssertEqual(dataSource.initialItemIndex, initialPadding, "The initial item index should be padded for the preview controller.")
|
||||
|
||||
XCTAssertEqual(displayedItem.id, initialItem.id.eventOrTransactionID, "The displayed item should be the initial item.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialItem.id.eventOrTransactionID, "The current item should also be the initial item.")
|
||||
|
||||
// When the timeline loads the initial items.
|
||||
let deferred = deferFulfillment(dataSource.previewItemsPaginationPublisher) { _ in true }
|
||||
let loadedItems = initialMediaItems.map { RoomTimelineItemViewState(item: $0, groupStyle: .single) }
|
||||
dataSource.updatePreviewItems(itemViewStates: loadedItems)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the preview controller should still show the initial item with the other items loaded around it.
|
||||
previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media,
|
||||
"A preview item should be found.")
|
||||
|
||||
XCTAssertEqual(dataSource.previewItems.count, initialMediaViewStates.count, "The preview items should now be loaded.")
|
||||
XCTAssertEqual(previewItemCount, 1 + (2 * initialPadding), "The item count should not change as the padding will be reduced.")
|
||||
XCTAssertEqual(dataSource.initialItemIndex, initialPadding, "The item index should not change.")
|
||||
|
||||
XCTAssertEqual(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should not change.")
|
||||
}
|
||||
|
||||
func testTimelineUpdateWithoutInitialItem() async throws {
|
||||
// Given a data source built with no timeline items loaded.
|
||||
let initialItem = initialMediaItems[initialItemIndex]
|
||||
let dataSource = TimelineMediaPreviewDataSource(itemViewStates: [],
|
||||
initialItem: initialItem,
|
||||
initialPadding: initialPadding,
|
||||
paginationState: .initial)
|
||||
|
||||
// When the preview controller displays the data.
|
||||
var previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
var displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media,
|
||||
"A preview item should be found.")
|
||||
|
||||
// Then the preview controller should always show the initial item.
|
||||
XCTAssertEqual(dataSource.previewItems.count, 1, "The initial item should be in the preview items array.")
|
||||
XCTAssertEqual(previewItemCount, 1 + (2 * initialPadding), "The initial item count should be padded for the preview controller.")
|
||||
XCTAssertEqual(dataSource.initialItemIndex, initialPadding, "The initial item index should be padded for the preview controller.")
|
||||
|
||||
XCTAssertEqual(displayedItem.id, initialItem.id.eventOrTransactionID, "The displayed item should be the initial item.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialItem.id.eventOrTransactionID, "The current item should also be the initial item.")
|
||||
|
||||
// When the timeline loads more items but still doesn't include the initial item.
|
||||
let failure = deferFailure(dataSource.previewItemsPaginationPublisher, timeout: 1) { _ in true }
|
||||
let loadedItems = newChunk().map { RoomTimelineItemViewState(item: $0, groupStyle: .single) }
|
||||
dataSource.updatePreviewItems(itemViewStates: loadedItems)
|
||||
try await failure.fulfill()
|
||||
|
||||
// Then the preview controller shouldn't update the available preview items.
|
||||
previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media,
|
||||
"A preview item should be found.")
|
||||
|
||||
XCTAssertEqual(dataSource.previewItems.count, 1, "No new items should have been added to the array.")
|
||||
XCTAssertEqual(previewItemCount, 1 + (2 * initialPadding), "The initial item count should not change.")
|
||||
XCTAssertEqual(dataSource.initialItemIndex, initialPadding, "The initial item index should not change.")
|
||||
|
||||
XCTAssertEqual(displayedItem.id, initialItem.id.eventOrTransactionID, "The displayed item should not change.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialItem.id.eventOrTransactionID, "The current item not change.")
|
||||
}
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
func newChunk() -> [EventBasedMessageTimelineItemProtocol] {
|
||||
@ -178,7 +257,7 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
}
|
||||
|
||||
private extension TimelineMediaPreviewDataSource {
|
||||
var currentMediaItemID: TimelineItemIdentifier? {
|
||||
var currentMediaItemID: TimelineItemIdentifier.EventOrTransactionID? {
|
||||
switch currentItem {
|
||||
case .media(let mediaItem): mediaItem.id
|
||||
case .loading: nil
|
||||
|
@ -84,18 +84,22 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(context.viewState.currentItem, .media(context.viewState.dataSource.previewItems[0]))
|
||||
}
|
||||
|
||||
func testLoadingMoreItem() async throws {
|
||||
func testLoadingMoreItems() async throws {
|
||||
// Given a view model with a loaded item.
|
||||
try await testLoadingItem()
|
||||
XCTAssertEqual(timelineController.paginateBackwardsCallCount, 0)
|
||||
|
||||
// When swiping to a "loading more" item.
|
||||
// When swiping to a "loading more" item and there are more media items to load.
|
||||
timelineController.paginationState = .init(backward: .idle, forward: .timelineEndReached)
|
||||
timelineController.backPaginationResponses.append(RoomTimelineItemFixtures.mediaChunk)
|
||||
let failure = deferFailure(viewModel.state.previewControllerDriver, timeout: 1) { $0.isItemLoaded }
|
||||
context.send(viewAction: .updateCurrentItem(.loading(.paginating)))
|
||||
context.send(viewAction: .updateCurrentItem(.loading(.paginatingBackwards)))
|
||||
try await failure.fulfill()
|
||||
|
||||
// Then there should no longer be a media preview and no attempt should be made to load one.
|
||||
// Then there should no longer be a media preview and instead of loading any media, a pagination request should be made.
|
||||
XCTAssertEqual(mediaProvider.loadFileFromSourceFilenameCallsCount, 1)
|
||||
XCTAssertEqual(context.viewState.currentItem, .loading(.paginating))
|
||||
XCTAssertEqual(context.viewState.currentItem, .loading(.paginatingBackwards)) // Note: This item only changes when the preview controller handles the new items.
|
||||
XCTAssertEqual(timelineController.paginateBackwardsCallCount, 1)
|
||||
}
|
||||
|
||||
func testPagination() async throws {
|
||||
@ -130,7 +134,7 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
return
|
||||
}
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .viewInRoomTimeline(mediaItem.id) }
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .viewInRoomTimeline(mediaItem.timelineItem.id) }
|
||||
context.send(viewAction: .menuAction(.viewInRoomTimeline, item: mediaItem))
|
||||
|
||||
// Then the action should be sent upwards to make this happen.
|
||||
|
@ -37,9 +37,7 @@ setup_github_actions_environment() {
|
||||
unset HOMEBREW_NO_INSTALL_FROM_API
|
||||
export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1
|
||||
|
||||
brew update && brew install xcodegen swiftformat git-lfs a7ex/homebrew-formulae/xcresultparser
|
||||
|
||||
# brew "swiftlint" # Fails on the CI: `Target /usr/local/bin/swiftlint Target /usr/local/bin/swiftlint already exists`. Installed through https://github.com/actions/virtual-environments/blob/main/images/macos/macos-12-Readme.md#linters
|
||||
brew update && brew install xcodegen swiftlint swiftformat git-lfs a7ex/homebrew-formulae/xcresultparser
|
||||
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
Loading…
x
Reference in New Issue
Block a user