mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Add a view to show the user's account from Element Classic. (#5361)
No presentation logic yet.
This commit is contained in:
@@ -263,6 +263,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
||||
provisioningParameters: provisioningParameters,
|
||||
isBugReportServiceEnabled: bugReportService.isEnabled,
|
||||
appSettings: appSettings,
|
||||
mediaProvider: nil, // Currently unused.
|
||||
userIndicatorController: userIndicatorController)
|
||||
let coordinator = AuthenticationStartScreenCoordinator(parameters: parameters)
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
extension ClassicAppManagerMock {
|
||||
struct Configuration { }
|
||||
|
||||
convenience init(_ configuration: Configuration) {
|
||||
self.init()
|
||||
}
|
||||
}
|
||||
|
||||
extension ClassicAppAccount {
|
||||
static var mockAlice: ClassicAppAccount {
|
||||
ClassicAppAccount(userID: "@alice:matrix.org",
|
||||
displayName: "Alice",
|
||||
avatarURL: nil,
|
||||
serverName: "matrix.org",
|
||||
homeserverURL: "https://matrix-client.matrix.org/",
|
||||
cryptoStoreURL: .cachesDirectory,
|
||||
cryptoStorePassphrase: "1234567890",
|
||||
accessToken: "accessToken")
|
||||
}
|
||||
|
||||
static var mockDan: ClassicAppAccount {
|
||||
ClassicAppAccount(userID: "@dan:matrix.org",
|
||||
displayName: "Dan",
|
||||
avatarURL: .mockMXCUserAvatar,
|
||||
serverName: "matrix.org",
|
||||
homeserverURL: "https://matrix-client.matrix.org/",
|
||||
cryptoStoreURL: .cachesDirectory,
|
||||
cryptoStorePassphrase: "1234567890",
|
||||
accessToken: "accessToken")
|
||||
}
|
||||
}
|
||||
@@ -2239,6 +2239,77 @@ class CXProviderMock: CXProviderProtocol, @unchecked Sendable {
|
||||
reportCallWithEndedAtReasonClosure?(uuid, endedAt, reason)
|
||||
}
|
||||
}
|
||||
class ClassicAppManagerMock: ClassicAppManagerProtocol, @unchecked Sendable {
|
||||
|
||||
//MARK: - loadAccounts
|
||||
|
||||
var loadAccountsThrowableError: Error?
|
||||
var loadAccountsUnderlyingCallsCount = 0
|
||||
var loadAccountsCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return loadAccountsUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = loadAccountsUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
loadAccountsUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
loadAccountsUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var loadAccountsCalled: Bool {
|
||||
return loadAccountsCallsCount > 0
|
||||
}
|
||||
|
||||
var loadAccountsUnderlyingReturnValue: [ClassicAppAccount]!
|
||||
var loadAccountsReturnValue: [ClassicAppAccount]! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return loadAccountsUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: [ClassicAppAccount]? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = loadAccountsUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
loadAccountsUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
loadAccountsUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var loadAccountsClosure: (() throws -> [ClassicAppAccount])?
|
||||
|
||||
func loadAccounts() throws -> [ClassicAppAccount] {
|
||||
if let error = loadAccountsThrowableError {
|
||||
throw error
|
||||
}
|
||||
loadAccountsCallsCount += 1
|
||||
if let loadAccountsClosure = loadAccountsClosure {
|
||||
return try loadAccountsClosure()
|
||||
} else {
|
||||
return loadAccountsReturnValue
|
||||
}
|
||||
}
|
||||
}
|
||||
class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
|
||||
var actionsPublisher: AnyPublisher<ClientProxyAction, Never> {
|
||||
get { return underlyingActionsPublisher }
|
||||
|
||||
@@ -86,6 +86,7 @@ enum UserAvatarSizeOnScreen {
|
||||
case threadList
|
||||
case threadSummary
|
||||
case map
|
||||
case classicAppAccount
|
||||
|
||||
var value: CGFloat {
|
||||
switch self {
|
||||
@@ -115,7 +116,7 @@ enum UserAvatarSizeOnScreen {
|
||||
64
|
||||
case .dmDetails:
|
||||
75
|
||||
case .memberDetails, .editUserDetails:
|
||||
case .memberDetails, .editUserDetails, .classicAppAccount:
|
||||
96
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -14,9 +14,20 @@ struct AuthenticationStartScreenParameters {
|
||||
let provisioningParameters: AccountProvisioningParameters?
|
||||
let isBugReportServiceEnabled: Bool
|
||||
let appSettings: AppSettings
|
||||
let mediaProvider: MediaProviderProtocol?
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
}
|
||||
|
||||
enum AuthenticationStartScreenCoordinatorAction {
|
||||
case loginWithQR
|
||||
case login
|
||||
case register
|
||||
case reportProblem
|
||||
|
||||
case loginDirectlyWithOIDC(data: OIDCAuthorizationDataProxy, window: UIWindow)
|
||||
case loginDirectlyWithPassword(loginHint: String?)
|
||||
}
|
||||
|
||||
final class AuthenticationStartScreenCoordinator: CoordinatorProtocol {
|
||||
private var viewModel: AuthenticationStartScreenViewModelProtocol
|
||||
private let actionsSubject: PassthroughSubject<AuthenticationStartScreenCoordinatorAction, Never> = .init()
|
||||
@@ -31,6 +42,7 @@ final class AuthenticationStartScreenCoordinator: CoordinatorProtocol {
|
||||
provisioningParameters: parameters.provisioningParameters,
|
||||
isBugReportServiceEnabled: parameters.isBugReportServiceEnabled,
|
||||
appSettings: parameters.appSettings,
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
userIndicatorController: parameters.userIndicatorController)
|
||||
}
|
||||
|
||||
|
||||
+4
-12
@@ -8,18 +8,6 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Coordinator
|
||||
|
||||
enum AuthenticationStartScreenCoordinatorAction {
|
||||
case loginWithQR
|
||||
case login
|
||||
case register
|
||||
case reportProblem
|
||||
|
||||
case loginDirectlyWithOIDC(data: OIDCAuthorizationDataProxy, window: UIWindow)
|
||||
case loginDirectlyWithPassword(loginHint: String?)
|
||||
}
|
||||
|
||||
enum AuthenticationStartScreenViewModelAction: Equatable {
|
||||
case loginWithQR
|
||||
case login
|
||||
@@ -70,4 +58,8 @@ enum AuthenticationStartScreenViewAction {
|
||||
case login
|
||||
case register
|
||||
case reportProblem
|
||||
|
||||
case continueWithClassic(ClassicAppAccount)
|
||||
case otherOptions(ClassicAppAccount)
|
||||
case closeOtherOptions(ClassicAppAccount)
|
||||
}
|
||||
|
||||
+6
-3
@@ -24,11 +24,12 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
|
||||
var actions: AnyPublisher<AuthenticationStartScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
init(authenticationService: AuthenticationServiceProtocol,
|
||||
provisioningParameters: AccountProvisioningParameters?,
|
||||
isBugReportServiceEnabled: Bool,
|
||||
appSettings: AppSettings,
|
||||
mediaProvider: MediaProviderProtocol?,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
self.authenticationService = authenticationService
|
||||
self.provisioningParameters = provisioningParameters
|
||||
@@ -59,9 +60,9 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
|
||||
hideBrandChrome: appSettings.hideBrandChrome)
|
||||
}
|
||||
|
||||
super.init(initialViewState: initialViewState)
|
||||
super.init(initialViewState: initialViewState, mediaProvider: mediaProvider)
|
||||
}
|
||||
|
||||
|
||||
override func process(viewAction: AuthenticationStartScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .updateWindow(let window):
|
||||
@@ -77,6 +78,8 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
|
||||
if canReportProblem {
|
||||
actionsSubject.send(.reportProblem)
|
||||
}
|
||||
case .continueWithClassic, .otherOptions, .closeOtherOptions:
|
||||
break // To follow.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
//
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct AuthenticationClassicAppAccountView: View {
|
||||
@Bindable var context: AuthenticationStartScreenViewModel.Context
|
||||
|
||||
let classicAppAccount: ClassicAppAccount
|
||||
|
||||
var body: some View {
|
||||
FullscreenDialog(topPadding: 25, background: .gradient) {
|
||||
VStack(spacing: 38) {
|
||||
header
|
||||
.padding(.bottom, 20)
|
||||
|
||||
profile
|
||||
|
||||
buttons
|
||||
}
|
||||
} bottomContent: {
|
||||
// Buttons are intentionally shown inline on this screen.
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.alert(item: $context.alertInfo)
|
||||
.introspect(.window, on: .supportedVersions) { window in
|
||||
context.send(viewAction: .updateWindow(window))
|
||||
}
|
||||
}
|
||||
|
||||
var header: some View {
|
||||
VStack(spacing: 8) {
|
||||
AuthenticationStartLogo(size: 54, hideBrandChrome: false, isOnGradient: false)
|
||||
|
||||
Text(L10n.screenOnboardingWelcomeTitle)
|
||||
.font(.compound.headingMDBold)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
|
||||
var profile: some View {
|
||||
VStack(spacing: 16) {
|
||||
LoadableAvatarImage(url: classicAppAccount.avatarURL,
|
||||
name: classicAppAccount.displayName,
|
||||
contentID: classicAppAccount.userID,
|
||||
avatarSize: .user(on: .classicAppAccount),
|
||||
mediaProvider: context.mediaProvider)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Text(L10n.screenOnboardingWelcomeBack)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text(classicAppAccount.displayableName)
|
||||
.font(.compound.headingLGBold)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
Text(classicAppAccount.userID)
|
||||
.font(.compound.bodyLGSemibold)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
|
||||
var buttons: some View {
|
||||
VStack(spacing: 16) {
|
||||
Button(L10n.actionContinue) {
|
||||
context.send(viewAction: .continueWithClassic(classicAppAccount))
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
|
||||
Button(L10n.commonOtherOptions) {
|
||||
context.send(viewAction: .otherOptions(classicAppAccount))
|
||||
}
|
||||
.buttonStyle(.compound(.secondary))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ClassicAppAccount {
|
||||
var displayableName: String {
|
||||
if let displayName, !displayName.isEmpty {
|
||||
displayName
|
||||
} else if let localPart = userID.dropFirst().split(separator: ":").first, !localPart.isEmpty {
|
||||
String(localPart)
|
||||
} else {
|
||||
userID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct AuthenticationClassicAppAccountView_Previews: PreviewProvider { // Not Testable – snapshots generated by main screen.
|
||||
static let viewModel = makeViewModel()
|
||||
|
||||
static var previews: some View {
|
||||
ElementNavigationStack {
|
||||
AuthenticationClassicAppAccountView(context: viewModel.context, classicAppAccount: .mockDan)
|
||||
}
|
||||
}
|
||||
|
||||
static func makeViewModel() -> AuthenticationStartScreenViewModel {
|
||||
AuthenticationStartScreenViewModel(authenticationService: AuthenticationService.mock,
|
||||
provisioningParameters: nil,
|
||||
isBugReportServiceEnabled: false,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
}
|
||||
+1
@@ -69,6 +69,7 @@ struct AuthenticationClassicAppBackupInstructionsView_Previews: PreviewProvider,
|
||||
provisioningParameters: nil,
|
||||
isBugReportServiceEnabled: false,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -152,6 +152,7 @@ struct AuthenticationStartScreen_Previews: PreviewProvider, TestablePreview {
|
||||
provisioningParameters: provisionedServerName.map { .init(accountProvider: $0, loginHint: nil) },
|
||||
isBugReportServiceEnabled: true,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,10 +68,12 @@ class ClassicAppAccountManager {
|
||||
|
||||
return ClassicAppAccount(userID: userID,
|
||||
displayName: user?.displayName,
|
||||
avatarURL: user?.avatarURL.flatMap(URL.init(string:)),
|
||||
avatarURL: user?.avatarURL,
|
||||
serverName: serverName,
|
||||
homeserverURL: mxAccount.homeserverURL,
|
||||
cryptoStoreURL: cryptoStoreURL(for: userID),
|
||||
cryptoStorePassphrase: cryptoStorePassphrase)
|
||||
cryptoStorePassphrase: cryptoStorePassphrase.base64EncodedString(),
|
||||
accessToken: mxAccount.accessToken)
|
||||
}
|
||||
|
||||
private func loadUser(for mxAccount: ClassicAppMXAccount) -> ClassicAppMXUser? {
|
||||
|
||||
@@ -7,13 +7,23 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ClassicAppAccount: Equatable {
|
||||
struct ClassicAppAccount: Equatable, CustomStringConvertible {
|
||||
let userID: String
|
||||
let displayName: String?
|
||||
let avatarURL: URL?
|
||||
|
||||
let serverName: String
|
||||
let homeserverURL: URL
|
||||
|
||||
let cryptoStoreURL: URL
|
||||
let cryptoStorePassphrase: Data
|
||||
let cryptoStorePassphrase: String
|
||||
|
||||
let accessToken: String // For avatar loading and key backup detection.
|
||||
|
||||
/// Custom `CustomStringConvertible` without the access token.
|
||||
var description: String {
|
||||
"ClassicAppAccount(userID: \(userID), homeserverURL: \(homeserverURL))"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: NSCoding Types
|
||||
@@ -21,8 +31,10 @@ struct ClassicAppAccount: Equatable {
|
||||
final class ClassicAppMXAccount: NSObject, NSCoding {
|
||||
/// The obtained user ID.
|
||||
var userID: String
|
||||
/// The access token to create a MXRestClient.
|
||||
var accessToken: String
|
||||
/// The homeserver url (ex: "https://matrix.org").
|
||||
var homeserver: String?
|
||||
var homeserverURL: URL
|
||||
|
||||
/// Disable the account without logging out (NO by default).
|
||||
///
|
||||
@@ -37,6 +49,11 @@ final class ClassicAppMXAccount: NSObject, NSCoding {
|
||||
!isDisabled && !isSoftLogout
|
||||
}
|
||||
|
||||
/// Override the existing `CustomStringConvertible` conformance.
|
||||
override var description: String {
|
||||
"ClassicAppMXAccount(userID: \(userID), homeserverURL: \(homeserverURL), isDisabled: \(isDisabled), isSoftLogout: \(isSoftLogout))"
|
||||
}
|
||||
|
||||
// MARK: NSCoding
|
||||
|
||||
enum Keys {
|
||||
@@ -64,12 +81,15 @@ final class ClassicAppMXAccount: NSObject, NSCoding {
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
guard let userID = coder.decodeObject(forKey: Keys.userID) as? String,
|
||||
let homeserver = coder.decodeObject(forKey: Keys.homeserverURL) as? String else {
|
||||
let accessToken = coder.decodeObject(forKey: Keys.accessToken) as? String,
|
||||
let homeserver = coder.decodeObject(forKey: Keys.homeserverURL) as? String,
|
||||
let homeserverURL = URL(string: homeserver) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.userID = userID
|
||||
self.homeserver = homeserver
|
||||
self.accessToken = accessToken
|
||||
self.homeserverURL = homeserverURL
|
||||
|
||||
isDisabled = coder.decodeBool(forKey: Keys.isDisabled)
|
||||
isSoftLogout = coder.decodeBool(forKey: Keys.isSoftLogout)
|
||||
@@ -89,7 +109,7 @@ final class ClassicAppMXUser: NSObject, NSCoding {
|
||||
/// The user display name.
|
||||
let displayName: String?
|
||||
/// The url of the user of the avatar.
|
||||
let avatarURL: String?
|
||||
let avatarURL: URL?
|
||||
|
||||
// MARK: NSCoding
|
||||
|
||||
@@ -110,7 +130,9 @@ final class ClassicAppMXUser: NSObject, NSCoding {
|
||||
|
||||
self.userID = userID
|
||||
displayName = aDecoder.decodeObject(forKey: Keys.displayName) as? String
|
||||
avatarURL = aDecoder.decodeObject(forKey: Keys.avatarURL) as? String
|
||||
|
||||
let avatarURLString = aDecoder.decodeObject(forKey: Keys.avatarURL) as? String
|
||||
avatarURL = avatarURLString.flatMap { URL(string: $0) }
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import Foundation
|
||||
import KeychainAccess
|
||||
import MatrixRustSDK
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol ClassicAppManagerProtocol {
|
||||
func loadAccounts() throws -> [ClassicAppAccount]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A media provider that can download a `ClassicAppAccount`'s avatar image.
|
||||
class ClassicAppMediaLoader: MediaLoaderProtocol {
|
||||
let classicAppAccount: ClassicAppAccount
|
||||
let urlSession: URLSession
|
||||
|
||||
init(classicAppAccount: ClassicAppAccount, urlSession: URLSession = .shared) {
|
||||
self.classicAppAccount = classicAppAccount
|
||||
self.urlSession = urlSession
|
||||
}
|
||||
|
||||
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data {
|
||||
try await loadMedia(source: source)
|
||||
}
|
||||
|
||||
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data {
|
||||
try await loadMedia(source: source, width: width, height: height)
|
||||
}
|
||||
|
||||
func loadMediaFileForSource(_ source: MediaSourceProxy, filename: String?) async throws -> MediaFileHandleProxy {
|
||||
throw MediaLoaderError.notSupported // Not needed for LoadableImage
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func loadMedia(source: MediaSourceProxy, width: UInt? = nil, height: UInt? = nil) async throws -> Data {
|
||||
guard let mxcURL = source.url else {
|
||||
MXLog.error("The provided media source is missing the URL")
|
||||
throw MediaLoaderError.invalidURL
|
||||
}
|
||||
|
||||
guard let request = mediaURLRequest(from: mxcURL, width: width, height: height) else {
|
||||
MXLog.error("Failed to construct media URL for source: \(mxcURL)")
|
||||
throw MediaLoaderError.invalidURL
|
||||
}
|
||||
|
||||
let (data, response) = try await urlSession.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
|
||||
MXLog.error("Unexpected response fetching ClassicAppAccount media: \(source.url?.absoluteString ?? "nil")")
|
||||
throw MediaLoaderError.unexpectedResponse
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/// Constructs an authenticated `URLRequest` to download Matrix media from the homeserver.
|
||||
///
|
||||
/// - When `width` and `height` are provided, converts `mxc://{serverName}/{mediaId}` to
|
||||
/// `{homeserverURL}/_matrix/client/v1/media/thumbnail/{serverName}/{mediaId}?width=…&height=…`
|
||||
/// - Otherwise converts to `{homeserverURL}/_matrix/client/v1/media/download/{serverName}/{mediaId}`
|
||||
///
|
||||
/// Sets the `Authorization: Bearer` header when an access token is provided.
|
||||
private func mediaURLRequest(from mxcURL: URL, width: UInt?, height: UInt?) -> URLRequest? {
|
||||
guard mxcURL.scheme == "mxc" else { return nil }
|
||||
|
||||
let serverName = mxcURL.host() ?? ""
|
||||
let mediaID = mxcURL.path().trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
|
||||
guard !serverName.isEmpty, !mediaID.isEmpty else { return nil }
|
||||
|
||||
let isThumbnail = width != nil && height != nil
|
||||
let endpoint = isThumbnail ? "_matrix/client/v1/media/thumbnail" : "_matrix/client/v1/media/download"
|
||||
|
||||
var components = URLComponents(url: classicAppAccount.homeserverURL
|
||||
.appending(path: endpoint)
|
||||
.appending(path: serverName)
|
||||
.appending(path: mediaID), resolvingAgainstBaseURL: false)
|
||||
|
||||
if let width, let height {
|
||||
components?.queryItems = [
|
||||
URLQueryItem(name: "width", value: String(width)),
|
||||
URLQueryItem(name: "height", value: String(height))
|
||||
]
|
||||
}
|
||||
|
||||
guard let url = components?.url else { return nil }
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue("Bearer \(classicAppAccount.accessToken)", forHTTPHeaderField: "Authorization")
|
||||
return request
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@ import Foundation
|
||||
|
||||
enum MediaLoaderError: Error {
|
||||
case missingClient
|
||||
case notSupported
|
||||
case invalidURL
|
||||
case unexpectedResponse
|
||||
}
|
||||
|
||||
// sourcery: AutoMockable
|
||||
|
||||
Reference in New Issue
Block a user