Revert weak media provider (#4483)

* Revert "Address the real lifetime issue of the SDK's `Client` by making `Context.mediaProvider` weak. (#4466)"

This reverts commit c794144ef1.

* Better docs.
This commit is contained in:
Doug
2025-09-08 17:48:58 +01:00
committed by GitHub
parent 90504d89c8
commit 2f2e6f6226
58 changed files with 161 additions and 380 deletions
+5 -3
View File
@@ -81,9 +81,11 @@ extension ClientProxyMock {
recentlyVisitedRoomsReturnValue = .success([])
recentConversationCounterpartsReturnValue = []
loadMediaContentForSourceThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic)
loadMediaThumbnailForSourceWidthHeightThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic)
loadMediaFileForSourceFilenameThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic)
let mediaLoader = MediaLoaderMock()
mediaLoader.loadMediaContentForSourceThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic)
mediaLoader.loadMediaThumbnailForSourceWidthHeightThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic)
mediaLoader.loadMediaFileForSourceFilenameThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic)
self.mediaLoader = mediaLoader
secureBackupController = SecureBackupControllerMock(.init(recoveryState: configuration.recoveryState))
resetIdentityReturnValue = .success(IdentityResetHandleSDKMock(.init()))
@@ -2130,6 +2130,11 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
}
var underlyingHideInviteAvatarsPublisher: CurrentValuePublisher<Bool, Never>!
var pusherNotificationClientIdentifier: String?
var mediaLoader: MediaLoaderProtocol {
get { return underlyingMediaLoader }
set(value) { underlyingMediaLoader = value }
}
var underlyingMediaLoader: MediaLoaderProtocol!
var roomSummaryProvider: RoomSummaryProviderProtocol {
get { return underlyingRoomSummaryProvider }
set(value) { underlyingRoomSummaryProvider = value }
@@ -5204,228 +5209,6 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
return setHideInviteAvatarsReturnValue
}
}
//MARK: - loadMediaContentForSource
var loadMediaContentForSourceThrowableError: Error?
var loadMediaContentForSourceUnderlyingCallsCount = 0
var loadMediaContentForSourceCallsCount: Int {
get {
if Thread.isMainThread {
return loadMediaContentForSourceUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = loadMediaContentForSourceUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
loadMediaContentForSourceUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
loadMediaContentForSourceUnderlyingCallsCount = newValue
}
}
}
}
var loadMediaContentForSourceCalled: Bool {
return loadMediaContentForSourceCallsCount > 0
}
var loadMediaContentForSourceReceivedSource: MediaSourceProxy?
var loadMediaContentForSourceReceivedInvocations: [MediaSourceProxy] = []
var loadMediaContentForSourceUnderlyingReturnValue: Data!
var loadMediaContentForSourceReturnValue: Data! {
get {
if Thread.isMainThread {
return loadMediaContentForSourceUnderlyingReturnValue
} else {
var returnValue: Data? = nil
DispatchQueue.main.sync {
returnValue = loadMediaContentForSourceUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
loadMediaContentForSourceUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
loadMediaContentForSourceUnderlyingReturnValue = newValue
}
}
}
}
var loadMediaContentForSourceClosure: ((MediaSourceProxy) async throws -> Data)?
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data {
if let error = loadMediaContentForSourceThrowableError {
throw error
}
loadMediaContentForSourceCallsCount += 1
loadMediaContentForSourceReceivedSource = source
DispatchQueue.main.async {
self.loadMediaContentForSourceReceivedInvocations.append(source)
}
if let loadMediaContentForSourceClosure = loadMediaContentForSourceClosure {
return try await loadMediaContentForSourceClosure(source)
} else {
return loadMediaContentForSourceReturnValue
}
}
//MARK: - loadMediaThumbnailForSource
var loadMediaThumbnailForSourceWidthHeightThrowableError: Error?
var loadMediaThumbnailForSourceWidthHeightUnderlyingCallsCount = 0
var loadMediaThumbnailForSourceWidthHeightCallsCount: Int {
get {
if Thread.isMainThread {
return loadMediaThumbnailForSourceWidthHeightUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = loadMediaThumbnailForSourceWidthHeightUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
loadMediaThumbnailForSourceWidthHeightUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
loadMediaThumbnailForSourceWidthHeightUnderlyingCallsCount = newValue
}
}
}
}
var loadMediaThumbnailForSourceWidthHeightCalled: Bool {
return loadMediaThumbnailForSourceWidthHeightCallsCount > 0
}
var loadMediaThumbnailForSourceWidthHeightReceivedArguments: (source: MediaSourceProxy, width: UInt, height: UInt)?
var loadMediaThumbnailForSourceWidthHeightReceivedInvocations: [(source: MediaSourceProxy, width: UInt, height: UInt)] = []
var loadMediaThumbnailForSourceWidthHeightUnderlyingReturnValue: Data!
var loadMediaThumbnailForSourceWidthHeightReturnValue: Data! {
get {
if Thread.isMainThread {
return loadMediaThumbnailForSourceWidthHeightUnderlyingReturnValue
} else {
var returnValue: Data? = nil
DispatchQueue.main.sync {
returnValue = loadMediaThumbnailForSourceWidthHeightUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
loadMediaThumbnailForSourceWidthHeightUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
loadMediaThumbnailForSourceWidthHeightUnderlyingReturnValue = newValue
}
}
}
}
var loadMediaThumbnailForSourceWidthHeightClosure: ((MediaSourceProxy, UInt, UInt) async throws -> Data)?
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data {
if let error = loadMediaThumbnailForSourceWidthHeightThrowableError {
throw error
}
loadMediaThumbnailForSourceWidthHeightCallsCount += 1
loadMediaThumbnailForSourceWidthHeightReceivedArguments = (source: source, width: width, height: height)
DispatchQueue.main.async {
self.loadMediaThumbnailForSourceWidthHeightReceivedInvocations.append((source: source, width: width, height: height))
}
if let loadMediaThumbnailForSourceWidthHeightClosure = loadMediaThumbnailForSourceWidthHeightClosure {
return try await loadMediaThumbnailForSourceWidthHeightClosure(source, width, height)
} else {
return loadMediaThumbnailForSourceWidthHeightReturnValue
}
}
//MARK: - loadMediaFileForSource
var loadMediaFileForSourceFilenameThrowableError: Error?
var loadMediaFileForSourceFilenameUnderlyingCallsCount = 0
var loadMediaFileForSourceFilenameCallsCount: Int {
get {
if Thread.isMainThread {
return loadMediaFileForSourceFilenameUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = loadMediaFileForSourceFilenameUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
loadMediaFileForSourceFilenameUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
loadMediaFileForSourceFilenameUnderlyingCallsCount = newValue
}
}
}
}
var loadMediaFileForSourceFilenameCalled: Bool {
return loadMediaFileForSourceFilenameCallsCount > 0
}
var loadMediaFileForSourceFilenameReceivedArguments: (source: MediaSourceProxy, filename: String?)?
var loadMediaFileForSourceFilenameReceivedInvocations: [(source: MediaSourceProxy, filename: String?)] = []
var loadMediaFileForSourceFilenameUnderlyingReturnValue: MediaFileHandleProxy!
var loadMediaFileForSourceFilenameReturnValue: MediaFileHandleProxy! {
get {
if Thread.isMainThread {
return loadMediaFileForSourceFilenameUnderlyingReturnValue
} else {
var returnValue: MediaFileHandleProxy? = nil
DispatchQueue.main.sync {
returnValue = loadMediaFileForSourceFilenameUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
loadMediaFileForSourceFilenameUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
loadMediaFileForSourceFilenameUnderlyingReturnValue = newValue
}
}
}
}
var loadMediaFileForSourceFilenameClosure: ((MediaSourceProxy, String?) async throws -> MediaFileHandleProxy)?
func loadMediaFileForSource(_ source: MediaSourceProxy, filename: String?) async throws -> MediaFileHandleProxy {
if let error = loadMediaFileForSourceFilenameThrowableError {
throw error
}
loadMediaFileForSourceFilenameCallsCount += 1
loadMediaFileForSourceFilenameReceivedArguments = (source: source, filename: filename)
DispatchQueue.main.async {
self.loadMediaFileForSourceFilenameReceivedInvocations.append((source: source, filename: filename))
}
if let loadMediaFileForSourceFilenameClosure = loadMediaFileForSourceFilenameClosure {
return try await loadMediaFileForSourceFilenameClosure(source, filename)
} else {
return loadMediaFileForSourceFilenameReturnValue
}
}
}
class CompletionSuggestionServiceMock: CompletionSuggestionServiceProtocol, @unchecked Sendable {
var suggestionsPublisher: AnyPublisher<[SuggestionItem], Never> {
@@ -7,17 +7,11 @@
import SwiftUI
extension MediaProviderProtocol where Self == MediaProviderMock {
static var mock: MediaProviderMock { .shared }
}
extension MediaProviderMock {
static let shared = MediaProviderMock(.init())
struct Configuration { }
// swiftlint:disable:next cyclomatic_complexity
convenience init(_ configuration: Configuration) {
convenience init(configuration: Configuration) {
self.init()
imageFromSourceSizeClosure = { mediaSource, _ in
+1 -1
View File
@@ -17,7 +17,7 @@ extension UserSessionMock {
self.init()
clientProxy = configuration.clientProxy
mediaProvider = .mock
mediaProvider = MediaProviderMock(configuration: .init())
voiceMessageMediaManager = VoiceMessageMediaManagerMock()
sessionSecurityStatePublisher = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .verified, recoveryState: .enabled)).asCurrentValuePublisher()
@@ -42,6 +42,8 @@ struct PillView: View {
}
struct PillView_Previews: PreviewProvider, TestablePreview {
static let mockMediaProvider = MediaProviderMock(configuration: .init())
static var previews: some View {
PillView(context: PillContext.mock(viewState: .mention(isOwnMention: false,
displayText: PillUtilities.userPillDisplayText(username: "User",
@@ -70,7 +70,7 @@ struct ShimmerOverlay_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
VStack(spacing: 0) {
ForEach(0...8, id: \.self) { _ in
HomeScreenRoomCell(room: .placeholder(), isSelected: false, mediaProvider: .mock) { _ in }
HomeScreenRoomCell(room: .placeholder(), isSelected: false, mediaProvider: MediaProviderMock(configuration: .init())) { _ in }
}
}
.redacted(reason: .placeholder)
@@ -65,12 +65,7 @@ class StateStoreViewModel<State: BindableState, ViewAction> {
/// An optional image loading service so that views can manage themselves
/// Intentionally non-generic so that it doesn't grow uncontrollably
///
/// We noticed that the keyboard appears to hold onto the Context of the last screen that had text input focus:
/// https://github.com/element-hq/element-x-ios/issues/4465
/// Therefore this is `weak` so that the underlying `MatrixRustSDK.Client` isn't retained when e.g. clearing
/// the cache, otherwise we have the potential for 2 `Client`s to be alive at the same time causing havoc.
private(set) weak var mediaProvider: MediaProviderProtocol?
let mediaProvider: MediaProviderProtocol?
/// Set-able/Bindable access to the bindable state.
subscript<T>(dynamicMember keyPath: WritableKeyPath<State.BindStateType, T>) -> T {
@@ -67,12 +67,7 @@ class StateStoreViewModelV2<State: BindableState, ViewAction> {
/// An optional image loading service so that views can manage themselves
/// Intentionally non-generic so that it doesn't grow uncontrollably
///
/// We noticed that the keyboard appears to hold onto the Context of the last screen that had text input focus:
/// https://github.com/element-hq/element-x-ios/issues/4465
/// Therefore this is `weak` so that the underlying `MatrixRustSDK.Client` isn't retained when e.g. clearing
/// the cache, otherwise we have the potential for 2 `Client`s to be alive at the same time causing havoc.
private(set) weak var mediaProvider: MediaProviderProtocol?
let mediaProvider: MediaProviderProtocol?
/// Set-able access to the bindable state.
subscript<T>(dynamicMember keyPath: WritableKeyPath<State.BindStateType, T>) -> T {
@@ -234,7 +234,7 @@ struct AvatarHeaderView_Previews: PreviewProvider, TestablePreview {
isPublic: true,
isDirect: false),
avatarSize: .room(on: .details),
mediaProvider: .mock) {
mediaProvider: MediaProviderMock(configuration: .init())) {
HStack(spacing: 32) {
ShareLink(item: "test") {
CompoundIcon(\.shareIos)
@@ -248,7 +248,7 @@ struct AvatarHeaderView_Previews: PreviewProvider, TestablePreview {
Form {
AvatarHeaderView(accountOwner: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockMe), dmRecipient: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockAlice),
mediaProvider: .mock) {
mediaProvider: MediaProviderMock(configuration: .init())) {
HStack(spacing: 32) {
ShareLink(item: "test") {
CompoundIcon(\.shareIos)
@@ -263,16 +263,16 @@ struct AvatarHeaderView_Previews: PreviewProvider, TestablePreview {
VStack(spacing: 16) {
AvatarHeaderView(member: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockAlice),
avatarSize: .room(on: .details),
mediaProvider: .mock) { Text("") }
mediaProvider: MediaProviderMock(configuration: .init())) { Text("") }
AvatarHeaderView(member: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockBob),
isVerified: true,
avatarSize: .room(on: .details),
mediaProvider: .mock) { Text("") }
mediaProvider: MediaProviderMock(configuration: .init())) { Text("") }
AvatarHeaderView(member: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockBanned[3]),
avatarSize: .room(on: .details),
mediaProvider: .mock) { Text("") }
mediaProvider: MediaProviderMock(configuration: .init())) { Text("") }
}
.padding()
.background(Color.compound.bgSubtleSecondaryLevel0)
@@ -396,7 +396,7 @@ struct LoadableImage_Previews: PreviewProvider, TestablePreview {
}
static func makeMediaProvider(isLoading: Bool = false) -> MediaProviderProtocol {
let mediaProvider = MediaProviderMock(.init())
let mediaProvider = MediaProviderMock(configuration: .init())
if isLoading {
mediaProvider.imageFromSourceSizeClosure = { _, _ in nil }
@@ -133,27 +133,27 @@ struct RoomAvatarImage_Previews: PreviewProvider, TestablePreview {
name: "Room",
avatarURL: nil),
avatarSize: .room(on: .chats),
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .room(id: "!2:server.com",
name: "Room",
avatarURL: .mockMXCAvatar),
avatarSize: .room(on: .chats),
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .space(id: "!space:server.com",
name: "Room",
avatarURL: nil),
avatarSize: .room(on: .chats),
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .space(id: "!otherspace:server.com",
name: "Room",
avatarURL: .mockMXCAvatar),
avatarSize: .room(on: .chats),
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .tombstoned, avatarSize: .room(on: .chats), mediaProvider: .mock)
RoomAvatarImage(avatar: .tombstoned, avatarSize: .room(on: .chats), mediaProvider: MediaProviderMock(configuration: .init()))
}
HStack(spacing: 12) {
@@ -161,18 +161,18 @@ struct RoomAvatarImage_Previews: PreviewProvider, TestablePreview {
displayName: "User",
avatarURL: nil)]),
avatarSize: .room(on: .chats),
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .heroes([.init(userID: "@user:server.com",
displayName: "User",
avatarURL: .mockMXCAvatar)]),
avatarSize: .room(on: .chats),
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .heroes([.init(userID: "@alice:server.com", displayName: "Alice", avatarURL: nil),
.init(userID: "@bob:server.net", displayName: "Bob", avatarURL: nil)]),
avatarSize: .room(on: .chats),
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
}
}
}
@@ -75,7 +75,7 @@ struct RoomHeaderView_Previews: PreviewProvider, TestablePreview {
name: "Some Room Name",
avatarURL: avatarURL),
dmRecipientVerificationState: verificationState,
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
.padding()
}
}
@@ -63,13 +63,13 @@ struct RoomInviterLabel_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
VStack(spacing: 10) {
RoomInviterLabel(inviter: .init(member: RoomMemberProxyMock.mockAlice),
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
RoomInviterLabel(inviter: .init(member: RoomMemberProxyMock.mockDan),
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
RoomInviterLabel(inviter: .init(member: RoomMemberProxyMock.mockNoName),
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
RoomInviterLabel(inviter: .init(member: RoomMemberProxyMock.mockCharlie),
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
.foregroundStyle(.compound.textPrimary)
}
.font(.compound.bodyMD)
@@ -54,13 +54,13 @@ struct StackedAvatarsView_Previews: PreviewProvider, TestablePreview {
lineWidth: 2,
avatars: avatars,
avatarSize: .user(on: .knockingUsersBannerStack),
mediaProvider: .mock)
mediaProvider: MediaProviderMock())
StackedAvatarsView(overlap: 16,
lineWidth: 2,
shouldStackFromLast: true,
avatars: avatars,
avatarSize: .user(on: .knockingUsersBannerStack),
mediaProvider: .mock)
mediaProvider: MediaProviderMock())
}
}
}
@@ -65,21 +65,21 @@ struct UserProfileCell_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
Form {
UserProfileListRow(user: .mockAlice, membership: nil, mediaProvider: .mock,
UserProfileListRow(user: .mockAlice, membership: nil, mediaProvider: MediaProviderMock(configuration: .init()),
kind: .multiSelection(isSelected: true, action: action))
UserProfileListRow(user: .mockBob, membership: nil, mediaProvider: .mock,
UserProfileListRow(user: .mockBob, membership: nil, mediaProvider: MediaProviderMock(configuration: .init()),
kind: .multiSelection(isSelected: false, action: action))
UserProfileListRow(user: .mockCharlie, membership: .join, mediaProvider: .mock,
UserProfileListRow(user: .mockCharlie, membership: .join, mediaProvider: MediaProviderMock(configuration: .init()),
kind: .multiSelection(isSelected: true, action: action))
.disabled(true)
UserProfileListRow(user: .init(userID: "@someone:matrix.org"), membership: .join, mediaProvider: .mock,
UserProfileListRow(user: .init(userID: "@someone:matrix.org"), membership: .join, mediaProvider: MediaProviderMock(configuration: .init()),
kind: .multiSelection(isSelected: false, action: action))
.disabled(true)
UserProfileListRow(user: .init(userID: "@someone:matrix.org"), membership: nil, mediaProvider: .mock,
UserProfileListRow(user: .init(userID: "@someone:matrix.org"), membership: nil, mediaProvider: MediaProviderMock(configuration: .init()),
kind: .multiSelection(isSelected: false, action: action))
}
.compoundList()
@@ -135,12 +135,9 @@ private struct MediaPreviewViewController: UIViewControllerRepresentable {
// MARK: - Previews
struct TimelineMediaPreviewModifier_Previews: PreviewProvider {
static let viewModel = makeViewModel(mediaProvider: mediaProvider)
static let mediaProvider = MediaProviderMock(.init())
static let downloadingViewModel = makeViewModel(isDownloading: true, mediaProvider: downloadingMediaProvider)
static let downloadingMediaProvider = MediaProviderMock(.init())
static let downloadErrorViewModel = makeViewModel(isDownloadError: true, mediaProvider: downloadErrorMediaProvider)
static let downloadErrorMediaProvider = MediaProviderMock(.init())
static let viewModel = makeViewModel()
static let downloadingViewModel = makeViewModel(isDownloading: true)
static let downloadErrorViewModel = makeViewModel(isDownloadError: true)
static var previews: some View {
MediaPreviewViewController(viewModel: viewModel, dismissalPublisher: .init()) { }
@@ -151,7 +148,7 @@ struct TimelineMediaPreviewModifier_Previews: PreviewProvider {
.previewDisplayName("Download Error")
}
static func makeViewModel(isDownloading: Bool = false, isDownloadError: Bool = false, mediaProvider: MediaProviderMock) -> TimelineMediaPreviewViewModel {
static func makeViewModel(isDownloading: Bool = false, isDownloadError: Bool = false) -> TimelineMediaPreviewViewModel {
let item = FileRoomTimelineItem(id: .randomEvent,
timestamp: .mock,
isOutgoing: false,
@@ -168,6 +165,8 @@ struct TimelineMediaPreviewModifier_Previews: PreviewProvider {
let timelineController = MockTimelineController(timelineKind: .media(.mediaFilesScreen))
timelineController.timelineItems = [item]
let mediaProvider = MediaProviderMock(configuration: .init())
if isDownloading {
mediaProvider.loadFileFromSourceFilenameClosure = { _, _ in
try? await Task.sleep(for: .seconds(3600))
@@ -229,7 +229,7 @@ struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePrevie
let viewModel = TimelineMediaPreviewViewModel(initialItem: item,
timelineViewModel: TimelineViewModel.mock(timelineKind: timelineKind,
timelineController: timelineController),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
photoLibraryManager: PhotoLibraryManagerMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
appMediator: AppMediatorMock())
@@ -147,7 +147,7 @@ struct TimelineMediaPreviewRedactConfirmationView_Previews: PreviewProvider, Tes
return TimelineMediaPreviewViewModel(initialItem: item,
timelineViewModel: TimelineViewModel.mock(timelineKind: timelineController.timelineKind,
timelineController: timelineController),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
photoLibraryManager: PhotoLibraryManagerMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
appMediator: AppMediatorMock())
@@ -206,7 +206,7 @@ private class GlobalSearchTextField: UITextField {
struct GlobalSearchScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = GlobalSearchScreenViewModel(roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))),
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
static var previews: some View {
NavigationStack {
@@ -37,7 +37,7 @@ struct GlobalSearchScreenListRow: View {
struct GlobalSearchScreenListRow_Previews: PreviewProvider, TestablePreview {
static let viewModel = GlobalSearchScreenViewModel(roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))),
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
static var previews: some View {
List {
@@ -176,17 +176,17 @@ struct HomeScreenRoomCell_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
VStack(spacing: 0) {
ForEach(genericRooms) { room in
HomeScreenRoomCell(room: room, isSelected: false, mediaProvider: .mock) { _ in }
HomeScreenRoomCell(room: room, isSelected: false, mediaProvider: MediaProviderMock(configuration: .init())) { _ in }
}
HomeScreenRoomCell(room: .placeholder(), isSelected: false, mediaProvider: .mock) { _ in }
HomeScreenRoomCell(room: .placeholder(), isSelected: false, mediaProvider: MediaProviderMock(configuration: .init())) { _ in }
.redacted(reason: .placeholder)
}
.previewDisplayName("Generic")
VStack(spacing: 0) {
ForEach(notificationsStateRooms) { room in
HomeScreenRoomCell(room: room, isSelected: false, mediaProvider: .mock) { _ in }
HomeScreenRoomCell(room: room, isSelected: false, mediaProvider: MediaProviderMock(configuration: .init())) { _ in }
}
}
.previewLayout(.sizeThatFits)
@@ -53,7 +53,7 @@ struct InviteUsersScreenSelectedItem_Previews: PreviewProvider, TestablePreview
ScrollView(.horizontal) {
HStack(spacing: 28) {
ForEach(people, id: \.userID) { user in
InviteUsersScreenSelectedItem(user: user, mediaProvider: .mock) { }
InviteUsersScreenSelectedItem(user: user, mediaProvider: MediaProviderMock(configuration: .init())) { }
.frame(width: 72)
}
}
@@ -163,7 +163,7 @@ extension KnockRequestsListScreenViewModel {
knockRequestsState: requestsState,
ownUserID: RoomMemberProxyMock.mockAdmin.userID,
joinRule: .knock)),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(),
userIndicatorController: UserIndicatorControllerMock())
}
}
@@ -113,6 +113,6 @@ private extension ManageRoomMemberSheetViewModel {
roomProxy: JoinedRoomProxyMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
analyticsService: ServiceLocator.shared.analytics,
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
}
}
@@ -285,7 +285,7 @@ struct MediaEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
MediaEventsTimelineScreenViewModel(mediaTimelineViewModel: makeTimelineViewModel(empty: empty),
filesTimelineViewModel: makeTimelineViewModel(empty: empty),
initialScreenMode: screenMode,
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: UserIndicatorControllerMock(),
appMediator: AppMediatorMock())
}
@@ -120,13 +120,13 @@ struct SessionVerificationRequestDetailsView_Previews: PreviewProvider, Testable
static var previews: some View {
SessionVerificationRequestDetailsView(details: details,
isUserVerification: true,
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
.padding()
.previewDisplayName("User")
SessionVerificationRequestDetailsView(details: details,
isUserVerification: false,
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
.padding()
.previewDisplayName("Device")
}
@@ -263,7 +263,7 @@ struct SessionVerification_Previews: PreviewProvider, TestablePreview {
let viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: SessionVerificationControllerProxyMock.configureMock(),
flow: flow,
appSettings: AppSettings(),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
verificationState: state)
return SessionVerificationScreen(context: viewModel.context)
@@ -146,7 +146,7 @@ struct RoomChangeRolesScreen_Previews: PreviewProvider, TestablePreview {
return RoomChangeRolesScreenViewModel(mode: mode,
roomProxy: JoinedRoomProxyMock(.init(members: members)),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: UserIndicatorControllerMock(),
analytics: ServiceLocator.shared.analytics)
}
@@ -39,34 +39,34 @@ struct RoomChangeRolesScreenRow_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
Form {
RoomChangeRolesScreenRow(member: .init(withProxy: RoomMemberProxyMock.mockAlice),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
isSelected: true,
action: action)
RoomChangeRolesScreenRow(member: .init(withProxy: RoomMemberProxyMock.mockBob),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
isSelected: false,
action: action)
RoomChangeRolesScreenRow(member: .init(withProxy: RoomMemberProxyMock.mockInvited),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
isSelected: false,
action: action)
RoomChangeRolesScreenRow(member: .init(withProxy: RoomMemberProxyMock.mockCharlie),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
isSelected: true,
action: action)
.disabled(true)
RoomChangeRolesScreenRow(member: .init(withProxy: RoomMemberProxyMock(with: .init(userID: "@someone:matrix.org", membership: .join))),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
isSelected: false,
action: action)
.disabled(true)
RoomChangeRolesScreenRow(member: .init(withProxy: RoomMemberProxyMock(with: .init(userID: "@someone:matrix.org", membership: .join))),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
isSelected: false,
action: action)
}
@@ -75,7 +75,7 @@ struct RoomChangeRolesScreenSelectedItem_Previews: PreviewProvider, TestablePrev
HStack(spacing: 12) {
ForEach(members, id: \.id) { member in
RoomChangeRolesScreenSelectedItem(member: member,
mediaProvider: .mock) { }
mediaProvider: MediaProviderMock(configuration: .init())) { }
.frame(width: 72)
}
}
@@ -55,7 +55,7 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview {
name: "Test title",
avatarURL: nil),
canBeJoined: true),
mediaProvider: .mock) { }
mediaProvider: MediaProviderMock(configuration: .init())) { }
RoomDirectorySearchCell(result: .init(id: "!test_id_2:matrix.org",
alias: "#test:example.com",
@@ -65,7 +65,7 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview {
name: nil,
avatarURL: nil),
canBeJoined: true),
mediaProvider: .mock) { }
mediaProvider: MediaProviderMock(configuration: .init())) { }
RoomDirectorySearchCell(result: .init(id: "!test_id_3:example.com",
alias: "#test_no_topic:example.com",
@@ -75,7 +75,7 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview {
name: "Test title no topic",
avatarURL: nil),
canBeJoined: true),
mediaProvider: .mock) { }
mediaProvider: MediaProviderMock(configuration: .init())) { }
RoomDirectorySearchCell(result: .init(id: "!test_id_4:example.com",
alias: "#test_no_topic:example.com",
@@ -85,7 +85,7 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview {
name: nil,
avatarURL: nil),
canBeJoined: true),
mediaProvider: .mock) { }
mediaProvider: MediaProviderMock(configuration: .init())) { }
RoomDirectorySearchCell(result: .init(id: "!test_id_5:example.com",
alias: nil,
@@ -95,7 +95,7 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview {
name: "Test title no alias",
avatarURL: nil),
canBeJoined: false),
mediaProvider: .mock) { }
mediaProvider: MediaProviderMock(configuration: .init())) { }
RoomDirectorySearchCell(result: .init(id: "!test_id_6:example.com",
alias: nil,
@@ -105,7 +105,7 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview {
name: "Test title no alias",
avatarURL: nil),
canBeJoined: false),
mediaProvider: .mock) { }
mediaProvider: MediaProviderMock(configuration: .init())) { }
RoomDirectorySearchCell(result: .init(id: "!test_id_7:example.com",
alias: nil,
@@ -115,7 +115,7 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview {
name: nil,
avatarURL: nil),
canBeJoined: false),
mediaProvider: .mock) { }
mediaProvider: MediaProviderMock(configuration: .init())) { }
RoomDirectorySearchCell(result: .init(id: "!test_id_8:example.com",
alias: nil,
name: nil,
@@ -124,7 +124,7 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview {
name: nil,
avatarURL: nil),
canBeJoined: false),
mediaProvider: .mock) { }
mediaProvider: MediaProviderMock(configuration: .init())) { }
}
.compoundList()
}
@@ -113,13 +113,13 @@ struct CompletionSuggestion_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
// Putting them is VStack allows the preview to work properly in tests
VStack(spacing: 8) {
CompletionSuggestionView(mediaProvider: .mock,
CompletionSuggestionView(mediaProvider: MediaProviderMock(configuration: .init()),
items: [.init(suggestionType: .user(.init(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil)), range: .init(), rawSuggestionText: ""),
.init(suggestionType: .user(.init(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: .mockMXCUserAvatar)), range: .init(), rawSuggestionText: "")]) { _ in }
}
VStack(spacing: 8) {
CompletionSuggestionView(mediaProvider: .mock,
CompletionSuggestionView(mediaProvider: MediaProviderMock(configuration: .init()),
items: multipleItems) { _ in }
}
}
@@ -333,7 +333,7 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview {
static let composerViewModel = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()),
wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init(suggestions: suggestions)),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
@@ -383,7 +383,7 @@ extension ComposerToolbar {
let model = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()),
wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
@@ -400,7 +400,7 @@ extension ComposerToolbar {
let model = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()),
wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
@@ -417,7 +417,7 @@ extension ComposerToolbar {
let model = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()),
wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
@@ -435,7 +435,7 @@ extension ComposerToolbar {
let model = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()),
wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
@@ -456,7 +456,7 @@ extension ComposerToolbar {
let model = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()),
wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
@@ -479,7 +479,7 @@ extension ComposerToolbar {
let model = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()),
wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
@@ -45,14 +45,16 @@ struct MentionSuggestionItemView: View {
}
struct MentionSuggestionItemView_Previews: PreviewProvider, TestablePreview {
static let mockMediaProvider = MediaProviderMock(configuration: .init())
static var previews: some View {
MentionSuggestionItemView(mediaProvider: .mock, item: .init(suggestionType: .user(.init(id: "test", displayName: "Test", avatarURL: .mockMXCUserAvatar)), range: .init(), rawSuggestionText: ""))
MentionSuggestionItemView(mediaProvider: mockMediaProvider, item: .init(suggestionType: .user(.init(id: "test", displayName: "Test", avatarURL: .mockMXCUserAvatar)), range: .init(), rawSuggestionText: ""))
.previewDisplayName("User")
MentionSuggestionItemView(mediaProvider: .mock, item: .init(suggestionType: .user(.init(id: "test2", displayName: nil, avatarURL: nil)), range: .init(), rawSuggestionText: ""))
MentionSuggestionItemView(mediaProvider: mockMediaProvider, item: .init(suggestionType: .user(.init(id: "test2", displayName: nil, avatarURL: nil)), range: .init(), rawSuggestionText: ""))
.previewDisplayName("User no display name")
MentionSuggestionItemView(mediaProvider: .mock, item: .init(suggestionType: .allUsers(.room(id: "room", name: "Room", avatarURL: .mockMXCAvatar)), range: .init(), rawSuggestionText: ""))
MentionSuggestionItemView(mediaProvider: mockMediaProvider, item: .init(suggestionType: .allUsers(.room(id: "room", name: "Room", avatarURL: .mockMXCAvatar)), range: .init(), rawSuggestionText: ""))
.previewDisplayName("All users")
MentionSuggestionItemView(mediaProvider: .mock,
MentionSuggestionItemView(mediaProvider: mockMediaProvider,
item: .init(suggestionType: .room(.init(id: "room",
canonicalAlias: "#room:matrix.org",
name: "Room",
@@ -91,7 +91,7 @@ struct RoomAttachmentPicker_Previews: PreviewProvider, TestablePreview {
static let viewModel = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()),
wysiwygViewModel: WysiwygComposerViewModel(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
@@ -166,11 +166,11 @@ struct RoomScreenFooterView_Previews: PreviewProvider, TestablePreview {
learnMoreURL: "https://element.io/")
static var previews: some View {
RoomScreenFooterView(details: bobDetails, mediaProvider: .mock) { _ in }
RoomScreenFooterView(details: bobDetails, mediaProvider: MediaProviderMock(configuration: .init())) { _ in }
.previewDisplayName("With displayname")
RoomScreenFooterView(details: noNameDetails, mediaProvider: .mock) { _ in }
RoomScreenFooterView(details: noNameDetails, mediaProvider: MediaProviderMock(configuration: .init())) { _ in }
.previewDisplayName("Without displayname")
RoomScreenFooterView(details: verificationViolationDetails, mediaProvider: .mock) { _ in }
RoomScreenFooterView(details: verificationViolationDetails, mediaProvider: MediaProviderMock(configuration: .init())) { _ in }
.previewDisplayName("Verification Violation")
}
}
@@ -160,12 +160,14 @@ struct SpaceHeaderMembersView: View {
}
struct SpaceHeaderView_Previews: PreviewProvider, TestablePreview {
static let mediaProvider = MediaProviderMock(configuration: .init())
static let spaces = makeSpaceRooms()
static var previews: some View {
VStack(spacing: 0) {
ForEach(spaces, id: \.id) { space in
SpaceHeaderView(spaceRoomProxy: space, mediaProvider: .mock)
SpaceHeaderView(spaceRoomProxy: space, mediaProvider: mediaProvider)
}
}
}
@@ -134,6 +134,8 @@ struct SpaceRoomCellButtonStyle: ButtonStyle {
}
struct SpaceRoomCell_Previews: PreviewProvider, TestablePreview {
static let mediaProvider = MediaProviderMock(configuration: .init())
static let spaces = [SpaceRoomProxyProtocol].mockSpaceList
static var previews: some View {
@@ -141,7 +143,7 @@ struct SpaceRoomCell_Previews: PreviewProvider, TestablePreview {
ForEach(spaces, id: \.id) { space in
SpaceRoomCell(spaceRoomProxy: space,
isSelected: false,
mediaProvider: .mock) { _ in }
mediaProvider: mediaProvider) { _ in }
}
}
}
@@ -78,7 +78,7 @@ struct SpaceScreen_Previews: PreviewProvider, TestablePreview {
let viewModel = SpaceScreenViewModel(spaceRoomListProxy: spaceRoomListProxy,
spaceServiceProxy: SpaceServiceProxyMock(.init()),
selectedSpaceRoomPublisher: .init(nil),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: UserIndicatorControllerMock())
return viewModel
}
@@ -63,18 +63,18 @@ struct ReadReceiptCell_Previews: PreviewProvider, TestablePreview {
formattedTimestamp: "10:00"),
memberState: .init(displayName: "Test",
avatarURL: nil),
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
.previewDisplayName("No Image")
ReadReceiptCell(readReceipt: .init(userID: "@test:matrix.org",
formattedTimestamp: "10:00"),
memberState: .init(displayName: "Test",
avatarURL: .mockMXCUserAvatar),
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
.previewDisplayName("With Image")
ReadReceiptCell(readReceipt: .init(userID: "@test:matrix.org",
formattedTimestamp: "10:00"),
memberState: nil,
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
.previewDisplayName("Loading Member")
}
}
@@ -139,7 +139,7 @@ struct ReactionsSummaryView_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
ReactionsSummaryView(reactions: AggregatedReaction.mockReactions,
members: [:],
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
selectedReactionKey: AggregatedReaction.mockReactions[0].key)
}
}
@@ -17,7 +17,7 @@ class ClientProxy: ClientProxyProtocol {
private let networkMonitor: NetworkMonitorProtocol
private let appSettings: AppSettings
private let mediaLoader: MediaLoaderProtocol
let mediaLoader: MediaLoaderProtocol
private let clientQueue: DispatchQueue
private var roomListService: RoomListService
@@ -1117,20 +1117,6 @@ class ClientProxy: ClientProxyProtocol {
}
}
extension ClientProxy: MediaLoaderProtocol {
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data {
try await mediaLoader.loadMediaContentForSource(source)
}
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data {
try await mediaLoader.loadMediaThumbnailForSource(source, width: width, height: height)
}
func loadMediaFileForSource(_ source: MediaSourceProxy, filename: String?) async throws -> MediaFileHandleProxy {
try await mediaLoader.loadMediaFileForSource(source, filename: filename)
}
}
private class ClientDelegateWrapper: ClientDelegate {
private let authErrorCallback: (Bool) -> Void
@@ -73,7 +73,7 @@ enum TimelineMediaVisibility: Decodable {
}
// sourcery: AutoMockable
protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
protocol ClientProxyProtocol: AnyObject {
var actionsPublisher: AnyPublisher<ClientProxyAction, Never> { get }
var loadingStatePublisher: CurrentValuePublisher<ClientProxyLoadingState, Never> { get }
@@ -105,6 +105,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
var pusherNotificationClientIdentifier: String? { get }
var mediaLoader: MediaLoaderProtocol { get }
var roomSummaryProvider: RoomSummaryProviderProtocol { get }
/// Used for listing rooms that shouldn't be affected by the main `roomSummaryProvider` filtering
@@ -16,7 +16,17 @@ private final class MediaRequest {
}
actor MediaLoader: MediaLoaderProtocol {
private let client: ClientProtocol
// We noticed that the keyboard appears to hold onto a reference to the `Context` of the last
// screen that had text input focus, resulting in its MediaProvider staying alive which in
// turn keeps this loader alive: https://github.com/element-hq/element-x-ios/issues/4465
// Therefore the client is `weak` so that the underlying `MatrixRustSDK.Client` is released
// when e.g. clearing the cache, otherwise we have the potential for 2 `Client`s to be alive
// at the same time causing havoc.
//
// Whilst a more correct fix would be to make `Context.mediaProvider` weak, this requires a
// bunch of workarounds in our preview tests to keep the mock provider alive as some ViewModels
// don't have an accompanying ClientMock to own it.
private weak var client: ClientProtocol?
private var ongoingRequests = [MediaSourceProxy: MediaRequest]()
init(client: ClientProtocol) {
@@ -24,18 +34,21 @@ actor MediaLoader: MediaLoaderProtocol {
}
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data {
try await enqueueLoadMediaRequest(forSource: source) {
try await self.client.getMediaContent(mediaSource: source.underlyingSource)
try await enqueueLoadMediaRequest(forSource: source) { [weak client] in
guard let client else { throw MediaLoaderError.missingClient }
return try await client.getMediaContent(mediaSource: source.underlyingSource)
}
}
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data {
try await enqueueLoadMediaRequest(forSource: source) {
try await self.client.getMediaThumbnail(mediaSource: source.underlyingSource, width: UInt64(width), height: UInt64(height))
try await enqueueLoadMediaRequest(forSource: source) { [weak client] in
guard let client else { throw MediaLoaderError.missingClient }
return try await client.getMediaThumbnail(mediaSource: source.underlyingSource, width: UInt64(width), height: UInt64(height))
}
}
func loadMediaFileForSource(_ source: MediaSourceProxy, filename: String?) async throws -> MediaFileHandleProxy {
guard let client else { throw MediaLoaderError.missingClient }
let result = try await client.getMediaFile(mediaSource: source.underlyingSource,
filename: filename,
mimeType: source.mimeType ?? "application/octet-stream",
@@ -7,6 +7,10 @@
import Foundation
enum MediaLoaderError: Error {
case missingClient
}
// sourcery: AutoMockable
protocol MediaLoaderProtocol {
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data
@@ -9,7 +9,7 @@ import Combine
import Kingfisher
import UIKit
class MediaProvider: MediaProviderProtocol {
struct MediaProvider: MediaProviderProtocol {
private let mediaLoader: MediaLoaderProtocol
private let imageCache: Kingfisher.ImageCache
private let homeserverReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never>?
@@ -17,7 +17,7 @@ enum MediaProviderError: Error {
}
// sourcery: AutoMockable
protocol MediaProviderProtocol: AnyObject {
protocol MediaProviderProtocol {
func imageFromSource(_ source: MediaSourceProxy?, size: CGSize?) -> UIImage?
func loadImageFromSource(_ source: MediaSourceProxy, size: CGSize?) async -> Result<UIImage, MediaProviderError>
func loadImageDataFromSource(_ source: MediaSourceProxy) async -> Result<Data, MediaProviderError>
@@ -94,7 +94,7 @@ class UserSessionStore: UserSessionStoreProtocol {
// MARK: - Private
private func buildUserSessionWithClient(_ clientProxy: ClientProxyProtocol) -> UserSessionProtocol {
let mediaProvider = MediaProvider(mediaLoader: clientProxy,
let mediaProvider = MediaProvider(mediaLoader: clientProxy.mediaLoader,
imageCache: .onlyInMemory,
homeserverReachabilityPublisher: clientProxy.homeserverReachabilityPublisher)
@@ -562,7 +562,7 @@ class MockScreen: Identifiable {
let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationControllerProxy,
flow: .deviceInitiator,
appSettings: ServiceLocator.shared.settings,
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
return SessionVerificationScreenCoordinator(parameters: parameters)
case .userSessionScreen, .userSessionScreenReply, .userSessionSpacesFlow:
let appSettings: AppSettings = ServiceLocator.shared.settings
@@ -618,7 +618,7 @@ class MockScreen: Identifiable {
let navigationStackCoordinator = NavigationStackCoordinator()
navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator())
let coordinator = RoomRolesAndPermissionsFlowCoordinator(parameters: .init(roomProxy: JoinedRoomProxyMock(.init(members: .allMembersAsAdmin)),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
navigationStackCoordinator: navigationStackCoordinator,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
analytics: ServiceLocator.shared.analytics))
@@ -750,7 +750,7 @@ class MockScreen: Identifiable {
timelineItemFactory: RoomTimelineItemFactory(userID: "@alice:matrix.org",
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
stateEventStringBuilder: RoomStateEventStringBuilder(userID: "@alice:matrix.org")),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
appSettings: ServiceLocator.shared.settings)
let flowCoordinator = ChatsFlowCoordinator(isNewLogin: false,
@@ -98,7 +98,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
viewModel = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()),
wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: mockCompletionSuggestionService,
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
@@ -683,7 +683,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
viewModel = ComposerToolbarViewModel(roomProxy: roomProxyMock,
wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: mockCompletionSuggestionService,
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
@@ -727,7 +727,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
viewModel = ComposerToolbarViewModel(roomProxy: roomProxyMock,
wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: mockCompletionSuggestionService,
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
@@ -761,7 +761,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
viewModel = ComposerToolbarViewModel(roomProxy: roomProxyMock,
wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: mockCompletionSuggestionService,
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
@@ -796,7 +796,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
roomProxy: JoinedRoomProxyMock(.init()),
wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: completionSuggestionServiceMock,
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
@@ -19,7 +19,7 @@ class GlobalSearchScreenViewModelTests: XCTestCase {
override func setUpWithError() throws {
cancellables.removeAll()
viewModel = GlobalSearchScreenViewModel(roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))),
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
context = viewModel.context
}
@@ -24,7 +24,7 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
func testLoadingState() async throws {
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loading, joinRule: .knock))
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
mediaProvider: .mock,
mediaProvider: MediaProviderMock(),
userIndicatorController: UserIndicatorControllerMock())
let deferred = deferFulfillment(context.$viewState) { state in
@@ -42,7 +42,7 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
func testEmptyState() async throws {
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([]), joinRule: .knock))
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
mediaProvider: .mock,
mediaProvider: MediaProviderMock(),
userIndicatorController: UserIndicatorControllerMock())
let deferred = deferFulfillment(context.$viewState) { state in
@@ -66,7 +66,7 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
ownUserID: RoomMemberProxyMock.mockAdmin.userID,
joinRule: .knock))
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
mediaProvider: .mock,
mediaProvider: MediaProviderMock(),
userIndicatorController: UserIndicatorControllerMock())
var deferred = deferFulfillment(context.$viewState) { state in
@@ -141,7 +141,7 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org"))]),
joinRule: .knock))
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
mediaProvider: .mock,
mediaProvider: MediaProviderMock(),
userIndicatorController: UserIndicatorControllerMock())
var deferred = deferFulfillment(context.$viewState) { state in
@@ -189,7 +189,7 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
ownUserID: RoomMemberProxyMock.mockAdmin.userID,
joinRule: .invite))
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
mediaProvider: .mock,
mediaProvider: MediaProviderMock(),
userIndicatorController: UserIndicatorControllerMock())
let deferred = deferFulfillment(context.$viewState) { state in
@@ -210,7 +210,7 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
joinRule: .knock,
powerLevelsConfiguration: .init(canUserInvite: false)))
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
mediaProvider: .mock,
mediaProvider: MediaProviderMock(),
userIndicatorController: UserIndicatorControllerMock())
let deferred = deferFulfillment(context.$viewState) { state in
@@ -32,7 +32,7 @@ class ManageRoomMemberSheetViewModelTests: XCTestCase {
roomProxy: roomProxy,
userIndicatorController: UserIndicatorControllerMock(),
analyticsService: ServiceLocator.shared.analytics,
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
let deferred = deferFulfillment(context.observe(\.viewState.bindings.alertInfo)) { $0 != nil }
let deferredAction = deferFulfillment(viewModel.actions) { action in
@@ -63,7 +63,7 @@ class ManageRoomMemberSheetViewModelTests: XCTestCase {
roomProxy: roomProxy,
userIndicatorController: UserIndicatorControllerMock(),
analyticsService: ServiceLocator.shared.analytics,
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
let deferred = deferFulfillment(context.observe(\.viewState.bindings.alertInfo)) { $0 != nil }
context.send(viewAction: .ban)
@@ -85,7 +85,7 @@ class ManageRoomMemberSheetViewModelTests: XCTestCase {
roomProxy: roomProxy,
userIndicatorController: UserIndicatorControllerMock(),
analyticsService: ServiceLocator.shared.analytics,
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
let deferredAction = deferFulfillment(viewModel.actions) { action in
action == .dismiss(shouldShowDetails: true)
@@ -196,7 +196,7 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase {
roomProxy = JoinedRoomProxyMock(.init(members: .allMembersAsAdmin))
viewModel = RoomChangeRolesScreenViewModel(mode: mode,
roomProxy: roomProxy,
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: UserIndicatorControllerMock(),
analytics: ServiceLocator.shared.analytics)
}
@@ -21,7 +21,7 @@ class SessionVerificationViewModelTests: XCTestCase {
viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: sessionVerificationController,
flow: .deviceInitiator,
appSettings: AppSettings(),
mediaProvider: .mock)
mediaProvider: MediaProviderMock(configuration: .init()))
context = viewModel.context
}
@@ -118,7 +118,7 @@ class SpaceScreenViewModelTests: XCTestCase {
viewModel = SpaceScreenViewModel(spaceRoomListProxy: spaceRoomListProxy,
spaceServiceProxy: spaceServiceProxy,
selectedSpaceRoomPublisher: .init(nil),
mediaProvider: .mock,
mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: UserIndicatorControllerMock())
}
}
@@ -281,7 +281,7 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
timelineController = MockTimelineController(timelineKind: .media(.mediaFilesScreen))
timelineController.timelineItems = initialItems
mediaProvider = MediaProviderMock(.init())
mediaProvider = MediaProviderMock(configuration: .init())
photoLibraryManager = PhotoLibraryManagerMock(.init(authorizationDenied: photoLibraryAuthorizationDenied))
viewModel = TimelineMediaPreviewViewModel(initialItem: initialItems[initialItemIndex],
@@ -21,7 +21,7 @@ class VoiceMessageMediaManagerTests: XCTestCase {
override func setUp() async throws {
voiceMessageCache = VoiceMessageCacheMock()
mediaProvider = .mock
mediaProvider = MediaProviderMock(configuration: .init())
mediaProvider.loadFileFromSourceFilenameClosure = nil
mediaProvider.loadFileFromSourceFilenameReturnValue = .failure(.failedRetrievingFile)
voiceMessageMediaManager = VoiceMessageMediaManager(mediaProvider: mediaProvider,