mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Handle media previews and invite avatars through the account data (#4142)
* added account data media display policy handling to NSE * pr suggestions * update compound * code improvement
This commit is contained in:
@@ -8836,7 +8836,7 @@
|
|||||||
repositoryURL = "https://github.com/element-hq/compound-ios";
|
repositoryURL = "https://github.com/element-hq/compound-ios";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = revision;
|
kind = revision;
|
||||||
revision = 81ba8bd8b3971beac252129c5466d7eac2f2ec31;
|
revision = 139d4f3d1ed19cc513686e5e39750ecef54c8403;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
F76A08D0EA29A07A54F4EB4D /* XCRemoteSwiftPackageReference "swift-collections" */ = {
|
F76A08D0EA29A07A54F4EB4D /* XCRemoteSwiftPackageReference "swift-collections" */ = {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/element-hq/compound-ios",
|
"location" : "https://github.com/element-hq/compound-ios",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "81ba8bd8b3971beac252129c5466d7eac2f2ec31"
|
"revision" : "139d4f3d1ed19cc513686e5e39750ecef54c8403"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -203,8 +203,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "e2e28f4e56e1769c2ec3c61c9355fc64eb7a535a",
|
"revision" : "3dd282d3269b061853a3b3bcd23a509d2aa166ce",
|
||||||
"version" : "5.3.0"
|
"version" : "6.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,11 +30,12 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
private var userSession: UserSessionProtocol? {
|
private var userSession: UserSessionProtocol? {
|
||||||
didSet {
|
didSet {
|
||||||
userSessionObserver?.cancel()
|
userSessionObserver?.cancel()
|
||||||
if userSession != nil {
|
if let userSession {
|
||||||
configureElementCallService()
|
configureElementCallService()
|
||||||
configureNotificationManager()
|
configureNotificationManager()
|
||||||
observeUserSessionChanges()
|
observeUserSessionChanges()
|
||||||
startSync()
|
startSync()
|
||||||
|
performSettingsToAccountDataMigration(userSession: userSession)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -399,17 +400,54 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
Tracing.deleteLogFiles()
|
Tracing.deleteLogFiles()
|
||||||
MXLog.info("Migrating to v1.6.7, log files have been wiped")
|
MXLog.info("Migrating to v1.6.7, log files have been wiped")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This could be removed once the adotpion of 25.06.x is widespread.
|
||||||
|
private func performSettingsToAccountDataMigration(userSession: UserSessionProtocol) {
|
||||||
|
guard let userDefaults = UserDefaults(suiteName: InfoPlistReader.main.appGroupIdentifier) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if oldVersion < Version(25, 4, 2) {
|
let hideInviteAvatars = userDefaults.value(forKey: "hideInviteAvatars") as? Bool
|
||||||
MXLog.info("Migrating to v25.04.2, checking if hideTimelineMedia flag can be migrated to timelineMediaVisibility")
|
let timelineMediaVisibility = userDefaults
|
||||||
// Migration for the old `hideTimelineMedia` flag.
|
.data(forKey: "timelineMediaVisibility")
|
||||||
if let userDefaults = UserDefaults(suiteName: InfoPlistReader.main.appGroupIdentifier),
|
.flatMap {
|
||||||
let hideTimelineMedia = userDefaults.value(forKey: "hideTimelineMedia") as? Bool {
|
try? JSONDecoder().decode(TimelineMediaVisibility.self, from: $0)
|
||||||
appSettings.timelineMediaVisibility = hideTimelineMedia ? .never : .always
|
}
|
||||||
|
let hideTimelineMedia = userDefaults.value(forKey: "hideTimelineMedia") as? Bool
|
||||||
|
|
||||||
|
guard hideInviteAvatars != nil || timelineMediaVisibility != nil || hideTimelineMedia != nil else {
|
||||||
|
// No migration needed, no local settings found.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Task {
|
||||||
|
switch await userSession.clientProxy.fetchMediaPreviewConfiguration() {
|
||||||
|
case let .success(config):
|
||||||
|
guard config == nil else {
|
||||||
|
// Found a server configuration, no need to migrate.
|
||||||
|
userDefaults.removeObject(forKey: "hideInviteAvatars")
|
||||||
|
userDefaults.removeObject(forKey: "timelineMediaVisibility")
|
||||||
|
userDefaults.removeObject(forKey: "hideTimelineMedia")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let hideInviteAvatars, case .success = await userSession.clientProxy.setHideInviteAvatars(hideInviteAvatars) {
|
||||||
|
userDefaults.removeObject(forKey: "hideInviteAvatars")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let timelineMediaVisibility, case .success = await userSession.clientProxy.setTimelineMediaVisibility(timelineMediaVisibility) {
|
||||||
|
userDefaults.removeObject(forKey: "timelineMediaVisibility")
|
||||||
|
} else if let hideTimelineMedia, case .success = await userSession.clientProxy.setTimelineMediaVisibility(hideTimelineMedia ? .never : .always) {
|
||||||
|
userDefaults.removeObject(forKey: "hideTimelineMedia")
|
||||||
|
}
|
||||||
|
case let .failure(error):
|
||||||
|
MXLog.error("Could not perform migration, failed to fetch media preview config: \(error)")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears the keychain, app support directory etc ready for a fresh use.
|
/// Clears the keychain, app support directory etc ready for a fresh use.
|
||||||
/// - Parameter includingSettings: Whether to additionally wipe the user's app settings too.
|
/// - Parameter includingSettings: Whether to additionally wipe the user's app settings too.
|
||||||
private func wipeUserData(includingSettings: Bool = false) {
|
private func wipeUserData(includingSettings: Bool = false) {
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ protocol CommonSettingsProtocol {
|
|||||||
var logLevel: LogLevel { get }
|
var logLevel: LogLevel { get }
|
||||||
var traceLogPacks: Set<TraceLogPack> { get }
|
var traceLogPacks: Set<TraceLogPack> { get }
|
||||||
var enableOnlySignedDeviceIsolationMode: Bool { get }
|
var enableOnlySignedDeviceIsolationMode: Bool { get }
|
||||||
var hideInviteAvatars: Bool { get }
|
|
||||||
var timelineMediaVisibility: TimelineMediaVisibility { get }
|
|
||||||
var hideQuietNotificationAlerts: Bool { get }
|
var hideQuietNotificationAlerts: Bool { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,8 +44,6 @@ final class AppSettings {
|
|||||||
case optimizeMediaUploads
|
case optimizeMediaUploads
|
||||||
case appAppearance
|
case appAppearance
|
||||||
case sharePresence
|
case sharePresence
|
||||||
case hideInviteAvatars
|
|
||||||
case timelineMediaVisibility
|
|
||||||
case isNewBloomEnabled
|
case isNewBloomEnabled
|
||||||
|
|
||||||
case elementCallBaseURLOverride
|
case elementCallBaseURLOverride
|
||||||
@@ -368,20 +364,8 @@ final class AppSettings {
|
|||||||
@UserPreference(key: UserDefaultsKeys.enableOnlySignedDeviceIsolationMode, defaultValue: false, storageType: .userDefaults(store))
|
@UserPreference(key: UserDefaultsKeys.enableOnlySignedDeviceIsolationMode, defaultValue: false, storageType: .userDefaults(store))
|
||||||
var enableOnlySignedDeviceIsolationMode
|
var enableOnlySignedDeviceIsolationMode
|
||||||
|
|
||||||
@UserPreference(key: UserDefaultsKeys.hideInviteAvatars, defaultValue: false, storageType: .userDefaults(store))
|
|
||||||
var hideInviteAvatars
|
|
||||||
|
|
||||||
@UserPreference(key: UserDefaultsKeys.timelineMediaVisibility, defaultValue: TimelineMediaVisibility.always, storageType: .userDefaults(store))
|
|
||||||
var timelineMediaVisibility
|
|
||||||
|
|
||||||
@UserPreference(key: UserDefaultsKeys.hideQuietNotificationAlerts, defaultValue: false, storageType: .userDefaults(store))
|
@UserPreference(key: UserDefaultsKeys.hideQuietNotificationAlerts, defaultValue: false, storageType: .userDefaults(store))
|
||||||
var hideQuietNotificationAlerts
|
var hideQuietNotificationAlerts
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppSettings: CommonSettingsProtocol { }
|
extension AppSettings: CommonSettingsProtocol { }
|
||||||
|
|
||||||
enum TimelineMediaVisibility: Codable {
|
|
||||||
case always
|
|
||||||
case privateOnly
|
|
||||||
case never
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -226,7 +226,9 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
private func presentAdvancedSettings() {
|
private func presentAdvancedSettings() {
|
||||||
let coordinator = AdvancedSettingsScreenCoordinator(parameters: .init(appSettings: parameters.appSettings,
|
let coordinator = AdvancedSettingsScreenCoordinator(parameters: .init(appSettings: parameters.appSettings,
|
||||||
analytics: parameters.analytics))
|
analytics: parameters.analytics,
|
||||||
|
clientProxy: parameters.userSession.clientProxy,
|
||||||
|
userIndicatorController: parameters.userIndicatorController))
|
||||||
navigationStackCoordinator.push(coordinator)
|
navigationStackCoordinator.push(coordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ struct ClientProxyMockConfiguration {
|
|||||||
var roomDirectorySearchProxy: RoomDirectorySearchProxyProtocol?
|
var roomDirectorySearchProxy: RoomDirectorySearchProxyProtocol?
|
||||||
|
|
||||||
var recoveryState: SecureBackupRecoveryState = .enabled
|
var recoveryState: SecureBackupRecoveryState = .enabled
|
||||||
|
|
||||||
|
var timelineMediaVisibility = TimelineMediaVisibility.always
|
||||||
|
var hideInviteAvatars = false
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ClientProxyMockError: Error {
|
enum ClientProxyMockError: Error {
|
||||||
@@ -94,5 +97,8 @@ extension ClientProxyMock {
|
|||||||
userIdentityForReturnValue = .success(UserIdentityProxyMock(configuration: .init()))
|
userIdentityForReturnValue = .success(UserIdentityProxyMock(configuration: .init()))
|
||||||
|
|
||||||
underlyingIsReportRoomSupported = true
|
underlyingIsReportRoomSupported = true
|
||||||
|
|
||||||
|
underlyingTimelineMediaVisibilityPublisher = CurrentValueSubject<TimelineMediaVisibility, Never>(configuration.timelineMediaVisibility).asCurrentValuePublisher()
|
||||||
|
underlyingHideInviteAvatarsPublisher = CurrentValueSubject<Bool, Never>(configuration.hideInviteAvatars).asCurrentValuePublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2271,6 +2271,16 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
|
|||||||
set(value) { underlyingIgnoredUsersPublisher = value }
|
set(value) { underlyingIgnoredUsersPublisher = value }
|
||||||
}
|
}
|
||||||
var underlyingIgnoredUsersPublisher: CurrentValuePublisher<[String]?, Never>!
|
var underlyingIgnoredUsersPublisher: CurrentValuePublisher<[String]?, Never>!
|
||||||
|
var timelineMediaVisibilityPublisher: CurrentValuePublisher<TimelineMediaVisibility, Never> {
|
||||||
|
get { return underlyingTimelineMediaVisibilityPublisher }
|
||||||
|
set(value) { underlyingTimelineMediaVisibilityPublisher = value }
|
||||||
|
}
|
||||||
|
var underlyingTimelineMediaVisibilityPublisher: CurrentValuePublisher<TimelineMediaVisibility, Never>!
|
||||||
|
var hideInviteAvatarsPublisher: CurrentValuePublisher<Bool, Never> {
|
||||||
|
get { return underlyingHideInviteAvatarsPublisher }
|
||||||
|
set(value) { underlyingHideInviteAvatarsPublisher = value }
|
||||||
|
}
|
||||||
|
var underlyingHideInviteAvatarsPublisher: CurrentValuePublisher<Bool, Never>!
|
||||||
var pusherNotificationClientIdentifier: String?
|
var pusherNotificationClientIdentifier: String?
|
||||||
var roomSummaryProvider: RoomSummaryProviderProtocol {
|
var roomSummaryProvider: RoomSummaryProviderProtocol {
|
||||||
get { return underlyingRoomSummaryProvider }
|
get { return underlyingRoomSummaryProvider }
|
||||||
@@ -4363,6 +4373,70 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
|
|||||||
return clearCachesReturnValue
|
return clearCachesReturnValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//MARK: - fetchMediaPreviewConfiguration
|
||||||
|
|
||||||
|
var fetchMediaPreviewConfigurationUnderlyingCallsCount = 0
|
||||||
|
var fetchMediaPreviewConfigurationCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return fetchMediaPreviewConfigurationUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = fetchMediaPreviewConfigurationUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
fetchMediaPreviewConfigurationUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
fetchMediaPreviewConfigurationUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var fetchMediaPreviewConfigurationCalled: Bool {
|
||||||
|
return fetchMediaPreviewConfigurationCallsCount > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var fetchMediaPreviewConfigurationUnderlyingReturnValue: Result<MediaPreviewConfig?, ClientProxyError>!
|
||||||
|
var fetchMediaPreviewConfigurationReturnValue: Result<MediaPreviewConfig?, ClientProxyError>! {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return fetchMediaPreviewConfigurationUnderlyingReturnValue
|
||||||
|
} else {
|
||||||
|
var returnValue: Result<MediaPreviewConfig?, ClientProxyError>? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = fetchMediaPreviewConfigurationUnderlyingReturnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
fetchMediaPreviewConfigurationUnderlyingReturnValue = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
fetchMediaPreviewConfigurationUnderlyingReturnValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var fetchMediaPreviewConfigurationClosure: (() async -> Result<MediaPreviewConfig?, ClientProxyError>)?
|
||||||
|
|
||||||
|
func fetchMediaPreviewConfiguration() async -> Result<MediaPreviewConfig?, ClientProxyError> {
|
||||||
|
fetchMediaPreviewConfigurationCallsCount += 1
|
||||||
|
if let fetchMediaPreviewConfigurationClosure = fetchMediaPreviewConfigurationClosure {
|
||||||
|
return await fetchMediaPreviewConfigurationClosure()
|
||||||
|
} else {
|
||||||
|
return fetchMediaPreviewConfigurationReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
//MARK: - ignoreUser
|
//MARK: - ignoreUser
|
||||||
|
|
||||||
var ignoreUserUnderlyingCallsCount = 0
|
var ignoreUserUnderlyingCallsCount = 0
|
||||||
@@ -5103,6 +5177,146 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
|
|||||||
return userIdentityForReturnValue
|
return userIdentityForReturnValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//MARK: - setTimelineMediaVisibility
|
||||||
|
|
||||||
|
var setTimelineMediaVisibilityUnderlyingCallsCount = 0
|
||||||
|
var setTimelineMediaVisibilityCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return setTimelineMediaVisibilityUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = setTimelineMediaVisibilityUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
setTimelineMediaVisibilityUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
setTimelineMediaVisibilityUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var setTimelineMediaVisibilityCalled: Bool {
|
||||||
|
return setTimelineMediaVisibilityCallsCount > 0
|
||||||
|
}
|
||||||
|
var setTimelineMediaVisibilityReceivedValue: TimelineMediaVisibility?
|
||||||
|
var setTimelineMediaVisibilityReceivedInvocations: [TimelineMediaVisibility] = []
|
||||||
|
|
||||||
|
var setTimelineMediaVisibilityUnderlyingReturnValue: Result<Void, ClientProxyError>!
|
||||||
|
var setTimelineMediaVisibilityReturnValue: Result<Void, ClientProxyError>! {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return setTimelineMediaVisibilityUnderlyingReturnValue
|
||||||
|
} else {
|
||||||
|
var returnValue: Result<Void, ClientProxyError>? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = setTimelineMediaVisibilityUnderlyingReturnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
setTimelineMediaVisibilityUnderlyingReturnValue = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
setTimelineMediaVisibilityUnderlyingReturnValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var setTimelineMediaVisibilityClosure: ((TimelineMediaVisibility) async -> Result<Void, ClientProxyError>)?
|
||||||
|
|
||||||
|
func setTimelineMediaVisibility(_ value: TimelineMediaVisibility) async -> Result<Void, ClientProxyError> {
|
||||||
|
setTimelineMediaVisibilityCallsCount += 1
|
||||||
|
setTimelineMediaVisibilityReceivedValue = value
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.setTimelineMediaVisibilityReceivedInvocations.append(value)
|
||||||
|
}
|
||||||
|
if let setTimelineMediaVisibilityClosure = setTimelineMediaVisibilityClosure {
|
||||||
|
return await setTimelineMediaVisibilityClosure(value)
|
||||||
|
} else {
|
||||||
|
return setTimelineMediaVisibilityReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - setHideInviteAvatars
|
||||||
|
|
||||||
|
var setHideInviteAvatarsUnderlyingCallsCount = 0
|
||||||
|
var setHideInviteAvatarsCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return setHideInviteAvatarsUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = setHideInviteAvatarsUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
setHideInviteAvatarsUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
setHideInviteAvatarsUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var setHideInviteAvatarsCalled: Bool {
|
||||||
|
return setHideInviteAvatarsCallsCount > 0
|
||||||
|
}
|
||||||
|
var setHideInviteAvatarsReceivedValue: Bool?
|
||||||
|
var setHideInviteAvatarsReceivedInvocations: [Bool] = []
|
||||||
|
|
||||||
|
var setHideInviteAvatarsUnderlyingReturnValue: Result<Void, ClientProxyError>!
|
||||||
|
var setHideInviteAvatarsReturnValue: Result<Void, ClientProxyError>! {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return setHideInviteAvatarsUnderlyingReturnValue
|
||||||
|
} else {
|
||||||
|
var returnValue: Result<Void, ClientProxyError>? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = setHideInviteAvatarsUnderlyingReturnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
setHideInviteAvatarsUnderlyingReturnValue = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
setHideInviteAvatarsUnderlyingReturnValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var setHideInviteAvatarsClosure: ((Bool) async -> Result<Void, ClientProxyError>)?
|
||||||
|
|
||||||
|
func setHideInviteAvatars(_ value: Bool) async -> Result<Void, ClientProxyError> {
|
||||||
|
setHideInviteAvatarsCallsCount += 1
|
||||||
|
setHideInviteAvatarsReceivedValue = value
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.setHideInviteAvatarsReceivedInvocations.append(value)
|
||||||
|
}
|
||||||
|
if let setHideInviteAvatarsClosure = setHideInviteAvatarsClosure {
|
||||||
|
return await setHideInviteAvatarsClosure(value)
|
||||||
|
} else {
|
||||||
|
return setHideInviteAvatarsReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
//MARK: - loadMediaContentForSource
|
//MARK: - loadMediaContentForSource
|
||||||
|
|
||||||
var loadMediaContentForSourceThrowableError: Error?
|
var loadMediaContentForSourceThrowableError: Error?
|
||||||
|
|||||||
@@ -1050,6 +1050,75 @@ open class ClientSDKMock: MatrixRustSDK.Client, @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//MARK: - fetchMediaPreviewConfig
|
||||||
|
|
||||||
|
open var fetchMediaPreviewConfigThrowableError: Error?
|
||||||
|
var fetchMediaPreviewConfigUnderlyingCallsCount = 0
|
||||||
|
open var fetchMediaPreviewConfigCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return fetchMediaPreviewConfigUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = fetchMediaPreviewConfigUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
fetchMediaPreviewConfigUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
fetchMediaPreviewConfigUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open var fetchMediaPreviewConfigCalled: Bool {
|
||||||
|
return fetchMediaPreviewConfigCallsCount > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var fetchMediaPreviewConfigUnderlyingReturnValue: MediaPreviewConfig?
|
||||||
|
open var fetchMediaPreviewConfigReturnValue: MediaPreviewConfig? {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return fetchMediaPreviewConfigUnderlyingReturnValue
|
||||||
|
} else {
|
||||||
|
var returnValue: MediaPreviewConfig?? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = fetchMediaPreviewConfigUnderlyingReturnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
fetchMediaPreviewConfigUnderlyingReturnValue = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
fetchMediaPreviewConfigUnderlyingReturnValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open var fetchMediaPreviewConfigClosure: (() async throws -> MediaPreviewConfig?)?
|
||||||
|
|
||||||
|
open override func fetchMediaPreviewConfig() async throws -> MediaPreviewConfig? {
|
||||||
|
if let error = fetchMediaPreviewConfigThrowableError {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
fetchMediaPreviewConfigCallsCount += 1
|
||||||
|
if let fetchMediaPreviewConfigClosure = fetchMediaPreviewConfigClosure {
|
||||||
|
return try await fetchMediaPreviewConfigClosure()
|
||||||
|
} else {
|
||||||
|
return fetchMediaPreviewConfigReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//MARK: - getDmRoom
|
//MARK: - getDmRoom
|
||||||
|
|
||||||
open var getDmRoomUserIdThrowableError: Error?
|
open var getDmRoomUserIdThrowableError: Error?
|
||||||
@@ -1156,13 +1225,13 @@ open class ClientSDKMock: MatrixRustSDK.Client, @unchecked Sendable {
|
|||||||
return getInviteAvatarsDisplayPolicyCallsCount > 0
|
return getInviteAvatarsDisplayPolicyCallsCount > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var getInviteAvatarsDisplayPolicyUnderlyingReturnValue: InviteAvatars!
|
var getInviteAvatarsDisplayPolicyUnderlyingReturnValue: InviteAvatars?
|
||||||
open var getInviteAvatarsDisplayPolicyReturnValue: InviteAvatars! {
|
open var getInviteAvatarsDisplayPolicyReturnValue: InviteAvatars? {
|
||||||
get {
|
get {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
return getInviteAvatarsDisplayPolicyUnderlyingReturnValue
|
return getInviteAvatarsDisplayPolicyUnderlyingReturnValue
|
||||||
} else {
|
} else {
|
||||||
var returnValue: InviteAvatars? = nil
|
var returnValue: InviteAvatars?? = nil
|
||||||
DispatchQueue.main.sync {
|
DispatchQueue.main.sync {
|
||||||
returnValue = getInviteAvatarsDisplayPolicyUnderlyingReturnValue
|
returnValue = getInviteAvatarsDisplayPolicyUnderlyingReturnValue
|
||||||
}
|
}
|
||||||
@@ -1180,9 +1249,9 @@ open class ClientSDKMock: MatrixRustSDK.Client, @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
open var getInviteAvatarsDisplayPolicyClosure: (() async throws -> InviteAvatars)?
|
open var getInviteAvatarsDisplayPolicyClosure: (() async throws -> InviteAvatars?)?
|
||||||
|
|
||||||
open override func getInviteAvatarsDisplayPolicy() async throws -> InviteAvatars {
|
open override func getInviteAvatarsDisplayPolicy() async throws -> InviteAvatars? {
|
||||||
if let error = getInviteAvatarsDisplayPolicyThrowableError {
|
if let error = getInviteAvatarsDisplayPolicyThrowableError {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@@ -1375,13 +1444,13 @@ open class ClientSDKMock: MatrixRustSDK.Client, @unchecked Sendable {
|
|||||||
return getMediaPreviewDisplayPolicyCallsCount > 0
|
return getMediaPreviewDisplayPolicyCallsCount > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var getMediaPreviewDisplayPolicyUnderlyingReturnValue: MediaPreviews!
|
var getMediaPreviewDisplayPolicyUnderlyingReturnValue: MediaPreviews?
|
||||||
open var getMediaPreviewDisplayPolicyReturnValue: MediaPreviews! {
|
open var getMediaPreviewDisplayPolicyReturnValue: MediaPreviews? {
|
||||||
get {
|
get {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
return getMediaPreviewDisplayPolicyUnderlyingReturnValue
|
return getMediaPreviewDisplayPolicyUnderlyingReturnValue
|
||||||
} else {
|
} else {
|
||||||
var returnValue: MediaPreviews? = nil
|
var returnValue: MediaPreviews?? = nil
|
||||||
DispatchQueue.main.sync {
|
DispatchQueue.main.sync {
|
||||||
returnValue = getMediaPreviewDisplayPolicyUnderlyingReturnValue
|
returnValue = getMediaPreviewDisplayPolicyUnderlyingReturnValue
|
||||||
}
|
}
|
||||||
@@ -1399,9 +1468,9 @@ open class ClientSDKMock: MatrixRustSDK.Client, @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
open var getMediaPreviewDisplayPolicyClosure: (() async throws -> MediaPreviews)?
|
open var getMediaPreviewDisplayPolicyClosure: (() async throws -> MediaPreviews?)?
|
||||||
|
|
||||||
open override func getMediaPreviewDisplayPolicy() async throws -> MediaPreviews {
|
open override func getMediaPreviewDisplayPolicy() async throws -> MediaPreviews? {
|
||||||
if let error = getMediaPreviewDisplayPolicyThrowableError {
|
if let error = getMediaPreviewDisplayPolicyThrowableError {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ extension SDKListener: QrLoginProgressListener where T == QrLoginProgress {
|
|||||||
|
|
||||||
// MARK: ClientProxy
|
// MARK: ClientProxy
|
||||||
|
|
||||||
|
extension SDKListener: MediaPreviewConfigListener where T == MediaPreviewConfig? {
|
||||||
|
func onChange(mediaPreviewConfig: MediaPreviewConfig?) { onUpdateClosure(mediaPreviewConfig) }
|
||||||
|
}
|
||||||
|
|
||||||
extension SDKListener: SyncServiceStateObserver where T == SyncServiceState {
|
extension SDKListener: SyncServiceStateObserver where T == SyncServiceState {
|
||||||
func onUpdate(state: SyncServiceState) { onUpdateClosure(state) }
|
func onUpdate(state: SyncServiceState) { onUpdateClosure(state) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
appSettings.$hideInviteAvatars
|
userSession.clientProxy.hideInviteAvatarsPublisher
|
||||||
|
.removeDuplicates()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
.weakAssign(to: \.state.hideInviteAvatars, on: self)
|
.weakAssign(to: \.state.hideInviteAvatars, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
|||||||
|
|
||||||
super.init(initialViewState: JoinRoomScreenViewState(roomID: roomID), mediaProvider: mediaProvider)
|
super.init(initialViewState: JoinRoomScreenViewState(roomID: roomID), mediaProvider: mediaProvider)
|
||||||
|
|
||||||
appSettings.$hideInviteAvatars
|
clientProxy.hideInviteAvatarsPublisher
|
||||||
|
.removeDuplicates()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
.weakAssign(to: \.state.hideInviteAvatars, on: self)
|
.weakAssign(to: \.state.hideInviteAvatars, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
|||||||
@@ -363,9 +363,8 @@ struct JoinRoomScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
static func makeViewModel(mode: JoinRoomScreenMode, hideInviteAvatars: Bool = false) -> JoinRoomScreenViewModel {
|
static func makeViewModel(mode: JoinRoomScreenMode, hideInviteAvatars: Bool = false) -> JoinRoomScreenViewModel {
|
||||||
let appSettings = AppSettings()
|
let appSettings = AppSettings()
|
||||||
appSettings.knockingEnabled = true
|
appSettings.knockingEnabled = true
|
||||||
appSettings.hideInviteAvatars = hideInviteAvatars
|
|
||||||
|
|
||||||
let clientProxy = ClientProxyMock(.init())
|
let clientProxy = ClientProxyMock(.init(hideInviteAvatars: hideInviteAvatars))
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
case .unknown:
|
case .unknown:
|
||||||
|
|||||||
+2
-1
@@ -158,7 +158,8 @@ struct SecurityAndPrivacyScreen: View {
|
|||||||
|
|
||||||
Section {
|
Section {
|
||||||
ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyRoomDirectoryVisibilityToggleTitle),
|
ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyRoomDirectoryVisibilityToggleTitle),
|
||||||
details: .isWaiting(context.desiredSettings.isVisibileInRoomDirectory == nil),
|
details:
|
||||||
|
context.desiredSettings.isVisibileInRoomDirectory == nil ? .isWaiting(true) : nil,
|
||||||
kind: context.desiredSettings.isVisibileInRoomDirectory == nil ? .label : .toggle(binding))
|
kind: context.desiredSettings.isVisibileInRoomDirectory == nil ? .label : .toggle(binding))
|
||||||
} footer: {
|
} footer: {
|
||||||
Text(L10n.screenSecurityAndPrivacyRoomDirectoryVisibilitySectionFooter(context.viewState.serverName))
|
Text(L10n.screenSecurityAndPrivacyRoomDirectoryVisibilitySectionFooter(context.viewState.serverName))
|
||||||
|
|||||||
+6
-1
@@ -11,13 +11,18 @@ import SwiftUI
|
|||||||
struct AdvancedSettingsScreenCoordinatorParameters {
|
struct AdvancedSettingsScreenCoordinatorParameters {
|
||||||
let appSettings: AppSettings
|
let appSettings: AppSettings
|
||||||
let analytics: AnalyticsService
|
let analytics: AnalyticsService
|
||||||
|
let clientProxy: ClientProxyProtocol
|
||||||
|
let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
final class AdvancedSettingsScreenCoordinator: CoordinatorProtocol {
|
final class AdvancedSettingsScreenCoordinator: CoordinatorProtocol {
|
||||||
private var viewModel: AdvancedSettingsScreenViewModelProtocol
|
private var viewModel: AdvancedSettingsScreenViewModelProtocol
|
||||||
|
|
||||||
init(parameters: AdvancedSettingsScreenCoordinatorParameters) {
|
init(parameters: AdvancedSettingsScreenCoordinatorParameters) {
|
||||||
viewModel = AdvancedSettingsScreenViewModel(advancedSettings: parameters.appSettings, analytics: parameters.analytics)
|
viewModel = AdvancedSettingsScreenViewModel(advancedSettings: parameters.appSettings,
|
||||||
|
analytics: parameters.analytics,
|
||||||
|
clientProxy: parameters.clientProxy,
|
||||||
|
userIndicatorController: parameters.userIndicatorController)
|
||||||
}
|
}
|
||||||
|
|
||||||
func toPresentable() -> AnyView {
|
func toPresentable() -> AnyView {
|
||||||
|
|||||||
+6
-2
@@ -8,6 +8,10 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct AdvancedSettingsScreenViewState: BindableState {
|
struct AdvancedSettingsScreenViewState: BindableState {
|
||||||
|
var timelineMediaVisibility: TimelineMediaVisibility
|
||||||
|
var hideInviteAvatars: Bool
|
||||||
|
var isWaitingTimelineMediaVisibility = false
|
||||||
|
var isWaitingHideInviteAvatars = false
|
||||||
var bindings: AdvancedSettingsScreenViewStateBindings
|
var bindings: AdvancedSettingsScreenViewStateBindings
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,14 +32,14 @@ struct AdvancedSettingsScreenViewStateBindings {
|
|||||||
|
|
||||||
enum AdvancedSettingsScreenViewAction {
|
enum AdvancedSettingsScreenViewAction {
|
||||||
case optimizeMediaUploadsChanged
|
case optimizeMediaUploadsChanged
|
||||||
|
case updateTimelineMediaVisibility(TimelineMediaVisibility)
|
||||||
|
case updateHideInviteAvatars(Bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol AdvancedSettingsProtocol: AnyObject {
|
protocol AdvancedSettingsProtocol: AnyObject {
|
||||||
var viewSourceEnabled: Bool { get set }
|
var viewSourceEnabled: Bool { get set }
|
||||||
var appAppearance: AppAppearance { get set }
|
var appAppearance: AppAppearance { get set }
|
||||||
var sharePresence: Bool { get set }
|
var sharePresence: Bool { get set }
|
||||||
var timelineMediaVisibility: TimelineMediaVisibility { get set }
|
|
||||||
var hideInviteAvatars: Bool { get set }
|
|
||||||
var optimizeMediaUploads: Bool { get set }
|
var optimizeMediaUploads: Bool { get set }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+72
-2
@@ -12,12 +12,36 @@ typealias AdvancedSettingsScreenViewModelType = StateStoreViewModelV2<AdvancedSe
|
|||||||
|
|
||||||
class AdvancedSettingsScreenViewModel: AdvancedSettingsScreenViewModelType, AdvancedSettingsScreenViewModelProtocol {
|
class AdvancedSettingsScreenViewModel: AdvancedSettingsScreenViewModelType, AdvancedSettingsScreenViewModelProtocol {
|
||||||
private let analytics: AnalyticsService
|
private let analytics: AnalyticsService
|
||||||
|
private let clientProxy: ClientProxyProtocol
|
||||||
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
|
|
||||||
init(advancedSettings: AdvancedSettingsProtocol, analytics: AnalyticsService) {
|
private var timelineMediaVisibilityTask: Task<Void, Never>?
|
||||||
|
private var hideInviteAvatarsTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
init(advancedSettings: AdvancedSettingsProtocol,
|
||||||
|
analytics: AnalyticsService,
|
||||||
|
clientProxy: ClientProxyProtocol,
|
||||||
|
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||||
self.analytics = analytics
|
self.analytics = analytics
|
||||||
|
self.clientProxy = clientProxy
|
||||||
|
self.userIndicatorController = userIndicatorController
|
||||||
|
|
||||||
let state = AdvancedSettingsScreenViewState(bindings: .init(advancedSettings: advancedSettings))
|
let state = AdvancedSettingsScreenViewState(timelineMediaVisibility: clientProxy.timelineMediaVisibilityPublisher.value,
|
||||||
|
hideInviteAvatars: clientProxy.hideInviteAvatarsPublisher.value,
|
||||||
|
bindings: .init(advancedSettings: advancedSettings))
|
||||||
super.init(initialViewState: state)
|
super.init(initialViewState: state)
|
||||||
|
|
||||||
|
clientProxy.timelineMediaVisibilityPublisher
|
||||||
|
.removeDuplicates()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.weakAssign(to: \.state.timelineMediaVisibility, on: self)
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
clientProxy.hideInviteAvatarsPublisher
|
||||||
|
.removeDuplicates()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.weakAssign(to: \.state.hideInviteAvatars, on: self)
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func process(viewAction: AdvancedSettingsScreenViewAction) {
|
override func process(viewAction: AdvancedSettingsScreenViewAction) {
|
||||||
@@ -25,6 +49,52 @@ class AdvancedSettingsScreenViewModel: AdvancedSettingsScreenViewModelType, Adva
|
|||||||
case .optimizeMediaUploadsChanged:
|
case .optimizeMediaUploadsChanged:
|
||||||
// Note: Using a view action here as sinking the AppSettings publisher tracks the initial value.
|
// Note: Using a view action here as sinking the AppSettings publisher tracks the initial value.
|
||||||
analytics.trackInteraction(name: state.bindings.optimizeMediaUploads ? .MobileSettingsOptimizeMediaUploadsEnabled : .MobileSettingsOptimizeMediaUploadsDisabled)
|
analytics.trackInteraction(name: state.bindings.optimizeMediaUploads ? .MobileSettingsOptimizeMediaUploadsEnabled : .MobileSettingsOptimizeMediaUploadsDisabled)
|
||||||
|
case let .updateHideInviteAvatars(value):
|
||||||
|
hideInviteAvatarsTask = Task { [weak self] in await self?.updateHideInviteAvatars(value) }
|
||||||
|
case let .updateTimelineMediaVisibility(value):
|
||||||
|
timelineMediaVisibilityTask = Task { [weak self] in await self?.updateTimelineMediaVisibility(value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTimelineMediaVisibility(_ value: TimelineMediaVisibility) async {
|
||||||
|
defer {
|
||||||
|
timelineMediaVisibilityTask = nil
|
||||||
|
state.isWaitingTimelineMediaVisibility = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let previousState = state.timelineMediaVisibility
|
||||||
|
state.isWaitingTimelineMediaVisibility = true
|
||||||
|
state.timelineMediaVisibility = value
|
||||||
|
// If the other value is updating wait also for it to finish
|
||||||
|
await hideInviteAvatarsTask?.value
|
||||||
|
|
||||||
|
switch await clientProxy.setTimelineMediaVisibility(value) {
|
||||||
|
case .success:
|
||||||
|
break
|
||||||
|
case .failure:
|
||||||
|
state.timelineMediaVisibility = previousState
|
||||||
|
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateHideInviteAvatars(_ value: Bool) async {
|
||||||
|
defer {
|
||||||
|
hideInviteAvatarsTask = nil
|
||||||
|
state.isWaitingHideInviteAvatars = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let previousState = state.hideInviteAvatars
|
||||||
|
state.isWaitingHideInviteAvatars = true
|
||||||
|
state.hideInviteAvatars = value
|
||||||
|
// If the other value is updating wait also for it to finish
|
||||||
|
await timelineMediaVisibilityTask?.value
|
||||||
|
|
||||||
|
switch await clientProxy.setHideInviteAvatars(value) {
|
||||||
|
case .success:
|
||||||
|
break
|
||||||
|
case .failure:
|
||||||
|
state.hideInviteAvatars = previousState
|
||||||
|
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-3
@@ -42,21 +42,39 @@ struct AdvancedSettingsScreen: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
private var moderationAndSafetySection: some View {
|
private var moderationAndSafetySection: some View {
|
||||||
|
let binding = Binding(get: {
|
||||||
|
context.viewState.hideInviteAvatars
|
||||||
|
}, set: { newValue in
|
||||||
|
context.send(viewAction: .updateHideInviteAvatars(newValue))
|
||||||
|
})
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
ListRow(label: .plain(title: L10n.screenAdvancedSettingsHideInviteAvatarsToggleTitle),
|
ListRow(label: .plain(title: L10n.screenAdvancedSettingsHideInviteAvatarsToggleTitle),
|
||||||
kind: .toggle($context.hideInviteAvatars))
|
details: context.viewState.isWaitingHideInviteAvatars ? .isWaiting(true) : nil,
|
||||||
|
kind: .toggle(binding))
|
||||||
|
.disabled(context.viewState.isWaitingHideInviteAvatars)
|
||||||
} header: {
|
} header: {
|
||||||
Text(L10n.screenAdvancedSettingsModerationAndSafetySectionTitle)
|
Text(L10n.screenAdvancedSettingsModerationAndSafetySectionTitle)
|
||||||
.compoundListSectionHeader()
|
.compoundListSectionHeader()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
private var timelineMediaSection: some View {
|
private var timelineMediaSection: some View {
|
||||||
|
let binding = Binding(get: {
|
||||||
|
context.viewState.timelineMediaVisibility
|
||||||
|
}, set: { newValue in
|
||||||
|
context.send(viewAction: .updateTimelineMediaVisibility(newValue))
|
||||||
|
})
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
ListRow(label: .plain(title: L10n.screenAdvancedSettingsShowMediaTimelineTitle),
|
ListRow(label: .plain(title: L10n.screenAdvancedSettingsShowMediaTimelineTitle),
|
||||||
kind: .inlinePicker(selection: $context.timelineMediaVisibility,
|
details: .isWaiting(context.viewState.isWaitingTimelineMediaVisibility),
|
||||||
|
kind: .inlinePicker(selection: binding,
|
||||||
items: TimelineMediaVisibility.items))
|
items: TimelineMediaVisibility.items))
|
||||||
|
.disabled(context.viewState.isWaitingTimelineMediaVisibility)
|
||||||
} header: {
|
} header: {
|
||||||
Text(L10n.screenAdvancedSettingsShowMediaTimelineTitle)
|
Text(L10n.screenAdvancedSettingsShowMediaTimelineTitle)
|
||||||
.compoundListSectionHeader()
|
.compoundListSectionHeader()
|
||||||
@@ -84,7 +102,9 @@ private extension AppAppearance {
|
|||||||
|
|
||||||
struct AdvancedSettingsScreen_Previews: PreviewProvider, TestablePreview {
|
struct AdvancedSettingsScreen_Previews: PreviewProvider, TestablePreview {
|
||||||
static let viewModel = AdvancedSettingsScreenViewModel(advancedSettings: ServiceLocator.shared.settings,
|
static let viewModel = AdvancedSettingsScreenViewModel(advancedSettings: ServiceLocator.shared.settings,
|
||||||
analytics: ServiceLocator.shared.analytics)
|
analytics: ServiceLocator.shared.analytics,
|
||||||
|
clientProxy: ClientProxyMock(.init()),
|
||||||
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
AdvancedSettingsScreen(context: viewModel.context)
|
AdvancedSettingsScreen(context: viewModel.context)
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
timelineControllerFactory: timelineControllerFactory,
|
timelineControllerFactory: timelineControllerFactory,
|
||||||
clientProxy: clientProxy)
|
clientProxy: clientProxy)
|
||||||
|
|
||||||
let hideTimelineMedia = switch appSettings.timelineMediaVisibility {
|
let hideTimelineMedia = switch clientProxy.timelineMediaVisibilityPublisher.value {
|
||||||
case .always:
|
case .always:
|
||||||
false
|
false
|
||||||
case .privateOnly:
|
case .privateOnly:
|
||||||
@@ -525,7 +525,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
.weakAssign(to: \.state.isViewSourceEnabled, on: self)
|
.weakAssign(to: \.state.isViewSourceEnabled, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
appSettings.$timelineMediaVisibility
|
clientProxy.timelineMediaVisibilityPublisher
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.flatMap { [weak self] timelineMediaVisibility -> AnyPublisher<Bool, Never> in
|
.flatMap { [weak self] timelineMediaVisibility -> AnyPublisher<Bool, Never> in
|
||||||
switch timelineMediaVisibility {
|
switch timelineMediaVisibility {
|
||||||
@@ -538,10 +538,10 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
return roomProxy.infoPublisher
|
return roomProxy.infoPublisher
|
||||||
.map { !$0.isPrivate }
|
.map { !$0.isPrivate }
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
.weakAssign(to: \.state.hideTimelineMedia, on: self)
|
.weakAssign(to: \.state.hideTimelineMedia, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
// periphery:ignore - required for instance retention in the rust codebase
|
// periphery:ignore - required for instance retention in the rust codebase
|
||||||
private var sendQueueListenerTaskHandle: TaskHandle?
|
private var sendQueueListenerTaskHandle: TaskHandle?
|
||||||
|
|
||||||
|
// periphery:ignore - required for instance retention in the rust codebase
|
||||||
|
private var mediaPreviewConfigListenerTaskHandle: TaskHandle?
|
||||||
|
|
||||||
private var delegateHandle: TaskHandle?
|
private var delegateHandle: TaskHandle?
|
||||||
|
|
||||||
// These following summary providers both operate on the same allRooms() list but
|
// These following summary providers both operate on the same allRooms() list but
|
||||||
@@ -132,6 +135,16 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
verificationStateSubject.asCurrentValuePublisher()
|
verificationStateSubject.asCurrentValuePublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let timelineMediaVisibilitySubject = CurrentValueSubject<TimelineMediaVisibility, Never>(.always)
|
||||||
|
var timelineMediaVisibilityPublisher: CurrentValuePublisher<TimelineMediaVisibility, Never> {
|
||||||
|
timelineMediaVisibilitySubject.asCurrentValuePublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
private let hideInviteAvatarsSubject = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
var hideInviteAvatarsPublisher: CurrentValuePublisher<Bool, Never> {
|
||||||
|
hideInviteAvatarsSubject.asCurrentValuePublisher()
|
||||||
|
}
|
||||||
|
|
||||||
var roomsToAwait: Set<String> = []
|
var roomsToAwait: Set<String> = []
|
||||||
|
|
||||||
private let sendQueueStatusSubject = CurrentValueSubject<Bool, Never>(false)
|
private let sendQueueStatusSubject = CurrentValueSubject<Bool, Never>(false)
|
||||||
@@ -230,6 +243,10 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
MXLog.error("Failed setting media retention policy with error: \(error)")
|
MXLog.error("Failed setting media retention policy with error: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Task {
|
||||||
|
mediaPreviewConfigListenerTaskHandle = await createMediaPreviewConfigObserver()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var userID: String {
|
var userID: String {
|
||||||
@@ -724,6 +741,16 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
return .failure(.sdkError(error))
|
return .failure(.sdkError(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fetchMediaPreviewConfiguration() async -> Result<MediaPreviewConfig?, ClientProxyError> {
|
||||||
|
do {
|
||||||
|
let config = try await client.fetchMediaPreviewConfig()
|
||||||
|
return .success(config)
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed fetching media preview config with error: \(error)")
|
||||||
|
return .failure(.sdkError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Ignored users
|
// MARK: Ignored users
|
||||||
|
|
||||||
@@ -798,6 +825,28 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
return users.elements
|
return users.elements
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Moderation & Safety
|
||||||
|
|
||||||
|
func setTimelineMediaVisibility(_ value: TimelineMediaVisibility) async -> Result<Void, ClientProxyError> {
|
||||||
|
do {
|
||||||
|
try await client.setMediaPreviewDisplayPolicy(policy: value.rustValue)
|
||||||
|
return .success(())
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed to set timeline media visibility: \(error)")
|
||||||
|
return .failure(.sdkError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setHideInviteAvatars(_ value: Bool) async -> Result<Void, ClientProxyError> {
|
||||||
|
do {
|
||||||
|
try await client.setInviteAvatarsDisplayPolicy(policy: value ? .off : .on)
|
||||||
|
return .success(())
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed to set hide invite avatars: \(error)")
|
||||||
|
return .failure(.sdkError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func cacheAccountURL() async {
|
private func cacheAccountURL() async {
|
||||||
@@ -867,6 +916,26 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func createMediaPreviewConfigObserver() async -> TaskHandle? {
|
||||||
|
do {
|
||||||
|
return try await client.subscribeToMediaPreviewConfig(listener: SDKListener { [weak self] config in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
if let config {
|
||||||
|
timelineMediaVisibilitySubject.send(config.mediaPreviewVisibility)
|
||||||
|
hideInviteAvatarsSubject.send(config.hideInviteAvatars)
|
||||||
|
} else {
|
||||||
|
// return default values
|
||||||
|
timelineMediaVisibilitySubject.send(.always)
|
||||||
|
hideInviteAvatarsSubject.send(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed creating media preview config observer: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func createRoomListServiceObserver(_ roomListService: RoomListService) -> TaskHandle {
|
private func createRoomListServiceObserver(_ roomListService: RoomListService) -> TaskHandle {
|
||||||
roomListService.state(listener: SDKListener { [weak self] state in
|
roomListService.state(listener: SDKListener { [weak self] state in
|
||||||
@@ -1133,3 +1202,38 @@ private struct ClientProxyServices {
|
|||||||
self.roomListService = roomListService
|
self.roomListService = roomListService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension MediaPreviewConfig {
|
||||||
|
var mediaPreviewVisibility: TimelineMediaVisibility {
|
||||||
|
switch mediaPreviews {
|
||||||
|
case .on:
|
||||||
|
.always
|
||||||
|
case .private:
|
||||||
|
.privateOnly
|
||||||
|
case .off:
|
||||||
|
.never
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hideInviteAvatars: Bool {
|
||||||
|
switch inviteAvatars {
|
||||||
|
case .off:
|
||||||
|
true
|
||||||
|
case .on:
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension TimelineMediaVisibility {
|
||||||
|
var rustValue: MediaPreviews {
|
||||||
|
switch self {
|
||||||
|
case .always:
|
||||||
|
.on
|
||||||
|
case .never:
|
||||||
|
.off
|
||||||
|
case .privateOnly:
|
||||||
|
.private
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,6 +63,13 @@ enum SessionVerificationState {
|
|||||||
case unverified
|
case unverified
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The `Decodable` conformance is just for the purpose of migration
|
||||||
|
enum TimelineMediaVisibility: Decodable {
|
||||||
|
case always
|
||||||
|
case privateOnly
|
||||||
|
case never
|
||||||
|
}
|
||||||
|
|
||||||
// sourcery: AutoMockable
|
// sourcery: AutoMockable
|
||||||
protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||||
var actionsPublisher: AnyPublisher<ClientProxyAction, Never> { get }
|
var actionsPublisher: AnyPublisher<ClientProxyAction, Never> { get }
|
||||||
@@ -93,6 +100,10 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
|||||||
/// We delay fetching this until after the first sync. Nil until then
|
/// We delay fetching this until after the first sync. Nil until then
|
||||||
var ignoredUsersPublisher: CurrentValuePublisher<[String]?, Never> { get }
|
var ignoredUsersPublisher: CurrentValuePublisher<[String]?, Never> { get }
|
||||||
|
|
||||||
|
var timelineMediaVisibilityPublisher: CurrentValuePublisher<TimelineMediaVisibility, Never> { get }
|
||||||
|
|
||||||
|
var hideInviteAvatarsPublisher: CurrentValuePublisher<Bool, Never> { get }
|
||||||
|
|
||||||
var pusherNotificationClientIdentifier: String? { get }
|
var pusherNotificationClientIdentifier: String? { get }
|
||||||
|
|
||||||
var roomSummaryProvider: RoomSummaryProviderProtocol { get }
|
var roomSummaryProvider: RoomSummaryProviderProtocol { get }
|
||||||
@@ -185,6 +196,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
|||||||
func isAliasAvailable(_ alias: String) async -> Result<Bool, ClientProxyError>
|
func isAliasAvailable(_ alias: String) async -> Result<Bool, ClientProxyError>
|
||||||
|
|
||||||
@discardableResult func clearCaches() async -> Result<Void, ClientProxyError>
|
@discardableResult func clearCaches() async -> Result<Void, ClientProxyError>
|
||||||
|
|
||||||
|
func fetchMediaPreviewConfiguration() async -> Result<MediaPreviewConfig?, ClientProxyError>
|
||||||
|
|
||||||
// MARK: - Ignored users
|
// MARK: - Ignored users
|
||||||
|
|
||||||
@@ -210,4 +223,9 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
|||||||
func resetIdentity() async -> Result<IdentityResetHandle?, ClientProxyError>
|
func resetIdentity() async -> Result<IdentityResetHandle?, ClientProxyError>
|
||||||
|
|
||||||
func userIdentity(for userID: String) async -> Result<UserIdentityProxyProtocol?, ClientProxyError>
|
func userIdentity(for userID: String) async -> Result<UserIdentityProxyProtocol?, ClientProxyError>
|
||||||
|
|
||||||
|
// MARK: - Moderation & Safety
|
||||||
|
|
||||||
|
func setTimelineMediaVisibility(_ value: TimelineMediaVisibility) async -> Result<Void, ClientProxyError>
|
||||||
|
func setHideInviteAvatars(_ value: Bool) async -> Result<Void, ClientProxyError>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,28 @@ final class NSEUserSession {
|
|||||||
imageCache: .onlyOnDisk,
|
imageCache: .onlyOnDisk,
|
||||||
networkMonitor: nil)
|
networkMonitor: nil)
|
||||||
private let delegateHandle: TaskHandle?
|
private let delegateHandle: TaskHandle?
|
||||||
|
|
||||||
|
var mediaPreviewVisibility: MediaPreviews {
|
||||||
|
get async {
|
||||||
|
do {
|
||||||
|
return try await baseClient.getMediaPreviewDisplayPolicy() ?? .on
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed to get media preview visibility, defaulting to on. Error: \(error)")
|
||||||
|
return .on
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var inviteAvatarsVisibility: InviteAvatars {
|
||||||
|
get async {
|
||||||
|
do {
|
||||||
|
return try await baseClient.getInviteAvatarsDisplayPolicy() ?? .on
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed to get invite avatars visibility, defaulting to on. Error: \(error)")
|
||||||
|
return .on
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init(credentials: KeychainCredentials,
|
init(credentials: KeychainCredentials,
|
||||||
roomID: String,
|
roomID: String,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import Version
|
|||||||
|
|
||||||
struct NotificationContentBuilder {
|
struct NotificationContentBuilder {
|
||||||
let messageEventStringBuilder: RoomMessageEventStringBuilder
|
let messageEventStringBuilder: RoomMessageEventStringBuilder
|
||||||
let settings: CommonSettingsProtocol
|
let userSession: NSEUserSession
|
||||||
|
|
||||||
/// Process the given notification item proxy
|
/// Process the given notification item proxy
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -104,7 +104,7 @@ struct NotificationContentBuilder {
|
|||||||
senderID: notificationItem.senderID,
|
senderID: notificationItem.senderID,
|
||||||
senderName: notificationItem.senderDisplayName ?? notificationItem.roomDisplayName,
|
senderName: notificationItem.senderDisplayName ?? notificationItem.roomDisplayName,
|
||||||
icon: icon(for: notificationItem),
|
icon: icon(for: notificationItem),
|
||||||
forcePlaceholder: settings.hideInviteAvatars,
|
forcePlaceholder: userSession.inviteAvatarsVisibility == .off,
|
||||||
mediaProvider: mediaProvider)
|
mediaProvider: mediaProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,8 +146,9 @@ struct NotificationContentBuilder {
|
|||||||
let displayName = notificationItem.senderDisplayName ?? notificationItem.roomDisplayName
|
let displayName = notificationItem.senderDisplayName ?? notificationItem.roomDisplayName
|
||||||
notificationContent.body = String(messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: displayName, isOutgoing: false).characters)
|
notificationContent.body = String(messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: displayName, isOutgoing: false).characters)
|
||||||
|
|
||||||
guard settings.timelineMediaVisibility == .always ||
|
let timelineMediaVisibility = await userSession.mediaPreviewVisibility
|
||||||
(settings.timelineMediaVisibility == .privateOnly && notificationItem.isRoomPrivate)
|
guard timelineMediaVisibility == .on ||
|
||||||
|
(timelineMediaVisibility == .private && notificationItem.isRoomPrivate)
|
||||||
else {
|
else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class NotificationHandler {
|
|||||||
destination: .notification)
|
destination: .notification)
|
||||||
|
|
||||||
notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: eventStringBuilder,
|
notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: eventStringBuilder,
|
||||||
settings: settings)
|
userSession: userSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
func processEvent(_ eventID: String, roomID: String) async {
|
func processEvent(_ eventID: String, roomID: String) async {
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@ packages:
|
|||||||
# path: ../matrix-rust-sdk
|
# path: ../matrix-rust-sdk
|
||||||
Compound:
|
Compound:
|
||||||
url: https://github.com/element-hq/compound-ios
|
url: https://github.com/element-hq/compound-ios
|
||||||
revision: 81ba8bd8b3971beac252129c5466d7eac2f2ec31
|
revision: 139d4f3d1ed19cc513686e5e39750ecef54c8403
|
||||||
# path: ../compound-ios
|
# path: ../compound-ios
|
||||||
AnalyticsEvents:
|
AnalyticsEvents:
|
||||||
url: https://github.com/matrix-org/matrix-analytics-events
|
url: https://github.com/matrix-org/matrix-analytics-events
|
||||||
|
|||||||
Reference in New Issue
Block a user