mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 19:44:05 +00:00
Prevent multiple conversion of the same voice message audio file. (#1887)
This commit is contained in:
@@ -328,14 +328,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
mentionBuilder: MentionBuilder(mentionsEnabled: appSettings.mentionsEnabled)),
|
||||
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID),
|
||||
appSettings: appSettings)
|
||||
|
||||
let voiceMesssageMediaManager = VoiceMessageMediaManager(mediaProvider: userSession.mediaProvider)
|
||||
|
||||
|
||||
let timelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy,
|
||||
timelineItemFactory: timelineItemFactory,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
mediaPlayerProvider: mediaPlayerProvider,
|
||||
voiceMessageMediaManager: voiceMesssageMediaManager)
|
||||
voiceMessageMediaManager: userSession.voiceMessageMediaManager)
|
||||
self.timelineController = timelineController
|
||||
|
||||
analytics.trackViewRoom(isDM: roomProxy.isDirect, isSpace: roomProxy.isSpace)
|
||||
|
||||
@@ -115,6 +115,49 @@ class AnalyticsClientMock: AnalyticsClientProtocol {
|
||||
updateUserPropertiesClosure?(userProperties)
|
||||
}
|
||||
}
|
||||
class AudioConverterMock: AudioConverterProtocol {
|
||||
|
||||
//MARK: - convertToOpusOgg
|
||||
|
||||
var convertToOpusOggSourceURLDestinationURLThrowableError: Error?
|
||||
var convertToOpusOggSourceURLDestinationURLCallsCount = 0
|
||||
var convertToOpusOggSourceURLDestinationURLCalled: Bool {
|
||||
return convertToOpusOggSourceURLDestinationURLCallsCount > 0
|
||||
}
|
||||
var convertToOpusOggSourceURLDestinationURLReceivedArguments: (sourceURL: URL, destinationURL: URL)?
|
||||
var convertToOpusOggSourceURLDestinationURLReceivedInvocations: [(sourceURL: URL, destinationURL: URL)] = []
|
||||
var convertToOpusOggSourceURLDestinationURLClosure: ((URL, URL) throws -> Void)?
|
||||
|
||||
func convertToOpusOgg(sourceURL: URL, destinationURL: URL) throws {
|
||||
if let error = convertToOpusOggSourceURLDestinationURLThrowableError {
|
||||
throw error
|
||||
}
|
||||
convertToOpusOggSourceURLDestinationURLCallsCount += 1
|
||||
convertToOpusOggSourceURLDestinationURLReceivedArguments = (sourceURL: sourceURL, destinationURL: destinationURL)
|
||||
convertToOpusOggSourceURLDestinationURLReceivedInvocations.append((sourceURL: sourceURL, destinationURL: destinationURL))
|
||||
try convertToOpusOggSourceURLDestinationURLClosure?(sourceURL, destinationURL)
|
||||
}
|
||||
//MARK: - convertToMPEG4AAC
|
||||
|
||||
var convertToMPEG4AACSourceURLDestinationURLThrowableError: Error?
|
||||
var convertToMPEG4AACSourceURLDestinationURLCallsCount = 0
|
||||
var convertToMPEG4AACSourceURLDestinationURLCalled: Bool {
|
||||
return convertToMPEG4AACSourceURLDestinationURLCallsCount > 0
|
||||
}
|
||||
var convertToMPEG4AACSourceURLDestinationURLReceivedArguments: (sourceURL: URL, destinationURL: URL)?
|
||||
var convertToMPEG4AACSourceURLDestinationURLReceivedInvocations: [(sourceURL: URL, destinationURL: URL)] = []
|
||||
var convertToMPEG4AACSourceURLDestinationURLClosure: ((URL, URL) throws -> Void)?
|
||||
|
||||
func convertToMPEG4AAC(sourceURL: URL, destinationURL: URL) throws {
|
||||
if let error = convertToMPEG4AACSourceURLDestinationURLThrowableError {
|
||||
throw error
|
||||
}
|
||||
convertToMPEG4AACSourceURLDestinationURLCallsCount += 1
|
||||
convertToMPEG4AACSourceURLDestinationURLReceivedArguments = (sourceURL: sourceURL, destinationURL: destinationURL)
|
||||
convertToMPEG4AACSourceURLDestinationURLReceivedInvocations.append((sourceURL: sourceURL, destinationURL: destinationURL))
|
||||
try convertToMPEG4AACSourceURLDestinationURLClosure?(sourceURL, destinationURL)
|
||||
}
|
||||
}
|
||||
class AudioPlayerMock: AudioPlayerProtocol {
|
||||
var actions: AnyPublisher<AudioPlayerAction, Never> {
|
||||
get { return underlyingActions }
|
||||
@@ -2191,6 +2234,67 @@ class UserNotificationCenterMock: UserNotificationCenterProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
class VoiceMessageCacheMock: VoiceMessageCacheProtocol {
|
||||
|
||||
//MARK: - fileURL
|
||||
|
||||
var fileURLForCallsCount = 0
|
||||
var fileURLForCalled: Bool {
|
||||
return fileURLForCallsCount > 0
|
||||
}
|
||||
var fileURLForReceivedMediaSource: MediaSourceProxy?
|
||||
var fileURLForReceivedInvocations: [MediaSourceProxy] = []
|
||||
var fileURLForReturnValue: URL?
|
||||
var fileURLForClosure: ((MediaSourceProxy) -> URL?)?
|
||||
|
||||
func fileURL(for mediaSource: MediaSourceProxy) -> URL? {
|
||||
fileURLForCallsCount += 1
|
||||
fileURLForReceivedMediaSource = mediaSource
|
||||
fileURLForReceivedInvocations.append(mediaSource)
|
||||
if let fileURLForClosure = fileURLForClosure {
|
||||
return fileURLForClosure(mediaSource)
|
||||
} else {
|
||||
return fileURLForReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - cache
|
||||
|
||||
var cacheMediaSourceUsingMoveThrowableError: Error?
|
||||
var cacheMediaSourceUsingMoveCallsCount = 0
|
||||
var cacheMediaSourceUsingMoveCalled: Bool {
|
||||
return cacheMediaSourceUsingMoveCallsCount > 0
|
||||
}
|
||||
var cacheMediaSourceUsingMoveReceivedArguments: (mediaSource: MediaSourceProxy, fileURL: URL, move: Bool)?
|
||||
var cacheMediaSourceUsingMoveReceivedInvocations: [(mediaSource: MediaSourceProxy, fileURL: URL, move: Bool)] = []
|
||||
var cacheMediaSourceUsingMoveReturnValue: URL!
|
||||
var cacheMediaSourceUsingMoveClosure: ((MediaSourceProxy, URL, Bool) throws -> URL)?
|
||||
|
||||
func cache(mediaSource: MediaSourceProxy, using fileURL: URL, move: Bool) throws -> URL {
|
||||
if let error = cacheMediaSourceUsingMoveThrowableError {
|
||||
throw error
|
||||
}
|
||||
cacheMediaSourceUsingMoveCallsCount += 1
|
||||
cacheMediaSourceUsingMoveReceivedArguments = (mediaSource: mediaSource, fileURL: fileURL, move: move)
|
||||
cacheMediaSourceUsingMoveReceivedInvocations.append((mediaSource: mediaSource, fileURL: fileURL, move: move))
|
||||
if let cacheMediaSourceUsingMoveClosure = cacheMediaSourceUsingMoveClosure {
|
||||
return try cacheMediaSourceUsingMoveClosure(mediaSource, fileURL, move)
|
||||
} else {
|
||||
return cacheMediaSourceUsingMoveReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - clearCache
|
||||
|
||||
var clearCacheCallsCount = 0
|
||||
var clearCacheCalled: Bool {
|
||||
return clearCacheCallsCount > 0
|
||||
}
|
||||
var clearCacheClosure: (() -> Void)?
|
||||
|
||||
func clearCache() {
|
||||
clearCacheCallsCount += 1
|
||||
clearCacheClosure?()
|
||||
}
|
||||
}
|
||||
class VoiceMessageMediaManagerMock: VoiceMessageMediaManagerProtocol {
|
||||
|
||||
//MARK: - loadVoiceMessageFromSource
|
||||
|
||||
@@ -77,7 +77,8 @@ extension View {
|
||||
|
||||
struct ShimmerOverlay_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = HomeScreenViewModel(userSession: MockUserSession(clientProxy: MockClientProxy(userID: ""),
|
||||
mediaProvider: MockMediaProvider()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock()),
|
||||
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
|
||||
mentionBuilder: MentionBuilder(mentionsEnabled: ServiceLocator.shared.settings.mentionsEnabled)),
|
||||
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
|
||||
|
||||
@@ -75,7 +75,8 @@ struct WaitlistScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let successViewModel = {
|
||||
let viewModel = WaitlistScreenViewModel(homeserver: .mockMatrixDotOrg)
|
||||
viewModel.update(userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@alice:matrix.org"),
|
||||
mediaProvider: MockMediaProvider()))
|
||||
mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock()))
|
||||
return viewModel
|
||||
}()
|
||||
|
||||
|
||||
@@ -207,7 +207,8 @@ struct CreateRoomScreen: View {
|
||||
struct CreateRoom_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = {
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@userid:example.com"),
|
||||
mediaProvider: MockMediaProvider())
|
||||
mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
let parameters = CreateRoomFlowParameters()
|
||||
let selectedUsers: [UserProfileProxy] = [.mockAlice, .mockBob, .mockCharlie]
|
||||
|
||||
@@ -220,7 +221,8 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
static let emtpyViewModel = {
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@userid:example.com"),
|
||||
mediaProvider: MockMediaProvider())
|
||||
mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
let parameters = CreateRoomFlowParameters()
|
||||
return CreateRoomViewModel(userSession: userSession,
|
||||
createRoomParameters: .init(parameters),
|
||||
|
||||
@@ -335,7 +335,8 @@ struct HomeScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static func viewModel(_ state: MockRoomSummaryProviderState) -> HomeScreenViewModel {
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@alice:example.com",
|
||||
roomSummaryProvider: MockRoomSummaryProvider(state: state)),
|
||||
mediaProvider: MockMediaProvider())
|
||||
mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
|
||||
return HomeScreenViewModel(userSession: userSession,
|
||||
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: ServiceLocator.shared.settings.mentionsEnabled)),
|
||||
|
||||
@@ -151,7 +151,8 @@ struct HomeScreenEmptyStateView_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = {
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@user:example.com",
|
||||
roomSummaryProvider: MockRoomSummaryProvider(state: .loaded([]))),
|
||||
mediaProvider: MockMediaProvider())
|
||||
mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
|
||||
return HomeScreenViewModel(userSession: userSession,
|
||||
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: ServiceLocator.shared.settings.mentionsEnabled)),
|
||||
|
||||
@@ -185,7 +185,8 @@ struct HomeScreenRoomCell_Previews: PreviewProvider, TestablePreview {
|
||||
let summaryProvider = MockRoomSummaryProvider(state: .loaded(.mockRooms))
|
||||
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "John Doe", roomSummaryProvider: summaryProvider),
|
||||
mediaProvider: MockMediaProvider())
|
||||
mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
|
||||
let viewModel = HomeScreenViewModel(userSession: userSession,
|
||||
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
|
||||
|
||||
@@ -80,7 +80,8 @@ struct InvitesScreen_Previews: PreviewProvider, TestablePreview {
|
||||
private extension InvitesScreenViewModel {
|
||||
static let noInvites: InvitesScreenViewModel = {
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@userid:example.com"),
|
||||
mediaProvider: MockMediaProvider())
|
||||
mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
let regularViewModel = InvitesScreenViewModel(userSession: userSession,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
@@ -93,7 +94,8 @@ private extension InvitesScreenViewModel {
|
||||
clientProxy.inviteSummaryProvider = MockRoomSummaryProvider(state: .loaded(.mockInvites))
|
||||
clientProxy.roomSummaryProvider = MockRoomSummaryProvider(state: .loaded(.mockInvites))
|
||||
let userSession = MockUserSession(clientProxy: clientProxy,
|
||||
mediaProvider: MockMediaProvider())
|
||||
mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
let regularViewModel = InvitesScreenViewModel(userSession: userSession,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
|
||||
+5
-3
@@ -76,7 +76,8 @@ struct NotificationSettingsEditScreen_Previews: PreviewProvider, TestablePreview
|
||||
notificationSettingsProxy.getRoomsWithUserDefinedRulesReturnValue = [RoomSummary].mockRooms.compactMap(\.id)
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@alice:example.com",
|
||||
roomSummaryProvider: MockRoomSummaryProvider(state: .loaded(.mockRooms))),
|
||||
mediaProvider: MockMediaProvider())
|
||||
mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
var viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat,
|
||||
userSession: userSession,
|
||||
notificationSettingsProxy: notificationSettingsProxy)
|
||||
@@ -90,7 +91,8 @@ struct NotificationSettingsEditScreen_Previews: PreviewProvider, TestablePreview
|
||||
notificationSettingsProxy.getRoomsWithUserDefinedRulesReturnValue = []
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@alice:example.com",
|
||||
roomSummaryProvider: MockRoomSummaryProvider(state: .loaded(.mockRooms))),
|
||||
mediaProvider: MockMediaProvider())
|
||||
mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
var viewModel = NotificationSettingsEditScreenViewModel(chatType: .oneToOneChat,
|
||||
userSession: userSession,
|
||||
notificationSettingsProxy: notificationSettingsProxy)
|
||||
@@ -102,7 +104,7 @@ struct NotificationSettingsEditScreen_Previews: PreviewProvider, TestablePreview
|
||||
let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init())
|
||||
notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly
|
||||
notificationSettingsProxy.getRoomsWithUserDefinedRulesReturnValue = []
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "John Doe"), mediaProvider: MockMediaProvider())
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "John Doe"), mediaProvider: MockMediaProvider(), voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
|
||||
var viewModel = NotificationSettingsEditScreenViewModel(chatType: .oneToOneChat,
|
||||
userSession: userSession,
|
||||
|
||||
+2
-1
@@ -62,7 +62,8 @@ struct NotificationSettingsEditScreenRoomCell_Previews: PreviewProvider, Testabl
|
||||
let summaryProvider = MockRoomSummaryProvider(state: .loaded(.mockRooms))
|
||||
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "John Doe", roomSummaryProvider: summaryProvider),
|
||||
mediaProvider: MockMediaProvider())
|
||||
mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
|
||||
let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init())
|
||||
notificationSettingsProxy.getRoomsWithUserDefinedRulesReturnValue = []
|
||||
|
||||
+3
-2
@@ -199,7 +199,8 @@ struct NotificationSettingsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
notificationSettingsProxy.isRoomMentionEnabledReturnValue = true
|
||||
notificationSettingsProxy.isCallEnabledReturnValue = false
|
||||
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "John Doe"), mediaProvider: MockMediaProvider())
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "John Doe"), mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
|
||||
var viewModel = NotificationSettingsScreenViewModel(userSession: userSession,
|
||||
appSettings: appSettings,
|
||||
@@ -228,7 +229,7 @@ struct NotificationSettingsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
notificationSettingsProxy.isRoomMentionEnabledReturnValue = true
|
||||
notificationSettingsProxy.isCallEnabledReturnValue = false
|
||||
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "John Doe"), mediaProvider: MockMediaProvider())
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "John Doe"), mediaProvider: MockMediaProvider(), voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
|
||||
var viewModel = NotificationSettingsScreenViewModel(userSession: userSession,
|
||||
appSettings: appSettings,
|
||||
|
||||
@@ -235,7 +235,8 @@ struct SettingsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
let userSession = MockUserSession(sessionVerificationController: verificationController,
|
||||
clientProxy: MockClientProxy(userID: "@userid:example.com",
|
||||
deviceID: "AAAAAAAAAAA"),
|
||||
mediaProvider: MockMediaProvider())
|
||||
mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
return SettingsScreenViewModel(userSession: userSession,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
}()
|
||||
|
||||
@@ -137,7 +137,8 @@ struct StartChatScreen: View {
|
||||
struct StartChatScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = {
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@userid:example.com"),
|
||||
mediaProvider: MockMediaProvider())
|
||||
mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
let userDiscoveryService = UserDiscoveryServiceMock()
|
||||
userDiscoveryService.fetchSuggestionsReturnValue = .success([.mockAlice])
|
||||
userDiscoveryService.searchProfilesWithReturnValue = .success([.mockAlice])
|
||||
|
||||
@@ -22,7 +22,12 @@ enum AudioConverterError: Error {
|
||||
case conversionFailed(Error?)
|
||||
}
|
||||
|
||||
struct AudioConverter {
|
||||
enum AudioConverterPreferredFileExtension: String {
|
||||
case mpeg4aac = "m4a"
|
||||
case ogg
|
||||
}
|
||||
|
||||
struct AudioConverter: AudioConverterProtocol {
|
||||
func convertToOpusOgg(sourceURL: URL, destinationURL: URL) throws {
|
||||
do {
|
||||
try OGGConverter.convertM4aFileToOpusOGG(src: sourceURL, dest: destinationURL)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol AudioConverterProtocol {
|
||||
func convertToOpusOgg(sourceURL: URL, destinationURL: URL) throws
|
||||
func convertToMPEG4AAC(sourceURL: URL, destinationURL: URL) throws
|
||||
}
|
||||
|
||||
// sourcery: AutoMockable
|
||||
extension AudioConverterProtocol { }
|
||||
@@ -63,7 +63,8 @@ class MockAuthenticationServiceProxy: AuthenticationServiceProxyProtocol {
|
||||
}
|
||||
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: username),
|
||||
mediaProvider: MockMediaProvider())
|
||||
mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
return .success(userSession)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,11 @@ struct MockMediaProvider: MediaProviderProtocol {
|
||||
return .success(data)
|
||||
}
|
||||
|
||||
var loadFileFromSourceReturnValue: MediaFileHandleProxy?
|
||||
func loadFileFromSource(_ source: MediaSourceProxy, body: String?) async -> Result<MediaFileHandleProxy, MediaProviderError> {
|
||||
.failure(.failedRetrievingFile)
|
||||
if let loadFileFromSourceReturnValue {
|
||||
return .success(loadFileFromSourceReturnValue)
|
||||
}
|
||||
return .failure(.failedRetrievingFile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,4 +24,5 @@ struct MockUserSession: UserSessionProtocol {
|
||||
var homeserver: String { clientProxy.homeserver }
|
||||
let clientProxy: ClientProxyProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol
|
||||
}
|
||||
|
||||
@@ -28,12 +28,14 @@ class UserSession: UserSessionProtocol {
|
||||
|
||||
let clientProxy: ClientProxyProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol
|
||||
let callbacks = PassthroughSubject<UserSessionCallback, Never>()
|
||||
private(set) var sessionVerificationController: SessionVerificationControllerProxyProtocol?
|
||||
|
||||
init(clientProxy: ClientProxyProtocol, mediaProvider: MediaProviderProtocol) {
|
||||
init(clientProxy: ClientProxyProtocol, mediaProvider: MediaProviderProtocol, voiceMessageMediaManager: VoiceMessageMediaManagerProtocol) {
|
||||
self.clientProxy = clientProxy
|
||||
self.mediaProvider = mediaProvider
|
||||
self.voiceMessageMediaManager = voiceMessageMediaManager
|
||||
|
||||
setupSessionVerificationWatchdog()
|
||||
setupAuthErrorWatchdog()
|
||||
|
||||
@@ -30,6 +30,7 @@ protocol UserSessionProtocol {
|
||||
|
||||
var clientProxy: ClientProxyProtocol { get }
|
||||
var mediaProvider: MediaProviderProtocol { get }
|
||||
var voiceMessageMediaManager: VoiceMessageMediaManagerProtocol { get }
|
||||
|
||||
var sessionVerificationController: SessionVerificationControllerProxyProtocol? { get }
|
||||
|
||||
|
||||
@@ -94,10 +94,16 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
let imageCache = ImageCache.onlyInMemory
|
||||
imageCache.memoryStorage.config.keepWhenEnteringBackground = true
|
||||
|
||||
let mediaProvider = MediaProvider(mediaLoader: clientProxy,
|
||||
imageCache: imageCache,
|
||||
backgroundTaskService: backgroundTaskService)
|
||||
|
||||
let voiceMessageMediaManager = VoiceMessageMediaManager(mediaProvider: mediaProvider,
|
||||
backgroundTaskService: backgroundTaskService)
|
||||
|
||||
return UserSession(clientProxy: clientProxy,
|
||||
mediaProvider: MediaProvider(mediaLoader: clientProxy,
|
||||
imageCache: imageCache,
|
||||
backgroundTaskService: backgroundTaskService))
|
||||
mediaProvider: mediaProvider,
|
||||
voiceMessageMediaManager: voiceMessageMediaManager)
|
||||
}
|
||||
|
||||
private func restorePreviousLogin(_ credentials: KeychainCredentials) async -> Result<ClientProxyProtocol, UserSessionStoreError> {
|
||||
|
||||
@@ -16,32 +16,33 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class VoiceMessageCache {
|
||||
var temporaryFilesFolderURL: URL {
|
||||
enum VoiceMessageCacheError: Error {
|
||||
case invalidFileExtension
|
||||
}
|
||||
|
||||
class VoiceMessageCache: VoiceMessageCacheProtocol {
|
||||
private let preferredFileExtension = "m4a"
|
||||
private var temporaryFilesFolderURL: URL {
|
||||
FileManager.default.temporaryDirectory.appendingPathComponent("media/voice-message")
|
||||
}
|
||||
|
||||
func cacheURL(for mediaSource: MediaSourceProxy, replacingExtension newExtension: String? = nil) -> URL {
|
||||
var newURL = temporaryFilesFolderURL.appendingPathComponent(mediaSource.url.lastPathComponent)
|
||||
if let newExtension {
|
||||
newURL = newURL.deletingPathExtension().appendingPathExtension(newExtension)
|
||||
}
|
||||
return newURL
|
||||
}
|
||||
|
||||
func fileURL(for mediaSource: MediaSourceProxy, withExtension fileExtension: String? = nil) -> URL? {
|
||||
var url = temporaryFilesFolderURL.appendingPathComponent(mediaSource.url.lastPathComponent)
|
||||
if let fileExtension {
|
||||
url = url.deletingPathExtension().appendingPathExtension(fileExtension)
|
||||
}
|
||||
|
||||
func fileURL(for mediaSource: MediaSourceProxy) -> URL? {
|
||||
let url = cacheURL(for: mediaSource)
|
||||
return FileManager.default.fileExists(atPath: url.path()) ? url : nil
|
||||
}
|
||||
|
||||
func cache(mediaSource: MediaSourceProxy, using fileURL: URL) throws -> URL {
|
||||
func cache(mediaSource: MediaSourceProxy, using fileURL: URL, move: Bool = false) throws -> URL {
|
||||
guard fileURL.pathExtension == preferredFileExtension else {
|
||||
throw VoiceMessageCacheError.invalidFileExtension
|
||||
}
|
||||
setupTemporaryFilesFolder()
|
||||
let url = cacheURL(for: mediaSource)
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
try FileManager.default.copyItem(at: fileURL, to: url)
|
||||
if move {
|
||||
try FileManager.default.moveItem(at: fileURL, to: url)
|
||||
} else {
|
||||
try FileManager.default.copyItem(at: fileURL, to: url)
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
@@ -64,4 +65,8 @@ class VoiceMessageCache {
|
||||
MXLog.error("Failed to setup audio cache manager.")
|
||||
}
|
||||
}
|
||||
|
||||
private func cacheURL(for mediaSource: MediaSourceProxy) -> URL {
|
||||
temporaryFilesFolderURL.appendingPathComponent(mediaSource.url.lastPathComponent).appendingPathExtension(preferredFileExtension)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol VoiceMessageCacheProtocol {
|
||||
func fileURL(for mediaSource: MediaSourceProxy) -> URL?
|
||||
func cache(mediaSource: MediaSourceProxy, using fileURL: URL, move: Bool) throws -> URL
|
||||
func clearCache()
|
||||
}
|
||||
|
||||
// sourcery: AutoMockable
|
||||
extension VoiceMessageCacheProtocol { }
|
||||
@@ -20,31 +20,47 @@ enum VoiceMessageMediaManagerError: Error {
|
||||
case unsupportedMimeTye
|
||||
}
|
||||
|
||||
private final class VoiceMessageConversionRequest {
|
||||
var continuations: [CheckedContinuation<URL, Error>] = []
|
||||
}
|
||||
|
||||
class VoiceMessageMediaManager: VoiceMessageMediaManagerProtocol {
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let cache: VoiceMessageCache
|
||||
|
||||
private let supportedVoiceMessageMimeType = "audio/ogg"
|
||||
|
||||
/// Preferred audio file extension after conversion
|
||||
private let preferredAudioExtension = "m4a"
|
||||
|
||||
init(mediaProvider: MediaProviderProtocol) {
|
||||
self.mediaProvider = mediaProvider
|
||||
cache = VoiceMessageCache()
|
||||
}
|
||||
private let voiceMessageCache: VoiceMessageCacheProtocol
|
||||
private let audioConverter: AudioConverterProtocol
|
||||
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol?
|
||||
private let processingQueue: DispatchQueue
|
||||
private var conversionRequests = [MediaSourceProxy: VoiceMessageConversionRequest]()
|
||||
|
||||
private let supportedVoiceMessageMimeType = "audio/ogg"
|
||||
|
||||
init(mediaProvider: MediaProviderProtocol,
|
||||
voiceMessageCache: VoiceMessageCacheProtocol = VoiceMessageCache(),
|
||||
audioConverter: AudioConverterProtocol = AudioConverter(),
|
||||
processingQueue: DispatchQueue = .global(),
|
||||
backgroundTaskService: BackgroundTaskServiceProtocol?) {
|
||||
self.mediaProvider = mediaProvider
|
||||
self.voiceMessageCache = voiceMessageCache
|
||||
self.audioConverter = audioConverter
|
||||
self.processingQueue = processingQueue
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
}
|
||||
|
||||
deinit {
|
||||
cache.clearCache()
|
||||
voiceMessageCache.clearCache()
|
||||
}
|
||||
|
||||
func loadVoiceMessageFromSource(_ source: MediaSourceProxy, body: String?) async throws -> URL {
|
||||
let loadFileBgTask = await backgroundTaskService?.startBackgroundTask(withName: "LoadFile: \(source.url.hashValue)")
|
||||
defer { loadFileBgTask?.stop() }
|
||||
|
||||
guard let mimeType = source.mimeType, mimeType == supportedVoiceMessageMimeType else {
|
||||
throw VoiceMessageMediaManagerError.unsupportedMimeTye
|
||||
}
|
||||
|
||||
// Do we already have a converted version?
|
||||
if let fileURL = cache.fileURL(for: source, withExtension: preferredAudioExtension) {
|
||||
if let fileURL = voiceMessageCache.fileURL(for: source) {
|
||||
return fileURL
|
||||
}
|
||||
|
||||
@@ -52,16 +68,50 @@ class VoiceMessageMediaManager: VoiceMessageMediaManagerProtocol {
|
||||
guard case .success(let fileHandle) = await mediaProvider.loadFileFromSource(source, body: body) else {
|
||||
throw MediaProviderError.failedRetrievingFile
|
||||
}
|
||||
let fileURL = try cache.cache(mediaSource: source, using: fileHandle.url)
|
||||
|
||||
// Convert from ogg
|
||||
let audioConverter = AudioConverter()
|
||||
let convertedFileURL = cache.cacheURL(for: source, replacingExtension: preferredAudioExtension)
|
||||
try audioConverter.convertToMPEG4AAC(sourceURL: fileURL, destinationURL: convertedFileURL)
|
||||
|
||||
// we don't need the original file anymore
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
return try await enqueueVoiceMessageConversionRequest(forSource: source) { [audioConverter, voiceMessageCache] in
|
||||
// Do we already have a converted version?
|
||||
if let fileURL = voiceMessageCache.fileURL(for: source) {
|
||||
return fileURL
|
||||
}
|
||||
|
||||
// Convert from ogg
|
||||
let convertedFileURL = URL.temporaryDirectory.appendingPathComponent(fileHandle.url.deletingPathExtension().lastPathComponent).appendingPathExtension(AudioConverterPreferredFileExtension.mpeg4aac.rawValue)
|
||||
try audioConverter.convertToMPEG4AAC(sourceURL: fileHandle.url, destinationURL: convertedFileURL)
|
||||
|
||||
// Cache the file and return the url
|
||||
return try voiceMessageCache.cache(mediaSource: source, using: convertedFileURL, move: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func enqueueVoiceMessageConversionRequest(forSource source: MediaSourceProxy, operation: @escaping () throws -> URL) async throws -> URL {
|
||||
if let conversionRequests = conversionRequests[source] {
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
conversionRequests.continuations.append(continuation)
|
||||
}
|
||||
}
|
||||
|
||||
return convertedFileURL
|
||||
let conversionRequest = VoiceMessageConversionRequest()
|
||||
conversionRequests[source] = conversionRequest
|
||||
|
||||
defer {
|
||||
conversionRequests[source] = nil
|
||||
}
|
||||
|
||||
do {
|
||||
let result = try await Task.dispatch(on: processingQueue) {
|
||||
try operation()
|
||||
}
|
||||
|
||||
conversionRequest.continuations.forEach { $0.resume(returning: result) }
|
||||
|
||||
return result
|
||||
|
||||
} catch {
|
||||
conversionRequest.continuations.forEach { $0.resume(throwing: error) }
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,8 @@ class MockScreen: Identifiable {
|
||||
case .home:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let session = MockUserSession(clientProxy: MockClientProxy(userID: "@mock:matrix.org"),
|
||||
mediaProvider: MockMediaProvider())
|
||||
mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
let coordinator = HomeScreenCoordinator(parameters: .init(userSession: session,
|
||||
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: true)),
|
||||
bugReportService: BugReportServiceMock(),
|
||||
@@ -177,7 +178,7 @@ class MockScreen: Identifiable {
|
||||
let clientProxy = MockClientProxy(userID: "@mock:client.com")
|
||||
let coordinator = SettingsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
||||
userIndicatorController: nil,
|
||||
userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()),
|
||||
userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider(), voiceMessageMediaManager: VoiceMessageMediaManagerMock()),
|
||||
bugReportService: BugReportServiceMock(),
|
||||
notificationSettings: NotificationSettingsProxyMock(with: .init()),
|
||||
appSettings: ServiceLocator.shared.settings))
|
||||
@@ -207,7 +208,8 @@ class MockScreen: Identifiable {
|
||||
let userNotificationCenter = UserNotificationCenterMock()
|
||||
userNotificationCenter.authorizationStatusReturnValue = .denied
|
||||
let session = MockUserSession(clientProxy: MockClientProxy(userID: "@mock:matrix.org"),
|
||||
mediaProvider: MockMediaProvider())
|
||||
mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
let parameters = NotificationSettingsScreenCoordinatorParameters(userSession: session,
|
||||
userNotificationCenter: userNotificationCenter,
|
||||
notificationSettings: NotificationSettingsProxyMock(with: .init()),
|
||||
@@ -217,7 +219,8 @@ class MockScreen: Identifiable {
|
||||
let userNotificationCenter = UserNotificationCenterMock()
|
||||
userNotificationCenter.authorizationStatusReturnValue = .denied
|
||||
let session = MockUserSession(clientProxy: MockClientProxy(userID: "@mock:matrix.org"),
|
||||
mediaProvider: MockMediaProvider())
|
||||
mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
let notificationSettings = NotificationSettingsProxyMock(with: .init())
|
||||
notificationSettings.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in
|
||||
switch (isEncrypted, isOneToOne) {
|
||||
@@ -439,7 +442,7 @@ class MockScreen: Identifiable {
|
||||
ServiceLocator.shared.settings.migratedAccounts[clientProxy.userID] = true
|
||||
ServiceLocator.shared.settings.hasShownWelcomeScreen = true
|
||||
|
||||
let coordinator = UserSessionFlowCoordinator(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()),
|
||||
let coordinator = UserSessionFlowCoordinator(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider(), voiceMessageMediaManager: VoiceMessageMediaManagerMock()),
|
||||
navigationSplitCoordinator: navigationSplitCoordinator,
|
||||
bugReportService: BugReportServiceMock(),
|
||||
roomTimelineControllerFactory: MockRoomTimelineControllerFactory(),
|
||||
@@ -614,7 +617,7 @@ class MockScreen: Identifiable {
|
||||
let userDiscoveryMock = UserDiscoveryServiceMock()
|
||||
userDiscoveryMock.fetchSuggestionsReturnValue = .success([.mockAlice, .mockBob, .mockCharlie])
|
||||
userDiscoveryMock.searchProfilesWithReturnValue = .success([])
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"), mediaProvider: MockMediaProvider())
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"), mediaProvider: MockMediaProvider(), voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
let parameters: StartChatScreenCoordinatorParameters = .init(userSession: userSession, navigationStackCoordinator: navigationStackCoordinator, userDiscoveryService: userDiscoveryMock)
|
||||
let coordinator = StartChatScreenCoordinator(parameters: parameters)
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
@@ -625,7 +628,7 @@ class MockScreen: Identifiable {
|
||||
let userDiscoveryMock = UserDiscoveryServiceMock()
|
||||
userDiscoveryMock.fetchSuggestionsReturnValue = .success([])
|
||||
userDiscoveryMock.searchProfilesWithReturnValue = .success([.mockBob, .mockBobby])
|
||||
let userSession = MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider())
|
||||
let userSession = MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider(), voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
let coordinator = StartChatScreenCoordinator(parameters: .init(userSession: userSession, navigationStackCoordinator: navigationStackCoordinator, userDiscoveryService: userDiscoveryMock))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
@@ -662,7 +665,7 @@ class MockScreen: Identifiable {
|
||||
let summaryProvider = MockRoomSummaryProvider(state: .loaded(.mockInvites))
|
||||
clientProxy.inviteSummaryProvider = summaryProvider
|
||||
|
||||
let coordinator = InvitesScreenCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider())))
|
||||
let coordinator = InvitesScreenCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider(), voiceMessageMediaManager: VoiceMessageMediaManagerMock())))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .invites:
|
||||
@@ -674,13 +677,13 @@ class MockScreen: Identifiable {
|
||||
let summaryProvider = MockRoomSummaryProvider(state: .loaded(.mockInvites))
|
||||
clientProxy.inviteSummaryProvider = summaryProvider
|
||||
|
||||
let coordinator = InvitesScreenCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider())))
|
||||
let coordinator = InvitesScreenCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider(), voiceMessageMediaManager: VoiceMessageMediaManagerMock())))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .invitesNoInvites:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let clientProxy = MockClientProxy(userID: "@mock:client.com")
|
||||
let coordinator = InvitesScreenCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider())))
|
||||
let coordinator = InvitesScreenCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider(), voiceMessageMediaManager: VoiceMessageMediaManagerMock())))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .inviteUsers, .inviteUsersInRoom, .inviteUsersInRoomExistingMembers:
|
||||
@@ -715,7 +718,7 @@ class MockScreen: Identifiable {
|
||||
case .createRoom:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let clientProxy = MockClientProxy(userID: "@mock:client.com")
|
||||
let mockUserSession = MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider())
|
||||
let mockUserSession = MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider(), voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
let createRoomParameters = CreateRoomFlowParameters()
|
||||
let selectedUsers: [UserProfileProxy] = [.mockAlice, .mockBob, .mockCharlie]
|
||||
let parameters = CreateRoomCoordinatorParameters(userSession: mockUserSession, userIndicatorController: nil, createRoomParameters: .init(createRoomParameters), selectedUsers: .init(selectedUsers))
|
||||
@@ -725,7 +728,7 @@ class MockScreen: Identifiable {
|
||||
case .createRoomNoUsers:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let clientProxy = MockClientProxy(userID: "@mock:client.com")
|
||||
let mockUserSession = MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider())
|
||||
let mockUserSession = MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider(), voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
||||
let createRoomParameters = CreateRoomFlowParameters()
|
||||
let parameters = CreateRoomCoordinatorParameters(userSession: mockUserSession, userIndicatorController: nil, createRoomParameters: .init(createRoomParameters), selectedUsers: .init([]))
|
||||
let coordinator = CreateRoomCoordinator(parameters: parameters)
|
||||
|
||||
Reference in New Issue
Block a user