Media gallery - part 1(#3588)

* Introduce a `MediaEventsTimelineFlowCoordinator`
* Update SDK API and architecture
* Add a feature flag, add translations
* Move the media events timeline presentation under the room flow coordinator state machine
* Rename `TimelineViewState.timelineViewState` of type `TimelineState` to `timelineState`
* Enabled SwiftLint's `trailing_closure` rule and fix the warnings.
This commit is contained in:
Stefan Ceriu
2024-12-06 16:58:14 +02:00
committed by GitHub
parent a9e4837b62
commit caaa89af62
83 changed files with 1247 additions and 279 deletions
@@ -79,7 +79,7 @@ class AttributedStringBuilderTests: XCTestCase {
XCTAssertEqual(attributedString.runs.count, 3)
let link = attributedString.runs.first(where: { $0.link != nil })?.link
let link = attributedString.runs.first { $0.link != nil }?.link
XCTAssertEqual(link?.host, "www.matrix.org")
}
@@ -96,7 +96,7 @@ class AttributedStringBuilderTests: XCTestCase {
XCTAssertEqual(attributedString.runs.count, 3)
let link = attributedString.runs.first(where: { $0.link != nil })?.link
let link = attributedString.runs.first { $0.link != nil }?.link
XCTAssertEqual(link?.host, "www.matrix.org")
}
@@ -113,7 +113,7 @@ class AttributedStringBuilderTests: XCTestCase {
XCTAssertEqual(attributedString.runs.count, 3)
let link = attributedString.runs.first(where: { $0.link != nil })?.link
let link = attributedString.runs.first { $0.link != nil }?.link
XCTAssertEqual(link?.host, "www.matrix.org")
}
@@ -130,7 +130,7 @@ class AttributedStringBuilderTests: XCTestCase {
XCTAssertEqual(attributedString.runs.count, 3)
let link = attributedString.runs.first(where: { $0.link != nil })?.link
let link = attributedString.runs.first { $0.link != nil }?.link
XCTAssertEqual(link, "https://matrix.org")
}
@@ -20,7 +20,7 @@ class BlockedUsersScreenViewModelTests: XCTestCase {
mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: ServiceLocator.shared.userIndicatorController)
let deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { $0.blockedUsers.contains(where: { $0.displayName != nil }) }
let deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { $0.blockedUsers.contains { $0.displayName != nil } }
try await deferred.fulfill()
XCTAssertFalse(viewModel.context.viewState.blockedUsers.isEmpty)
@@ -35,7 +35,7 @@ class BlockedUsersScreenViewModelTests: XCTestCase {
mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: ServiceLocator.shared.userIndicatorController)
let deferred = deferFulfillment(viewModel.context.$viewState) { $0.blockedUsers.contains(where: { $0.displayName != nil }) }
let deferred = deferFulfillment(viewModel.context.$viewState) { $0.blockedUsers.contains { $0.displayName != nil } }
try await deferred.fulfill()
XCTAssertFalse(viewModel.context.viewState.blockedUsers.isEmpty)
@@ -54,10 +54,10 @@ class ComposerToolbarViewModelTests: XCTestCase {
.map(\.composerMode)
.removeDuplicates()
.dropFirst()
.sink(receiveValue: { composerMode in
.sink { composerMode in
XCTAssertEqual(composerMode, mode)
expectation.fulfill()
})
}
viewModel.process(timelineAction: .setMode(mode: mode))
@@ -84,7 +84,7 @@ class JoinRoomScreenViewModelTests: XCTestCase {
topic: nil,
avatarURL: nil,
memberCount: 0,
isHistoryWorldReadable: false,
isHistoryWorldReadable: nil,
isJoined: false,
isInvited: false,
isPublic: false,
@@ -34,7 +34,7 @@ class MessageForwardingScreenViewModelTests: XCTestCase {
}
func testInitialState() {
XCTAssertNil(context.viewState.rooms.first(where: { $0.id == forwardingItem.roomID }), "The source room ID shouldn't be shown")
XCTAssertNil(context.viewState.rooms.first { $0.id == forwardingItem.roomID }, "The source room ID shouldn't be shown")
}
func testRoomSelection() {
@@ -29,7 +29,7 @@ class PollFormScreenViewModelTests: XCTestCase {
XCTAssertFalse(context.viewState.bindings.isUndisclosed)
// Cancellation should work without confirmation
let deferred = deferFulfillment(viewModel.actions, until: { _ in true })
let deferred = deferFulfillment(viewModel.actions) { _ in true }
context.send(viewAction: .cancel)
let action = try await deferred.fulfill()
XCTAssertNil(context.alertInfo)
@@ -45,7 +45,7 @@ class PollFormScreenViewModelTests: XCTestCase {
XCTAssertFalse(context.viewState.bindings.isUndisclosed)
// Cancellation should work without confirmation
let deferred = deferFulfillment(viewModel.actions, until: { _ in true })
let deferred = deferFulfillment(viewModel.actions) { _ in true }
context.send(viewAction: .cancel)
let action = try await deferred.fulfill()
XCTAssertNil(context.alertInfo)
@@ -161,8 +161,8 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase {
// Then no warning should be shown, and the call to update the users should be made straight away.
XCTAssertTrue(roomProxy.updatePowerLevelsForUsersCalled)
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.count, 2)
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains(where: { $0.userID == existingModerator.id && $0.powerLevel == 0 }), true)
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains(where: { $0.userID == firstUser.id && $0.powerLevel == 50 }), true)
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains { $0.userID == existingModerator.id && $0.powerLevel == 0 }, true)
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains { $0.userID == firstUser.id && $0.powerLevel == 50 }, true)
}
func testSavePromotedAdministrator() async throws {
@@ -189,7 +189,7 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase {
// Then the user should be made into an administrator.
XCTAssertTrue(roomProxy.updatePowerLevelsForUsersCalled)
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.count, 1)
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains(where: { $0.userID == firstUser.id && $0.powerLevel == 100 }), true)
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains { $0.userID == firstUser.id && $0.powerLevel == 100 }, true)
}
private func setupViewModel(mode: RoomMemberDetails.Role) {
@@ -322,7 +322,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
topic: nil,
avatarURL: nil,
memberCount: 0,
isHistoryWorldReadable: false,
isHistoryWorldReadable: nil,
isJoined: false,
isInvited: true,
isPublic: false,
@@ -49,7 +49,7 @@ class RoomRolesAndPermissionsScreenViewModelTests: XCTestCase {
context.send(viewAction: .editOwnUserRole)
XCTAssertNotNil(context.alertInfo)
context.alertInfo?.verticalButtons?.first(where: { $0.title.localizedStandardContains("moderator") })?.action?()
context.alertInfo?.verticalButtons?.first { $0.title.localizedStandardContains("moderator") }?.action?()
try await Task.sleep(for: .milliseconds(100))
@@ -64,7 +64,7 @@ class RoomRolesAndPermissionsScreenViewModelTests: XCTestCase {
context.send(viewAction: .editOwnUserRole)
XCTAssertNotNil(context.alertInfo)
context.alertInfo?.verticalButtons?.first(where: { $0.title.localizedStandardContains("member") })?.action?()
context.alertInfo?.verticalButtons?.first { $0.title.localizedStandardContains("member") }?.action?()
try await Task.sleep(for: .milliseconds(100))
+35 -35
View File
@@ -45,9 +45,9 @@ class TimelineViewModelTests: XCTestCase {
let viewModel = makeViewModel(timelineController: timelineController)
// Then the messages should be grouped together.
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[1].groupStyle, .middle, "Nothing should prevent the middle message from being grouped.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[2].groupStyle, .last, "Nothing should prevent the last message from being grouped.")
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.")
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[1].groupStyle, .middle, "Nothing should prevent the middle message from being grouped.")
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[2].groupStyle, .last, "Nothing should prevent the last message from being grouped.")
}
func testMessageGroupingMultipleSenders() {
@@ -73,12 +73,12 @@ class TimelineViewModelTests: XCTestCase {
let viewModel = makeViewModel(timelineController: timelineController)
// Then the messages should be grouped by sender.
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .single, "A message should not be grouped when the sender changes.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[1].groupStyle, .single, "A message should not be grouped when the sender changes.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[2].groupStyle, .first, "A group should start with a new sender if there are more messages from that sender.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[3].groupStyle, .last, "A group should be ended when the sender changes in the next message.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[4].groupStyle, .first, "A group should start with a new sender if there are more messages from that sender.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[5].groupStyle, .last, "A group should be ended when the sender changes in the next message.")
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[0].groupStyle, .single, "A message should not be grouped when the sender changes.")
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[1].groupStyle, .single, "A message should not be grouped when the sender changes.")
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[2].groupStyle, .first, "A group should start with a new sender if there are more messages from that sender.")
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[3].groupStyle, .last, "A group should be ended when the sender changes in the next message.")
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[4].groupStyle, .first, "A group should start with a new sender if there are more messages from that sender.")
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[5].groupStyle, .last, "A group should be ended when the sender changes in the next message.")
}
func testMessageGroupingWithLeadingReactions() {
@@ -99,9 +99,9 @@ class TimelineViewModelTests: XCTestCase {
let viewModel = makeViewModel(timelineController: timelineController)
// Then the first message should not be grouped but the other two should.
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .single, "When the first message has reactions it should not be grouped.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[1].groupStyle, .first, "A new group should be made when the preceding message has reactions.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[2].groupStyle, .last, "Nothing should prevent the last message from being grouped.")
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[0].groupStyle, .single, "When the first message has reactions it should not be grouped.")
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[1].groupStyle, .first, "A new group should be made when the preceding message has reactions.")
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[2].groupStyle, .last, "Nothing should prevent the last message from being grouped.")
}
func testMessageGroupingWithInnerReactions() {
@@ -122,9 +122,9 @@ class TimelineViewModelTests: XCTestCase {
let viewModel = makeViewModel(timelineController: timelineController)
// Then the first and second messages should be grouped and the last one should not.
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[1].groupStyle, .last, "When the message has reactions, the group should end here.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[2].groupStyle, .single, "The last message should not be grouped when the preceding message has reactions.")
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.")
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[1].groupStyle, .last, "When the message has reactions, the group should end here.")
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[2].groupStyle, .single, "The last message should not be grouped when the preceding message has reactions.")
}
func testMessageGroupingWithTrailingReactions() {
@@ -145,9 +145,9 @@ class TimelineViewModelTests: XCTestCase {
let viewModel = makeViewModel(timelineController: timelineController)
// Then the messages should be grouped together.
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[1].groupStyle, .middle, "Nothing should prevent the second message from being grouped.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[2].groupStyle, .last, "Reactions on the last message should not prevent it from being grouped.")
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.")
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[1].groupStyle, .middle, "Nothing should prevent the second message from being grouped.")
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[2].groupStyle, .last, "Reactions on the last message should not prevent it from being grouped.")
}
// MARK: - Focussing
@@ -162,18 +162,18 @@ class TimelineViewModelTests: XCTestCase {
let viewModel = makeViewModel(timelineController: timelineController)
XCTAssertEqual(timelineController.focusOnEventCallCount, 0)
XCTAssertTrue(viewModel.context.viewState.timelineViewState.isLive)
XCTAssertNil(viewModel.context.viewState.timelineViewState.focussedEvent)
XCTAssertTrue(viewModel.context.viewState.timelineState.isLive)
XCTAssertNil(viewModel.context.viewState.timelineState.focussedEvent)
// When focussing on an item that isn't loaded.
let deferred = deferFulfillment(viewModel.context.$viewState) { !$0.timelineViewState.isLive }
let deferred = deferFulfillment(viewModel.context.$viewState) { !$0.timelineState.isLive }
await viewModel.focusOnEvent(eventID: "t4")
try await deferred.fulfill()
// Then a new timeline should be loaded and the room focussed on that event.
XCTAssertEqual(timelineController.focusOnEventCallCount, 1)
XCTAssertFalse(viewModel.context.viewState.timelineViewState.isLive)
XCTAssertEqual(viewModel.context.viewState.timelineViewState.focussedEvent, .init(eventID: "t4", appearance: .immediate))
XCTAssertFalse(viewModel.context.viewState.timelineState.isLive)
XCTAssertEqual(viewModel.context.viewState.timelineState.focussedEvent, .init(eventID: "t4", appearance: .immediate))
}
func testFocusLoadedItem() async throws {
@@ -186,18 +186,18 @@ class TimelineViewModelTests: XCTestCase {
let viewModel = makeViewModel(timelineController: timelineController)
XCTAssertEqual(timelineController.focusOnEventCallCount, 0)
XCTAssertTrue(viewModel.context.viewState.timelineViewState.isLive)
XCTAssertNil(viewModel.context.viewState.timelineViewState.focussedEvent)
XCTAssertTrue(viewModel.context.viewState.timelineState.isLive)
XCTAssertNil(viewModel.context.viewState.timelineState.focussedEvent)
// When focussing on a loaded item.
let deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { !$0.timelineViewState.isLive }
let deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { !$0.timelineState.isLive }
await viewModel.focusOnEvent(eventID: "t1")
try await deferred.fulfill()
// Then the timeline should remain live and the item should be focussed.
XCTAssertEqual(timelineController.focusOnEventCallCount, 0)
XCTAssertTrue(viewModel.context.viewState.timelineViewState.isLive)
XCTAssertEqual(viewModel.context.viewState.timelineViewState.focussedEvent, .init(eventID: "t1", appearance: .animated))
XCTAssertTrue(viewModel.context.viewState.timelineState.isLive)
XCTAssertEqual(viewModel.context.viewState.timelineState.focussedEvent, .init(eventID: "t1", appearance: .animated))
}
func testFocusLive() async throws {
@@ -210,30 +210,30 @@ class TimelineViewModelTests: XCTestCase {
let viewModel = makeViewModel(timelineController: timelineController)
var deferred = deferFulfillment(viewModel.context.$viewState) { !$0.timelineViewState.isLive }
var deferred = deferFulfillment(viewModel.context.$viewState) { !$0.timelineState.isLive }
await viewModel.focusOnEvent(eventID: "t4")
try await deferred.fulfill()
XCTAssertEqual(timelineController.focusLiveCallCount, 0)
XCTAssertFalse(viewModel.context.viewState.timelineViewState.isLive)
XCTAssertEqual(viewModel.context.viewState.timelineViewState.focussedEvent, .init(eventID: "t4", appearance: .immediate))
XCTAssertFalse(viewModel.context.viewState.timelineState.isLive)
XCTAssertEqual(viewModel.context.viewState.timelineState.focussedEvent, .init(eventID: "t4", appearance: .immediate))
// When switching back to a live timeline.
deferred = deferFulfillment(viewModel.context.$viewState) { $0.timelineViewState.isLive }
deferred = deferFulfillment(viewModel.context.$viewState) { $0.timelineState.isLive }
viewModel.context.send(viewAction: .focusLive)
try await deferred.fulfill()
// Then the timeline should switch back to being live and the event focus should be removed.
XCTAssertEqual(timelineController.focusLiveCallCount, 1)
XCTAssertTrue(viewModel.context.viewState.timelineViewState.isLive)
XCTAssertNil(viewModel.context.viewState.timelineViewState.focussedEvent)
XCTAssertTrue(viewModel.context.viewState.timelineState.isLive)
XCTAssertNil(viewModel.context.viewState.timelineState.focussedEvent)
}
func testInitialFocusViewState() async throws {
let timelineController = MockRoomTimelineController()
let viewModel = makeViewModel(focussedEventID: "t10", timelineController: timelineController)
XCTAssertEqual(viewModel.context.viewState.timelineViewState.focussedEvent, .init(eventID: "t10", appearance: .immediate))
XCTAssertEqual(viewModel.context.viewState.timelineState.focussedEvent, .init(eventID: "t10", appearance: .immediate))
}
// MARK: - Read Receipts