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
|
- name: Setup environment
|
||||||
run:
|
run:
|
||||||
brew install danger/tap/danger-swift
|
brew install danger/tap/danger-swift swiftlint
|
||||||
|
|
||||||
- name: Danger
|
- name: Danger
|
||||||
run:
|
run:
|
||||||
|
@ -41,6 +41,10 @@ function_body_length:
|
|||||||
warning: 100
|
warning: 100
|
||||||
error: 100
|
error: 100
|
||||||
|
|
||||||
|
function_parameter_count:
|
||||||
|
warning: 10
|
||||||
|
error: 10
|
||||||
|
|
||||||
cyclomatic_complexity:
|
cyclomatic_complexity:
|
||||||
ignores_case_statements: true
|
ignores_case_statements: true
|
||||||
|
|
||||||
|
@ -64,7 +64,8 @@ class MediaEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||||
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userSession.clientProxy.userID))
|
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,
|
presentation: .mediaFilesScreen,
|
||||||
roomProxy: roomProxy,
|
roomProxy: roomProxy,
|
||||||
timelineItemFactory: timelineItemFactory,
|
timelineItemFactory: timelineItemFactory,
|
||||||
@ -73,7 +74,8 @@ class MediaEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
return
|
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,
|
presentation: .mediaFilesScreen,
|
||||||
roomProxy: roomProxy,
|
roomProxy: roomProxy,
|
||||||
timelineItemFactory: timelineItemFactory,
|
timelineItemFactory: timelineItemFactory,
|
||||||
|
@ -6158,15 +6158,15 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
//MARK: - messageFilteredTimeline
|
//MARK: - messageFilteredTimeline
|
||||||
|
|
||||||
var messageFilteredTimelineAllowedMessageTypesPresentationUnderlyingCallsCount = 0
|
var messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingCallsCount = 0
|
||||||
var messageFilteredTimelineAllowedMessageTypesPresentationCallsCount: Int {
|
var messageFilteredTimelineFocusAllowedMessageTypesPresentationCallsCount: Int {
|
||||||
get {
|
get {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
return messageFilteredTimelineAllowedMessageTypesPresentationUnderlyingCallsCount
|
return messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingCallsCount
|
||||||
} else {
|
} else {
|
||||||
var returnValue: Int? = nil
|
var returnValue: Int? = nil
|
||||||
DispatchQueue.main.sync {
|
DispatchQueue.main.sync {
|
||||||
returnValue = messageFilteredTimelineAllowedMessageTypesPresentationUnderlyingCallsCount
|
returnValue = messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingCallsCount
|
||||||
}
|
}
|
||||||
|
|
||||||
return returnValue!
|
return returnValue!
|
||||||
@ -6174,29 +6174,29 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
messageFilteredTimelineAllowedMessageTypesPresentationUnderlyingCallsCount = newValue
|
messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingCallsCount = newValue
|
||||||
} else {
|
} else {
|
||||||
DispatchQueue.main.sync {
|
DispatchQueue.main.sync {
|
||||||
messageFilteredTimelineAllowedMessageTypesPresentationUnderlyingCallsCount = newValue
|
messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingCallsCount = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var messageFilteredTimelineAllowedMessageTypesPresentationCalled: Bool {
|
var messageFilteredTimelineFocusAllowedMessageTypesPresentationCalled: Bool {
|
||||||
return messageFilteredTimelineAllowedMessageTypesPresentationCallsCount > 0
|
return messageFilteredTimelineFocusAllowedMessageTypesPresentationCallsCount > 0
|
||||||
}
|
}
|
||||||
var messageFilteredTimelineAllowedMessageTypesPresentationReceivedArguments: (allowedMessageTypes: [RoomMessageEventMessageType], presentation: TimelineKind.MediaPresentation)?
|
var messageFilteredTimelineFocusAllowedMessageTypesPresentationReceivedArguments: (focus: TimelineFocus, allowedMessageTypes: [TimelineAllowedMessageType], presentation: TimelineKind.MediaPresentation)?
|
||||||
var messageFilteredTimelineAllowedMessageTypesPresentationReceivedInvocations: [(allowedMessageTypes: [RoomMessageEventMessageType], presentation: TimelineKind.MediaPresentation)] = []
|
var messageFilteredTimelineFocusAllowedMessageTypesPresentationReceivedInvocations: [(focus: TimelineFocus, allowedMessageTypes: [TimelineAllowedMessageType], presentation: TimelineKind.MediaPresentation)] = []
|
||||||
|
|
||||||
var messageFilteredTimelineAllowedMessageTypesPresentationUnderlyingReturnValue: Result<TimelineProxyProtocol, RoomProxyError>!
|
var messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingReturnValue: Result<TimelineProxyProtocol, RoomProxyError>!
|
||||||
var messageFilteredTimelineAllowedMessageTypesPresentationReturnValue: Result<TimelineProxyProtocol, RoomProxyError>! {
|
var messageFilteredTimelineFocusAllowedMessageTypesPresentationReturnValue: Result<TimelineProxyProtocol, RoomProxyError>! {
|
||||||
get {
|
get {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
return messageFilteredTimelineAllowedMessageTypesPresentationUnderlyingReturnValue
|
return messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingReturnValue
|
||||||
} else {
|
} else {
|
||||||
var returnValue: Result<TimelineProxyProtocol, RoomProxyError>? = nil
|
var returnValue: Result<TimelineProxyProtocol, RoomProxyError>? = nil
|
||||||
DispatchQueue.main.sync {
|
DispatchQueue.main.sync {
|
||||||
returnValue = messageFilteredTimelineAllowedMessageTypesPresentationUnderlyingReturnValue
|
returnValue = messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingReturnValue
|
||||||
}
|
}
|
||||||
|
|
||||||
return returnValue!
|
return returnValue!
|
||||||
@ -6204,26 +6204,26 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
messageFilteredTimelineAllowedMessageTypesPresentationUnderlyingReturnValue = newValue
|
messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingReturnValue = newValue
|
||||||
} else {
|
} else {
|
||||||
DispatchQueue.main.sync {
|
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> {
|
func messageFilteredTimeline(focus: TimelineFocus, allowedMessageTypes: [TimelineAllowedMessageType], presentation: TimelineKind.MediaPresentation) async -> Result<TimelineProxyProtocol, RoomProxyError> {
|
||||||
messageFilteredTimelineAllowedMessageTypesPresentationCallsCount += 1
|
messageFilteredTimelineFocusAllowedMessageTypesPresentationCallsCount += 1
|
||||||
messageFilteredTimelineAllowedMessageTypesPresentationReceivedArguments = (allowedMessageTypes: allowedMessageTypes, presentation: presentation)
|
messageFilteredTimelineFocusAllowedMessageTypesPresentationReceivedArguments = (focus: focus, allowedMessageTypes: allowedMessageTypes, presentation: presentation)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.messageFilteredTimelineAllowedMessageTypesPresentationReceivedInvocations.append((allowedMessageTypes: allowedMessageTypes, presentation: presentation))
|
self.messageFilteredTimelineFocusAllowedMessageTypesPresentationReceivedInvocations.append((focus: focus, allowedMessageTypes: allowedMessageTypes, presentation: presentation))
|
||||||
}
|
}
|
||||||
if let messageFilteredTimelineAllowedMessageTypesPresentationClosure = messageFilteredTimelineAllowedMessageTypesPresentationClosure {
|
if let messageFilteredTimelineFocusAllowedMessageTypesPresentationClosure = messageFilteredTimelineFocusAllowedMessageTypesPresentationClosure {
|
||||||
return await messageFilteredTimelineAllowedMessageTypesPresentationClosure(allowedMessageTypes, presentation)
|
return await messageFilteredTimelineFocusAllowedMessageTypesPresentationClosure(focus, allowedMessageTypes, presentation)
|
||||||
} else {
|
} else {
|
||||||
return messageFilteredTimelineAllowedMessageTypesPresentationReturnValue
|
return messageFilteredTimelineFocusAllowedMessageTypesPresentationReturnValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//MARK: - enableEncryption
|
//MARK: - enableEncryption
|
||||||
@ -14488,15 +14488,15 @@ class TimelineControllerFactoryMock: TimelineControllerFactoryProtocol, @uncheck
|
|||||||
}
|
}
|
||||||
//MARK: - buildMessageFilteredTimelineController
|
//MARK: - buildMessageFilteredTimelineController
|
||||||
|
|
||||||
var buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = 0
|
var buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = 0
|
||||||
var buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderCallsCount: Int {
|
var buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderCallsCount: Int {
|
||||||
get {
|
get {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
return buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount
|
return buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount
|
||||||
} else {
|
} else {
|
||||||
var returnValue: Int? = nil
|
var returnValue: Int? = nil
|
||||||
DispatchQueue.main.sync {
|
DispatchQueue.main.sync {
|
||||||
returnValue = buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount
|
returnValue = buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount
|
||||||
}
|
}
|
||||||
|
|
||||||
return returnValue!
|
return returnValue!
|
||||||
@ -14504,29 +14504,29 @@ class TimelineControllerFactoryMock: TimelineControllerFactoryProtocol, @uncheck
|
|||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue
|
buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue
|
||||||
} else {
|
} else {
|
||||||
DispatchQueue.main.sync {
|
DispatchQueue.main.sync {
|
||||||
buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue
|
buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderCalled: Bool {
|
var buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderCalled: Bool {
|
||||||
return buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderCallsCount > 0
|
return buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderCallsCount > 0
|
||||||
}
|
}
|
||||||
var buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReceivedArguments: (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 buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReceivedInvocations: [(allowedMessageTypes: [RoomMessageEventMessageType], 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 buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue: Result<TimelineControllerProtocol, TimelineFactoryControllerError>!
|
||||||
var buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReturnValue: Result<TimelineControllerProtocol, TimelineFactoryControllerError>! {
|
var buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReturnValue: Result<TimelineControllerProtocol, TimelineFactoryControllerError>! {
|
||||||
get {
|
get {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
return buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue
|
return buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue
|
||||||
} else {
|
} else {
|
||||||
var returnValue: Result<TimelineControllerProtocol, TimelineFactoryControllerError>? = nil
|
var returnValue: Result<TimelineControllerProtocol, TimelineFactoryControllerError>? = nil
|
||||||
DispatchQueue.main.sync {
|
DispatchQueue.main.sync {
|
||||||
returnValue = buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue
|
returnValue = buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue
|
||||||
}
|
}
|
||||||
|
|
||||||
return returnValue!
|
return returnValue!
|
||||||
@ -14534,26 +14534,26 @@ class TimelineControllerFactoryMock: TimelineControllerFactoryProtocol, @uncheck
|
|||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue
|
buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue
|
||||||
} else {
|
} else {
|
||||||
DispatchQueue.main.sync {
|
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> {
|
func buildMessageFilteredTimelineController(focus: TimelineFocus, allowedMessageTypes: [TimelineAllowedMessageType], presentation: TimelineKind.MediaPresentation, roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol) async -> Result<TimelineControllerProtocol, TimelineFactoryControllerError> {
|
||||||
buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderCallsCount += 1
|
buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderCallsCount += 1
|
||||||
buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReceivedArguments = (allowedMessageTypes: allowedMessageTypes, presentation: presentation, roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider)
|
buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReceivedArguments = (focus: focus, allowedMessageTypes: allowedMessageTypes, presentation: presentation, roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider)
|
||||||
DispatchQueue.main.async {
|
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 {
|
if let buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderClosure = buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderClosure {
|
||||||
return await buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderClosure(allowedMessageTypes, presentation, roomProxy, timelineItemFactory, mediaProvider)
|
return await buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderClosure(focus, allowedMessageTypes, presentation, roomProxy, timelineItemFactory, mediaProvider)
|
||||||
} else {
|
} else {
|
||||||
return buildMessageFilteredTimelineControllerAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReturnValue
|
return buildMessageFilteredTimelineControllerFocusAllowedMessageTypesPresentationRoomProxyTimelineItemFactoryMediaProviderReturnValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,9 +41,16 @@ class TimelineMediaPreviewDataSource: NSObject, QLPreviewControllerDataSource {
|
|||||||
previewItems = itemViewStates.compactMap(TimelineMediaPreviewItem.Media.init)
|
previewItems = itemViewStates.compactMap(TimelineMediaPreviewItem.Media.init)
|
||||||
self.initialItem = initialItem
|
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
|
initialItemIndex = initialItemArrayIndex + initialPadding
|
||||||
currentItem = .media(previewItems[initialItemArrayIndex])
|
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
|
backwardPadding = initialPadding
|
||||||
forwardPadding = initialPadding
|
forwardPadding = initialPadding
|
||||||
@ -83,10 +90,18 @@ class TimelineMediaPreviewDataSource: NSObject, QLPreviewControllerDataSource {
|
|||||||
hasPaginated = true
|
hasPaginated = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Do nothing! Not ideal but if we reload the data source the current item will
|
// When the timeline is loading items from the store and the initial item is the only
|
||||||
// also be, reloaded resetting any interaction the user has made with it. If we
|
// preview in the array, we don't want to wipe it out, so if the existing items aren't
|
||||||
// ignore the pagination, then the next time they swipe they'll land on a different
|
// 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!
|
// 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
|
previewItems = newItems
|
||||||
@ -109,9 +124,9 @@ class TimelineMediaPreviewDataSource: NSObject, QLPreviewControllerDataSource {
|
|||||||
let arrayIndex = index - backwardPadding
|
let arrayIndex = index - backwardPadding
|
||||||
|
|
||||||
if index < firstPreviewItemIndex {
|
if index < firstPreviewItemIndex {
|
||||||
return paginationState.backward == .timelineEndReached ? TimelineMediaPreviewItem.Loading.timelineStart : .paginating
|
return paginationState.backward == .timelineEndReached ? TimelineMediaPreviewItem.Loading.timelineStart : .paginatingBackwards
|
||||||
} else if index > lastPreviewItemIndex {
|
} else if index > lastPreviewItemIndex {
|
||||||
return paginationState.forward == .timelineEndReached ? TimelineMediaPreviewItem.Loading.timelineEnd : .paginating
|
return paginationState.forward == .timelineEndReached ? TimelineMediaPreviewItem.Loading.timelineEnd : .paginatingForwards
|
||||||
} else {
|
} else {
|
||||||
return previewItems[arrayIndex]
|
return previewItems[arrayIndex]
|
||||||
}
|
}
|
||||||
@ -151,7 +166,15 @@ enum TimelineMediaPreviewItem: Equatable {
|
|||||||
|
|
||||||
// MARK: Identifiable
|
// 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
|
// MARK: QLPreviewItem
|
||||||
|
|
||||||
@ -274,11 +297,12 @@ enum TimelineMediaPreviewItem: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Loading: NSObject, QLPreviewItem {
|
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 timelineStart = Loading(state: .timelineStart)
|
||||||
static let timelineEnd = Loading(state: .timelineEnd)
|
static let timelineEnd = Loading(state: .timelineEnd)
|
||||||
|
|
||||||
enum State { case paginating, timelineStart, timelineEnd }
|
enum State { case paginating(PaginationDirection), timelineStart, timelineEnd }
|
||||||
let state: State
|
let state: State
|
||||||
|
|
||||||
let previewItemURL: URL? = nil
|
let previewItemURL: URL? = nil
|
||||||
|
@ -14,7 +14,7 @@ enum TimelineMediaPreviewViewModelAction: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum TimelineMediaPreviewDriverAction {
|
enum TimelineMediaPreviewDriverAction {
|
||||||
case itemLoaded(TimelineItemIdentifier)
|
case itemLoaded(TimelineItemIdentifier.EventOrTransactionID)
|
||||||
case showItemDetails(TimelineMediaPreviewItem.Media)
|
case showItemDetails(TimelineMediaPreviewItem.Media)
|
||||||
case exportFile(TimelineMediaPreviewFileExportPicker.File)
|
case exportFile(TimelineMediaPreviewFileExportPicker.File)
|
||||||
case authorizationRequired(appMediator: AppMediatorProtocol)
|
case authorizationRequired(appMediator: AppMediatorProtocol)
|
||||||
|
@ -63,7 +63,11 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
|||||||
|
|
||||||
timelineViewModel.context.$viewState.map(\.timelineState.paginationState)
|
timelineViewModel.context.$viewState.map(\.timelineState.paginationState)
|
||||||
.removeDuplicates()
|
.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)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +81,7 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
|||||||
switch action {
|
switch action {
|
||||||
case .viewInRoomTimeline:
|
case .viewInRoomTimeline:
|
||||||
state.previewControllerDriver.send(.dismissDetailsSheet)
|
state.previewControllerDriver.send(.dismissDetailsSheet)
|
||||||
actionsSubject.send(.viewInRoomTimeline(item.id))
|
actionsSubject.send(.viewInRoomTimeline(item.timelineItem.id))
|
||||||
case .save:
|
case .save:
|
||||||
Task { await saveCurrentItem() }
|
Task { await saveCurrentItem() }
|
||||||
case .redact:
|
case .redact:
|
||||||
@ -111,6 +115,23 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
|||||||
mediaItem.downloadError = error
|
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) {
|
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.bindings.redactConfirmationItem = nil
|
||||||
state.previewControllerDriver.send(.dismissDetailsSheet)
|
state.previewControllerDriver.send(.dismissDetailsSheet)
|
||||||
actionsSubject.send(.dismiss)
|
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 }
|
guard (currentPreviewItem as? TimelineMediaPreviewItem.Media)?.id == itemID else { return }
|
||||||
refreshCurrentPreviewItem()
|
refreshCurrentPreviewItem()
|
||||||
}
|
}
|
||||||
@ -301,7 +301,8 @@ private struct DownloadIndicatorView: View {
|
|||||||
private var shouldShowDownloadIndicator: Bool {
|
private var shouldShowDownloadIndicator: Bool {
|
||||||
switch currentItem {
|
switch currentItem {
|
||||||
case .media(let mediaItem): mediaItem.fileHandle == nil
|
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,
|
thumbnailInfo: .mockThumbnail,
|
||||||
contentType: contentType))
|
contentType: contentType))
|
||||||
|
|
||||||
let timelineKind = TimelineKind.media(isPresentedOnRoomScreen ? .roomScreen : .mediaFilesScreen)
|
let timelineKind = TimelineKind.media(isPresentedOnRoomScreen ? .roomScreenLive : .mediaFilesScreen)
|
||||||
let timelineController = MockTimelineController(timelineKind: timelineKind)
|
let timelineController = MockTimelineController(timelineKind: timelineKind)
|
||||||
timelineController.timelineItems = [item]
|
timelineController.timelineItems = [item]
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ struct TimelineMediaPreviewRedactConfirmationView: View {
|
|||||||
.scaledFrame(size: 40)
|
.scaledFrame(size: 40)
|
||||||
.background {
|
.background {
|
||||||
LoadableImage(mediaSource: mediaSource,
|
LoadableImage(mediaSource: mediaSource,
|
||||||
mediaType: .timelineItem(uniqueID: item.id.uniqueID),
|
mediaType: .generic,
|
||||||
blurhash: item.blurhash,
|
blurhash: item.blurhash,
|
||||||
mediaProvider: context.mediaProvider) {
|
mediaProvider: context.mediaProvider) {
|
||||||
Color.compound.bgSubtleSecondary
|
Color.compound.bgSubtleSecondary
|
||||||
|
@ -79,6 +79,10 @@ final class MediaEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
viewModel.stop()
|
||||||
|
}
|
||||||
|
|
||||||
func toPresentable() -> AnyView {
|
func toPresentable() -> AnyView {
|
||||||
AnyView(MediaEventsTimelineScreen(context: viewModel.context))
|
AnyView(MediaEventsTimelineScreen(context: viewModel.context))
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,19 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.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
|
filesTimelineViewModel.context.$viewState.sink { [weak self] timelineViewState in
|
||||||
guard let self, state.bindings.screenMode == .files else {
|
guard let self, state.bindings.screenMode == .files else {
|
||||||
return
|
return
|
||||||
@ -73,6 +86,19 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.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)
|
updateWithTimelineViewState(activeTimelineViewModel.context.viewState)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,10 +116,15 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
|||||||
case .oldestItemDidDisappear:
|
case .oldestItemDidDisappear:
|
||||||
isOldestItemVisible = false
|
isOldestItemVisible = false
|
||||||
case .tappedItem(let item):
|
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
|
// MARK: - Private
|
||||||
|
|
||||||
private func updateWithTimelineViewState(_ timelineViewState: TimelineViewState) {
|
private func updateWithTimelineViewState(_ timelineViewState: TimelineViewState) {
|
||||||
@ -146,26 +177,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleItemTapped(_ item: RoomTimelineItemViewState) {
|
private func displayMediaPreview(_ viewModel: TimelineMediaPreviewViewModel) {
|
||||||
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)
|
|
||||||
viewModel.actions.sink { [weak self] action in
|
viewModel.actions.sink { [weak self] action in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
switch action {
|
switch action {
|
||||||
|
@ -11,4 +11,6 @@ import Combine
|
|||||||
protocol MediaEventsTimelineScreenViewModelProtocol {
|
protocol MediaEventsTimelineScreenViewModelProtocol {
|
||||||
var actionsPublisher: AnyPublisher<MediaEventsTimelineScreenViewModelAction, Never> { get }
|
var actionsPublisher: AnyPublisher<MediaEventsTimelineScreenViewModelAction, Never> { get }
|
||||||
var context: MediaEventsTimelineScreenViewModelType.Context { get }
|
var context: MediaEventsTimelineScreenViewModelType.Context { get }
|
||||||
|
|
||||||
|
func stop()
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,9 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
switch action {
|
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:
|
case .dismiss:
|
||||||
self.actionsSubject.send(.dismiss)
|
self.actionsSubject.send(.dismiss)
|
||||||
}
|
}
|
||||||
@ -77,6 +80,8 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
actionsSubject.send(.displayUser(userID: userID))
|
actionsSubject.send(.displayUser(userID: userID))
|
||||||
case .displayMessageForwarding(let forwardingItem):
|
case .displayMessageForwarding(let forwardingItem):
|
||||||
actionsSubject.send(.displayMessageForwarding(forwardingItem: forwardingItem))
|
actionsSubject.send(.displayMessageForwarding(forwardingItem: forwardingItem))
|
||||||
|
case .displayMediaPreview(let mediaPreviewViewModel):
|
||||||
|
viewModel.displayMediaPreview(mediaPreviewViewModel)
|
||||||
case .displayLocation(_, let geoURI, let description):
|
case .displayLocation(_, let geoURI, let description):
|
||||||
actionsSubject.send(.presentLocationViewer(geoURI: geoURI, description: description))
|
actionsSubject.send(.presentLocationViewer(geoURI: geoURI, description: description))
|
||||||
case .viewInRoomTimeline(let eventID):
|
case .viewInRoomTimeline(let eventID):
|
||||||
@ -92,6 +97,10 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
viewModel.stop()
|
||||||
|
}
|
||||||
|
|
||||||
func toPresentable() -> AnyView {
|
func toPresentable() -> AnyView {
|
||||||
AnyView(PinnedEventsTimelineScreen(context: viewModel.context, timelineContext: timelineViewModel.context))
|
AnyView(PinnedEventsTimelineScreen(context: viewModel.context, timelineContext: timelineViewModel.context))
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,18 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum PinnedEventsTimelineScreenViewModelAction {
|
enum PinnedEventsTimelineScreenViewModelAction {
|
||||||
|
case viewInRoomTimeline(itemID: TimelineItemIdentifier)
|
||||||
case dismiss
|
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 {
|
enum PinnedEventsTimelineScreenViewAction {
|
||||||
case close
|
case close
|
||||||
|
@ -34,4 +34,23 @@ class PinnedEventsTimelineScreenViewModel: PinnedEventsTimelineScreenViewModelTy
|
|||||||
actionsSubject.send(.dismiss)
|
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 {
|
protocol PinnedEventsTimelineScreenViewModelProtocol {
|
||||||
var actionsPublisher: AnyPublisher<PinnedEventsTimelineScreenViewModelAction, Never> { get }
|
var actionsPublisher: AnyPublisher<PinnedEventsTimelineScreenViewModelAction, Never> { get }
|
||||||
var context: PinnedEventsTimelineScreenViewModelType.Context { get }
|
var context: PinnedEventsTimelineScreenViewModelType.Context { get }
|
||||||
|
|
||||||
|
func stop()
|
||||||
|
|
||||||
|
func displayMediaPreview(_ mediaPreviewViewModel: TimelineMediaPreviewViewModel)
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ struct PinnedEventsTimelineScreen: View {
|
|||||||
.toolbar { toolbar }
|
.toolbar { toolbar }
|
||||||
.background(.compound.bgCanvasDefault)
|
.background(.compound.bgCanvasDefault)
|
||||||
.interactiveDismissDisabled()
|
.interactiveDismissDisabled()
|
||||||
.interactiveQuickLook(item: $timelineContext.mediaPreviewItem)
|
.timelineMediaPreview(viewModel: $context.mediaPreviewViewModel)
|
||||||
.sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) }
|
.sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) }
|
||||||
.sheet(item: $timelineContext.actionMenuInfo) { info in
|
.sheet(item: $timelineContext.actionMenuInfo) { info in
|
||||||
let actions = TimelineItemMenuActionProvider(timelineItem: info.item,
|
let actions = TimelineItemMenuActionProvider(timelineItem: info.item,
|
||||||
|
@ -126,6 +126,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
actionsSubject.send(.presentMediaUploadPicker(.photoLibrary))
|
actionsSubject.send(.presentMediaUploadPicker(.photoLibrary))
|
||||||
case .displayDocumentPicker:
|
case .displayDocumentPicker:
|
||||||
actionsSubject.send(.presentMediaUploadPicker(.documents))
|
actionsSubject.send(.presentMediaUploadPicker(.documents))
|
||||||
|
case .displayMediaPreview(let mediaPreviewViewModel):
|
||||||
|
roomViewModel.displayMediaPreview(mediaPreviewViewModel)
|
||||||
case .displayLocationPicker:
|
case .displayLocationPicker:
|
||||||
actionsSubject.send(.presentLocationPicker)
|
actionsSubject.send(.presentLocationPicker)
|
||||||
case .displayPollForm(let mode):
|
case .displayPollForm(let mode):
|
||||||
@ -199,7 +201,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
func stop() {
|
func stop() {
|
||||||
composerViewModel.saveDraft()
|
composerViewModel.saveDraft()
|
||||||
timelineViewModel.stop()
|
roomViewModel.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func toPresentable() -> AnyView {
|
func toPresentable() -> AnyView {
|
||||||
|
@ -67,7 +67,10 @@ struct RoomScreenViewState: BindableState {
|
|||||||
var bindings: RoomScreenViewStateBindings
|
var bindings: RoomScreenViewStateBindings
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RoomScreenViewStateBindings { }
|
struct RoomScreenViewStateBindings {
|
||||||
|
/// The view model used to present a QuickLook media preview.
|
||||||
|
var mediaPreviewViewModel: TimelineMediaPreviewViewModel?
|
||||||
|
}
|
||||||
|
|
||||||
enum RoomScreenFooterViewAction {
|
enum RoomScreenFooterViewAction {
|
||||||
case resolvePinViolation(userID: String)
|
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) {
|
func timelineHasScrolled(direction: ScrollDirection) {
|
||||||
state.lastScrollDirection = direction
|
state.lastScrollDirection = direction
|
||||||
}
|
}
|
||||||
@ -123,6 +128,20 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
state.pinnedEventsBannerState.setSelectedPinnedEventID(eventID)
|
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
|
// MARK: - Private
|
||||||
|
|
||||||
private func setupSubscriptions(ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>) {
|
private func setupSubscriptions(ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>) {
|
||||||
|
@ -12,6 +12,9 @@ protocol RoomScreenViewModelProtocol {
|
|||||||
var actions: AnyPublisher<RoomScreenViewModelAction, Never> { get }
|
var actions: AnyPublisher<RoomScreenViewModelAction, Never> { get }
|
||||||
var context: RoomScreenViewModel.Context { get }
|
var context: RoomScreenViewModel.Context { get }
|
||||||
|
|
||||||
|
func stop()
|
||||||
|
|
||||||
func timelineHasScrolled(direction: ScrollDirection)
|
func timelineHasScrolled(direction: ScrollDirection)
|
||||||
func setSelectedPinnedEventID(_ eventID: String)
|
func setSelectedPinnedEventID(_ eventID: String)
|
||||||
|
func displayMediaPreview(_ mediaPreviewViewModel: TimelineMediaPreviewViewModel)
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,7 @@ struct RoomScreen: View {
|
|||||||
ReadReceiptsSummaryView(orderedReadReceipts: $0.orderedReceipts)
|
ReadReceiptsSummaryView(orderedReadReceipts: $0.orderedReceipts)
|
||||||
.environmentObject(timelineContext)
|
.environmentObject(timelineContext)
|
||||||
}
|
}
|
||||||
.interactiveQuickLook(item: $timelineContext.mediaPreviewItem)
|
.timelineMediaPreview(viewModel: $roomContext.mediaPreviewViewModel)
|
||||||
.track(screen: .Room)
|
.track(screen: .Room)
|
||||||
.onDrop(of: ["public.item", "public.file-url"], isTargeted: $dragOver) { providers -> Bool in
|
.onDrop(of: ["public.item", "public.file-url"], isTargeted: $dragOver) { providers -> Bool in
|
||||||
guard let provider = providers.first,
|
guard let provider = providers.first,
|
||||||
|
@ -502,7 +502,7 @@ class TimelineInteractionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func processItemTap(_ itemID: TimelineItemIdentifier) async -> TimelineControllerAction {
|
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
|
return .none
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -510,8 +510,14 @@ class TimelineInteractionHandler {
|
|||||||
case let item as LocationRoomTimelineItem:
|
case let item as LocationRoomTimelineItem:
|
||||||
guard let geoURI = item.content.geoURI else { return .none }
|
guard let geoURI = item.content.geoURI else { return .none }
|
||||||
return .displayLocation(body: item.content.body, geoURI: geoURI, description: item.content.description)
|
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:
|
default:
|
||||||
return await displayMediaActionIfPossible(timelineItem: timelineItem)
|
return .none
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -528,39 +534,57 @@ class TimelineInteractionHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func displayMediaActionIfPossible(timelineItem: RoomTimelineItemProtocol) async -> TimelineControllerAction {
|
private func mediaPreviewAction(for item: EventBasedMessageTimelineItemProtocol, messageTypes: [TimelineAllowedMessageType]) async -> TimelineControllerAction {
|
||||||
var source: MediaSourceProxy?
|
var newTimelineFocus: TimelineFocus?
|
||||||
var filename: String
|
var newTimelinePresentation: TimelineKind.MediaPresentation?
|
||||||
var caption: String?
|
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 {
|
if let newTimelineFocus, let newTimelinePresentation {
|
||||||
case let item as ImageRoomTimelineItem:
|
let timelineItemFactory = RoomTimelineItemFactory(userID: roomProxy.ownUserID,
|
||||||
source = item.content.imageInfo.source
|
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||||
filename = item.content.filename
|
stateEventStringBuilder: RoomStateEventStringBuilder(userID: roomProxy.ownUserID))
|
||||||
caption = item.content.caption
|
|
||||||
case let item as VideoRoomTimelineItem:
|
guard case let .success(timelineController) = await timelineControllerFactory.buildMessageFilteredTimelineController(focus: newTimelineFocus,
|
||||||
source = item.content.videoInfo.source
|
allowedMessageTypes: messageTypes,
|
||||||
filename = item.content.filename
|
presentation: newTimelinePresentation,
|
||||||
caption = item.content.caption
|
roomProxy: roomProxy,
|
||||||
case let item as FileRoomTimelineItem:
|
timelineItemFactory: timelineItemFactory,
|
||||||
source = item.content.source
|
mediaProvider: mediaProvider) else {
|
||||||
filename = item.content.filename
|
MXLog.error("Failed presenting media timeline")
|
||||||
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:
|
|
||||||
return .none
|
return .none
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let source else { return .none }
|
let timelineViewModel = TimelineViewModel(roomProxy: roomProxy,
|
||||||
switch await mediaProvider.loadFileFromSource(source, filename: filename) {
|
timelineController: timelineController,
|
||||||
case .success(let file):
|
mediaProvider: mediaProvider,
|
||||||
return .displayMediaFile(file: file, title: caption ?? filename)
|
mediaPlayerProvider: mediaPlayerProvider,
|
||||||
case .failure:
|
voiceMessageMediaManager: voiceMessageMediaManager,
|
||||||
return .none
|
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 displayMediaUploadPreviewScreen(url: URL)
|
||||||
case tappedOnSenderDetails(userID: String)
|
case tappedOnSenderDetails(userID: String)
|
||||||
case displayMessageForwarding(forwardingItem: MessageForwardingItem)
|
case displayMessageForwarding(forwardingItem: MessageForwardingItem)
|
||||||
|
case displayMediaPreview(TimelineMediaPreviewViewModel)
|
||||||
case displayLocation(body: String, geoURI: GeoURI, description: String?)
|
case displayLocation(body: String, geoURI: GeoURI, description: String?)
|
||||||
case displayResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy)
|
case displayResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy)
|
||||||
case composer(action: TimelineComposerAction)
|
case composer(action: TimelineComposerAction)
|
||||||
@ -123,9 +124,6 @@ struct TimelineViewStateBindings {
|
|||||||
/// Key is itemID, value is the collapsed state.
|
/// Key is itemID, value is the collapsed state.
|
||||||
var reactionsCollapsed: [TimelineItemIdentifier: Bool]
|
var reactionsCollapsed: [TimelineItemIdentifier: Bool]
|
||||||
|
|
||||||
/// A media item that will be previewed with QuickLook.
|
|
||||||
var mediaPreviewItem: MediaPreviewItem?
|
|
||||||
|
|
||||||
var alertInfo: AlertInfo<RoomScreenAlertInfoType>?
|
var alertInfo: AlertInfo<RoomScreenAlertInfoType>?
|
||||||
|
|
||||||
var debugInfo: TimelineItemDebugInfo?
|
var debugInfo: TimelineItemDebugInfo?
|
||||||
|
@ -23,6 +23,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
|
|
||||||
private let roomProxy: JoinedRoomProxyProtocol
|
private let roomProxy: JoinedRoomProxyProtocol
|
||||||
private let timelineController: TimelineControllerProtocol
|
private let timelineController: TimelineControllerProtocol
|
||||||
|
private let mediaProvider: MediaProviderProtocol
|
||||||
private let mediaPlayerProvider: MediaPlayerProviderProtocol
|
private let mediaPlayerProvider: MediaPlayerProviderProtocol
|
||||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
private let appMediator: AppMediatorProtocol
|
private let appMediator: AppMediatorProtocol
|
||||||
@ -56,6 +57,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
emojiProvider: EmojiProviderProtocol,
|
emojiProvider: EmojiProviderProtocol,
|
||||||
timelineControllerFactory: TimelineControllerFactoryProtocol) {
|
timelineControllerFactory: TimelineControllerFactoryProtocol) {
|
||||||
self.timelineController = timelineController
|
self.timelineController = timelineController
|
||||||
|
self.mediaProvider = mediaProvider
|
||||||
self.mediaPlayerProvider = mediaPlayerProvider
|
self.mediaPlayerProvider = mediaPlayerProvider
|
||||||
self.roomProxy = roomProxy
|
self.roomProxy = roomProxy
|
||||||
self.appSettings = appSettings
|
self.appSettings = appSettings
|
||||||
@ -123,11 +125,6 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
|
|
||||||
func stop() {
|
|
||||||
// Work around QLPreviewController dismissal issues, see the InteractiveQuickLookModifier.
|
|
||||||
state.bindings.mediaPreviewItem = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
override func process(viewAction: TimelineViewAction) {
|
override func process(viewAction: TimelineViewAction) {
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .itemAppeared(let id):
|
case .itemAppeared(let id):
|
||||||
@ -544,9 +541,11 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
let action = await timelineInteractionHandler.processItemTap(itemID)
|
let action = await timelineInteractionHandler.processItemTap(itemID)
|
||||||
|
|
||||||
switch action {
|
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.
|
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):
|
case .displayLocation(let body, let geoURI, let description):
|
||||||
actionsSubject.send(.displayLocation(body: body, geoURI: geoURI, description: description))
|
actionsSubject.send(.displayLocation(body: body, geoURI: geoURI, description: description))
|
||||||
case .none:
|
case .none:
|
||||||
@ -655,6 +654,21 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
analyticsService.trackComposer(inThread: false, isEditing: isEdit, isReply: isReply, startsThread: nil)
|
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
|
// MARK: - Timeline Item Building
|
||||||
|
|
||||||
private func buildTimelineViews(timelineItems: [RoomTimelineItemProtocol], isSwitchingTimelines: Bool = false) {
|
private func buildTimelineViews(timelineItems: [RoomTimelineItemProtocol], isSwitchingTimelines: Bool = false) {
|
||||||
|
@ -13,8 +13,8 @@ import SwiftUI
|
|||||||
protocol TimelineViewModelProtocol {
|
protocol TimelineViewModelProtocol {
|
||||||
var actions: AnyPublisher<TimelineViewModelAction, Never> { get }
|
var actions: AnyPublisher<TimelineViewModelAction, Never> { get }
|
||||||
var context: TimelineViewModel.Context { get }
|
var context: TimelineViewModel.Context { get }
|
||||||
|
|
||||||
func process(composerAction: ComposerToolbarViewModelAction)
|
func process(composerAction: ComposerToolbarViewModelAction)
|
||||||
/// Updates the timeline to show and highlight the item with the corresponding event ID.
|
/// Updates the timeline to show and highlight the item with the corresponding event ID.
|
||||||
func focusOnEvent(eventID: String) async
|
func focusOnEvent(eventID: String) async
|
||||||
func stop()
|
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ struct TimelineItemMenuActionProvider {
|
|||||||
var actions: [TimelineItemMenuAction] = []
|
var actions: [TimelineItemMenuAction] = []
|
||||||
var secondaryActions: [TimelineItemMenuAction] = []
|
var secondaryActions: [TimelineItemMenuAction] = []
|
||||||
|
|
||||||
if timelineKind == .pinned || timelineKind == .media(.mediaFilesScreen) {
|
if timelineKind == .pinned || timelineKind == .media(.mediaFilesScreen) || timelineKind == .media(.pinnedEventsScreen) {
|
||||||
actions.append(.viewInRoomTimeline)
|
actions.append(.viewInRoomTimeline)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -375,7 +375,6 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable:next function_parameter_count
|
|
||||||
func createRoom(name: String,
|
func createRoom(name: String,
|
||||||
topic: String?,
|
topic: String?,
|
||||||
isRoomPrivate: Bool,
|
isRoomPrivate: Bool,
|
||||||
|
@ -117,7 +117,6 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
|||||||
|
|
||||||
func createDirectRoom(with userID: String, expectedRoomName: String?) async -> Result<String, ClientProxyError>
|
func createDirectRoom(with userID: String, expectedRoomName: String?) async -> Result<String, ClientProxyError>
|
||||||
|
|
||||||
// swiftlint:disable:next function_parameter_count
|
|
||||||
func createRoom(name: String,
|
func createRoom(name: String,
|
||||||
topic: String?,
|
topic: String?,
|
||||||
isRoomPrivate: Bool,
|
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> {
|
presentation: TimelineKind.MediaPresentation) async -> Result<any TimelineProxyProtocol, RoomProxyError> {
|
||||||
do {
|
do {
|
||||||
let sdkTimeline = try await room.timelineWithConfiguration(configuration: .init(focus: .live,
|
let rustFocus: MatrixRustSDK.TimelineFocus = switch focus {
|
||||||
allowedMessageTypes: .only(types: allowedMessageTypes),
|
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,
|
internalIdPrefix: nil,
|
||||||
dateDividerMode: .monthly))
|
dateDividerMode: .monthly))
|
||||||
|
|
||||||
|
@ -78,7 +78,8 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
|
|||||||
|
|
||||||
func timelineFocusedOnEvent(eventID: String, numberOfEvents: UInt16) async -> Result<TimelineProxyProtocol, RoomProxyError>
|
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>
|
presentation: TimelineKind.MediaPresentation) async -> Result<TimelineProxyProtocol, RoomProxyError>
|
||||||
|
|
||||||
func enableEncryption() async -> Result<Void, RoomProxyError>
|
func enableEncryption() async -> Result<Void, RoomProxyError>
|
||||||
|
@ -75,7 +75,10 @@ class MockTimelineController: TimelineControllerProtocol {
|
|||||||
callbacks.send(.isLive(true))
|
callbacks.send(.isLive(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private(set) var paginateBackwardsCallCount = 0
|
||||||
func paginateBackwards(requestSize: UInt16) async -> Result<Void, TimelineControllerError> {
|
func paginateBackwards(requestSize: UInt16) async -> Result<Void, TimelineControllerError> {
|
||||||
|
paginateBackwardsCallCount += 1
|
||||||
|
|
||||||
paginationState = PaginationState(backward: .paginating, forward: .timelineEndReached)
|
paginationState = PaginationState(backward: .paginating, forward: .timelineEndReached)
|
||||||
|
|
||||||
if client == nil {
|
if client == nil {
|
||||||
@ -85,9 +88,10 @@ class MockTimelineController: TimelineControllerProtocol {
|
|||||||
return .success(())
|
return .success(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private(set) var paginateForwardsCallCount = 0
|
||||||
func paginateForwards(requestSize: UInt16) async -> Result<Void, TimelineControllerError> {
|
func paginateForwards(requestSize: UInt16) async -> Result<Void, TimelineControllerError> {
|
||||||
// try? await simulateForwardPagination()
|
paginateForwardsCallCount += 1
|
||||||
.success(())
|
return .success(())
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendReadReceipt(for itemID: TimelineItemIdentifier) async {
|
func sendReadReceipt(for itemID: TimelineItemIdentifier) async {
|
||||||
|
@ -42,7 +42,7 @@ class TimelineController: TimelineControllerProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var timelineKind: TimelineKind {
|
var timelineKind: TimelineKind {
|
||||||
liveTimelineProvider.kind
|
activeTimelineProvider.kind
|
||||||
}
|
}
|
||||||
|
|
||||||
init(roomProxy: JoinedRoomProxyProtocol,
|
init(roomProxy: JoinedRoomProxyProtocol,
|
||||||
|
@ -36,12 +36,13 @@ struct TimelineControllerFactory: TimelineControllerFactoryProtocol {
|
|||||||
appSettings: ServiceLocator.shared.settings)
|
appSettings: ServiceLocator.shared.settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildMessageFilteredTimelineController(allowedMessageTypes: [RoomMessageEventMessageType],
|
func buildMessageFilteredTimelineController(focus: TimelineFocus,
|
||||||
|
allowedMessageTypes: [TimelineAllowedMessageType],
|
||||||
presentation: TimelineKind.MediaPresentation,
|
presentation: TimelineKind.MediaPresentation,
|
||||||
roomProxy: JoinedRoomProxyProtocol,
|
roomProxy: JoinedRoomProxyProtocol,
|
||||||
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
||||||
mediaProvider: MediaProviderProtocol) async -> Result<TimelineControllerProtocol, TimelineFactoryControllerError> {
|
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):
|
case .success(let timelineProxy):
|
||||||
return .success(TimelineController(roomProxy: roomProxy,
|
return .success(TimelineController(roomProxy: roomProxy,
|
||||||
timelineProxy: timelineProxy,
|
timelineProxy: timelineProxy,
|
||||||
|
@ -23,7 +23,8 @@ protocol TimelineControllerFactoryProtocol {
|
|||||||
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
||||||
mediaProvider: MediaProviderProtocol) async -> TimelineControllerProtocol?
|
mediaProvider: MediaProviderProtocol) async -> TimelineControllerProtocol?
|
||||||
|
|
||||||
func buildMessageFilteredTimelineController(allowedMessageTypes: [RoomMessageEventMessageType],
|
func buildMessageFilteredTimelineController(focus: TimelineFocus,
|
||||||
|
allowedMessageTypes: [TimelineAllowedMessageType],
|
||||||
presentation: TimelineKind.MediaPresentation,
|
presentation: TimelineKind.MediaPresentation,
|
||||||
roomProxy: JoinedRoomProxyProtocol,
|
roomProxy: JoinedRoomProxyProtocol,
|
||||||
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
||||||
|
@ -16,7 +16,14 @@ enum TimelineControllerCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum TimelineControllerAction {
|
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 displayLocation(body: String, geoURI: GeoURI, description: String?)
|
||||||
case none
|
case none
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,6 @@ struct RoomStateEventStringBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable:next function_parameter_count
|
|
||||||
func buildProfileChangeString(displayName: String?, previousDisplayName: String?,
|
func buildProfileChangeString(displayName: String?, previousDisplayName: String?,
|
||||||
avatarURLString: String?, previousAvatarURLString: String?,
|
avatarURLString: String?, previousAvatarURLString: String?,
|
||||||
member: String, memberIsYou: Bool) -> String? {
|
member: String, memberIsYou: Bool) -> String? {
|
||||||
|
@ -380,7 +380,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
|
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable:next function_parameter_count
|
|
||||||
private func buildPollTimelineItem(_ question: String,
|
private func buildPollTimelineItem(_ question: String,
|
||||||
_ pollKind: PollKind,
|
_ pollKind: PollKind,
|
||||||
_ maxSelections: UInt64,
|
_ maxSelections: UInt64,
|
||||||
@ -645,7 +644,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
return buildStateTimelineItem(for: eventItemProxy, text: text, isOutgoing: isOutgoing)
|
return buildStateTimelineItem(for: eventItemProxy, text: text, isOutgoing: isOutgoing)
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable:next function_parameter_count
|
|
||||||
private func buildStateProfileChangeTimelineItem(for eventItemProxy: EventTimelineItemProxy,
|
private func buildStateProfileChangeTimelineItem(for eventItemProxy: EventTimelineItemProxy,
|
||||||
displayName: String?,
|
displayName: String?,
|
||||||
previousDisplayName: String?,
|
previousDisplayName: String?,
|
||||||
|
@ -591,8 +591,8 @@ final class TimelineProxy: TimelineProxyProtocol {
|
|||||||
backPaginationStatusSubject.send(.idle)
|
backPaginationStatusSubject.send(.idle)
|
||||||
forwardPaginationStatusSubject.send(.idle)
|
forwardPaginationStatusSubject.send(.idle)
|
||||||
case .media(let presentation):
|
case .media(let presentation):
|
||||||
backPaginationStatusSubject.send(.idle)
|
backPaginationStatusSubject.send(presentation == .pinnedEventsScreen ? .timelineEndReached : .idle)
|
||||||
forwardPaginationStatusSubject.send(presentation == .mediaFilesScreen ? .timelineEndReached : .idle)
|
forwardPaginationStatusSubject.send(presentation == .roomScreenDetached ? .idle : .timelineEndReached)
|
||||||
case .pinned:
|
case .pinned:
|
||||||
backPaginationStatusSubject.send(.timelineEndReached)
|
backPaginationStatusSubject.send(.timelineEndReached)
|
||||||
forwardPaginationStatusSubject.send(.timelineEndReached)
|
forwardPaginationStatusSubject.send(.timelineEndReached)
|
||||||
|
@ -14,10 +14,20 @@ enum TimelineKind: Equatable {
|
|||||||
case detached
|
case detached
|
||||||
case pinned
|
case pinned
|
||||||
|
|
||||||
enum MediaPresentation { case roomScreen, mediaFilesScreen }
|
enum MediaPresentation { case roomScreenLive, roomScreenDetached, pinnedEventsScreen, mediaFilesScreen }
|
||||||
case media(MediaPresentation)
|
case media(MediaPresentation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum TimelineFocus {
|
||||||
|
case live
|
||||||
|
case eventID(String)
|
||||||
|
case pinned
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TimelineAllowedMessageType {
|
||||||
|
case audio, file, image, video
|
||||||
|
}
|
||||||
|
|
||||||
enum TimelineProxyError: Error {
|
enum TimelineProxyError: Error {
|
||||||
case sdkError(Error)
|
case sdkError(Error)
|
||||||
|
|
||||||
|
@ -14,7 +14,6 @@ import XCTest
|
|||||||
class HomeScreenRoomTests: XCTestCase {
|
class HomeScreenRoomTests: XCTestCase {
|
||||||
var roomSummary: RoomSummary!
|
var roomSummary: RoomSummary!
|
||||||
|
|
||||||
// swiftlint:disable:next function_parameter_count
|
|
||||||
func setupRoomSummary(isMarkedUnread: Bool,
|
func setupRoomSummary(isMarkedUnread: Bool,
|
||||||
unreadMessagesCount: UInt,
|
unreadMessagesCount: UInt,
|
||||||
unreadMentionsCount: UInt,
|
unreadMentionsCount: UInt,
|
||||||
|
@ -23,7 +23,7 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
|||||||
initialMediaViewStates = initialMediaItems.map { RoomTimelineItemViewState(item: $0, groupStyle: .single) }
|
initialMediaViewStates = initialMediaItems.map { RoomTimelineItemViewState(item: $0, groupStyle: .single) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func testInitialItems() -> TimelineMediaPreviewDataSource {
|
func testInitialItems() throws -> TimelineMediaPreviewDataSource {
|
||||||
// Given a data source built with the initial items.
|
// Given a data source built with the initial items.
|
||||||
let dataSource = TimelineMediaPreviewDataSource(itemViewStates: initialMediaViewStates,
|
let dataSource = TimelineMediaPreviewDataSource(itemViewStates: initialMediaViewStates,
|
||||||
initialItem: initialMediaItems[initialItemIndex],
|
initialItem: initialMediaItems[initialItemIndex],
|
||||||
@ -32,12 +32,13 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
|||||||
|
|
||||||
// When the preview controller displays the data.
|
// When the preview controller displays the data.
|
||||||
let previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
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.
|
// 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(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(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should be the initial item.")
|
||||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id, "The current item should also 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(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.")
|
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
|
return dataSource
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCurrentUpdateItem() {
|
func testCurrentUpdateItem() throws {
|
||||||
// Given a data source built with the initial items.
|
// Given a data source built with the initial items.
|
||||||
let dataSource = TimelineMediaPreviewDataSource(itemViewStates: initialMediaViewStates,
|
let dataSource = TimelineMediaPreviewDataSource(itemViewStates: initialMediaViewStates,
|
||||||
initialItem: initialMediaItems[initialItemIndex],
|
initialItem: initialMediaItems[initialItemIndex],
|
||||||
paginationState: .initial)
|
paginationState: .initial)
|
||||||
|
|
||||||
// When a different item is displayed.
|
// When a different item is displayed.
|
||||||
guard let previewItem = dataSource.previewController(previewController, previewItemAt: 1 + initialPadding) as? TimelineMediaPreviewItem.Media else {
|
let previewItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: 1 + initialPadding) as? TimelineMediaPreviewItem.Media,
|
||||||
XCTFail("A preview item should be found.")
|
"A preview item should be found.")
|
||||||
return
|
|
||||||
}
|
|
||||||
dataSource.updateCurrentItem(.media(previewItem))
|
dataSource.updateCurrentItem(.media(previewItem))
|
||||||
|
|
||||||
// Then the data source should reflect the change of item.
|
// Then the data source should reflect the change of item.
|
||||||
@ -74,7 +73,7 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
|||||||
|
|
||||||
func testUpdatedItems() async throws {
|
func testUpdatedItems() async throws {
|
||||||
// Given a data source built with the initial items.
|
// 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.
|
// When one of the items changes but no pagination has occurred.
|
||||||
let deferred = deferFailure(dataSource.previewItemsPaginationPublisher, timeout: 1) { _ in true }
|
let deferred = deferFailure(dataSource.previewItemsPaginationPublisher, timeout: 1) { _ in true }
|
||||||
@ -84,9 +83,9 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
|||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
|
|
||||||
let previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
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)
|
||||||
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
XCTAssertEqual(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id, "The current 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(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.")
|
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 {
|
func testPagination() async throws {
|
||||||
// Given a data source built with the initial items.
|
// 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.
|
// When more items are loaded in a back pagination.
|
||||||
var deferred = deferFulfillment(dataSource.previewItemsPaginationPublisher) { _ in true }
|
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.")
|
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
||||||
|
|
||||||
var previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
var previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||||
var displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media
|
var displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media)
|
||||||
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
XCTAssertEqual(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id, "The current 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")
|
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
||||||
|
|
||||||
// When more items are loaded in a forward pagination or sync.
|
// 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.")
|
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
||||||
|
|
||||||
previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||||
displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media
|
displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media)
|
||||||
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
XCTAssertEqual(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id, "The current 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")
|
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPaginationLimits() async throws {
|
func testPaginationLimits() async throws {
|
||||||
// Given a data source with a small amount of padding remaining.
|
// Given a data source with a small amount of padding remaining.
|
||||||
initialPadding = 2
|
initialPadding = 2
|
||||||
let dataSource = testInitialItems()
|
let dataSource = try testInitialItems()
|
||||||
|
|
||||||
// When paginating backwards by more than the available padding.
|
// When paginating backwards by more than the available padding.
|
||||||
var deferred = deferFulfillment(dataSource.previewItemsPaginationPublisher) { _ in true }
|
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.")
|
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
||||||
|
|
||||||
var previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
var previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||||
var displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media
|
var displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media)
|
||||||
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
XCTAssertEqual(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id, "The current 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")
|
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
||||||
|
|
||||||
// When paginating forwards by more than the available padding.
|
// 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.")
|
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
||||||
|
|
||||||
previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||||
displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media
|
displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media)
|
||||||
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
XCTAssertEqual(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id, "The current 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")
|
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
|
// MARK: Helpers
|
||||||
|
|
||||||
func newChunk() -> [EventBasedMessageTimelineItemProtocol] {
|
func newChunk() -> [EventBasedMessageTimelineItemProtocol] {
|
||||||
@ -178,7 +257,7 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private extension TimelineMediaPreviewDataSource {
|
private extension TimelineMediaPreviewDataSource {
|
||||||
var currentMediaItemID: TimelineItemIdentifier? {
|
var currentMediaItemID: TimelineItemIdentifier.EventOrTransactionID? {
|
||||||
switch currentItem {
|
switch currentItem {
|
||||||
case .media(let mediaItem): mediaItem.id
|
case .media(let mediaItem): mediaItem.id
|
||||||
case .loading: nil
|
case .loading: nil
|
||||||
|
@ -84,18 +84,22 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(context.viewState.currentItem, .media(context.viewState.dataSource.previewItems[0]))
|
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.
|
// Given a view model with a loaded item.
|
||||||
try await testLoadingItem()
|
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 }
|
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()
|
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(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 {
|
func testPagination() async throws {
|
||||||
@ -130,7 +134,7 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
|||||||
return
|
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))
|
context.send(viewAction: .menuAction(.viewInRoomTimeline, item: mediaItem))
|
||||||
|
|
||||||
// Then the action should be sent upwards to make this happen.
|
// 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
|
unset HOMEBREW_NO_INSTALL_FROM_API
|
||||||
export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1
|
export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1
|
||||||
|
|
||||||
brew update && brew install xcodegen swiftformat git-lfs a7ex/homebrew-formulae/xcresultparser
|
brew update && brew install xcodegen swiftlint 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
|
|
||||||
|
|
||||||
bundle config path vendor/bundle
|
bundle config path vendor/bundle
|
||||||
bundle install --jobs 4 --retry 3
|
bundle install --jobs 4 --retry 3
|
||||||
|
Loading…
x
Reference in New Issue
Block a user