From ff721f65772c75885daf7c01b6ab427bdb33e411 Mon Sep 17 00:00:00 2001
From: Mauro Romito <mauro.romito@element.io>
Date: Wed, 19 Mar 2025 16:04:08 +0100
Subject: [PATCH] hide invite avatars when such flag is on

This affects:
- Invited room preview inviter avatar
- Invited room preview room avatar
- Invited room cell inviter avatar in the room list
- Invited room cell room avatar in the room list
- Push notification for an invite
---
 .../Sources/Application/AppSettings.swift     |  5 +++++
 .../Extensions/UNNotificationContent.swift    | 16 ++++----------
 .../SwiftUI/Views/RoomInviterLabel.swift      |  3 ++-
 .../Screens/HomeScreen/HomeScreenModels.swift |  2 ++
 .../HomeScreen/HomeScreenViewModel.swift      |  4 ++++
 .../View/HomeScreenInviteCell.swift           | 10 +++++++--
 .../JoinRoomScreen/JoinRoomScreenModels.swift | 21 ++++++++++++++++---
 .../JoinRoomScreenViewModel.swift             |  4 ++++
 .../JoinRoomScreen/View/JoinRoomScreen.swift  |  4 +++-
 .../AdvancedSettingsScreenModels.swift        |  3 ++-
 .../DeveloperOptionsScreenModels.swift        |  1 -
 .../View/DeveloperOptionsScreen.swift         |  6 ------
 NSE/Sources/NotificationContentBuilder.swift  |  3 ++-
 13 files changed, 54 insertions(+), 28 deletions(-)

diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift
index 73fc31e2b..071bea0d4 100644
--- a/ElementX/Sources/Application/AppSettings.swift
+++ b/ElementX/Sources/Application/AppSettings.swift
@@ -12,6 +12,7 @@ import SwiftUI
 protocol CommonSettingsProtocol {
     var logLevel: LogLevel { get }
     var enableOnlySignedDeviceIsolationMode: Bool { get }
+    var hideInviteAvatars: Bool { get }
     var hideTimelineMedia: Bool { get }
     var eventCacheEnabled: Bool { get }
 }
@@ -40,6 +41,7 @@ final class AppSettings {
         case appAppearance
         case sharePresence
         case hideUnreadMessagesBadge
+        case hideInviteAvatars
         case hideTimelineMedia
         
         case elementCallBaseURLOverride
@@ -307,6 +309,9 @@ final class AppSettings {
     @UserPreference(key: UserDefaultsKeys.enableOnlySignedDeviceIsolationMode, defaultValue: false, storageType: .userDefaults(store))
     var enableOnlySignedDeviceIsolationMode
     
+    @UserPreference(key: UserDefaultsKeys.hideInviteAvatars, defaultValue: false, storageType: .userDefaults(store))
+    var hideInviteAvatars
+    
     @UserPreference(key: UserDefaultsKeys.hideTimelineMedia, defaultValue: false, storageType: .userDefaults(store))
     var hideTimelineMedia
     
diff --git a/ElementX/Sources/Other/Extensions/UNNotificationContent.swift b/ElementX/Sources/Other/Extensions/UNNotificationContent.swift
index a06467346..3833ce03a 100644
--- a/ElementX/Sources/Other/Extensions/UNNotificationContent.swift
+++ b/ElementX/Sources/Other/Extensions/UNNotificationContent.swift
@@ -101,32 +101,24 @@ extension UNMutableNotificationContent {
     func addSenderIcon(using mediaProvider: MediaProviderProtocol?,
                        senderID: String,
                        senderName: String,
-                       icon: NotificationIcon) async throws -> UNMutableNotificationContent {
-        // We display the placeholder only if...
-        var needsPlaceholder = false
-
+                       icon: NotificationIcon,
+                       forcePlaceholder: Bool = false) async throws -> UNMutableNotificationContent {
         var fetchedImage: INImage?
         let image: INImage
-        if let mediaSource = icon.mediaSource {
+        if !forcePlaceholder, let mediaSource = icon.mediaSource {
             switch await mediaProvider?.loadThumbnailForSource(source: mediaSource, size: .init(width: 100, height: 100)) {
             case .success(let data):
                 fetchedImage = INImage(imageData: data)
             case .failure(let error):
                 MXLog.error("Couldn't add sender icon: \(error)")
-                // ...The provider failed to fetch
-                needsPlaceholder = true
             case .none:
                 break
             }
-        } else {
-            // ...There is no media
-            needsPlaceholder = true
         }
 
         if let fetchedImage {
             image = fetchedImage
-        } else if needsPlaceholder,
-                  let data = await getPlaceholderAvatarImageData(name: icon.groupInfo?.name ?? senderName,
+        } else if let data = await getPlaceholderAvatarImageData(name: icon.groupInfo?.name ?? senderName,
                                                                  id: icon.groupInfo?.id ?? senderID) {
             image = INImage(imageData: data)
         } else {
diff --git a/ElementX/Sources/Other/SwiftUI/Views/RoomInviterLabel.swift b/ElementX/Sources/Other/SwiftUI/Views/RoomInviterLabel.swift
index 49545886f..d735c8feb 100644
--- a/ElementX/Sources/Other/SwiftUI/Views/RoomInviterLabel.swift
+++ b/ElementX/Sources/Other/SwiftUI/Views/RoomInviterLabel.swift
@@ -38,12 +38,13 @@ struct RoomInviterDetails: Equatable {
 
 struct RoomInviterLabel: View {
     let inviter: RoomInviterDetails
+    var shouldHideAvatar = false
     
     let mediaProvider: MediaProviderProtocol?
     
     var body: some View {
         HStack(alignment: .firstTextBaseline, spacing: 8) {
-            LoadableAvatarImage(url: inviter.avatarURL,
+            LoadableAvatarImage(url: shouldHideAvatar ? nil : inviter.avatarURL,
                                 name: inviter.displayName,
                                 contentID: inviter.id,
                                 avatarSize: .custom(16),
diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift
index 501beedf0..b089efa1e 100644
--- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift
+++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift
@@ -106,6 +106,8 @@ struct HomeScreenViewState: BindableState {
         return rooms
     }
     
+    var hideInviteAvatars = false
+    
     var bindings = HomeScreenViewStateBindings()
     
     var placeholderRooms: [HomeScreenRoom] {
diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift
index c036f1536..335dfa572 100644
--- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift
+++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift
@@ -101,6 +101,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
             }
             .store(in: &cancellables)
         
+        appSettings.$hideInviteAvatars
+            .weakAssign(to: \.state.hideInviteAvatars, on: self)
+            .store(in: &cancellables)
+        
         let isSearchFieldFocused = context.$viewState.map(\.bindings.isSearchFieldFocused)
         let searchQuery = context.$viewState.map(\.bindings.searchQuery)
         let activeFilters = context.$viewState.map(\.bindings.filtersState.activeFilters)
diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift
index 2d61d8bc2..262b8c3d9 100644
--- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift
+++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift
@@ -16,10 +16,14 @@ struct HomeScreenInviteCell: View {
     let room: HomeScreenRoom
     let context: HomeScreenViewModel.Context
     
+    private var avatar: RoomAvatar {
+        context.viewState.hideInviteAvatars ? .room(id: room.id, name: room.name, avatarURL: nil) : room.avatar
+    }
+    
     var body: some View {
         HStack(alignment: .top, spacing: 16) {
             if dynamicTypeSize < .accessibility3 {
-                RoomAvatarImage(avatar: room.avatar,
+                RoomAvatarImage(avatar: avatar,
                                 avatarSize: .custom(52),
                                 mediaProvider: context.mediaProvider)
                     .dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1)
@@ -71,7 +75,9 @@ struct HomeScreenInviteCell: View {
     private var inviterView: some View {
         if let inviter = room.inviter,
            !room.isDirect {
-            RoomInviterLabel(inviter: inviter, mediaProvider: context.mediaProvider)
+            RoomInviterLabel(inviter: inviter,
+                             shouldHideAvatar: context.viewState.hideInviteAvatars,
+                             mediaProvider: context.mediaProvider)
                 .font(.compound.bodyMD)
                 .foregroundStyle(.compound.textSecondary)
         }
diff --git a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift
index 276adef7a..9c3d1777f 100644
--- a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift
+++ b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift
@@ -23,6 +23,15 @@ enum JoinRoomScreenMode: Equatable {
     case knocked
     case banned(sender: String?, reason: String?)
     case forbidden
+    
+    var isInvite: Bool {
+        switch self {
+        case .invited:
+            true
+        default:
+            false
+        }
+    }
 }
 
 struct JoinRoomScreenRoomDetails {
@@ -41,9 +50,15 @@ struct JoinRoomScreenViewState: BindableState {
     var roomDetails: JoinRoomScreenRoomDetails?
     
     var mode: JoinRoomScreenMode = .loading
+    
+    var hideInviteAvatars = false
         
     var bindings = JoinRoomScreenViewStateBindings()
     
+    var shouldHideAvatars: Bool {
+        hideInviteAvatars && mode.isInvite
+    }
+    
     var title: String {
         if isDMInvite, let inviter = roomDetails?.inviter {
             return inviter.displayName ?? inviter.id
@@ -68,9 +83,9 @@ struct JoinRoomScreenViewState: BindableState {
     
     var avatar: RoomAvatar? {
         if isDMInvite, let inviter = roomDetails?.inviter {
-            return .room(id: roomID, name: inviter.displayName, avatarURL: inviter.avatarURL)
-        } else if let avatar = roomDetails?.avatar {
-            return avatar
+            return .room(id: roomID, name: inviter.displayName, avatarURL: hideInviteAvatars ? nil : inviter.avatarURL)
+        } else if let roomDetails, let avatar = roomDetails.avatar {
+            return shouldHideAvatars ? .room(id: roomID, name: roomDetails.name, avatarURL: nil) : avatar
         } else if let name = roomDetails?.name {
             return .room(id: roomID, name: name, avatarURL: nil)
         } else {
diff --git a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift
index b7c741315..3e958e200 100644
--- a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift
+++ b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift
@@ -41,6 +41,10 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
         
         super.init(initialViewState: JoinRoomScreenViewState(roomID: roomID), mediaProvider: mediaProvider)
         
+        appSettings.$hideInviteAvatars
+            .weakAssign(to: \.state.hideInviteAvatars, on: self)
+            .store(in: &cancellables)
+        
         context.$viewState.map(\.mode)
             .removeDuplicates()
             .sink { mode in
diff --git a/ElementX/Sources/Screens/JoinRoomScreen/View/JoinRoomScreen.swift b/ElementX/Sources/Screens/JoinRoomScreen/View/JoinRoomScreen.swift
index 0f60d104c..374402369 100644
--- a/ElementX/Sources/Screens/JoinRoomScreen/View/JoinRoomScreen.swift
+++ b/ElementX/Sources/Screens/JoinRoomScreen/View/JoinRoomScreen.swift
@@ -58,7 +58,9 @@ struct JoinRoomScreen: View {
     private var defaultView: some View {
         VStack(spacing: 16) {
             if let inviter = context.viewState.roomDetails?.inviter {
-                RoomInviterLabel(inviter: inviter, mediaProvider: context.mediaProvider)
+                RoomInviterLabel(inviter: inviter,
+                                 shouldHideAvatar: context.viewState.hideInviteAvatars,
+                                 mediaProvider: context.mediaProvider)
                     .multilineTextAlignment(.center)
                     .font(.compound.bodyMD)
                     .foregroundStyle(.compound.textSecondary)
diff --git a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenModels.swift
index bccd03252..b7c7d620c 100644
--- a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenModels.swift
+++ b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenModels.swift
@@ -34,7 +34,8 @@ protocol AdvancedSettingsProtocol: AnyObject {
     var viewSourceEnabled: Bool { get set }
     var appAppearance: AppAppearance { get set }
     var sharePresence: Bool { get set }
-    
+    var hideTimelineMedia: Bool { get set }
+    var hideInviteAvatars: Bool { get set }
     var optimizeMediaUploads: Bool { get set }
 }
 
diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift
index 743f6564a..89ee2707d 100644
--- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift
+++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift
@@ -40,7 +40,6 @@ protocol DeveloperOptionsProtocol: AnyObject {
     var publicSearchEnabled: Bool { get set }
     var hideUnreadMessagesBadge: Bool { get set }
     var fuzzyRoomListSearchEnabled: Bool { get set }
-    var hideTimelineMedia: Bool { get set }
     var enableOnlySignedDeviceIsolationMode: Bool { get set }
     var elementCallBaseURLOverride: URL? { get set }
     var knockingEnabled: Bool { get set }
diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift
index 308fa9b03..353baff70 100644
--- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift
+++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift
@@ -46,12 +46,6 @@ struct DeveloperOptionsScreen: View {
                 }
             }
             
-            Section("Room") {
-                Toggle(isOn: $context.hideTimelineMedia) {
-                    Text("Hide image & video previews")
-                }
-            }
-            
             Section("Join rules") {
                 Toggle(isOn: $context.knockingEnabled) {
                     Text("Knocking")
diff --git a/NSE/Sources/NotificationContentBuilder.swift b/NSE/Sources/NotificationContentBuilder.swift
index cffc5424d..24774a3b1 100644
--- a/NSE/Sources/NotificationContentBuilder.swift
+++ b/NSE/Sources/NotificationContentBuilder.swift
@@ -93,7 +93,8 @@ struct NotificationContentBuilder {
         notification = try await notification.addSenderIcon(using: mediaProvider,
                                                             senderID: notificationItem.senderID,
                                                             senderName: notificationItem.senderDisplayName ?? notificationItem.roomDisplayName,
-                                                            icon: icon(for: notificationItem))
+                                                            icon: icon(for: notificationItem),
+                                                            forcePlaceholder: settings.hideInviteAvatars)
         notification.body = body
         
         return notification
-- 
GitLab