mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Run SwiftFormat.
This commit is contained in:
@@ -13,16 +13,16 @@ struct NotificationTone: Hashable, Comparable, Codable {
|
||||
var label: String {
|
||||
labelOverride ?? URL(filePath: "/\(relativePath.last, default: "")").deletingPathExtension().lastPathComponent
|
||||
}
|
||||
|
||||
|
||||
private let labelOverride: String?
|
||||
let storageLocationRoot: StorageLocation
|
||||
let relativePath: [String]
|
||||
|
||||
|
||||
/// The audio filename including its extension.
|
||||
var filename: String {
|
||||
relativePath.last ?? ""
|
||||
}
|
||||
|
||||
|
||||
/// - Parameters:
|
||||
/// - labelOverride: Optional custom display name. If `nil`, the filename stem is used.
|
||||
/// - storageLocationRoot: Where the file lives (system, bundle, or library).
|
||||
@@ -32,7 +32,7 @@ struct NotificationTone: Hashable, Comparable, Codable {
|
||||
self.storageLocationRoot = storageLocationRoot
|
||||
self.relativePath = relativePath
|
||||
}
|
||||
|
||||
|
||||
/// Indicates which storage root backs the tone's audio file.
|
||||
enum StorageLocation: Codable, Hashable {
|
||||
/// The device's system sounds directory.
|
||||
@@ -42,22 +42,22 @@ struct NotificationTone: Hashable, Comparable, Codable {
|
||||
/// The app's Library directory (user-imported tones).
|
||||
case appLibrary
|
||||
}
|
||||
|
||||
|
||||
/// Creates a tone backed by a file in the system sounds directory.
|
||||
static func createSystemSound(label: String?, filename: String, systemSoundsSubdirectory: [String] = []) -> NotificationTone {
|
||||
NotificationTone(labelOverride: label, storageLocationRoot: .system, relativePath: systemSoundsSubdirectory + [filename])
|
||||
}
|
||||
|
||||
|
||||
/// Creates a tone backed by a file in the app bundle.
|
||||
static func createBundledSound(label: String?, filename: String) -> NotificationTone {
|
||||
NotificationTone(labelOverride: label, storageLocationRoot: .appBundle, relativePath: [filename])
|
||||
}
|
||||
|
||||
|
||||
/// Creates a tone backed by a user-imported file in the app library.
|
||||
static func createCustomUserSound(filename: String) -> NotificationTone {
|
||||
NotificationTone(labelOverride: nil, storageLocationRoot: .appLibrary, relativePath: [filename])
|
||||
}
|
||||
|
||||
|
||||
static func < (lhs: NotificationTone, rhs: NotificationTone) -> Bool {
|
||||
lhs.label < rhs.label
|
||||
}
|
||||
|
||||
@@ -170,10 +170,10 @@ struct NotificationToneManager: NotificationToneManagerProtocol {
|
||||
try FileManager.default.moveItem(at: tempURL, to: destURL)
|
||||
MXLog.info("Converted \(sourceURL.path(percentEncoded: false)) to caf")
|
||||
}
|
||||
|
||||
|
||||
/// File URL of the active tone copied/linked for use by the system.
|
||||
private static let selectedToneLocation = NotificationToneManager.libraryLocation.deletingLastPathComponent().appending(component: selectedToneFilename)
|
||||
|
||||
|
||||
/// Pre-defined iOS system tones available for selection, sorted by name.
|
||||
private static let defaultSystemAlerts: [NotificationTone] = [
|
||||
.createSystemSound(label: L10n.screenNotificationSettingsSoundSystemTriTone,
|
||||
@@ -261,7 +261,7 @@ struct NotificationToneManager: NotificationToneManagerProtocol {
|
||||
.createBundledSound(label: L10n.screenNotificationSettingsSoundElementFade,
|
||||
filename: "sound_01.caf")
|
||||
].sorted()
|
||||
|
||||
|
||||
private static let systemLocation = {
|
||||
let systemRoot: URL
|
||||
if let simulatorRoot = ProcessInfo.processInfo.environment["SIMULATOR_ROOT"] {
|
||||
@@ -271,7 +271,7 @@ struct NotificationToneManager: NotificationToneManagerProtocol {
|
||||
}
|
||||
return systemRoot.appending(components: "System", "Library", "Audio", "UISounds", directoryHint: .isDirectory)
|
||||
}()
|
||||
|
||||
|
||||
private static let bundledLocation: URL = {
|
||||
guard let url = Bundle.app.resourceURL else {
|
||||
fatalError("The app is seriously corrupt if resourceURL is missing.")
|
||||
|
||||
@@ -15,7 +15,7 @@ protocol AnalyticsClientProtocol {
|
||||
|
||||
/// Starts the analytics client reporting data.
|
||||
func start(analyticsConfiguration: AnalyticsConfiguration)
|
||||
|
||||
|
||||
/// Reset all stored properties and any event queues on the client. Note that
|
||||
/// the client will remain active, but in a fresh unidentified state.
|
||||
func reset()
|
||||
|
||||
@@ -42,7 +42,7 @@ class AnalyticsService: AnalyticsServiceProtocol {
|
||||
// The order is important here. PostHog ignores the reset if stopped.
|
||||
reset()
|
||||
client.stop()
|
||||
|
||||
|
||||
MXLog.info("Stopped.")
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ class AnalyticsService: AnalyticsServiceProtocol {
|
||||
guard isEnabled, !client.isRunning, let configuration = appSettings.analyticsConfiguration else { return }
|
||||
|
||||
client.start(analyticsConfiguration: configuration)
|
||||
|
||||
|
||||
// Sanity check in case something went wrong.
|
||||
guard client.isRunning else { return }
|
||||
|
||||
@@ -139,17 +139,17 @@ extension AnalyticsService {
|
||||
}
|
||||
capture(event: AnalyticsEvent.JoinedRoom(isDM: isDM, isSpace: isSpace, roomSize: roomSize, trigger: nil))
|
||||
}
|
||||
|
||||
|
||||
func trackPollCreated(isUndisclosed: Bool, numberOfAnswers: Int) {
|
||||
capture(event: AnalyticsEvent.PollCreation(action: .Create,
|
||||
isUndisclosed: isUndisclosed,
|
||||
numberOfAnswers: numberOfAnswers))
|
||||
}
|
||||
|
||||
|
||||
func trackPollVote() {
|
||||
capture(event: AnalyticsEvent.PollVote(doNotUse: nil))
|
||||
}
|
||||
|
||||
|
||||
func trackPollEnd() {
|
||||
capture(event: AnalyticsEvent.PollEnd(doNotUse: nil))
|
||||
}
|
||||
@@ -185,7 +185,7 @@ extension AnalyticsService {
|
||||
case .settingUp:
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let event = AnalyticsEvent.CryptoSessionStateChange(recoveryState: analyticsRecoveryState, verificationState: analyticsVerificationState)
|
||||
client.capture(event)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ protocol AnalyticsServiceProtocol: AnyObject {
|
||||
|
||||
/// Whether analytics tracking is currently enabled.
|
||||
var isEnabled: Bool { get }
|
||||
|
||||
|
||||
/// Opts in to analytics tracking.
|
||||
func optIn()
|
||||
|
||||
@@ -45,7 +45,7 @@ protocol AnalyticsServiceProtocol: AnyObject {
|
||||
|
||||
/// Resets the consent state for analytics.
|
||||
func resetConsentState()
|
||||
|
||||
|
||||
/// Track the presentation of a screen.
|
||||
/// - Parameter screen: The screen that was shown.
|
||||
/// - Parameter milliseconds: An optional value representing how long the screen was shown for in milliseconds.
|
||||
@@ -121,11 +121,11 @@ extension AnalyticsServiceProtocol {
|
||||
func track(screen: AnalyticsEvent.MobileScreen.ScreenName) {
|
||||
track(screen: screen, duration: nil)
|
||||
}
|
||||
|
||||
|
||||
func trackInteraction(name: AnalyticsEvent.Interaction.Name) {
|
||||
trackInteraction(index: nil, name: name)
|
||||
}
|
||||
|
||||
|
||||
@_disfavoredOverload // make sure this doesn't cause infinite recursion
|
||||
func trackError(context: String?,
|
||||
domain: AnalyticsEvent.Error.Domain,
|
||||
@@ -146,7 +146,7 @@ extension AnalyticsServiceProtocol {
|
||||
userTrustsOwnIdentity: userTrustsOwnIdentity,
|
||||
wasVisibleToUser: wasVisibleToUser)
|
||||
}
|
||||
|
||||
|
||||
func trackComposer(inThread: Bool,
|
||||
isEditing: Bool,
|
||||
isReply: Bool,
|
||||
|
||||
@@ -49,7 +49,7 @@ class AudioPlayer: NSObject, AudioPlayerProtocol {
|
||||
|
||||
private(set) var playbackURL: URL?
|
||||
private(set) var playbackSpeed: Float = 1.0
|
||||
|
||||
|
||||
private var deinitInProgress = false
|
||||
|
||||
var duration: TimeInterval {
|
||||
@@ -129,14 +129,14 @@ class AudioPlayer: NSObject, AudioPlayerProtocol {
|
||||
let time = progress * duration
|
||||
await internalAudioPlayer.seek(to: CMTime(seconds: time, preferredTimescale: 60))
|
||||
}
|
||||
|
||||
|
||||
func setPlaybackSpeed(_ speed: Float) {
|
||||
playbackSpeed = speed
|
||||
if state == .playing {
|
||||
internalAudioPlayer?.rate = speed
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setupAudioSession() {
|
||||
@@ -178,7 +178,7 @@ class AudioPlayer: NSObject, AudioPlayerProtocol {
|
||||
playerItem = nil
|
||||
removeObservers()
|
||||
}
|
||||
|
||||
|
||||
private func addObservers() {
|
||||
guard let internalAudioPlayer, let playerItem else {
|
||||
return
|
||||
@@ -197,7 +197,7 @@ class AudioPlayer: NSObject, AudioPlayerProtocol {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
rateObserver = internalAudioPlayer.observe(\.rate, options: [.old, .new]) { [weak self] _, _ in
|
||||
guard let self else { return }
|
||||
|
||||
@@ -211,7 +211,7 @@ class AudioPlayer: NSObject, AudioPlayerProtocol {
|
||||
setInternalState(.playing)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
NotificationCenter.default.publisher(for: Notification.Name.AVPlayerItemDidPlayToEndTime)
|
||||
.sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
|
||||
@@ -50,7 +50,7 @@ class AudioPlayerState: ObservableObject, Identifiable {
|
||||
var showProgressIndicator: Bool {
|
||||
progress > 0
|
||||
}
|
||||
|
||||
|
||||
var isAttached: Bool {
|
||||
audioPlayer != nil
|
||||
}
|
||||
@@ -124,7 +124,7 @@ class AudioPlayerState: ObservableObject, Identifiable {
|
||||
playbackSpeed = speed
|
||||
audioPlayer?.setPlaybackSpeed(speed.rawValue)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func subscribeToAudioPlayer(audioPlayer: AudioPlayerProtocol) {
|
||||
@@ -246,7 +246,7 @@ class AudioPlayerState: ObservableObject, Identifiable {
|
||||
}
|
||||
|
||||
audioPlayer.pause()
|
||||
|
||||
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class AudioRecorder: AudioRecorderProtocol {
|
||||
private let maximumRecordingTime: TimeInterval = 1800 // 30 minutes
|
||||
private let silenceThreshold: Float = -50.0
|
||||
private var meterLevel: Float = 0
|
||||
|
||||
|
||||
private(set) var audioFileURL: URL?
|
||||
var currentTime: TimeInterval = .zero
|
||||
var isRecording: Bool {
|
||||
@@ -94,7 +94,7 @@ class AudioRecorder: AudioRecorderProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func averagePower() -> Float {
|
||||
meterLevel
|
||||
}
|
||||
@@ -161,7 +161,7 @@ class AudioRecorder: AudioRecorderProtocol {
|
||||
// Initialize a new audio engine
|
||||
let audioEngine = AVAudioEngine()
|
||||
self.audioEngine = audioEngine
|
||||
|
||||
|
||||
let inputNode = audioEngine.inputNode
|
||||
let inputFormat = inputNode.outputFormat(forBus: 0)
|
||||
let hardwareSampleRate = audioEngine.inputNode.outputFormat(forBus: 0).sampleRate
|
||||
@@ -204,12 +204,12 @@ class AudioRecorder: AudioRecorderProtocol {
|
||||
} else {
|
||||
audioConverter = nil
|
||||
}
|
||||
|
||||
|
||||
// Install tap to process audio buffers coming from the mixer
|
||||
mixer.installTap(onBus: 0, bufferSize: 1024, format: mixerFormat) { [weak self] buffer, _ in
|
||||
self?.processAudioBuffer(buffer)
|
||||
}
|
||||
|
||||
|
||||
do {
|
||||
try audioEngine.start()
|
||||
completion(.success(()))
|
||||
@@ -304,7 +304,7 @@ class AudioRecorder: AudioRecorderProtocol {
|
||||
// Write the buffer into the audio file
|
||||
do {
|
||||
try audioFile.write(from: inputBuffer)
|
||||
|
||||
|
||||
// Compute the sample value for the waveform
|
||||
updateMeterLevel(inputBuffer)
|
||||
|
||||
@@ -366,10 +366,10 @@ class AudioRecorder: AudioRecorderProtocol {
|
||||
setInternalState(.suspended)
|
||||
case .ended:
|
||||
MXLog.info("Interruption ended: \(notification)")
|
||||
|
||||
|
||||
guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
|
||||
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
|
||||
|
||||
|
||||
if options.contains(.shouldResume) {
|
||||
do {
|
||||
try audioEngine?.start()
|
||||
@@ -413,7 +413,7 @@ class AudioRecorder: AudioRecorderProtocol {
|
||||
actionsSubject.send(.didStopRecording)
|
||||
case .error(let error):
|
||||
cleanupAudioEngine()
|
||||
|
||||
|
||||
actionsSubject.send(.didFailWithError(error: error))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> {
|
||||
homeserverSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
|
||||
private(set) var flow: AuthenticationFlow
|
||||
|
||||
let classicAppAccount: ClassicAppAccount?
|
||||
@@ -195,7 +195,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
progressSubject.send(completion: .failure(.qrCodeError(.deviceNotSignedIn)))
|
||||
return progressSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
|
||||
// n.b. We deliberatley don't check whether the received server is in our appSettings.accountProviders
|
||||
|
||||
let listener = SDKListener { progress in
|
||||
@@ -382,7 +382,7 @@ extension AuthenticationService {
|
||||
static var mock: AuthenticationService {
|
||||
mock(classicAppManager: nil)
|
||||
}
|
||||
|
||||
|
||||
static func mock(classicAppManager: ClassicAppManagerProtocol?) -> AuthenticationService {
|
||||
AuthenticationService(userSessionStore: UserSessionStoreMock(.init()),
|
||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||
|
||||
@@ -43,7 +43,7 @@ protocol AuthenticationServiceProtocol: QRCodeLoginServiceProtocol {
|
||||
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { get }
|
||||
/// The type of flow the service is currently configured with.
|
||||
var flow: AuthenticationFlow { get }
|
||||
|
||||
|
||||
/// Sets up the service for login on the specified homeserver address.
|
||||
func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result<Void, AuthenticationServiceError>
|
||||
/// Performs login using OAuth for the current homeserver.
|
||||
|
||||
@@ -25,7 +25,7 @@ class BugReportService: NSObject, BugReportServiceProtocol {
|
||||
var isEnabled: Bool {
|
||||
rageshakeURL != .disabled
|
||||
}
|
||||
|
||||
|
||||
var lastCrashEventID: String?
|
||||
|
||||
init(rageshakeURLPublisher: CurrentValuePublisher<RageshakeConfiguration, Never>,
|
||||
@@ -45,7 +45,7 @@ class BugReportService: NSObject, BugReportServiceProtocol {
|
||||
.weakAssign(to: \.rageshakeURL, on: self)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - BugReportServiceProtocol
|
||||
|
||||
var crashedLastRun: Bool {
|
||||
@@ -110,7 +110,7 @@ class BugReportService: NSObject, BugReportServiceProtocol {
|
||||
for url in bugReport.files {
|
||||
params.append(MultipartFormData(key: "file", type: .file(url: url)))
|
||||
}
|
||||
|
||||
|
||||
let boundary = "Boundary-\(UUID().uuidString)"
|
||||
var body = Data()
|
||||
for param in params {
|
||||
@@ -122,13 +122,13 @@ class BugReportService: NSObject, BugReportServiceProtocol {
|
||||
}
|
||||
}
|
||||
body.appendString(string: "--\(boundary)--\r\n")
|
||||
|
||||
|
||||
var request = URLRequest(url: rageshakeURL)
|
||||
request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = body as Data
|
||||
|
||||
|
||||
progressSubject
|
||||
.receive(on: DispatchQueue.main)
|
||||
.weakAssign(to: \.value, on: progressListener)
|
||||
@@ -164,9 +164,9 @@ class BugReportService: NSObject, BugReportServiceProtocol {
|
||||
return .failure(.uploadFailure(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
private var defaultParams: [MultipartFormData] {
|
||||
let (localTime, utcTime) = localAndUTCTime(for: Date())
|
||||
let version = "\(InfoPlistReader.main.bundleShortVersionString) (\(InfoPlistReader.main.bundleVersion))"
|
||||
@@ -185,7 +185,7 @@ class BugReportService: NSObject, BugReportServiceProtocol {
|
||||
MultipartFormData(key: "base_bundle_identifier", type: .text(value: InfoPlistReader.main.baseBundleIdentifier))
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
private func localAndUTCTime(for date: Date) -> (String, String) {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
@@ -194,7 +194,7 @@ class BugReportService: NSObject, BugReportServiceProtocol {
|
||||
let utcTime = dateFormatter.string(from: date)
|
||||
return (localTime, utcTime)
|
||||
}
|
||||
|
||||
|
||||
private var os: String {
|
||||
if ProcessInfo.processInfo.isiOSAppOnMac {
|
||||
// The other APIs report macOS's equivalent iOS version, so lets use the right one to get the macOS version.
|
||||
@@ -203,7 +203,7 @@ class BugReportService: NSObject, BugReportServiceProtocol {
|
||||
"\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func zipFiles(_ logFiles: [URL]) async -> Logs {
|
||||
MXLog.info("zipFiles")
|
||||
|
||||
@@ -219,7 +219,7 @@ class BugReportService: NSObject, BugReportServiceProtocol {
|
||||
}
|
||||
|
||||
MXLog.info("zipFiles: originalSize: \(compressedLogs.originalSize), zippedSize: \(compressedLogs.zippedSize)")
|
||||
|
||||
|
||||
return compressedLogs
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@ private extension Data {
|
||||
append(data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mutating func appendParam(_ param: MultipartFormData, boundary: String) throws {
|
||||
appendString(string: "--\(boundary)\r\n")
|
||||
appendString(string: "Content-Disposition:form-data; name=\"\(param.key)\"")
|
||||
|
||||
@@ -27,7 +27,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
private var roomListStateUpdateTaskHandle: TaskHandle?
|
||||
// periphery: ignore - only for retain
|
||||
private var roomListStateLoadingStateUpdateTaskHandle: TaskHandle?
|
||||
|
||||
|
||||
private var syncService: SyncService
|
||||
// periphery: ignore - only for retain
|
||||
private var syncServiceStateUpdateTaskHandle: TaskHandle?
|
||||
@@ -46,7 +46,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
// periphery:ignore - required for instance retention in the rust codebase
|
||||
private var mediaPreviewConfigListenerTaskHandle: TaskHandle?
|
||||
|
||||
|
||||
// periphery:ignore - required for instance retention in the rust codebase
|
||||
private var liveLocationOwnInfoUpdatesListenerTaskHandle: TaskHandle?
|
||||
|
||||
@@ -60,7 +60,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
private(set) var staticRoomSummaryProvider: StaticRoomSummaryProviderProtocol
|
||||
|
||||
let notificationSettings: NotificationSettingsProxyProtocol
|
||||
|
||||
|
||||
let secureBackupController: SecureBackupControllerProtocol
|
||||
|
||||
private(set) var sessionVerificationController: SessionVerificationControllerProxyProtocol?
|
||||
@@ -128,7 +128,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
users: [:],
|
||||
events: [:])
|
||||
}
|
||||
|
||||
|
||||
private var loadCachedAvatarURLTask: Task<Void, Never>?
|
||||
private let userAvatarURLSubject = CurrentValueSubject<URL?, Never>(nil)
|
||||
var userAvatarURLPublisher: CurrentValuePublisher<URL?, Never> {
|
||||
@@ -191,12 +191,12 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
|
||||
var roomsToAwait: Set<String> = []
|
||||
|
||||
|
||||
private let liveLocationOwnInfoUpdatesSubject = PassthroughSubject<LiveLocationOwnInfoUpdate, Never>()
|
||||
var liveLocationOwnInfoUpdatesPublisher: AnyPublisher<LiveLocationOwnInfoUpdate, Never> {
|
||||
liveLocationOwnInfoUpdatesSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
private let sendQueueStatusSubject = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
init(client: ClientProtocol,
|
||||
@@ -240,7 +240,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
syncServiceStateUpdateTaskHandle = createSyncServiceStateObserver(syncService)
|
||||
roomListStateUpdateTaskHandle = createRoomListServiceObserver(roomListService)
|
||||
roomListStateLoadingStateUpdateTaskHandle = createRoomListLoadingStateUpdateObserver(roomListService)
|
||||
|
||||
|
||||
delegateHandle = try client.setDelegate(delegate: ClientDelegateWrapper { [weak self] isSoftLogout in
|
||||
self?.hasEncounteredAuthError = true
|
||||
self?.actionsSubject.send(.receivedAuthError(isSoftLogout: isSoftLogout))
|
||||
@@ -278,7 +278,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
Task {
|
||||
mediaPreviewConfigListenerTaskHandle = await createMediaPreviewConfigObserver()
|
||||
}
|
||||
|
||||
|
||||
liveLocationOwnInfoUpdatesListenerTaskHandle = createLiveLocationOwnInfoUpdatesObserver()
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
return "Unknown user identifier"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var deviceID: String? {
|
||||
do {
|
||||
return try client.deviceId()
|
||||
@@ -299,7 +299,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var homeserver: String {
|
||||
client.homeserver()
|
||||
}
|
||||
@@ -360,7 +360,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private(set) lazy var pusherNotificationClientIdentifier: String? = {
|
||||
// NOTE: The result is stored as part of the restoration token. Any changes
|
||||
// here would require a migration to correctly match incoming notifications.
|
||||
@@ -378,7 +378,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func hasDevicesToVerifyAgainst() async -> Result<Bool, ClientProxyError> {
|
||||
do {
|
||||
let result = try await client.encryption().hasDevicesToVerifyAgainst()
|
||||
@@ -388,7 +388,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func startSync() {
|
||||
guard !hasEncounteredAuthError else {
|
||||
MXLog.warning("Ignoring request, this client has an unknown token.")
|
||||
@@ -566,7 +566,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
func joinRoom(_ roomID: String, via: [String]) async -> Result<Void, ClientProxyError> {
|
||||
do {
|
||||
let _ = try await client.joinRoomByIdOrAlias(roomIdOrAlias: roomID, serverNames: via)
|
||||
|
||||
|
||||
await waitForRoomToSync(roomID: roomID, timeout: .seconds(30))
|
||||
|
||||
return .success(())
|
||||
@@ -652,7 +652,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
return .failure(ClientProxyError.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func roomForIdentifier(_ identifier: String) async -> RoomProxyType? {
|
||||
let shouldAwait = roomsToAwait.remove(identifier) != nil
|
||||
|
||||
@@ -706,7 +706,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func loadUserDisplayName() async -> Result<Void, ClientProxyError> {
|
||||
do {
|
||||
let displayName = try await client.displayName()
|
||||
@@ -730,7 +730,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func loadUserAvatarURL() async -> Result<Void, ClientProxyError> {
|
||||
do {
|
||||
let urlString = try await client.avatarUrl()
|
||||
@@ -748,7 +748,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
MXLog.error("Failed uploading, invalid media: \(media)")
|
||||
return .failure(.invalidMedia)
|
||||
}
|
||||
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: imageURL)
|
||||
try await client.uploadAvatar(mimeType: mimeType, data: data)
|
||||
@@ -875,7 +875,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func markAllRoomsAsRead() async -> Result<Void, ClientProxyError> {
|
||||
do {
|
||||
return try await .success(client.markAllRoomsAsRead())
|
||||
@@ -903,7 +903,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Ignored users
|
||||
|
||||
func ignoreUser(_ userID: String) async -> Result<Void, ClientProxyError> {
|
||||
@@ -1123,7 +1123,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
MXLog.error("Failed retrieving session verification controller proxy with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func loadUserAvatarURLFromCache() {
|
||||
loadCachedAvatarURLTask = Task {
|
||||
do {
|
||||
@@ -1172,7 +1172,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func createLiveLocationOwnInfoUpdatesObserver() -> TaskHandle? {
|
||||
do {
|
||||
return try client.subscribeToOwnBeaconInfoUpdates(listener: SDKListener { [weak self] update in
|
||||
@@ -1187,7 +1187,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func createRoomListServiceObserver(_ roomListService: RoomListService) -> TaskHandle {
|
||||
roomListService.state(listener: SDKListener { [weak self] state in
|
||||
guard let self else { return }
|
||||
@@ -1272,7 +1272,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
MXLog.info("Wait for \(roomID) failed: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func updateIgnoredUsers() {
|
||||
Task {
|
||||
do {
|
||||
@@ -1355,7 +1355,7 @@ private final class ClientDelegateWrapper: ClientDelegate {
|
||||
}
|
||||
|
||||
// MARK: - ClientDelegate
|
||||
|
||||
|
||||
func didReceiveAuthError(isSoftLogout: Bool) {
|
||||
MXLog.error("Received authentication error, softlogout=\(isSoftLogout)")
|
||||
authErrorCallback(isSoftLogout)
|
||||
|
||||
@@ -111,9 +111,9 @@ protocol ClientProxyProtocol: AnyObject {
|
||||
var homeserverReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never> { get }
|
||||
|
||||
var userID: String { get }
|
||||
|
||||
|
||||
var deviceID: String? { get }
|
||||
|
||||
|
||||
var homeserver: String { get }
|
||||
|
||||
var canDeactivateAccount: Bool { get }
|
||||
@@ -121,9 +121,9 @@ protocol ClientProxyProtocol: AnyObject {
|
||||
var userIDServerName: String? { get }
|
||||
|
||||
var userDisplayNamePublisher: CurrentValuePublisher<String?, Never> { get }
|
||||
|
||||
|
||||
var userAvatarURLPublisher: CurrentValuePublisher<URL?, Never> { get }
|
||||
|
||||
|
||||
/// We delay fetching this until after the first sync. Nil until then
|
||||
var ignoredUsersPublisher: CurrentValuePublisher<[String]?, Never> { get }
|
||||
|
||||
@@ -170,13 +170,13 @@ protocol ClientProxyProtocol: AnyObject {
|
||||
func hasDevicesToVerifyAgainst() async -> Result<Bool, ClientProxyError>
|
||||
|
||||
func startSync()
|
||||
|
||||
|
||||
func stopSync()
|
||||
|
||||
func stopSync(completion: (() -> Void)?) // Hopefully this will become async once we get SE-0371.
|
||||
|
||||
func expireSyncSessions() async
|
||||
|
||||
|
||||
func accountURL(action: AccountManagementAction) async -> URL?
|
||||
|
||||
func directRoomForUserID(_ userID: String) -> Result<String?, ClientProxyError>
|
||||
@@ -217,7 +217,7 @@ protocol ClientProxyProtocol: AnyObject {
|
||||
@discardableResult func loadUserDisplayName() async -> Result<Void, ClientProxyError>
|
||||
|
||||
func setUserDisplayName(_ name: String) async -> Result<Void, ClientProxyError>
|
||||
|
||||
|
||||
@discardableResult func loadUserAvatarURL() async -> Result<Void, ClientProxyError>
|
||||
|
||||
func setUserAvatar(media: MediaInfo) async -> Result<Void, ClientProxyError>
|
||||
@@ -229,7 +229,7 @@ protocol ClientProxyProtocol: AnyObject {
|
||||
func deactivateAccount(password: String?, eraseData: Bool) async -> Result<Void, ClientProxyError>
|
||||
|
||||
func logout() async
|
||||
|
||||
|
||||
func setPusher(with configuration: PusherConfiguration) async throws
|
||||
|
||||
func searchUsers(searchTerm: String, limit: UInt) async -> Result<SearchUsersResultsProxy, ClientProxyError>
|
||||
@@ -245,13 +245,13 @@ protocol ClientProxyProtocol: AnyObject {
|
||||
@discardableResult func clearCaches() async -> Result<Void, ClientProxyError>
|
||||
|
||||
@discardableResult func optimizeStores() async -> Result<Void, ClientProxyError>
|
||||
|
||||
|
||||
@discardableResult func markAllRoomsAsRead() async -> Result<Void, ClientProxyError>
|
||||
|
||||
func storeSizes() async -> Result<StoreSizes, ClientProxyError>
|
||||
|
||||
func fetchMediaPreviewConfiguration() async -> Result<MediaPreviewConfig?, ClientProxyError>
|
||||
|
||||
|
||||
// MARK: - Ignored users
|
||||
|
||||
func ignoreUser(_ userID: String) async -> Result<Void, ClientProxyError>
|
||||
@@ -277,10 +277,10 @@ protocol ClientProxyProtocol: AnyObject {
|
||||
func userIdentity(for userID: String, fallBackToServer: Bool) async -> Result<UserIdentityProxyProtocol?, ClientProxyError>
|
||||
|
||||
// MARK: - Live Location
|
||||
|
||||
|
||||
/// Publishes updates about the current user's own live location beacon info state changes (start/stop) as echoed by the server.
|
||||
var liveLocationOwnInfoUpdatesPublisher: AnyPublisher<LiveLocationOwnInfoUpdate, Never> { get }
|
||||
|
||||
|
||||
// MARK: - Moderation & Safety
|
||||
|
||||
func setTimelineMediaVisibility(_ value: TimelineMediaVisibility) async -> Result<Void, ClientProxyError>
|
||||
|
||||
@@ -154,7 +154,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - PKPushRegistryDelegate
|
||||
|
||||
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { }
|
||||
|
||||
@@ -12,7 +12,7 @@ struct EmojiItem: Equatable, Identifiable {
|
||||
var id: String {
|
||||
label
|
||||
}
|
||||
|
||||
|
||||
let label: String
|
||||
let unicode: String
|
||||
let keywords: [String]
|
||||
|
||||
@@ -13,7 +13,7 @@ import MatrixRustSDK
|
||||
enum KeychainControllerService: String {
|
||||
case sessions
|
||||
case tests
|
||||
|
||||
|
||||
var restorationTokenID: String {
|
||||
InfoPlistReader.main.baseBundleIdentifier + "." + rawValue
|
||||
}
|
||||
@@ -33,14 +33,14 @@ final class KeychainController: KeychainControllerProtocol {
|
||||
case appLockPINCode
|
||||
case appLockBiometricState
|
||||
}
|
||||
|
||||
|
||||
init(service: KeychainControllerService, accessGroup: String) {
|
||||
restorationTokenKeychain = Keychain(service: service.restorationTokenID, accessGroup: accessGroup)
|
||||
mainKeychain = Keychain(service: service.mainID, accessGroup: accessGroup)
|
||||
}
|
||||
|
||||
// MARK: - Restoration Tokens
|
||||
|
||||
|
||||
func setRestorationToken(_ restorationToken: RestorationToken, forUsername username: String) {
|
||||
do {
|
||||
let tokenData = try JSONEncoder().encode(restorationToken)
|
||||
@@ -49,7 +49,7 @@ final class KeychainController: KeychainControllerProtocol {
|
||||
MXLog.error("Failed storing user restore token with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func restorationTokenForUsername(_ username: String) -> RestorationToken? {
|
||||
do {
|
||||
guard let tokenData = try restorationTokenKeychain.getData(username) else {
|
||||
@@ -66,17 +66,17 @@ final class KeychainController: KeychainControllerProtocol {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func restorationTokens() -> [KeychainCredentials] {
|
||||
restorationTokenKeychain.allKeys().compactMap { username in
|
||||
guard let restorationToken = restorationTokenForUsername(username) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
return KeychainCredentials(userID: username, restorationToken: restorationToken)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func removeRestorationTokenForUsername(_ username: String) {
|
||||
MXLog.warning("Removing restoration token for user: \(username).")
|
||||
|
||||
@@ -86,7 +86,7 @@ final class KeychainController: KeychainControllerProtocol {
|
||||
MXLog.error("Failed removing restore token with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func removeAllRestorationTokens() {
|
||||
MXLog.warning("Removing all user restoration tokens.")
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ extension KeychainControllerMock {
|
||||
func retrieveSessionFromKeychain(userId: String) throws -> Session {
|
||||
fatalError("Not implemented")
|
||||
}
|
||||
|
||||
|
||||
func saveSessionInKeychain(session: Session) {
|
||||
fatalError("Not implemented")
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
||||
.sink { [weak self] sessions in
|
||||
guard let self else { return }
|
||||
syncActiveRoomProxies(with: sessions)
|
||||
|
||||
|
||||
if sessions.isEmpty {
|
||||
self.stopUpdatingLocation()
|
||||
} else {
|
||||
|
||||
@@ -20,7 +20,7 @@ protocol LiveLocationManagerProtocol: AnyObject {
|
||||
var hasDisplayedLiveLocationDisclaimer: Bool { get set }
|
||||
/// Publishes the current location authorization status.
|
||||
var authorizationStatus: CurrentValuePublisher<CLAuthorizationStatus, Never> { get }
|
||||
|
||||
|
||||
/// Requests "Always" location authorization from the user if the system allows it.
|
||||
///
|
||||
/// - Returns: `true` if the request was forwarded to the system and a prompt will be shown;
|
||||
|
||||
@@ -19,7 +19,7 @@ final class RoomLiveLocationService: RoomLiveLocationServiceProtocol {
|
||||
var liveLocationsPublisher: CurrentValuePublisher<[LiveLocationShare], Never> {
|
||||
liveLocationsSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
|
||||
init(liveLocationsObserver: LiveLocationsObserver) {
|
||||
self.liveLocationsObserver = liveLocationsObserver
|
||||
observationToken = liveLocationsObserver
|
||||
|
||||
@@ -358,7 +358,7 @@ struct MediaUploadingPreprocessor {
|
||||
|
||||
return .init(url: thumbnailURL, height: thumbnail.size.height, width: thumbnail.size.width, mimeType: "image/jpeg", blurhash: blurhash)
|
||||
}
|
||||
|
||||
|
||||
private func resizeImage(at url: URL, maxPixelSize: CGFloat, destination: URL, type: UTType) throws(MediaUploadingPreprocessorError) {
|
||||
guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil) else {
|
||||
throw .failedResizingImage
|
||||
@@ -440,7 +440,7 @@ struct MediaUploadingPreprocessor {
|
||||
private func convertVideoToMP4(_ url: URL, targetFileSize: UInt) async throws(MediaUploadingPreprocessorError) -> VideoProcessingInfo {
|
||||
let asset = AVURLAsset(url: url)
|
||||
let presetName = appSettings.optimizeMediaUploads ? AVAssetExportPreset1280x720 : AVAssetExportPreset1920x1080
|
||||
|
||||
|
||||
guard let exportSession = AVAssetExportSession(asset: asset, presetName: presetName) else {
|
||||
throw .failedConvertingVideo
|
||||
}
|
||||
@@ -524,11 +524,11 @@ private extension AVAssetTrack {
|
||||
guard mediaType == .video else {
|
||||
return naturalSize
|
||||
}
|
||||
|
||||
|
||||
// The naturalSize does not take the preferredTransform into consideration resulting
|
||||
// in portrait videos reporting inverted values.
|
||||
let transform = try await load(.preferredTransform)
|
||||
|
||||
|
||||
switch (transform.a, transform.b, transform.c, transform.d) {
|
||||
case (0, 1, -1, 0), (0, -1, 1, 0):
|
||||
return CGSize(width: naturalSize.height, height: naturalSize.width)
|
||||
|
||||
@@ -29,7 +29,7 @@ actor MediaLoader: MediaLoaderProtocol {
|
||||
// don't have an accompanying ClientMock to own it.
|
||||
private weak var client: ClientProtocol?
|
||||
private var ongoingRequests = [MediaSourceProxy: MediaRequest]()
|
||||
|
||||
|
||||
init(client: ClientProtocol) {
|
||||
self.client = client
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ enum MediaLoaderError: Error {
|
||||
// sourcery: AutoMockable
|
||||
protocol MediaLoaderProtocol: Sendable {
|
||||
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data
|
||||
|
||||
|
||||
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data
|
||||
|
||||
func loadMediaFileForSource(_ source: MediaSourceProxy, filename: String?) async throws -> MediaFileHandleProxy
|
||||
|
||||
@@ -44,7 +44,7 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
let image = cacheResult.image {
|
||||
return .success(image)
|
||||
}
|
||||
|
||||
|
||||
do {
|
||||
let imageData: Data
|
||||
if let size {
|
||||
@@ -52,14 +52,14 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
} else {
|
||||
imageData = try await mediaLoader.loadMediaContentForSource(source)
|
||||
}
|
||||
|
||||
|
||||
guard let image = UIImage(data: imageData) else {
|
||||
MXLog.error("Invalid image data")
|
||||
return .failure(.invalidImageData)
|
||||
}
|
||||
|
||||
|
||||
try await imageCache.store(image, forKey: cacheKey)
|
||||
|
||||
|
||||
return .success(image)
|
||||
} catch {
|
||||
MXLog.error("Failed retrieving image with error: \(error)")
|
||||
|
||||
@@ -11,7 +11,7 @@ import Foundation
|
||||
struct APSAlert: Encodable {
|
||||
let locKey: String
|
||||
let locArgs: [String]
|
||||
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case locKey = "loc-key"
|
||||
case locArgs = "loc-args"
|
||||
@@ -21,7 +21,7 @@ struct APSAlert: Encodable {
|
||||
struct APSInfo: Encodable {
|
||||
let mutableContent: Int
|
||||
let alert: APSAlert
|
||||
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case mutableContent = "mutable-content"
|
||||
case alert
|
||||
@@ -31,7 +31,7 @@ struct APSInfo: Encodable {
|
||||
struct APNSPayload: Encodable {
|
||||
let aps: APSInfo
|
||||
let pusherNotificationClientIdentifier: String?
|
||||
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case aps
|
||||
case pusherNotificationClientIdentifier = "pusher_notification_client_identifier"
|
||||
|
||||
@@ -26,9 +26,9 @@ final class NotificationManager: NSObject, NotificationManagerProtocol {
|
||||
self.appSettings = appSettings
|
||||
super.init()
|
||||
}
|
||||
|
||||
|
||||
// MARK: NotificationManagerProtocol
|
||||
|
||||
|
||||
weak var delegate: NotificationManagerDelegate?
|
||||
|
||||
func start() {
|
||||
@@ -80,14 +80,14 @@ final class NotificationManager: NSObject, NotificationManagerProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func register(with deviceToken: Data) async -> Bool {
|
||||
guard let userSession else {
|
||||
return false
|
||||
}
|
||||
return await setPusher(with: deviceToken, clientProxy: userSession.clientProxy)
|
||||
}
|
||||
|
||||
|
||||
func setUserSession(_ userSession: UserSessionProtocol?) {
|
||||
self.userSession = userSession
|
||||
|
||||
@@ -106,11 +106,11 @@ final class NotificationManager: NSObject, NotificationManagerProtocol {
|
||||
MXLog.info("Notification sound enabled: \(settings.soundSetting == .enabled)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func registrationFailed(with error: Error) {
|
||||
MXLog.error("Device token registration failed with error: \(error)")
|
||||
}
|
||||
|
||||
|
||||
func showLocalNotification(with title: String, subtitle: String?) async {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = title
|
||||
@@ -150,7 +150,7 @@ final class NotificationManager: NSObject, NotificationManagerProtocol {
|
||||
let lastMessageDate = roomsToLastMessageDates[roomID] else {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
return notification.date <= lastMessageDate
|
||||
}
|
||||
.map(\.request.identifier)
|
||||
@@ -161,14 +161,14 @@ final class NotificationManager: NSObject, NotificationManagerProtocol {
|
||||
private func removeReceivedWhileOfflineNotification() {
|
||||
notificationCenter.removeDeliveredNotifications(withIdentifiers: [NotificationServiceExtensionActor.receivedWhileOfflineNotificationID])
|
||||
}
|
||||
|
||||
|
||||
private func setPusher(with deviceToken: Data, clientProxy: ClientProxyProtocol) async -> Bool {
|
||||
do {
|
||||
let defaultPayload = APNSPayload(aps: APSInfo(mutableContent: 1,
|
||||
alert: APSAlert(locKey: "Notification",
|
||||
locArgs: [])),
|
||||
pusherNotificationClientIdentifier: clientProxy.pusherNotificationClientIdentifier)
|
||||
|
||||
|
||||
let configuration = try await PusherConfiguration(identifiers: .init(pushkey: deviceToken.base64EncodedString(),
|
||||
appId: appSettings.pusherAppID),
|
||||
kind: .http(data: .init(url: appSettings.pushGatewayNotifyEndpoint.absoluteString,
|
||||
@@ -186,7 +186,7 @@ final class NotificationManager: NSObject, NotificationManagerProtocol {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func pusherProfileTag() -> String {
|
||||
if let currentTag = appSettings.pusherProfileTag {
|
||||
return currentTag
|
||||
@@ -196,7 +196,7 @@ final class NotificationManager: NSObject, NotificationManagerProtocol {
|
||||
let offset = Int.random(in: 0..<chars.count)
|
||||
return String(chars[chars.index(chars.startIndex, offsetBy: offset)])
|
||||
}.joined()
|
||||
|
||||
|
||||
appSettings.pusherProfileTag = newTag
|
||||
return newTag
|
||||
}
|
||||
@@ -224,14 +224,14 @@ extension NotificationManager: UNUserNotificationCenterDelegate {
|
||||
guard let delegate else {
|
||||
return [.badge, .sound, .list, .banner]
|
||||
}
|
||||
|
||||
|
||||
guard delegate.shouldDisplayInAppNotification(content: notification.request.content) else {
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
return [.badge, .sound, .list, .banner]
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter,
|
||||
didReceive response: UNNotificationResponse) async {
|
||||
|
||||
@@ -24,7 +24,7 @@ protocol NotificationManagerDelegate: AnyObject {
|
||||
// sourcery: AutoMockable
|
||||
protocol NotificationManagerProtocol: AnyObject {
|
||||
var delegate: NotificationManagerDelegate? { get set }
|
||||
|
||||
|
||||
func start()
|
||||
func register(with deviceToken: Data) async -> Bool
|
||||
func registrationFailed(with error: Error)
|
||||
|
||||
@@ -17,12 +17,12 @@ enum NotificationConstants {
|
||||
static let pusherNotificationClientIdentifier = "pusher_notification_client_identifier"
|
||||
static let receiverIdentifier = "receiver_id"
|
||||
}
|
||||
|
||||
|
||||
enum Category {
|
||||
static let message = "message"
|
||||
static let invite = "invite"
|
||||
}
|
||||
|
||||
|
||||
enum Action {
|
||||
static let inlineReply = "inline-reply"
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ final class NotificationSettingsProxy: NotificationSettingsProxyProtocol {
|
||||
private(set) var notificationSettings: MatrixRustSDK.NotificationSettingsProtocol
|
||||
|
||||
let callbacks = PassthroughSubject<NotificationSettingsProxyCallback, Never>()
|
||||
|
||||
|
||||
init(notificationSettings: MatrixRustSDK.NotificationSettingsProtocol) {
|
||||
self.notificationSettings = notificationSettings
|
||||
notificationSettings.setDelegate(delegate: WeakNotificationSettingsProxy(proxy: self))
|
||||
@@ -55,7 +55,7 @@ final class NotificationSettingsProxy: NotificationSettingsProxyProtocol {
|
||||
let roomNotificationMode = await notificationSettings.getDefaultRoomNotificationMode(isEncrypted: isEncrypted, isOneToOne: isOneToOne)
|
||||
return RoomNotificationModeProxy.from(roomNotificationMode: roomNotificationMode)
|
||||
}
|
||||
|
||||
|
||||
func setDefaultRoomNotificationMode(isEncrypted: Bool, isOneToOne: Bool, mode: RoomNotificationModeProxy) async throws {
|
||||
do {
|
||||
try await notificationSettings.setDefaultRoomNotificationMode(isEncrypted: isEncrypted, isOneToOne: isOneToOne, mode: mode.roomNotificationMode)
|
||||
@@ -65,7 +65,7 @@ final class NotificationSettingsProxy: NotificationSettingsProxyProtocol {
|
||||
MXLog.warning("Unable to find the rule: \(ruleId)")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
await updatedSettings()
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ final class NotificationSettingsProxy: NotificationSettingsProxyProtocol {
|
||||
try await notificationSettings.restoreDefaultRoomNotificationMode(roomId: roomId)
|
||||
await updatedSettings()
|
||||
}
|
||||
|
||||
|
||||
func unmuteRoom(roomId: String, isEncrypted: Bool, isOneToOne: Bool) async throws {
|
||||
try await notificationSettings.unmuteRoom(roomId: roomId, isEncrypted: isEncrypted, isOneToOne: isOneToOne)
|
||||
await updatedSettings()
|
||||
|
||||
@@ -16,7 +16,7 @@ class BannedRoomProxy: BannedRoomProxyProtocol {
|
||||
lazy var ownUserID = room.ownUserId()
|
||||
|
||||
let info: BaseRoomInfoProxyProtocol
|
||||
|
||||
|
||||
init(room: Room) async throws {
|
||||
self.room = room
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class InvitedRoomProxy: InvitedRoomProxyProtocol {
|
||||
|
||||
let info: BaseRoomInfoProxyProtocol
|
||||
let inviter: RoomMemberProxyProtocol?
|
||||
|
||||
|
||||
init(room: Room) async throws {
|
||||
self.room = room
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
||||
var infoPublisher: CurrentValuePublisher<RoomInfoProxyProtocol, Never> {
|
||||
infoSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
|
||||
private let membersSubject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
|
||||
var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> {
|
||||
membersSubject.asCurrentValuePublisher()
|
||||
@@ -119,7 +119,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
||||
}
|
||||
|
||||
subscribedForUpdates = true
|
||||
|
||||
|
||||
do {
|
||||
try await roomListService.subscribeToRooms(roomIds: [id])
|
||||
} catch {
|
||||
@@ -353,7 +353,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
||||
MXLog.error("Failed updating members using sync API: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func getMember(userID: String) async -> Result<RoomMemberProxyProtocol, RoomProxyError> {
|
||||
if let member = membersPublisher.value.filter({ $0.userID == userID }).first {
|
||||
return .success(member)
|
||||
@@ -396,7 +396,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func setTopic(_ topic: String) async -> Result<Void, RoomProxyError> {
|
||||
do {
|
||||
return try await .success(room.setTopic(topic: topic))
|
||||
@@ -420,7 +420,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
||||
MXLog.error("Failed uploading avatar, invalid media: \(media)")
|
||||
return .failure(.invalidMedia)
|
||||
}
|
||||
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: imageURL)
|
||||
return try await .success(room.uploadAvatar(mimeType: mimeType, data: data, mediaInfo: nil))
|
||||
@@ -790,7 +790,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func subscribeToTypingNotifications() {
|
||||
|
||||
@@ -16,7 +16,7 @@ class KnockedRoomProxy: KnockedRoomProxyProtocol {
|
||||
lazy var ownUserID: String = room.ownUserId()
|
||||
|
||||
let info: BaseRoomInfoProxyProtocol
|
||||
|
||||
|
||||
init(room: Room) async throws {
|
||||
self.room = room
|
||||
|
||||
|
||||
@@ -15,28 +15,28 @@ struct RoomInfoProxy: RoomInfoProxyProtocol {
|
||||
var id: String {
|
||||
roomInfo.id
|
||||
}
|
||||
|
||||
|
||||
var creators: [String] {
|
||||
roomInfo.creators ?? []
|
||||
}
|
||||
|
||||
|
||||
var displayName: String? {
|
||||
roomInfo.displayName
|
||||
}
|
||||
|
||||
|
||||
var rawName: String? {
|
||||
roomInfo.rawName
|
||||
}
|
||||
|
||||
|
||||
var topic: String? {
|
||||
roomInfo.topic
|
||||
}
|
||||
|
||||
|
||||
/// The room's avatar URL. Use this for editing and favour ``avatar`` for display.
|
||||
var avatarURL: URL? {
|
||||
roomInfo.avatarUrl.flatMap(URL.init)
|
||||
}
|
||||
|
||||
|
||||
/// Here we're assuming unknown rooms are unencrypted.
|
||||
/// Fortunately https://github.com/matrix-org/matrix-rust-sdk/pull/4778 makes that very much of an edge case and we
|
||||
/// also automatically start a `latestEncryptionState` fetch if needed.
|
||||
@@ -53,7 +53,7 @@ struct RoomInfoProxy: RoomInfoProxyProtocol {
|
||||
var isDM: Bool {
|
||||
roomInfo.isDm
|
||||
}
|
||||
|
||||
|
||||
var isSpace: Bool {
|
||||
roomInfo.isSpace
|
||||
}
|
||||
@@ -61,59 +61,59 @@ struct RoomInfoProxy: RoomInfoProxyProtocol {
|
||||
var successor: SuccessorRoom? {
|
||||
roomInfo.successorRoom
|
||||
}
|
||||
|
||||
|
||||
var isFavourite: Bool {
|
||||
roomInfo.isFavourite
|
||||
}
|
||||
|
||||
|
||||
var canonicalAlias: String? {
|
||||
roomInfo.canonicalAlias
|
||||
}
|
||||
|
||||
|
||||
var alternativeAliases: [String] {
|
||||
roomInfo.alternativeAliases
|
||||
}
|
||||
|
||||
|
||||
var membership: Membership {
|
||||
roomInfo.membership
|
||||
}
|
||||
|
||||
|
||||
var inviter: RoomMemberProxyProtocol? {
|
||||
roomInfo.inviter.map(RoomMemberProxy.init)
|
||||
}
|
||||
|
||||
|
||||
var heroes: [RoomHero] {
|
||||
roomInfo.heroes
|
||||
}
|
||||
|
||||
|
||||
var activeMembersCount: Int {
|
||||
Int(roomInfo.activeMembersCount)
|
||||
}
|
||||
|
||||
|
||||
var invitedMembersCount: Int {
|
||||
Int(roomInfo.invitedMembersCount)
|
||||
}
|
||||
|
||||
|
||||
var joinedMembersCount: Int {
|
||||
Int(roomInfo.joinedMembersCount)
|
||||
}
|
||||
|
||||
|
||||
var highlightCount: Int {
|
||||
Int(roomInfo.highlightCount)
|
||||
}
|
||||
|
||||
|
||||
var notificationCount: Int {
|
||||
Int(roomInfo.notificationCount)
|
||||
}
|
||||
|
||||
|
||||
var cachedUserDefinedNotificationMode: RoomNotificationMode? {
|
||||
roomInfo.cachedUserDefinedNotificationMode
|
||||
}
|
||||
|
||||
|
||||
var hasRoomCall: Bool {
|
||||
roomInfo.hasRoomCall
|
||||
}
|
||||
|
||||
|
||||
var activeRoomCallIntent: CallIntent? {
|
||||
switch roomInfo.activeRoomCallConsensusIntent {
|
||||
case .full(let intent):
|
||||
@@ -128,35 +128,35 @@ struct RoomInfoProxy: RoomInfoProxyProtocol {
|
||||
var activeRoomCallParticipants: [String] {
|
||||
roomInfo.activeRoomCallParticipants
|
||||
}
|
||||
|
||||
|
||||
var isMarkedUnread: Bool {
|
||||
roomInfo.isMarkedUnread
|
||||
}
|
||||
|
||||
|
||||
var unreadMessagesCount: UInt {
|
||||
UInt(roomInfo.numUnreadMessages)
|
||||
}
|
||||
|
||||
|
||||
var unreadNotificationsCount: UInt {
|
||||
UInt(roomInfo.numUnreadNotifications)
|
||||
}
|
||||
|
||||
|
||||
var unreadMentionsCount: UInt {
|
||||
UInt(roomInfo.numUnreadMentions)
|
||||
}
|
||||
|
||||
|
||||
var fullyReadEventID: String? {
|
||||
roomInfo.fullyReadEventId
|
||||
}
|
||||
|
||||
|
||||
var pinnedEventIDs: Set<String> {
|
||||
Set(roomInfo.pinnedEventIds)
|
||||
}
|
||||
|
||||
|
||||
var joinRule: JoinRule? {
|
||||
roomInfo.joinRule.map(JoinRule.init)
|
||||
}
|
||||
|
||||
|
||||
var historyVisibility: RoomHistoryVisibility {
|
||||
roomInfo.historyVisibility
|
||||
}
|
||||
@@ -174,39 +174,39 @@ struct RoomPreviewInfoProxy: BaseRoomInfoProxyProtocol {
|
||||
var id: String {
|
||||
roomPreviewInfo.roomId
|
||||
}
|
||||
|
||||
|
||||
var displayName: String? {
|
||||
roomPreviewInfo.name
|
||||
}
|
||||
|
||||
|
||||
var heroes: [RoomHero] {
|
||||
roomPreviewInfo.heroes ?? []
|
||||
}
|
||||
|
||||
|
||||
var topic: String? {
|
||||
roomPreviewInfo.topic
|
||||
}
|
||||
|
||||
|
||||
var canonicalAlias: String? {
|
||||
roomPreviewInfo.canonicalAlias
|
||||
}
|
||||
|
||||
|
||||
var avatarURL: URL? {
|
||||
roomPreviewInfo.avatarUrl.flatMap(URL.init)
|
||||
}
|
||||
|
||||
|
||||
var isDirect: Bool {
|
||||
roomPreviewInfo.isDirect ?? false
|
||||
}
|
||||
|
||||
|
||||
var isSpace: Bool {
|
||||
roomPreviewInfo.roomType == .space
|
||||
}
|
||||
|
||||
|
||||
var activeMembersCount: Int {
|
||||
Int(roomPreviewInfo.numActiveMembers ?? roomPreviewInfo.numJoinedMembers)
|
||||
}
|
||||
|
||||
|
||||
var joinedMembersCount: Int {
|
||||
Int(roomPreviewInfo.numJoinedMembers)
|
||||
}
|
||||
@@ -214,7 +214,7 @@ struct RoomPreviewInfoProxy: BaseRoomInfoProxyProtocol {
|
||||
var joinRule: JoinRule? {
|
||||
roomPreviewInfo.joinRule.map(JoinRule.init)
|
||||
}
|
||||
|
||||
|
||||
var membership: Membership? {
|
||||
roomPreviewInfo.membership
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ protocol RoomInfoProxyProtocol: BaseRoomInfoProxyProtocol {
|
||||
var topic: String? { get }
|
||||
/// The room's avatar URL. Use this for editing and favour ``avatar`` for display.
|
||||
var avatarURL: URL? { get }
|
||||
|
||||
|
||||
var isEncrypted: Bool { get }
|
||||
var isDirect: Bool { get }
|
||||
var isDM: Bool { get }
|
||||
|
||||
@@ -19,7 +19,7 @@ struct RoomMemberDetails: Identifiable, Hashable {
|
||||
var isIgnored: Bool
|
||||
var isBanned: Bool
|
||||
var isActive: Bool
|
||||
|
||||
|
||||
let role: RoomRole
|
||||
let powerLevel: RoomPowerLevel
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ struct RoomPermissionsSetting: Identifiable {
|
||||
value = newValue.powerLevelValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The `RoomPermissions` property that this setting is for.
|
||||
let keyPath: KeyPath<RoomPermissions, Int64>
|
||||
/// The `RoomPowerLevelChanges` properties that this setting is saved into.
|
||||
|
||||
@@ -12,7 +12,7 @@ import MatrixRustSDK
|
||||
protocol RoomPowerLevelsProxyProtocol {
|
||||
var values: RoomPowerLevelsValues { get }
|
||||
var userPowerLevels: [String: Int64] { get }
|
||||
|
||||
|
||||
func canOwnUser(sendMessage messageType: MessageLikeEventType) -> Bool
|
||||
func canOwnUser(sendStateEvent event: StateEventType) -> Bool
|
||||
func canOwnUserInvite() -> Bool
|
||||
|
||||
@@ -76,7 +76,7 @@ struct RTCDeclinedEvent {
|
||||
// sourcery: AutoMockable
|
||||
protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
|
||||
var infoPublisher: CurrentValuePublisher<RoomInfoProxyProtocol, Never> { get }
|
||||
|
||||
|
||||
var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { get }
|
||||
|
||||
var typingMembersPublisher: CurrentValuePublisher<[String], Never> { get }
|
||||
@@ -116,11 +116,11 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
|
||||
func reportContent(_ eventID: String, reason: String?) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func reportRoom(reason: String) async -> Result<Void, RoomProxyError>
|
||||
|
||||
|
||||
func leaveRoom() async -> Result<Void, RoomProxyError>
|
||||
|
||||
func updateMembers() async
|
||||
|
||||
|
||||
func getMember(userID: String) async -> Result<RoomMemberProxyProtocol, RoomProxyError>
|
||||
|
||||
func invite(userID: String) async -> Result<Void, RoomProxyError>
|
||||
@@ -222,7 +222,7 @@ extension JoinedRoomProxyProtocol {
|
||||
isDirect: infoPublisher.value.isDirect,
|
||||
historySharingState: historySharingState)
|
||||
}
|
||||
|
||||
|
||||
func members() async -> [RoomMemberProxyProtocol]? {
|
||||
await updateMembers()
|
||||
return membersPublisher.value
|
||||
|
||||
@@ -49,7 +49,7 @@ extension RoomRole {
|
||||
self = .user
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var isAdminOrHigher: Bool {
|
||||
switch self {
|
||||
case .administrator, .creator, .owner:
|
||||
|
||||
@@ -70,7 +70,7 @@ struct RoomMessageEventStringBuilder {
|
||||
case .other(_, let body):
|
||||
message = AttributedString(body)
|
||||
}
|
||||
|
||||
|
||||
if style == .senderPrefixed {
|
||||
return prefix(message, with: isOutgoing ? L10n.commonYou : senderDisplayName)
|
||||
} else {
|
||||
@@ -90,7 +90,7 @@ struct RoomMessageEventStringBuilder {
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func buildMessage(for style: Style, caption: String?, type: String) -> AttributedString {
|
||||
guard let caption else {
|
||||
return AttributedString(type)
|
||||
|
||||
@@ -31,7 +31,7 @@ struct RoomSummary {
|
||||
}
|
||||
|
||||
enum LastMessageState { case sending, failed }
|
||||
|
||||
|
||||
let room: Room
|
||||
|
||||
let id: String
|
||||
@@ -66,15 +66,15 @@ struct RoomSummary {
|
||||
var hasUnreadMessages: Bool {
|
||||
unreadMessagesCount > 0
|
||||
}
|
||||
|
||||
|
||||
var hasUnreadMentions: Bool {
|
||||
unreadMentionsCount > 0
|
||||
}
|
||||
|
||||
|
||||
var hasUnreadNotifications: Bool {
|
||||
unreadNotificationsCount > 0
|
||||
}
|
||||
|
||||
|
||||
var isMuted: Bool {
|
||||
notificationMode == .mute
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
private let shouldUpdateVisibleRange: Bool
|
||||
private let notificationSettings: NotificationSettingsProxyProtocol
|
||||
private let appSettings: AppSettings
|
||||
|
||||
|
||||
private let roomListPageSize: UInt32
|
||||
|
||||
private let serialDispatchQueue: DispatchQueue
|
||||
@@ -40,7 +40,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
var roomListPublisher: CurrentValuePublisher<[RoomSummary], Never> {
|
||||
roomListSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
|
||||
var statePublisher: CurrentValuePublisher<RoomSummaryProviderState, Never> {
|
||||
stateSubject.asCurrentValuePublisher()
|
||||
}
|
||||
@@ -233,7 +233,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
|
||||
return updatedItems
|
||||
}
|
||||
|
||||
|
||||
private func fetchRoomDetails(from room: Room) -> (roomInfo: RoomInfo?, latestEvent: LatestEventValue?) {
|
||||
class FetchResult {
|
||||
var roomInfo: RoomInfo?
|
||||
|
||||
@@ -76,7 +76,7 @@ class RoomThreadListProxy: RoomThreadListServiceProxyProtocol {
|
||||
|
||||
let sender = TimelineItemSender(senderID: threadListItemEvent.sender, senderProfile: threadListItemEvent.senderProfile)
|
||||
let timestamp = Date(timeIntervalSince1970: TimeInterval(threadListItemEvent.timestamp / 1000))
|
||||
|
||||
|
||||
let message: AttributedString? = if let content = threadListItemEvent.content {
|
||||
eventStringBuilder.buildAttributedString(for: content,
|
||||
sender: sender,
|
||||
|
||||
@@ -154,7 +154,7 @@ class SecureBackupController: SecureBackupControllerProtocol {
|
||||
return .failure(.failedConfirmingRecoveryKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func waitForKeyBackupUpload(uploadStateSubject: CurrentValueSubject<SecureBackupSteadyState, Never>) async -> Result<Void, SecureBackupControllerError> {
|
||||
do {
|
||||
MXLog.info("Waiting for backup upload steady state")
|
||||
|
||||
@@ -42,7 +42,7 @@ enum SecureBackupControllerError: Error {
|
||||
|
||||
case failedGeneratingRecoveryKey
|
||||
case failedConfirmingRecoveryKey
|
||||
|
||||
|
||||
case failedUploadingForBackup
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ class UserSession: UserSessionProtocol {
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private var authErrorCancellable: AnyCancellable?
|
||||
|
||||
|
||||
let clientProxy: ClientProxyProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ class SessionVerificationControllerProxy: SessionVerificationControllerProxyProt
|
||||
return .failure(.failedAcceptingVerificationRequest)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func requestDeviceVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
|
||||
MXLog.info("Requesting device verification")
|
||||
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ protocol SessionVerificationControllerProxyProtocol {
|
||||
func acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) async -> Result<Void, SessionVerificationControllerProxyError>
|
||||
|
||||
func acceptVerificationRequest() async -> Result<Void, SessionVerificationControllerProxyError>
|
||||
|
||||
|
||||
func requestDeviceVerification() async -> Result<Void, SessionVerificationControllerProxyError>
|
||||
|
||||
func requestUserVerification(_ userID: String) async -> Result<Void, SessionVerificationControllerProxyError>
|
||||
|
||||
@@ -32,7 +32,7 @@ final class LeaveSpaceHandleProxy {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var selectedCount: Int {
|
||||
rooms.count { $0.isSelected }
|
||||
}
|
||||
|
||||
@@ -33,17 +33,17 @@ class SpaceRoomListProxy: SpaceRoomListProxyProtocol {
|
||||
|
||||
init(_ spaceRoomList: SpaceRoomListProtocol) async throws {
|
||||
guard let spaceRoom = spaceRoomList.space() else { throw SpaceRoomListProxyError.missingSpace }
|
||||
|
||||
|
||||
self.spaceRoomList = spaceRoomList
|
||||
spaceServiceRoomSubject = .init(SpaceServiceRoom(spaceRoom: spaceRoom))
|
||||
|
||||
|
||||
let paginationStateSubject = CurrentValueSubject<SpaceRoomListPaginationState, Never>(spaceRoomList.paginationState())
|
||||
paginationStatePublisher = paginationStateSubject.asCurrentValuePublisher()
|
||||
|
||||
|
||||
paginationStateHandle = spaceRoomList.subscribeToPaginationStateUpdates(listener: SDKListener { paginationState in
|
||||
paginationStateSubject.send(paginationState)
|
||||
})
|
||||
|
||||
|
||||
spaceRoomsHandle = await spaceRoomList.subscribeToRoomUpdate(listener: SDKListener { [weak self] updates in
|
||||
self?.handleUpdates(updates)
|
||||
})
|
||||
|
||||
@@ -100,7 +100,7 @@ enum RoomTimelineItemFixtures {
|
||||
TextRoomTimelineItem(text: "How are you?",
|
||||
senderDisplayName: "Alice")]
|
||||
}
|
||||
|
||||
|
||||
/// A small chunk of events, containing 2 text items.
|
||||
static var smallChunkWithReadReceipts: [RoomTimelineItemProtocol] {
|
||||
[TextRoomTimelineItem(text: "Hey there 👋",
|
||||
@@ -219,17 +219,17 @@ enum RoomTimelineItemFixtures {
|
||||
TextRoomTimelineItem(text: "Pork buffalo mollit culpa strip steak in leberkas flank cow.",
|
||||
senderDisplayName: "Alice")]
|
||||
}
|
||||
|
||||
|
||||
static var disclosedPolls: [RoomTimelineItemProtocol] {
|
||||
[PollRoomTimelineItem.mock(poll: .disclosed(), isOutgoing: false),
|
||||
PollRoomTimelineItem.mock(poll: .endedDisclosed)]
|
||||
}
|
||||
|
||||
|
||||
static var undisclosedPolls: [RoomTimelineItemProtocol] {
|
||||
[PollRoomTimelineItem.mock(poll: .undisclosed(), isOutgoing: false),
|
||||
PollRoomTimelineItem.mock(poll: .endedUndisclosed)]
|
||||
}
|
||||
|
||||
|
||||
static var outgoingPolls: [RoomTimelineItemProtocol] {
|
||||
[PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: true), isOutgoing: true)]
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import Foundation
|
||||
/// A structure that parses a geo URI (i.e. geo:53.99803101552848,-8.25347900390625;u=10) and constructs their constituent parts.
|
||||
struct GeoURI: Hashable {
|
||||
// MARK: - Properties
|
||||
|
||||
|
||||
let latitude: Double
|
||||
let longitude: Double
|
||||
let uncertainty: Double?
|
||||
@@ -20,9 +20,9 @@ struct GeoURI: Hashable {
|
||||
var bodyMessage: String {
|
||||
"Location was shared at \(string)"
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
|
||||
init?(string: String) {
|
||||
guard let parsedURI = Self.parseGeoURI(from: string) else {
|
||||
MXLog.warning("\(Self.self) failed to parse the string: \(string)")
|
||||
@@ -30,13 +30,13 @@ struct GeoURI: Hashable {
|
||||
}
|
||||
self = parsedURI
|
||||
}
|
||||
|
||||
|
||||
init(latitude: Double, longitude: Double, uncertainty: Double? = nil) {
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
self.uncertainty = uncertainty
|
||||
}
|
||||
|
||||
|
||||
var string: String {
|
||||
if let uncertainty {
|
||||
return "geo:\(string(for: latitude)),\(string(for: longitude));u=\(string(for: uncertainty))"
|
||||
@@ -44,9 +44,9 @@ struct GeoURI: Hashable {
|
||||
return "geo:\(string(for: latitude)),\(string(for: longitude))"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
/// Parse a geo URI string like "geo:53.99803101552848,-8.25347900390625;u=10"
|
||||
private static func parseGeoURI(from string: String) -> GeoURI? {
|
||||
guard
|
||||
@@ -56,11 +56,11 @@ struct GeoURI: Hashable {
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
let uncertainty = matchOutput.uncertainty.flatMap(Double.init)
|
||||
return .init(latitude: latitude, longitude: longitude, uncertainty: uncertainty)
|
||||
}
|
||||
|
||||
|
||||
private func string(for number: Double) -> String {
|
||||
NumberFormatter.decimal.string(from: .init(floatLiteral: number)) ?? "\(number)"
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class MockTimelineController: TimelineControllerProtocol {
|
||||
var roomID: String {
|
||||
roomProxy?.id ?? "MockRoomIdentifier"
|
||||
}
|
||||
|
||||
|
||||
var timelineKind: TimelineKind
|
||||
|
||||
let callbacks = PassthroughSubject<TimelineControllerCallback, Never>()
|
||||
@@ -43,7 +43,7 @@ class MockTimelineController: TimelineControllerProtocol {
|
||||
callbacks.send(.updatedTimelineItems(timelineItems: timelineItems, isSwitchingTimelines: false))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var timelineItemsTimestamp: [TimelineItemIdentifier: Date] = [:]
|
||||
|
||||
private var client: UITestsSignalling.Client?
|
||||
@@ -120,7 +120,7 @@ class MockTimelineController: TimelineControllerProtocol {
|
||||
func processItemAppearance(_ itemID: TimelineItemIdentifier) async { }
|
||||
|
||||
func processItemDisappearance(_ itemID: TimelineItemIdentifier) async { }
|
||||
|
||||
|
||||
func toggleReaction(_ reaction: String, to eventID: TimelineItemIdentifier.EventOrTransactionID) async {
|
||||
if let timelineProxy {
|
||||
_ = await timelineProxy.toggleReaction(reaction, to: eventID)
|
||||
@@ -173,7 +173,7 @@ class MockTimelineController: TimelineControllerProtocol {
|
||||
func sendHandle(for itemID: TimelineItemIdentifier) -> SendHandleProxy? {
|
||||
nil
|
||||
}
|
||||
|
||||
|
||||
func eventTimestamp(for itemID: TimelineItemIdentifier) -> Date? {
|
||||
timelineItemsTimestamp[itemID] ?? .now
|
||||
}
|
||||
@@ -328,7 +328,7 @@ class MockTimelineController: TimelineControllerProtocol {
|
||||
}
|
||||
return .success(())
|
||||
}
|
||||
|
||||
|
||||
// MARK: - UI Test signalling
|
||||
|
||||
/// The cancellable used for UI Tests signalling.
|
||||
|
||||
@@ -35,7 +35,7 @@ class TimelineController: TimelineControllerProtocol {
|
||||
callbacks.send(.paginationState(paginationState))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var roomID: String {
|
||||
roomProxy.id
|
||||
}
|
||||
@@ -128,7 +128,7 @@ class TimelineController: TimelineControllerProtocol {
|
||||
guard let eventID = itemID.eventID else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
_ = await activeTimeline.sendReadReceipt(for: eventID, type: receiptType)
|
||||
}
|
||||
|
||||
@@ -571,7 +571,7 @@ class TimelineController: TimelineControllerProtocol {
|
||||
guard let eventID = timelineItem.id.eventID else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
switch timelineItem.properties.replyDetails {
|
||||
case .notLoaded:
|
||||
activeTimeline.fetchDetails(for: eventID)
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import MatrixRustSDK
|
||||
|
||||
struct TimelineControllerFactory: TimelineControllerFactoryProtocol {
|
||||
let appSettings: AppSettings
|
||||
|
||||
|
||||
func buildTimelineController(roomProxy: JoinedRoomProxyProtocol,
|
||||
initialFocussedEventID: String?,
|
||||
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ protocol TimelineControllerProtocol: Sendable {
|
||||
func removeCaption(_ eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID) async
|
||||
|
||||
func toggleReaction(_ reaction: String, to eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID) async
|
||||
|
||||
|
||||
func redact(_ eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID) async
|
||||
|
||||
func pin(eventID: String) async
|
||||
|
||||
@@ -12,58 +12,58 @@ import MatrixRustSDK
|
||||
|
||||
class TimelineItemProvider: TimelineItemProviderProtocol {
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
|
||||
private var roomTimelineObservationToken: TaskHandle?
|
||||
|
||||
|
||||
/// Bridge from the SDK's synchronous callback into Swift Concurrency. Yielding is safe from any
|
||||
/// thread; a single long-lived `for await` consumer (set up in `init`) applies the diffs on the
|
||||
/// main actor in FIFO order, guaranteeing one in-flight update at a time.
|
||||
private let diffContinuation: AsyncStream<[TimelineDiff]>.Continuation
|
||||
|
||||
|
||||
private let paginationStateSubject = CurrentValueSubject<TimelinePaginationState, Never>(.initial)
|
||||
var paginationState: TimelinePaginationState {
|
||||
paginationStateSubject.value
|
||||
}
|
||||
|
||||
|
||||
private let itemProxiesSubject: CurrentValueSubject<[TimelineItemProxy], Never>
|
||||
private(set) var itemProxies: [TimelineItemProxy] = [] {
|
||||
didSet {
|
||||
itemProxiesSubject.send(itemProxies)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var updatePublisher: AnyPublisher<([TimelineItemProxy], TimelinePaginationState), Never> {
|
||||
itemProxiesSubject
|
||||
.combineLatest(paginationStateSubject)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
let kind: TimelineKind
|
||||
|
||||
|
||||
private let membershipChangeSubject = PassthroughSubject<Void, Never>()
|
||||
var membershipChangePublisher: AnyPublisher<Void, Never> {
|
||||
membershipChangeSubject
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
diffContinuation.finish()
|
||||
roomTimelineObservationToken?.cancel()
|
||||
}
|
||||
|
||||
|
||||
init(timeline: Timeline, kind: TimelineKind, paginationStatePublisher: AnyPublisher<TimelinePaginationState, Never>) {
|
||||
itemProxiesSubject = CurrentValueSubject<[TimelineItemProxy], Never>([])
|
||||
self.kind = kind
|
||||
|
||||
|
||||
let (diffStream, diffContinuation) = AsyncStream<[TimelineDiff]>.makeStream()
|
||||
self.diffContinuation = diffContinuation
|
||||
|
||||
|
||||
paginationStatePublisher
|
||||
.sink { [weak self] in
|
||||
self?.paginationStateSubject.send($0)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
||||
// `for await` guarantees FIFO ordering and that only one diff is being applied at a time.
|
||||
// The stream also allows to handle the sendable items in the listener.
|
||||
Task { [weak self] in
|
||||
@@ -71,7 +71,7 @@ class TimelineItemProvider: TimelineItemProviderProtocol {
|
||||
self?.updateItemsWithDiffs(diffs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Task {
|
||||
roomTimelineObservationToken = await timeline.addListener(listener: SDKListener { diffs in
|
||||
diffContinuation.yield(diffs)
|
||||
@@ -128,11 +128,11 @@ class TimelineItemProvider: TimelineItemProviderProtocol {
|
||||
changes.append(.insert(offset: Int(index), element: itemProxy, associatedWith: nil))
|
||||
case .popBack:
|
||||
guard let itemProxy = itemProxies.last else { fatalError() }
|
||||
|
||||
|
||||
changes.append(.remove(offset: itemProxies.count - 1, element: itemProxy, associatedWith: nil))
|
||||
case .popFront:
|
||||
guard let itemProxy = itemProxies.first else { fatalError() }
|
||||
|
||||
|
||||
changes.append(.remove(offset: 0, element: itemProxy, associatedWith: nil))
|
||||
case .pushBack(let item):
|
||||
let itemProxy = TimelineItemProxy(item: item)
|
||||
@@ -154,7 +154,7 @@ class TimelineItemProvider: TimelineItemProviderProtocol {
|
||||
for (index, itemProxy) in itemProxies.enumerated() {
|
||||
changes.append(.remove(offset: index, element: itemProxy, associatedWith: nil))
|
||||
}
|
||||
|
||||
|
||||
for (index, timelineItem) in items.enumerated() {
|
||||
changes.append(.insert(offset: index, element: TimelineItemProxy(item: timelineItem), associatedWith: nil))
|
||||
}
|
||||
|
||||
@@ -101,11 +101,11 @@ class EventTimelineItemProxy {
|
||||
}()
|
||||
|
||||
lazy var canBeRepliedTo = item.canBeRepliedTo
|
||||
|
||||
|
||||
lazy var content = item.content
|
||||
|
||||
|
||||
lazy var isOwn = item.isOwn
|
||||
|
||||
|
||||
lazy var isEditable = item.isEditable
|
||||
|
||||
lazy var sender = TimelineItemSender(senderID: item.sender, senderProfile: item.senderProfile)
|
||||
@@ -192,11 +192,11 @@ struct SendHandleProxy: Hashable {
|
||||
}
|
||||
|
||||
// MARK: - Hashable
|
||||
|
||||
|
||||
static func == (lhs: SendHandleProxy, rhs: SendHandleProxy) -> Bool {
|
||||
lhs.itemID == rhs.itemID
|
||||
}
|
||||
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(itemID)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ struct TimelineItemSender: Identifiable, Hashable {
|
||||
avatarURL: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var disambiguatedDisplayName: String? {
|
||||
guard let displayName else {
|
||||
return nil
|
||||
|
||||
+8
-8
@@ -26,11 +26,11 @@ extension EventBasedTimelineItemProtocol {
|
||||
var description: String {
|
||||
"\(String(describing: Self.self)): id: \(id), timestamp: \(timestamp), isOutgoing: \(isOutgoing), properties: \(properties)"
|
||||
}
|
||||
|
||||
|
||||
var isForwardable: Bool {
|
||||
isRemoteMessage && !(self is PollRoomTimelineItem)
|
||||
}
|
||||
|
||||
|
||||
var isRemoteMessage: Bool {
|
||||
id.eventID != nil
|
||||
}
|
||||
@@ -50,11 +50,11 @@ extension EventBasedTimelineItemProtocol {
|
||||
var hasFailedToSend: Bool {
|
||||
properties.deliveryStatus?.isSendingFailed == true
|
||||
}
|
||||
|
||||
|
||||
var hasFailedDecryption: Bool {
|
||||
self is EncryptedRoomTimelineItem
|
||||
}
|
||||
|
||||
|
||||
var timelineMenuDescription: String {
|
||||
switch self {
|
||||
case is VoiceMessageRoomTimelineItem:
|
||||
@@ -63,7 +63,7 @@ extension EventBasedTimelineItemProtocol {
|
||||
return body.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The inline area to reserve at the trailing edge of the message text so the
|
||||
/// bubble's natural size accommodates the timestamp (which is overlaid on the
|
||||
/// bubble's bottom-trailing corner). Text wraps around this area: if it fits on
|
||||
@@ -86,7 +86,7 @@ extension EventBasedTimelineItemProtocol {
|
||||
width += 4
|
||||
return CGSize(width: ceil(width), height: ceil(height))
|
||||
}
|
||||
|
||||
|
||||
/// contains the timestamp and an optional edited localised prefix
|
||||
/// example: (edited) 12:17 PM
|
||||
var localizedSendInfo: String {
|
||||
@@ -96,12 +96,12 @@ extension EventBasedTimelineItemProtocol {
|
||||
}
|
||||
return start + timestamp.formattedTime()
|
||||
}
|
||||
|
||||
|
||||
var isCopyable: Bool {
|
||||
guard let messageBasedItem = self as? EventBasedMessageTimelineItemProtocol else {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
switch messageBasedItem.contentType {
|
||||
case .audio, .file, .image, .video, .location, .voice:
|
||||
return false
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
|
||||
let sender: TimelineItemSender
|
||||
|
||||
let content: AudioRoomTimelineItemContent
|
||||
|
||||
|
||||
var properties = RoomTimelineItemProperties()
|
||||
|
||||
var body: String {
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ struct LiveLocationRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
|
||||
var body: String {
|
||||
""
|
||||
}
|
||||
|
||||
|
||||
let timestamp: Date
|
||||
let isOutgoing: Bool
|
||||
let isEditable: Bool
|
||||
|
||||
+3
-3
@@ -16,15 +16,15 @@ struct LocationRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatabl
|
||||
let canBeRepliedTo: Bool
|
||||
|
||||
let sender: TimelineItemSender
|
||||
|
||||
|
||||
let content: LocationRoomTimelineItemContent
|
||||
|
||||
var properties = RoomTimelineItemProperties()
|
||||
|
||||
|
||||
var body: String {
|
||||
content.body
|
||||
}
|
||||
|
||||
|
||||
var contentType: EventBasedMessageTimelineItemContentType {
|
||||
.location(content)
|
||||
}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ struct LocationRoomTimelineItemContent: Hashable {
|
||||
let body: String
|
||||
let geoURI: GeoURI?
|
||||
let kind: StaticLocationKind
|
||||
|
||||
|
||||
init(body: String,
|
||||
geoURI: GeoURI? = nil,
|
||||
kind: StaticLocationKind = .sender) {
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ struct VideoRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
|
||||
var shouldBoost = false
|
||||
|
||||
let sender: TimelineItemSender
|
||||
|
||||
|
||||
let content: VideoRoomTimelineItemContent
|
||||
|
||||
var properties = RoomTimelineItemProperties()
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ struct VoiceMessageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equa
|
||||
let sender: TimelineItemSender
|
||||
|
||||
let content: AudioRoomTimelineItemContent
|
||||
|
||||
|
||||
var properties = RoomTimelineItemProperties()
|
||||
|
||||
var body: String {
|
||||
|
||||
+3
-3
@@ -29,11 +29,11 @@ struct Poll: Hashable {
|
||||
let endDate: Date?
|
||||
/// Whether the poll has been created by the account owner
|
||||
let createdByAccountOwner: Bool
|
||||
|
||||
|
||||
var hasEnded: Bool {
|
||||
endDate != nil
|
||||
}
|
||||
|
||||
|
||||
enum Kind: Hashable {
|
||||
case disclosed
|
||||
case undisclosed
|
||||
@@ -42,7 +42,7 @@ struct Poll: Hashable {
|
||||
var hasMaxSelections: Bool {
|
||||
options.filter(\.isSelected).count == maxSelections
|
||||
}
|
||||
|
||||
|
||||
struct Option: Hashable {
|
||||
let id: String
|
||||
let text: String
|
||||
|
||||
@@ -384,9 +384,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
let allVotes = votes.reduce(0) { count, pair in
|
||||
count + pair.value.count
|
||||
}
|
||||
|
||||
|
||||
let maxOptionVotes = votes.map(\.value.count).max()
|
||||
|
||||
|
||||
let options = answers.map { answer in
|
||||
let optionVotesCount = votes[answer.id]?.count
|
||||
|
||||
@@ -404,7 +404,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
case .undisclosed:
|
||||
.undisclosed
|
||||
}
|
||||
|
||||
|
||||
let poll = Poll(question: question,
|
||||
kind: pollKind,
|
||||
maxSelections: Int(maxSelections),
|
||||
@@ -412,7 +412,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
votes: votes,
|
||||
endDate: endTime.map { Date(timeIntervalSince1970: TimeInterval($0 / 1000)) },
|
||||
createdByAccountOwner: eventItemProxy.sender.id == userID)
|
||||
|
||||
|
||||
return PollRoomTimelineItem(id: eventItemProxy.id,
|
||||
poll: poll,
|
||||
body: poll.question,
|
||||
@@ -538,7 +538,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
if let audioWaveform = messageContent.audio?.waveform {
|
||||
waveform = EstimatedWaveform(data: audioWaveform)
|
||||
}
|
||||
|
||||
|
||||
return AudioRoomTimelineItemContent(filename: messageContent.filename,
|
||||
caption: messageContent.caption,
|
||||
formattedCaption: formattedCaption,
|
||||
@@ -602,13 +602,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
blurhash: messageContent.info?.blurhash,
|
||||
contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.filename))
|
||||
}
|
||||
|
||||
|
||||
private func buildLocationTimelineItemContent(_ locationContent: LocationContent) -> LocationRoomTimelineItemContent {
|
||||
LocationRoomTimelineItemContent(body: locationContent.body,
|
||||
geoURI: .init(string: locationContent.geoUri),
|
||||
kind: .init(from: locationContent.asset))
|
||||
}
|
||||
|
||||
|
||||
private func buildFileTimelineItemContent(_ messageContent: FileMessageContent) -> FileRoomTimelineItemContent {
|
||||
let htmlCaption = messageContent.formattedCaption?.format == .html ? messageContent.formattedCaption?.body : nil
|
||||
let formattedCaption = htmlCaption != nil ? attributedStringBuilder.fromHTML(htmlCaption) : attributedStringBuilder.fromPlain(messageContent.caption)
|
||||
@@ -636,7 +636,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
let name = senderDisplayName ?? senderID
|
||||
|
||||
let htmlBody = messageContent.formatted?.format == .html ? messageContent.formatted?.body : nil
|
||||
|
||||
|
||||
var formattedBody: AttributedString?
|
||||
if let htmlBody {
|
||||
formattedBody = buildEmoteFormattedBodyFromHTML(html: htmlBody, name: name)
|
||||
@@ -686,7 +686,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
return a.count > b.count
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func buildOrderedReadReceipts(_ receipts: [String: Receipt]) -> [ReadReceipt] {
|
||||
receipts
|
||||
.sorted { firstElement, secondElement in
|
||||
|
||||
@@ -25,7 +25,7 @@ struct RoomTimelineItemView: View {
|
||||
context?.send(viewAction: .itemDisappeared(itemID: viewState.identifier))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder private var timelineView: some View {
|
||||
switch viewState.type {
|
||||
case .text(let item):
|
||||
|
||||
@@ -12,17 +12,17 @@ import MatrixRustSDK
|
||||
final class RoomTimelineItemViewState: Identifiable, Equatable, ObservableObject {
|
||||
@Published var type: RoomTimelineItemType
|
||||
@Published var groupStyle: TimelineGroupStyle
|
||||
|
||||
|
||||
/// Contains all the identification info of the item, `uniqueID`, `eventID` and `transactionID`
|
||||
var identifier: TimelineItemIdentifier {
|
||||
type.id
|
||||
}
|
||||
|
||||
|
||||
init(type: RoomTimelineItemType, groupStyle: TimelineGroupStyle) {
|
||||
self.type = type
|
||||
self.groupStyle = groupStyle
|
||||
}
|
||||
|
||||
|
||||
convenience init(item: RoomTimelineItemProtocol, groupStyle: TimelineGroupStyle) {
|
||||
self.init(type: .init(item: item), groupStyle: groupStyle)
|
||||
}
|
||||
@@ -70,7 +70,7 @@ enum RoomTimelineItemType: Equatable {
|
||||
case callInvite(CallInviteRoomTimelineItem)
|
||||
case callNotification(CallNotificationRoomTimelineItem)
|
||||
case liveLocation(LiveLocationRoomTimelineItem)
|
||||
|
||||
|
||||
init(item: RoomTimelineItemProtocol) {
|
||||
switch item {
|
||||
case let item as TextRoomTimelineItem:
|
||||
@@ -123,7 +123,7 @@ enum RoomTimelineItemType: Equatable {
|
||||
fatalError("Unknown timeline item")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var id: TimelineItemIdentifier {
|
||||
switch self {
|
||||
case .text(let item as RoomTimelineItemProtocol),
|
||||
|
||||
@@ -24,7 +24,7 @@ final class TimelineProxy: TimelineProxyProtocol {
|
||||
private let forwardPaginationStateSubject = CurrentValueSubject<PaginationState, Never>(.endReached)
|
||||
|
||||
private let kind: TimelineKind
|
||||
|
||||
|
||||
private var innerTimelineItemProvider: TimelineItemProviderProtocol!
|
||||
var timelineItemProvider: TimelineItemProviderProtocol {
|
||||
innerTimelineItemProvider
|
||||
@@ -146,7 +146,7 @@ final class TimelineProxy: TimelineProxyProtocol {
|
||||
case .forwards: try await timeline.paginateForwards(numEvents: requestSize)
|
||||
}
|
||||
MXLog.info("Finished paginating \(direction.rawValue)")
|
||||
|
||||
|
||||
subject.send(timelineEndReached ? .endReached : .idle)
|
||||
return .success(())
|
||||
} catch {
|
||||
@@ -170,7 +170,7 @@ final class TimelineProxy: TimelineProxyProtocol {
|
||||
try await timeline.edit(eventOrTransactionId: eventOrTransactionID.rustValue, newContent: newContent)
|
||||
|
||||
MXLog.info("Finished editing timeline item: \(eventOrTransactionID)")
|
||||
|
||||
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed editing timeline item: \(eventOrTransactionID) with error: \(error)")
|
||||
@@ -412,10 +412,10 @@ final class TimelineProxy: TimelineProxyProtocol {
|
||||
} else {
|
||||
MXLog.error("Failed sending message with error: \(error)")
|
||||
}
|
||||
|
||||
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
@@ -473,7 +473,7 @@ final class TimelineProxy: TimelineProxyProtocol {
|
||||
}
|
||||
|
||||
// MARK: - Polls
|
||||
|
||||
|
||||
func createPoll(question: String, answers: [String],
|
||||
pollKind: Poll.Kind) async -> Result<Void, TimelineProxyError> {
|
||||
MXLog.info("Creating poll")
|
||||
@@ -528,7 +528,7 @@ final class TimelineProxy: TimelineProxyProtocol {
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func sendPollResponse(pollStartID: String, answers: [String]) async -> Result<Void, TimelineProxyError> {
|
||||
MXLog.info("Sending response for poll with eventID: \(pollStartID)")
|
||||
|
||||
@@ -543,7 +543,7 @@ final class TimelineProxy: TimelineProxyProtocol {
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func buildMessageContentFor(_ message: String,
|
||||
html: String?,
|
||||
intentionalMentions: Mentions) -> RoomMessageEventContentWithoutRelation {
|
||||
|
||||
@@ -15,7 +15,7 @@ struct SessionDirectories: Hashable, Codable {
|
||||
var dataPath: String {
|
||||
dataDirectory.path(percentEncoded: false)
|
||||
}
|
||||
|
||||
|
||||
var cachePath: String {
|
||||
cacheDirectory.path(percentEncoded: false)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
var hasSessions: Bool {
|
||||
!keychainController.restorationTokens().isEmpty
|
||||
}
|
||||
|
||||
|
||||
/// All the user IDs managed by the store.
|
||||
var userIDs: [String] {
|
||||
keychainController.restorationTokens().map(\.userID)
|
||||
@@ -100,7 +100,7 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
credentials.restorationToken.sessionDirectories.delete()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func buildUserSessionWithClient(_ clientProxy: ClientProxyProtocol) async -> UserSessionProtocol {
|
||||
|
||||
@@ -14,10 +14,10 @@ final class UserDiscoveryService: UserDiscoveryServiceProtocol {
|
||||
init(clientProxy: ClientProxyProtocol) {
|
||||
self.clientProxy = clientProxy
|
||||
}
|
||||
|
||||
|
||||
func searchProfiles(with searchQuery: String) async -> Result<[UserProfileProxy], UserDiscoveryErrorType> {
|
||||
async let queriedProfile = profileIfPossible(with: searchQuery)
|
||||
|
||||
|
||||
do {
|
||||
async let searchedUsers = clientProxy.searchUsers(searchTerm: searchQuery, limit: 10).get()
|
||||
let users = try await merge(queriedProfile: queriedProfile, searchResults: searchedUsers)
|
||||
@@ -31,18 +31,18 @@ final class UserDiscoveryService: UserDiscoveryServiceProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func merge(queriedProfile: UserProfileProxy?, searchResults: SearchUsersResultsProxy) -> [UserProfileProxy] {
|
||||
let searchResults = searchResults.results
|
||||
|
||||
guard let queriedProfile else {
|
||||
return searchResults
|
||||
}
|
||||
|
||||
|
||||
let filteredSearchResult = searchResults.filter {
|
||||
$0.userID != queriedProfile.userID
|
||||
}
|
||||
|
||||
|
||||
return [queriedProfile] + filteredSearchResult
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ final class UserDiscoveryService: UserDiscoveryServiceProtocol {
|
||||
// fallback to a "local profile" if the profile api fails
|
||||
return getProfileResult ?? .init(userID: searchQuery)
|
||||
}
|
||||
|
||||
|
||||
private func filterAccountOwner(_ profiles: [UserProfileProxy]) -> [UserProfileProxy] {
|
||||
let accountOwnerID = clientProxy.userID
|
||||
return profiles.filter { $0.userID != accountOwnerID }
|
||||
|
||||
@@ -38,7 +38,7 @@ class VoiceMessageCache: VoiceMessageCacheProtocol {
|
||||
}
|
||||
return .success(url)
|
||||
}
|
||||
|
||||
|
||||
func clearCache() {
|
||||
if FileManager.default.fileExists(atPath: temporaryFilesFolderURL.path) {
|
||||
do {
|
||||
|
||||
@@ -29,7 +29,7 @@ protocol VoiceMessageCacheProtocol {
|
||||
/// - move: wheter to move or copy the source file
|
||||
/// - Returns: the cached URL
|
||||
func cache(mediaSource: MediaSourceProxy, using fileURL: URL, move: Bool) -> Result<URL, VoiceMessageCacheError>
|
||||
|
||||
|
||||
/// Clears the cache
|
||||
func clearCache()
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class VoiceMessageMediaManager: VoiceMessageMediaManagerProtocol {
|
||||
self.audioConverter = audioConverter
|
||||
self.processingQueue = processingQueue
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
voiceMessageCache.clearCache()
|
||||
}
|
||||
@@ -50,7 +50,7 @@ class VoiceMessageMediaManager: VoiceMessageMediaManagerProtocol {
|
||||
if let fileURL = voiceMessageCache.fileURL(for: source) {
|
||||
return fileURL
|
||||
}
|
||||
|
||||
|
||||
// Otherwise, load the file from source
|
||||
guard case .success(let fileHandle) = await mediaProvider.loadFileFromSource(source, filename: body) else {
|
||||
throw MediaProviderError.failedRetrievingFile
|
||||
@@ -61,14 +61,14 @@ class VoiceMessageMediaManager: VoiceMessageMediaManagerProtocol {
|
||||
if let fileURL = voiceMessageCache.fileURL(for: source) {
|
||||
return fileURL
|
||||
}
|
||||
|
||||
|
||||
// Convert from ogg
|
||||
guard let url = fileHandle.url else {
|
||||
throw VoiceMessageMediaManagerError.missingURL
|
||||
}
|
||||
let convertedFileURL = URL.temporaryDirectory.appendingPathComponent(url.deletingPathExtension().lastPathComponent).appendingPathExtension(AudioConverterPreferredFileExtension.mpeg4aac.rawValue)
|
||||
try audioConverter.convertToMPEG4AAC(sourceURL: url, destinationURL: convertedFileURL)
|
||||
|
||||
|
||||
// Cache the file and return the url
|
||||
let result = voiceMessageCache.cache(mediaSource: source, using: convertedFileURL, move: true)
|
||||
switch result {
|
||||
|
||||
@@ -20,7 +20,7 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
|
||||
var actions: AnyPublisher<VoiceMessageRecorderAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
private let mp4accMimeType = "audio/m4a"
|
||||
|
||||
var isRecording: Bool {
|
||||
@@ -36,11 +36,11 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
|
||||
}
|
||||
|
||||
private var recordingCancelled = false
|
||||
|
||||
|
||||
private(set) var previewAudioPlayerState: AudioPlayerState?
|
||||
private(set) var previewAudioPlayer: AudioPlayerProtocol?
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
|
||||
init(audioRecorder: AudioRecorderProtocol = AudioRecorder(),
|
||||
mediaPlayerProvider: MediaPlayerProviderProtocol,
|
||||
voiceMessageCache: VoiceMessageCacheProtocol = VoiceMessageCache()) {
|
||||
@@ -86,7 +86,7 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
|
||||
previewAudioPlayer?.reset()
|
||||
previewAudioPlayerState = nil
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
func startPlayback() async -> Result<Void, VoiceMessageRecorderError> {
|
||||
@@ -158,13 +158,13 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
|
||||
// delete the temporary file
|
||||
try? FileManager.default.removeItem(at: oggFile)
|
||||
}
|
||||
|
||||
|
||||
do {
|
||||
try audioConverter.convertToOpusOgg(sourceURL: url, destinationURL: oggFile)
|
||||
} catch {
|
||||
return .failure(.failedSendingVoiceMessage)
|
||||
}
|
||||
|
||||
|
||||
// send it
|
||||
let size: UInt64
|
||||
do {
|
||||
@@ -189,7 +189,7 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func addObservers() {
|
||||
@@ -237,10 +237,10 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
|
||||
guard audioRecorder.audioFileURL != nil, audioRecorder.currentTime > 0 else {
|
||||
return .failure(.previewNotAvailable)
|
||||
}
|
||||
|
||||
|
||||
// Build the preview audio player state
|
||||
previewAudioPlayerState = await AudioPlayerState(id: .recorderPreview, title: L10n.commonVoiceMessage, duration: recordingDuration, waveform: EstimatedWaveform(data: []))
|
||||
|
||||
|
||||
// Build the preview audio player
|
||||
let audioPlayer = await mediaPlayerProvider.player
|
||||
previewAudioPlayer = audioPlayer
|
||||
|
||||
@@ -27,9 +27,9 @@ protocol VoiceMessageRecorderProtocol {
|
||||
var previewAudioPlayerState: AudioPlayerState? { get }
|
||||
var isRecording: Bool { get }
|
||||
var recordingURL: URL? { get }
|
||||
|
||||
|
||||
var actions: AnyPublisher<VoiceMessageRecorderAction, Never> { get }
|
||||
|
||||
|
||||
func startRecording() async
|
||||
func stopRecording() async
|
||||
func cancelRecording() async
|
||||
|
||||
Reference in New Issue
Block a user