From 834260c1448b0cdcce992f292e684ec69e8bd2b2 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Wed, 19 Jun 2024 11:27:10 +0100 Subject: [PATCH] Use the room heroes when computing a DM avatar. (#2900) * Use room heroes for DM avatar content ID. * Use RoomAvatar.heroes for the DM Details stack. --- ElementX.xcodeproj/project.pbxproj | 8 ++ .../Mocks/Generated/GeneratedMocks.swift | 5 + .../Mocks/Generated/SDKGeneratedMocks.swift | 65 +++++++++ ElementX/Sources/Mocks/RoomProxyMock.swift | 1 + .../Mocks/RoomSummaryProviderMock.swift | 9 ++ .../SwiftUI/Views/AvatarHeaderView.swift | 107 +++++---------- .../Other/SwiftUI/Views/RoomAvatarImage.swift | 125 ++++++++++++++++++ .../GlobalSearchScreenModels.swift | 2 +- .../GlobalSearchScreenViewModel.swift | 2 +- .../View/GlobalSearchScreenCell.swift | 12 +- .../Screens/HomeScreen/HomeScreenModels.swift | 12 +- .../View/HomeScreenInviteCell.swift | 15 ++- .../HomeScreen/View/HomeScreenRoomCell.swift | 12 +- .../JoinRoomScreen/JoinRoomScreenModels.swift | 8 ++ .../JoinRoomScreen/View/JoinRoomScreen.swift | 12 +- .../MessageForwardingScreenModels.swift | 2 +- .../MessageForwardingScreenViewModel.swift | 2 +- .../View/MessageForwardingScreen.swift | 8 +- .../View/RoomDirectoryCell.swift | 41 ++++-- .../View/RoomDirectorySearchScreen.swift | 8 +- .../Screens/RoomScreen/RoomScreenModels.swift | 2 +- .../RoomScreen/RoomScreenViewModel.swift | 10 +- .../RoomScreen/View/RoomHeaderView.swift | 31 +++-- .../Screens/RoomScreen/View/RoomScreen.swift | 9 +- .../View/Timeline/TimelineView.swift | 3 +- ...NotificationSettingsEditScreenModels.swift | 2 +- ...ificationSettingsEditScreenViewModel.swift | 2 +- ...tificationSettingsEditScreenRoomCell.swift | 11 +- .../Sources/Services/Room/RoomDetails.swift | 2 +- .../Sources/Services/Room/RoomProxy.swift | 12 ++ .../Services/Room/RoomProxyProtocol.swift | 7 +- .../Room/RoomSummary/RoomSummaryDetails.swift | 10 ++ .../RoomSummary/RoomSummaryProvider.swift | 1 + .../RoomDirectorySearchProxy.swift | 2 +- .../RoomDirectorySearchProxyProtocol.swift | 2 +- .../Services/Users/UserProfileProxy.swift | 12 ++ ...ettingsEditScreenRoomCell-iPad-en-GB.1.png | 4 +- ...ttingsEditScreenRoomCell-iPad-pseudo.1.png | 4 +- ...gsEditScreenRoomCell-iPhone-15-en-GB.1.png | 4 +- ...sEditScreenRoomCell-iPhone-15-pseudo.1.png | 4 +- .../test_roomAvatarImage-iPad-en-GB.1.png | 3 + .../test_roomAvatarImage-iPad-pseudo.1.png | 3 + ...test_roomAvatarImage-iPhone-15-en-GB.1.png | 3 + ...est_roomAvatarImage-iPhone-15-pseudo.1.png | 3 + .../test_roomScreen-iPad-en-GB.1.png | 4 +- .../test_roomScreen-iPad-pseudo.1.png | 4 +- .../test_roomScreen-iPhone-15-en-GB.1.png | 4 +- .../test_roomScreen-iPhone-15-pseudo.1.png | 4 +- .../test_timelineView-iPad-en-GB.1.png | 4 +- .../test_timelineView-iPad-pseudo.1.png | 4 +- .../test_timelineView-iPhone-15-en-GB.1.png | 4 +- .../test_timelineView-iPhone-15-pseudo.1.png | 4 +- UnitTests/Sources/HomeScreenRoomTests.swift | 1 + UnitTests/Sources/LoggingTests.swift | 3 + .../Sources/RoomSummaryDetailsTests.swift | 84 ++++++++++++ 55 files changed, 524 insertions(+), 198 deletions(-) create mode 100644 ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift create mode 100644 PreviewTests/__Snapshots__/PreviewTests/test_roomAvatarImage-iPad-en-GB.1.png create mode 100644 PreviewTests/__Snapshots__/PreviewTests/test_roomAvatarImage-iPad-pseudo.1.png create mode 100644 PreviewTests/__Snapshots__/PreviewTests/test_roomAvatarImage-iPhone-15-en-GB.1.png create mode 100644 PreviewTests/__Snapshots__/PreviewTests/test_roomAvatarImage-iPhone-15-pseudo.1.png create mode 100644 UnitTests/Sources/RoomSummaryDetailsTests.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 11c40e3a7..4e75b827d 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -558,6 +558,7 @@ 854E82E064BA53CD0BC45600 /* LocationRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6613DE16AD26B3A74DA1F5 /* LocationRoomTimelineItemContent.swift */; }; 85813D87DDD7F67A46BD9AF7 /* ImageProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E8A8047B50E3607ACD354E /* ImageProviderProtocol.swift */; }; 858276B19C7C0AD4CA98EA78 /* portrait_test_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = AF042B0FB2EE88977C91E330 /* portrait_test_image.jpg */; }; + 8587A53DE8EF94FD796DC375 /* RoomAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEF5FE93A06F563B477F024A /* RoomAvatarImage.swift */; }; 858C04B62166B5BAFCD20F2D /* TimelineItemMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1BF12A5E7C76777C4BF0F2B /* TimelineItemMenu.swift */; }; 859E2CA2EDF343BD24DE52EB /* RoomDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */; }; 85F89F3F320F4FADCFFFE68B /* ServerSelectionScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3059CFA00C67D8787273B20 /* ServerSelectionScreenViewModel.swift */; }; @@ -569,6 +570,7 @@ 8691186F9B99BCDDB7CACDD8 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */; }; 86F9D3028A1F4AE819D75560 /* RoomChangePermissionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D879FC4E881E748BB9B34DC /* RoomChangePermissionsScreenCoordinator.swift */; }; 872A6457DF573AF8CEAE927A /* LoginHomeserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9349F590E35CE514A71E6764 /* LoginHomeserver.swift */; }; + 8739553CDFA5D8ED5FD05CBC /* RoomSummaryDetailsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA520B4F65D162E555C8761 /* RoomSummaryDetailsTests.swift */; }; 874FEFB9D4A4AF447E0E086E /* AuthenticationStartScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0F7CCC4A9D1927223F559D5 /* AuthenticationStartScreenViewModelProtocol.swift */; }; 878070573C7BF19E735707B4 /* RoomTimelineItemProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DE8D25D6A91030175D52A20 /* RoomTimelineItemProperties.swift */; }; 87B4E59A4467F8EC75F82372 /* VoiceMessageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B70A50C41C5871B4DB905E7E /* VoiceMessageRoomTimelineView.swift */; }; @@ -1892,6 +1894,7 @@ BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenCoordinator.swift; sourceTree = ""; }; BEBA759D1347CFFB3D84ED1F /* UserSessionStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStoreProtocol.swift; sourceTree = ""; }; BEE365C5A4E90ACBE398EFFE /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/SAS.strings; sourceTree = ""; }; + BEF5FE93A06F563B477F024A /* RoomAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomAvatarImage.swift; sourceTree = ""; }; BF34A2FD6797535C95AC918D /* PlaceholderScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderScreenCoordinator.swift; sourceTree = ""; }; BFA9EA59D5C0DA1BFC7B3621 /* QRCodeLoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginScreen.swift; sourceTree = ""; }; BFDCAC6CAAD65A2C24EA9C4B /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = ""; }; @@ -1965,6 +1968,7 @@ CD700E035C85738EE4B97129 /* PerformanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformanceTests.swift; sourceTree = ""; }; CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = ""; }; CE47A97726F0675DEE387BF9 /* TypingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorView.swift; sourceTree = ""; }; + CEA520B4F65D162E555C8761 /* RoomSummaryDetailsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryDetailsTests.swift; sourceTree = ""; }; CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; CEE20623EB4A9B88FB29F2BA /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/SAS.strings; sourceTree = ""; }; CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = ""; }; @@ -2725,6 +2729,7 @@ 50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */, 648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */, C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */, + BEF5FE93A06F563B477F024A /* RoomAvatarImage.swift */, 839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */, DE7C80EF77AD102053D3646E /* RoundedLabelItem.swift */, AEB5FF7A09B79B0C6B528F7C /* SFNumberedListView.swift */, @@ -3595,6 +3600,7 @@ 48FEFF746DB341CDB18D7AAA /* RoomRolesAndPermissionsScreenViewModelTests.swift */, 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */, AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */, + CEA520B4F65D162E555C8761 /* RoomSummaryDetailsTests.swift */, 2E88534A39781D76487D59DF /* SecureBackupKeyBackupScreenViewModelTests.swift */, 848F69921527D31CAACB93AF /* SecureBackupLogoutConfirmationScreenViewModelTests.swift */, C0FF08D0BD7D0B4B6877AB7D /* SecureBackupRecoveryKeyScreenViewModelTests.swift */, @@ -5802,6 +5808,7 @@ 84C631E734FD2555B39B681C /* RoomRolesAndPermissionsScreenViewModelTests.swift in Sources */, 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */, CC0D088F505F33A20DC5590F /* RoomStateEventStringBuilderTests.swift in Sources */, + 8739553CDFA5D8ED5FD05CBC /* RoomSummaryDetailsTests.swift in Sources */, 7691233E3572A9173FD96CB3 /* SecureBackupKeyBackupScreenViewModelTests.swift in Sources */, EB87DF90CF6F8D5D12404C6E /* SecureBackupLogoutConfirmationScreenViewModelTests.swift in Sources */, 06B31F84CE52A7A7C271267C /* SecureBackupRecoveryKeyScreenViewModelTests.swift in Sources */, @@ -6297,6 +6304,7 @@ 680062C402ECB8FCAAE85A5C /* ResetRecoveryKeyScreenViewModelProtocol.swift in Sources */, A494741843F087881299ACF0 /* RestorationToken.swift in Sources */, 6E391F7F628D984AF44385D9 /* RoomAttachmentPicker.swift in Sources */, + 8587A53DE8EF94FD796DC375 /* RoomAvatarImage.swift in Sources */, F8C87130FD999F7F1076208C /* RoomChangePermissionsScreen.swift in Sources */, 86F9D3028A1F4AE819D75560 /* RoomChangePermissionsScreenCoordinator.swift in Sources */, 4D4D236F0BBCDC4D2CBCCBB5 /* RoomChangePermissionsScreenModels.swift in Sources */, diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 260eadd87..e99c9bca9 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -8056,6 +8056,11 @@ class RoomProxyMock: RoomProxyProtocol { var underlyingOwnUserID: String! var name: String? var topic: String? + var avatar: RoomAvatar { + get { return underlyingAvatar } + set(value) { underlyingAvatar = value } + } + var underlyingAvatar: RoomAvatar! var avatarURL: URL? var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { get { return underlyingMembersPublisher } diff --git a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift index 4b34a63a0..516ebe835 100644 --- a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift @@ -10733,6 +10733,71 @@ open class RoomSDKMock: MatrixRustSDK.Room { } } + //MARK: - heroes + + var heroesUnderlyingCallsCount = 0 + open var heroesCallsCount: Int { + get { + if Thread.isMainThread { + return heroesUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = heroesUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + heroesUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + heroesUnderlyingCallsCount = newValue + } + } + } + } + open var heroesCalled: Bool { + return heroesCallsCount > 0 + } + + var heroesUnderlyingReturnValue: [RoomHero]! + open var heroesReturnValue: [RoomHero]! { + get { + if Thread.isMainThread { + return heroesUnderlyingReturnValue + } else { + var returnValue: [RoomHero]? = nil + DispatchQueue.main.sync { + returnValue = heroesUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + heroesUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + heroesUnderlyingReturnValue = newValue + } + } + } + } + open var heroesClosure: (() -> [RoomHero])? + + open override func heroes() -> [RoomHero] { + heroesCallsCount += 1 + if let heroesClosure = heroesClosure { + return heroesClosure() + } else { + return heroesReturnValue + } + } + //MARK: - id var idUnderlyingCallsCount = 0 diff --git a/ElementX/Sources/Mocks/RoomProxyMock.swift b/ElementX/Sources/Mocks/RoomProxyMock.swift index 89b0f95f0..b1b18de72 100644 --- a/ElementX/Sources/Mocks/RoomProxyMock.swift +++ b/ElementX/Sources/Mocks/RoomProxyMock.swift @@ -54,6 +54,7 @@ extension RoomProxyMock { id = configuration.id name = configuration.name topic = configuration.topic + avatar = .room(id: configuration.id, name: configuration.name, avatarURL: configuration.avatarURL) // Note: This doesn't replicate the real proxy logic. avatarURL = configuration.avatarURL isDirect = configuration.isDirect isSpace = configuration.isSpace diff --git a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift index c4ac5c49e..d2233e564 100644 --- a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift +++ b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift @@ -84,6 +84,7 @@ extension Array where Element == RoomSummary { name: "Foundation ๐Ÿ”ญ๐Ÿช๐ŸŒŒ", isDirect: false, avatarURL: nil, + heroes: [], lastMessage: AttributedString("I do not wish to take the trouble to understand mysticism"), lastMessageFormattedTimestamp: "14:56", unreadMessagesCount: 0, @@ -100,6 +101,7 @@ extension Array where Element == RoomSummary { name: "Foundation and Empire", isDirect: false, avatarURL: URL.picturesDirectory, + heroes: [], lastMessage: AttributedString("How do you see the Emperor then? You think he keeps office hours?"), lastMessageFormattedTimestamp: "2:56 PM", unreadMessagesCount: 2, @@ -116,6 +118,7 @@ extension Array where Element == RoomSummary { name: "Second Foundation", isDirect: false, avatarURL: nil, + heroes: [], lastMessage: try? AttributedString(markdown: "He certainly seemed no *mental genius* to me"), lastMessageFormattedTimestamp: "Some time ago", unreadMessagesCount: 3, @@ -132,6 +135,7 @@ extension Array where Element == RoomSummary { name: "Foundation's Edge", isDirect: false, avatarURL: nil, + heroes: [], lastMessage: AttributedString("There's an archaic measure of time that's called the month"), lastMessageFormattedTimestamp: "Just now", unreadMessagesCount: 2, @@ -148,6 +152,7 @@ extension Array where Element == RoomSummary { name: "Foundation and Earth", isDirect: true, avatarURL: nil, + heroes: [], lastMessage: AttributedString("Clearly, if Earth is powerful enough to do that, it might also be capable of adjusting minds in order to force belief in its radioactivity"), lastMessageFormattedTimestamp: "1986", unreadMessagesCount: 1, @@ -164,6 +169,7 @@ extension Array where Element == RoomSummary { name: "Prelude to Foundation", isDirect: true, avatarURL: nil, + heroes: [], lastMessage: AttributedString("Are you groping for the word 'paranoia'?"), lastMessageFormattedTimestamp: "ใใ‚‡ใ†ใฏใ˜ใ‚…ใ†ใ„ใกใŒใคใ˜ใ‚…ใ†ใ„ใกใซใกใงใ™", unreadMessagesCount: 6, @@ -180,6 +186,7 @@ extension Array where Element == RoomSummary { name: "Unknown", isDirect: false, avatarURL: nil, + heroes: [], lastMessage: nil, lastMessageFormattedTimestamp: nil, unreadMessagesCount: 0, @@ -229,6 +236,7 @@ extension Array where Element == RoomSummary { name: "First room", isDirect: false, avatarURL: URL.picturesDirectory, + heroes: [], lastMessage: nil, lastMessageFormattedTimestamp: nil, unreadMessagesCount: 0, @@ -245,6 +253,7 @@ extension Array where Element == RoomSummary { name: "Second room", isDirect: true, avatarURL: nil, + heroes: [], lastMessage: nil, lastMessageFormattedTimestamp: nil, unreadMessagesCount: 0, diff --git a/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift b/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift index dc6bd2d80..47ecc6356 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift @@ -17,28 +17,9 @@ import SwiftUI struct AvatarHeaderView: View { - private struct AvatarInfo { - let id: String - let name: String? - let avatarURL: URL? - - init(from room: RoomDetails) { - id = room.id - name = room.name - avatarURL = room.avatarURL - } - - init(from member: RoomMemberDetails) { - id = member.id - name = member.isBanned ? nil : member.name - avatarURL = member.isBanned ? nil : member.avatarURL - } - - init(from user: UserProfileProxy) { - id = user.userID - name = user.displayName - avatarURL = user.avatarURL - } + private enum AvatarInfo { + case room(RoomAvatar) + case user(UserProfileProxy) } private enum Badge: Hashable { @@ -46,8 +27,8 @@ struct AvatarHeaderView: View { case `public` } - private let mainAvatarInfo: AvatarInfo - private let secondaryAvatarInfo: AvatarInfo? + private let avatarInfo: AvatarInfo + private let title: String private let subtitle: String? private let badges: [Badge] @@ -61,8 +42,8 @@ struct AvatarHeaderView: View { imageProvider: ImageProviderProtocol? = nil, onAvatarTap: (() -> Void)? = nil, @ViewBuilder footer: @escaping () -> Footer) { - mainAvatarInfo = .init(from: room) - secondaryAvatarInfo = nil + avatarInfo = .room(room.avatar) + title = room.name ?? room.id subtitle = room.canonicalAlias self.avatarSize = avatarSize @@ -83,9 +64,10 @@ struct AvatarHeaderView: View { imageProvider: ImageProviderProtocol? = nil, onAvatarTap: (() -> Void)? = nil, @ViewBuilder footer: @escaping () -> Footer) { - mainAvatarInfo = .init(from: dmRecipient) - secondaryAvatarInfo = .init(from: accountOwner) - subtitle = dmRecipient.isBanned ? nil : dmRecipient.name == nil ? nil : dmRecipient.id + let dmRecipientProfile = UserProfileProxy(member: dmRecipient) + avatarInfo = .room(.heroes([dmRecipientProfile, UserProfileProxy(member: accountOwner)])) + title = dmRecipientProfile.displayName ?? dmRecipientProfile.userID + subtitle = dmRecipientProfile.displayName == nil ? nil : dmRecipientProfile.userID avatarSize = .user(on: .dmDetails) self.imageProvider = imageProvider @@ -100,15 +82,13 @@ struct AvatarHeaderView: View { imageProvider: ImageProviderProtocol? = nil, onAvatarTap: (() -> Void)? = nil, @ViewBuilder footer: @escaping () -> Footer) { - mainAvatarInfo = .init(from: member) - secondaryAvatarInfo = nil - subtitle = member.isBanned ? nil : member.name == nil ? nil : member.id + let profile = UserProfileProxy(member: member) - self.avatarSize = avatarSize - self.imageProvider = imageProvider - self.onAvatarTap = onAvatarTap - self.footer = footer - badges = [] + self.init(user: profile, + avatarSize: avatarSize, + imageProvider: imageProvider, + onAvatarTap: onAvatarTap, + footer: footer) } init(user: UserProfileProxy, @@ -116,8 +96,8 @@ struct AvatarHeaderView: View { imageProvider: ImageProviderProtocol? = nil, onAvatarTap: (() -> Void)? = nil, @ViewBuilder footer: @escaping () -> Footer) { - mainAvatarInfo = .init(from: user) - secondaryAvatarInfo = nil + avatarInfo = .user(user) + title = user.displayName ?? user.userID subtitle = user.displayName == nil ? nil : user.userID self.avatarSize = avatarSize @@ -152,40 +132,15 @@ struct AvatarHeaderView: View { @ViewBuilder private var avatar: some View { - if let secondaryAvatarInfo { - ZStack { - LoadableAvatarImage(url: mainAvatarInfo.avatarURL, - name: mainAvatarInfo.name, - contentID: mainAvatarInfo.id, - avatarSize: avatarSize, - imageProvider: imageProvider) - .scaledFrame(size: 120, alignment: .topTrailing) - LoadableAvatarImage(url: secondaryAvatarInfo.avatarURL, - name: secondaryAvatarInfo.name, - contentID: secondaryAvatarInfo.id, - avatarSize: avatarSize, - imageProvider: imageProvider) - .mask { - Rectangle() - .fill(Color.white) - .overlay { - Circle() - .inset(by: -4) - .fill(Color.black) - .scaledOffset(x: 120 - avatarSize.value, - y: -120 + avatarSize.value) - } - .compositingGroup() - .luminanceToAlpha() - } - .scaledFrame(size: 120, alignment: .bottomLeading) - } - .scaledFrame(size: 120) - - } else { - LoadableAvatarImage(url: mainAvatarInfo.avatarURL, - name: mainAvatarInfo.name, - contentID: mainAvatarInfo.id, + switch avatarInfo { + case .room(let roomAvatar): + RoomAvatarImage(avatar: roomAvatar, + avatarSize: avatarSize, + imageProvider: imageProvider) + case .user(let userProfile): + LoadableAvatarImage(url: userProfile.avatarURL, + name: userProfile.displayName, + contentID: userProfile.userID, avatarSize: avatarSize, imageProvider: imageProvider) } @@ -203,7 +158,7 @@ struct AvatarHeaderView: View { Spacer() .frame(height: 9) - Text(mainAvatarInfo.name ?? mainAvatarInfo.id) + Text(title) .foregroundColor(.compound.textPrimary) .font(.compound.headingMDBold) .multilineTextAlignment(.center) @@ -237,7 +192,9 @@ struct AvatarHeaderView_Previews: PreviewProvider, TestablePreview { Form { AvatarHeaderView(room: .init(id: "@test:matrix.org", name: "Test Room", - avatarURL: URL.picturesDirectory, + avatar: .room(id: "@test:matrix.org", + name: "Test Room", + avatarURL: .picturesDirectory), canonicalAlias: "#test:matrix.org", isEncrypted: true, isPublic: true), diff --git a/ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift b/ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift new file mode 100644 index 000000000..459bb2f8a --- /dev/null +++ b/ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift @@ -0,0 +1,125 @@ +// +// Copyright 2024 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +/// Information about a room avatar such as it's URL or the heroes to use as a fallback. +enum RoomAvatar: Equatable { + /// An avatar generated from the room's details. + case room(id: String, name: String?, avatarURL: URL?) + /// An avatar generated from the room's heroes. + case heroes([UserProfileProxy]) +} + +/// A view that shows the avatar for a room, or a cluster of heroes if provided. +/// +/// This should be preferred over `LoadableAvatarImage` when displaying a +/// room avatar so that DMs have a consistent appearance throughout the app. +struct RoomAvatarImage: View { + let avatar: RoomAvatar + + let avatarSize: AvatarSize + let imageProvider: ImageProviderProtocol? + + var body: some View { + switch avatar { + case .room(let id, let name, let avatarURL): + LoadableAvatarImage(url: avatarURL, + name: name, + contentID: id, + avatarSize: avatarSize, + imageProvider: imageProvider) + case .heroes(let users): + // We will expand upon this with more stack sizes in the future. + if users.count == 0 { + let _ = assertionFailure("We should never pass empty heroes here.") + PlaceholderAvatarImage(name: nil, contentID: nil) + } else if users.count == 2 { + let clusterSize = avatarSize.value * 1.6 + ZStack { + LoadableAvatarImage(url: users[0].avatarURL, + name: users[0].displayName, + contentID: users[0].userID, + avatarSize: avatarSize, + imageProvider: imageProvider) + .scaledFrame(size: clusterSize, alignment: .topTrailing) + + LoadableAvatarImage(url: users[1].avatarURL, + name: users[1].displayName, + contentID: users[1].userID, + avatarSize: avatarSize, + imageProvider: imageProvider) + .mask { + Rectangle() + .fill(Color.white) + .overlay { + Circle() + .inset(by: -4) + .fill(Color.black) + .scaledOffset(x: clusterSize - avatarSize.value, + y: -clusterSize + avatarSize.value) + } + .compositingGroup() + .luminanceToAlpha() + } + .scaledFrame(size: clusterSize, alignment: .bottomLeading) + } + .scaledFrame(size: clusterSize) + } else { + LoadableAvatarImage(url: users[0].avatarURL, + name: users[0].displayName, + contentID: users[0].userID, + avatarSize: avatarSize, + imageProvider: imageProvider) + } + } + } +} + +struct RoomAvatarImage_Previews: PreviewProvider, TestablePreview { + static var previews: some View { + HStack(spacing: 8) { + RoomAvatarImage(avatar: .room(id: "!1:server.com", + name: "Room", + avatarURL: nil), + avatarSize: .room(on: .home), + imageProvider: MockMediaProvider()) + + RoomAvatarImage(avatar: .room(id: "!2:server.com", + name: "Room", + avatarURL: .picturesDirectory), + avatarSize: .room(on: .home), + imageProvider: MockMediaProvider()) + + RoomAvatarImage(avatar: .heroes([.init(userID: "@user:server.com", + displayName: "User", + avatarURL: nil)]), + avatarSize: .room(on: .home), + imageProvider: MockMediaProvider()) + + RoomAvatarImage(avatar: .heroes([.init(userID: "@user:server.com", + displayName: "User", + avatarURL: .picturesDirectory)]), + avatarSize: .room(on: .home), + imageProvider: MockMediaProvider()) + + RoomAvatarImage(avatar: .heroes([.init(userID: "@alice:server.com", displayName: "Alice", avatarURL: nil), + .init(userID: "@bob:server.net", displayName: "Bob", avatarURL: nil)]), + avatarSize: .room(on: .home), + imageProvider: MockMediaProvider()) + } + } +} diff --git a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenModels.swift b/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenModels.swift index a37f4a170..37e165d20 100644 --- a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenModels.swift +++ b/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenModels.swift @@ -41,5 +41,5 @@ struct GlobalSearchRoom: Identifiable, Equatable { let id: String let name: String let alias: String? - let avatarURL: URL? + let avatar: RoomAvatar } diff --git a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenViewModel.swift b/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenViewModel.swift index 2c12abcaf..d7deeb389 100644 --- a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenViewModel.swift +++ b/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenViewModel.swift @@ -81,7 +81,7 @@ class GlobalSearchScreenViewModel: GlobalSearchScreenViewModelType, GlobalSearch return GlobalSearchRoom(id: details.id, name: details.name, alias: details.canonicalAlias, - avatarURL: details.avatarURL) + avatar: details.avatar) } } } diff --git a/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreenCell.swift b/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreenCell.swift index 1bb47c656..f32aba0dc 100644 --- a/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreenCell.swift +++ b/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreenCell.swift @@ -35,11 +35,9 @@ struct GlobalSearchScreenListRow: View { @ViewBuilder @MainActor var avatar: some View { if dynamicTypeSize < .accessibility3 { - LoadableAvatarImage(url: room.avatarURL, - name: room.name, - contentID: room.id, - avatarSize: .room(on: .messageForwarding), - imageProvider: context.imageProvider) + RoomAvatarImage(avatar: room.avatar, + avatarSize: .room(on: .messageForwarding), + imageProvider: context.imageProvider) .dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1) .accessibilityHidden(true) } @@ -55,7 +53,9 @@ struct GlobalSearchScreenListRow_Previews: PreviewProvider, TestablePreview { GlobalSearchScreenListRow(room: .init(id: "123", name: "Tech central", alias: "The best place in the whole wide world", - avatarURL: .picturesDirectory), + avatar: .room(id: "123", + name: "Tech central", + avatarURL: .picturesDirectory)), context: viewModel.context) } } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index 9158704b2..b928c9df9 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -157,7 +157,7 @@ struct HomeScreenRoom: Identifiable, Equatable { let id: String /// The real room identifier this item points to - let roomId: String? + let roomID: String? let type: RoomType @@ -181,7 +181,7 @@ struct HomeScreenRoom: Identifiable, Equatable { let lastMessage: AttributedString? - let avatarURL: URL? + let avatar: RoomAvatar let inviter: InviterDetails? @@ -189,7 +189,7 @@ struct HomeScreenRoom: Identifiable, Equatable { static func placeholder() -> HomeScreenRoom { HomeScreenRoom(id: UUID().uuidString, - roomId: nil, + roomID: nil, type: .placeholder, badges: .init(isDotShown: false, isMentionShown: false, isMuteShown: false, isCallShown: false), name: "Placeholder room name", @@ -198,7 +198,7 @@ struct HomeScreenRoom: Identifiable, Equatable { isFavourite: false, timestamp: "Now", lastMessage: placeholderLastMessage, - avatarURL: nil, + avatar: .room(id: "", name: "", avatarURL: nil), inviter: nil, canonicalAlias: nil) } @@ -224,7 +224,7 @@ extension HomeScreenRoom { } self.init(id: identifier, - roomId: details.id, + roomID: details.id, type: details.isInvite ? .invite : .room, badges: .init(isDotShown: isDotShown, isMentionShown: isMentionShown, @@ -236,7 +236,7 @@ extension HomeScreenRoom { isFavourite: details.isFavourite, timestamp: details.lastMessageFormattedTimestamp, lastMessage: details.lastMessage, - avatarURL: details.avatarURL, + avatar: details.avatar, inviter: inviter, canonicalAlias: details.canonicalAlias) } diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift index 3e6022dea..5d25f6671 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift @@ -15,6 +15,7 @@ // import Combine +import Compound import SwiftUI @MainActor @@ -27,11 +28,9 @@ struct HomeScreenInviteCell: View { var body: some View { HStack(alignment: .top, spacing: 16) { if dynamicTypeSize < .accessibility3 { - LoadableAvatarImage(url: room.avatarURL, - name: title, - contentID: room.id, - avatarSize: .custom(52), - imageProvider: context.imageProvider) + RoomAvatarImage(avatar: room.avatar, + avatarSize: .custom(52), + imageProvider: context.imageProvider) .dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1) .accessibilityHidden(true) } @@ -48,8 +47,8 @@ struct HomeScreenInviteCell: View { .padding(.top, 12) .padding(.leading, 16) .onTapGesture { - if let roomId = room.roomId { - context.send(viewAction: .selectRoom(roomIdentifier: roomId)) + if let roomID = room.roomID { + context.send(viewAction: .selectRoom(roomIdentifier: roomID)) } } } @@ -214,6 +213,7 @@ private extension HomeScreenRoom { name: "Some Guy", isDirect: true, avatarURL: nil, + heroes: [.init(userID: "@someone:somewhere.com")], lastMessage: nil, lastMessageFormattedTimestamp: nil, unreadMessagesCount: 0, @@ -240,6 +240,7 @@ private extension HomeScreenRoom { name: "Awesome Room", isDirect: false, avatarURL: avatarURL, + heroes: [.init(userID: "@someone:somewhere.com")], lastMessage: nil, lastMessageFormattedTimestamp: nil, unreadMessagesCount: 0, diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift index 447b18a50..d9d9f0de5 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift @@ -31,8 +31,8 @@ struct HomeScreenRoomCell: View { var body: some View { Button { - if let roomId = room.roomId { - context.send(viewAction: .selectRoom(roomIdentifier: roomId)) + if let roomID = room.roomID { + context.send(viewAction: .selectRoom(roomIdentifier: roomID)) } } label: { HStack(spacing: 16.0) { @@ -57,11 +57,9 @@ struct HomeScreenRoomCell: View { @ViewBuilder @MainActor private var avatar: some View { if dynamicTypeSize < .accessibility3 { - LoadableAvatarImage(url: room.avatarURL, - name: room.name, - contentID: room.roomId, - avatarSize: .room(on: .home), - imageProvider: context.imageProvider) + RoomAvatarImage(avatar: room.avatar, + avatarSize: .room(on: .home), + imageProvider: context.imageProvider) .dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1) .accessibilityHidden(true) } diff --git a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift index 27aec4b01..d712bf447 100644 --- a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift +++ b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift @@ -38,6 +38,14 @@ struct JoinRoomScreenViewState: BindableState { var mode: JoinRoomScreenInteractionMode = .loading var bindings = JoinRoomScreenViewStateBindings() + + var title: String { + roomDetails?.name ?? L10n.screenJoinRoomTitleNoPreview + } + + var avatar: RoomAvatar { + .room(id: roomID, name: title, avatarURL: roomDetails?.avatarURL) + } } struct JoinRoomScreenViewStateBindings { diff --git a/ElementX/Sources/Screens/JoinRoomScreen/View/JoinRoomScreen.swift b/ElementX/Sources/Screens/JoinRoomScreen/View/JoinRoomScreen.swift index ce13decb6..bedf659dd 100644 --- a/ElementX/Sources/Screens/JoinRoomScreen/View/JoinRoomScreen.swift +++ b/ElementX/Sources/Screens/JoinRoomScreen/View/JoinRoomScreen.swift @@ -40,17 +40,13 @@ struct JoinRoomScreen: View { var mainContent: some View { VStack(spacing: 16) { - let title = context.viewState.roomDetails?.name ?? L10n.screenJoinRoomTitleNoPreview - - LoadableAvatarImage(url: context.viewState.roomDetails?.avatarURL, - name: title, - contentID: context.viewState.roomID, - avatarSize: .room(on: .joinRoom), - imageProvider: context.imageProvider) + RoomAvatarImage(avatar: context.viewState.avatar, + avatarSize: .room(on: .joinRoom), + imageProvider: context.imageProvider) .dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1) VStack(spacing: 8) { - Text(title) + Text(context.viewState.title) .font(.compound.headingMDBold) .foregroundStyle(.compound.textPrimary) .multilineTextAlignment(.center) diff --git a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenModels.swift b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenModels.swift index 246860a1f..a18697b78 100644 --- a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenModels.swift +++ b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenModels.swift @@ -44,7 +44,7 @@ struct MessageForwardingRoom: Identifiable, Equatable { let id: String let name: String let alias: String? - let avatarURL: URL? + let avatar: RoomAvatar } struct MessageForwardingItem: Hashable { diff --git a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift index 0be407e9e..c67c2013f 100644 --- a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift +++ b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift @@ -94,7 +94,7 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me continue } - let room = MessageForwardingRoom(id: details.id, name: details.name, alias: details.canonicalAlias, avatarURL: details.avatarURL) + let room = MessageForwardingRoom(id: details.id, name: details.name, alias: details.canonicalAlias, avatar: details.avatar) rooms.append(room) } } diff --git a/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift b/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift index a451af930..b755abed1 100644 --- a/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift +++ b/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift @@ -88,11 +88,9 @@ private struct MessageForwardingListRow: View { @ViewBuilder @MainActor var avatar: some View { if dynamicTypeSize < .accessibility3 { - LoadableAvatarImage(url: room.avatarURL, - name: room.name, - contentID: room.id, - avatarSize: .room(on: .messageForwarding), - imageProvider: context.imageProvider) + RoomAvatarImage(avatar: room.avatar, + avatarSize: .room(on: .messageForwarding), + imageProvider: context.imageProvider) .dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1) .accessibilityHidden(true) } diff --git a/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectoryCell.swift b/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectoryCell.swift index 232aefb50..53ca9e5b4 100644 --- a/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectoryCell.swift +++ b/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectoryCell.swift @@ -44,11 +44,9 @@ struct RoomDirectorySearchCell: View { } private var avatar: some View { - LoadableAvatarImage(url: result.avatarURL, - name: result.name, - contentID: result.id, - avatarSize: .room(on: .roomDirectorySearch), - imageProvider: imageProvider) + RoomAvatarImage(avatar: result.avatar, + avatarSize: .room(on: .roomDirectorySearch), + imageProvider: imageProvider) .accessibilityHidden(true) } } @@ -62,7 +60,9 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview { alias: "#test:example.com", name: "Test title", topic: "test description", - avatarURL: nil, + avatar: .room(id: "!test_id_1:matrix.org", + name: "Test title", + avatarURL: nil), canBeJoined: true), imageProvider: MockMediaProvider()) { } @@ -70,7 +70,9 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview { alias: "#test:example.com", name: nil, topic: "test description", - avatarURL: nil, + avatar: .room(id: "!test_id_2:matrix.org", + name: nil, + avatarURL: nil), canBeJoined: true), imageProvider: MockMediaProvider()) { } @@ -78,7 +80,9 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview { alias: "#test_no_topic:example.com", name: "Test title no topic", topic: nil, - avatarURL: nil, + avatar: .room(id: "!test_id_3:example.com", + name: "Test title no topic", + avatarURL: nil), canBeJoined: true), imageProvider: MockMediaProvider()) { } @@ -86,7 +90,9 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview { alias: "#test_no_topic:example.com", name: nil, topic: nil, - avatarURL: nil, + avatar: .room(id: "!test_id_4:example.com", + name: nil, + avatarURL: nil), canBeJoined: true), imageProvider: MockMediaProvider()) { } @@ -94,7 +100,9 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview { alias: nil, name: "Test title no alias", topic: nil, - avatarURL: nil, + avatar: .room(id: "!test_id_5:example.com", + name: "Test title no alias", + avatarURL: nil), canBeJoined: false), imageProvider: MockMediaProvider()) { } @@ -102,7 +110,9 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview { alias: nil, name: "Test title no alias", topic: "Topic", - avatarURL: nil, + avatar: .room(id: "!test_id_6:example.com", + name: "Test title no alias", + avatarURL: nil), canBeJoined: false), imageProvider: MockMediaProvider()) { } @@ -110,15 +120,18 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview { alias: nil, name: nil, topic: "Topic", - avatarURL: nil, + avatar: .room(id: "!test_id_7:example.com", + name: nil, + avatarURL: nil), canBeJoined: false), imageProvider: MockMediaProvider()) { } RoomDirectorySearchCell(result: .init(id: "!test_id_8:example.com", - alias: nil, name: nil, topic: nil, - avatarURL: nil, + avatar: .room(id: "!test_id_8:example.com", + name: nil, + avatarURL: nil), canBeJoined: false), imageProvider: MockMediaProvider()) { } } diff --git a/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectorySearchScreen.swift b/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectorySearchScreen.swift index ffccf4d77..19b8b395d 100644 --- a/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectorySearchScreen.swift +++ b/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectorySearchScreen.swift @@ -83,13 +83,17 @@ struct RoomDirectorySearchScreen_Previews: PreviewProvider, TestablePreview { alias: "#test_1:example.com", name: "Test 1", topic: "Test description 1", - avatarURL: nil, + avatar: .room(id: "test_1", + name: "Test 1", + avatarURL: nil), canBeJoined: true), RoomDirectorySearchResult(id: "test_2", alias: "#test_2:example.com", name: "Test 2", topic: nil, - avatarURL: URL.documentsDirectory, + avatar: .room(id: "test_2", + name: "Test 2", + avatarURL: .documentsDirectory), canBeJoined: false)] let roomDirectorySearchProxy = RoomDirectorySearchProxyMock(configuration: .init(results: results)) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 49a9625c7..9d8cb33dd 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -144,7 +144,7 @@ enum RoomScreenComposerAction { struct RoomScreenViewState: BindableState { var roomID: String var roomTitle = "" - var roomAvatarURL: URL? + var roomAvatar: RoomAvatar var members: [String: RoomMemberState] = [:] var typingMembers: [String] = [] var showLoading = false diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index f85007afe..dec557632 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -80,9 +80,9 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol appSettings: appSettings, analyticsService: analyticsService) - super.init(initialViewState: RoomScreenViewState(roomID: timelineController.roomID, + super.init(initialViewState: RoomScreenViewState(roomID: roomProxy.id, roomTitle: roomProxy.roomTitle, - roomAvatarURL: roomProxy.avatarURL, + roomAvatar: roomProxy.avatar, timelineStyle: appSettings.timelineStyle, isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom, timelineViewState: TimelineViewState(focussedEvent: focussedEventID.map { .init(eventID: $0, appearance: .immediate) }), @@ -389,9 +389,9 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol .throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true) .sink { [weak self] _ in guard let self else { return } - self.state.roomTitle = roomProxy.roomTitle - self.state.roomAvatarURL = roomProxy.avatarURL - self.state.hasOngoingCall = roomProxy.hasOngoingCall + state.roomTitle = roomProxy.roomTitle + state.roomAvatar = roomProxy.avatar + state.hasOngoingCall = roomProxy.hasOngoingCall } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift index 1480ac0fd..e0cc306f4 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift @@ -15,19 +15,18 @@ // import Combine -import Foundation +import Compound import SwiftUI struct RoomHeaderView: View { - let roomID: String let roomName: String - let avatarURL: URL? + let roomAvatar: RoomAvatar let imageProvider: ImageProviderProtocol? var body: some View { HStack(spacing: 12) { - roomAvatar + avatarImage .accessibilityHidden(true) Text(roomName) .font(.compound.bodyLGSemibold) @@ -37,28 +36,28 @@ struct RoomHeaderView: View { .frame(maxWidth: .infinity, alignment: .leading) } - private var roomAvatar: some View { - LoadableAvatarImage(url: avatarURL, - name: roomName, - contentID: roomID, - avatarSize: .room(on: .timeline), - imageProvider: imageProvider) + private var avatarImage: some View { + RoomAvatarImage(avatar: roomAvatar, + avatarSize: .room(on: .timeline), + imageProvider: imageProvider) .accessibilityIdentifier(A11yIdentifiers.roomScreen.avatar) } } struct RoomHeaderView_Previews: PreviewProvider, TestablePreview { static var previews: some View { - RoomHeaderView(roomID: "1", - roomName: "Some Room name", - avatarURL: URL.picturesDirectory, + RoomHeaderView(roomName: "Some Room name", + roomAvatar: .room(id: "1", + name: "Some Room Name", + avatarURL: URL.picturesDirectory), imageProvider: MockMediaProvider()) .previewLayout(.sizeThatFits) .padding() - RoomHeaderView(roomID: "1", - roomName: "Some Room name", - avatarURL: nil, + RoomHeaderView(roomName: "Some Room name", + roomAvatar: .room(id: "1", + name: "Some Room Name", + avatarURL: nil), imageProvider: MockMediaProvider()) .previewLayout(.sizeThatFits) .padding() diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index a9e1ce7ef..d5fb70158 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -137,9 +137,8 @@ struct RoomScreen: View { // .principal + .primaryAction works better than .navigation leading + trailing // as the latter disables interaction in the action button for rooms with long names ToolbarItem(placement: .principal) { - RoomHeaderView(roomID: context.viewState.roomID, - roomName: context.viewState.roomTitle, - avatarURL: context.viewState.roomAvatarURL, + RoomHeaderView(roomName: context.viewState.roomTitle, + roomAvatar: context.viewState.roomAvatar, imageProvider: context.imageProvider) // Using a button stops it from getting truncated in the navigation bar .onTapGesture { @@ -184,7 +183,9 @@ struct RoomScreen: View { // MARK: - Previews struct RoomScreen_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(name: "Preview room", hasOngoingCall: true)), + static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(id: "stable_id", + name: "Preview room", + hasOngoingCall: true)), timelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineView.swift index ca5ec5629..c1f795a4b 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineView.swift @@ -84,7 +84,8 @@ struct TimelineView: UIViewControllerRepresentable { // MARK: - Previews struct TimelineView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(name: "Preview room")), + static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(id: "stable_id", + name: "Preview room")), timelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenModels.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenModels.swift index 0110e13e5..8caecab81 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenModels.swift @@ -110,7 +110,7 @@ struct NotificationSettingsEditScreenRoom: Identifiable, Equatable { var name = "" - var avatarURL: URL? + var avatar: RoomAvatar var notificationMode: RoomNotificationModeProxy? } diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModel.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModel.swift index 567a6597b..78557aade 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModel.swift @@ -168,7 +168,7 @@ class NotificationSettingsEditScreenViewModel: NotificationSettingsEditScreenVie return NotificationSettingsEditScreenRoom(id: details.id, roomId: details.id, name: details.name, - avatarURL: details.avatarURL, + avatar: details.avatar, notificationMode: notificationMode) } diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreenRoomCell.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreenRoomCell.swift index d525452c8..fae4bed3e 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreenRoomCell.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreenRoomCell.swift @@ -39,11 +39,9 @@ struct NotificationSettingsEditScreenRoomCell: View { @ViewBuilder @MainActor var avatar: some View { if dynamicTypeSize < .accessibility3 { - LoadableAvatarImage(url: room.avatarURL, - name: room.name, - contentID: room.roomId, - avatarSize: .room(on: .notificationSettings), - imageProvider: context.imageProvider) + RoomAvatarImage(avatar: room.avatar, + avatarSize: .room(on: .notificationSettings), + imageProvider: context.imageProvider) .dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1) .accessibilityHidden(true) } @@ -76,7 +74,8 @@ struct NotificationSettingsEditScreenRoomCell_Previews: PreviewProvider, Testabl case .filled(let details): return NotificationSettingsEditScreenRoom(id: UUID().uuidString, roomId: details.id, - name: details.name) + name: details.name, + avatar: details.avatar) } } diff --git a/ElementX/Sources/Services/Room/RoomDetails.swift b/ElementX/Sources/Services/Room/RoomDetails.swift index c6e757e07..66b1f19a3 100644 --- a/ElementX/Sources/Services/Room/RoomDetails.swift +++ b/ElementX/Sources/Services/Room/RoomDetails.swift @@ -19,7 +19,7 @@ import Foundation struct RoomDetails { let id: String let name: String? - let avatarURL: URL? + let avatar: RoomAvatar let canonicalAlias: String? let isEncrypted: Bool let isPublic: Bool diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index 8b05c5574..94bf834ee 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -100,6 +100,18 @@ class RoomProxy: RoomProxyProtocol { var avatarURL: URL? { roomListItem.avatarUrl().flatMap(URL.init(string:)) } + + var avatar: RoomAvatar { + if isDirect, avatarURL == nil { + let heroes = room.heroes() + + if heroes.count == 1 { + return .heroes(heroes.map(UserProfileProxy.init)) + } + } + + return .room(id: id, name: name, avatarURL: avatarURL) + } var joinedMembersCount: Int { Int(room.joinedMembersCount()) diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index e65b3457b..011896b18 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -47,12 +47,15 @@ protocol RoomProxyProtocol { var topic: String? { get } + /// The room's avatar info for use in a ``RoomAvatarImage``. + var avatar: RoomAvatar { get } + /// The room's avatar URL. Use this for editing and favour ``avatar`` for display. var avatarURL: URL? { get } var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { get } var typingMembersPublisher: CurrentValuePublisher<[String], Never> { get } - + var joinedMembersCount: Int { get } var activeMembersCount: Int { get } @@ -146,7 +149,7 @@ extension RoomProxyProtocol { var details: RoomDetails { RoomDetails(id: id, name: name, - avatarURL: avatarURL, + avatar: avatar, canonicalAlias: canonicalAlias, isEncrypted: isEncrypted, isPublic: isPublic) diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryDetails.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryDetails.swift index 9cbaf5ff3..5374b5e99 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryDetails.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryDetails.swift @@ -26,6 +26,7 @@ struct RoomSummaryDetails { let name: String let isDirect: Bool let avatarURL: URL? + let heroes: [UserProfileProxy] let lastMessage: AttributedString? let lastMessageFormattedTimestamp: String? let unreadMessagesCount: UInt @@ -64,6 +65,7 @@ extension RoomSummaryDetails { name = string isDirect = true avatarURL = nil + heroes = [] lastMessage = AttributedString(string) lastMessageFormattedTimestamp = "Now" unreadMessagesCount = hasUnreadMessages ? 1 : 0 @@ -78,4 +80,12 @@ extension RoomSummaryDetails { isMarkedUnread = false isFavourite = false } + + var avatar: RoomAvatar { + if isDirect, avatarURL == nil, heroes.count == 1 { + .heroes(heroes) + } else { + .room(id: id, name: name, avatarURL: avatarURL) + } + } } diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift index 78af26ed0..22cafa00b 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift @@ -252,6 +252,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { name: roomInfo.displayName ?? roomInfo.id, isDirect: roomInfo.isDirect, avatarURL: roomInfo.avatarUrl.flatMap(URL.init(string:)), + heroes: roomInfo.heroes.map(UserProfileProxy.init), lastMessage: attributedLastMessage, lastMessageFormattedTimestamp: lastMessageFormattedTimestamp, unreadMessagesCount: UInt(roomInfo.numUnreadMessages), diff --git a/ElementX/Sources/Services/RoomDirectorySearch/RoomDirectorySearchProxy.swift b/ElementX/Sources/Services/RoomDirectorySearch/RoomDirectorySearchProxy.swift index 8193afb4d..73df08f08 100644 --- a/ElementX/Sources/Services/RoomDirectorySearch/RoomDirectorySearchProxy.swift +++ b/ElementX/Sources/Services/RoomDirectorySearch/RoomDirectorySearchProxy.swift @@ -154,7 +154,7 @@ final class RoomDirectorySearchProxy: RoomDirectorySearchProxyProtocol { alias: value.alias, name: value.name, topic: value.topic, - avatarURL: value.avatarUrl.flatMap(URL.init(string:)), + avatar: .room(id: value.roomId, name: value.name, avatarURL: value.avatarUrl.flatMap(URL.init(string:))), canBeJoined: value.joinRule == .public) } } diff --git a/ElementX/Sources/Services/RoomDirectorySearch/RoomDirectorySearchProxyProtocol.swift b/ElementX/Sources/Services/RoomDirectorySearch/RoomDirectorySearchProxyProtocol.swift index c1c0588d5..f55077bc3 100644 --- a/ElementX/Sources/Services/RoomDirectorySearch/RoomDirectorySearchProxyProtocol.swift +++ b/ElementX/Sources/Services/RoomDirectorySearch/RoomDirectorySearchProxyProtocol.swift @@ -34,6 +34,6 @@ struct RoomDirectorySearchResult: Identifiable { let alias: String? let name: String? let topic: String? - let avatarURL: URL? + let avatar: RoomAvatar let canBeJoined: Bool } diff --git a/ElementX/Sources/Services/Users/UserProfileProxy.swift b/ElementX/Sources/Services/Users/UserProfileProxy.swift index 9b4167dc8..dfcd83220 100644 --- a/ElementX/Sources/Services/Users/UserProfileProxy.swift +++ b/ElementX/Sources/Services/Users/UserProfileProxy.swift @@ -28,12 +28,24 @@ struct UserProfileProxy: Equatable, Hashable { self.avatarURL = avatarURL } + init(member: RoomMemberDetails) { + userID = member.id + displayName = member.isBanned ? nil : member.name + avatarURL = member.isBanned ? nil : member.avatarURL + } + init(sdkUserProfile: MatrixRustSDK.UserProfile) { userID = sdkUserProfile.userId displayName = sdkUserProfile.displayName avatarURL = sdkUserProfile.avatarUrl.flatMap(URL.init(string:)) } + init(sdkRoomHero: MatrixRustSDK.RoomHero) { + userID = sdkRoomHero.userId + displayName = sdkRoomHero.displayName + avatarURL = sdkRoomHero.avatarUrl.flatMap(URL.init(string:)) + } + /// A user is meant to be "verified" when the GET profile returns back either the display name or the avatar /// If isn't we aren't sure that the related matrix id really exists. var isVerified: Bool { diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_notificationSettingsEditScreenRoomCell-iPad-en-GB.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_notificationSettingsEditScreenRoomCell-iPad-en-GB.1.png index a05fa246f..9d38f8f13 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_notificationSettingsEditScreenRoomCell-iPad-en-GB.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_notificationSettingsEditScreenRoomCell-iPad-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:071913940bdcbe70a375842502c5c5efff0ff5c2725ea552ac2c1f8d491a83f5 -size 159526 +oid sha256:cc367376247f9fe140c23b8c0c72565c77f1786012bf9b0dd77e4d2ded78cb4a +size 163975 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_notificationSettingsEditScreenRoomCell-iPad-pseudo.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_notificationSettingsEditScreenRoomCell-iPad-pseudo.1.png index a05fa246f..9d38f8f13 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_notificationSettingsEditScreenRoomCell-iPad-pseudo.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_notificationSettingsEditScreenRoomCell-iPad-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:071913940bdcbe70a375842502c5c5efff0ff5c2725ea552ac2c1f8d491a83f5 -size 159526 +oid sha256:cc367376247f9fe140c23b8c0c72565c77f1786012bf9b0dd77e4d2ded78cb4a +size 163975 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_notificationSettingsEditScreenRoomCell-iPhone-15-en-GB.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_notificationSettingsEditScreenRoomCell-iPhone-15-en-GB.1.png index b421a8f0a..c672f0d6e 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_notificationSettingsEditScreenRoomCell-iPhone-15-en-GB.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_notificationSettingsEditScreenRoomCell-iPhone-15-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:381b73e3230cfb17b25dee9b8355d9118861c7018e57dfad7760e8290066411d -size 107842 +oid sha256:40bd6face9670a1b109a37d9c9678153b9706b97030317c4a42861d209b39d93 +size 111737 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_notificationSettingsEditScreenRoomCell-iPhone-15-pseudo.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_notificationSettingsEditScreenRoomCell-iPhone-15-pseudo.1.png index b421a8f0a..c672f0d6e 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_notificationSettingsEditScreenRoomCell-iPhone-15-pseudo.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_notificationSettingsEditScreenRoomCell-iPhone-15-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:381b73e3230cfb17b25dee9b8355d9118861c7018e57dfad7760e8290066411d -size 107842 +oid sha256:40bd6face9670a1b109a37d9c9678153b9706b97030317c4a42861d209b39d93 +size 111737 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_roomAvatarImage-iPad-en-GB.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_roomAvatarImage-iPad-en-GB.1.png new file mode 100644 index 000000000..3ff95d45f --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_roomAvatarImage-iPad-en-GB.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ec69becf9b8ca97645ef74fa5219282f9d499ba222f4d0b0dba4c9172072569 +size 94910 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_roomAvatarImage-iPad-pseudo.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_roomAvatarImage-iPad-pseudo.1.png new file mode 100644 index 000000000..3ff95d45f --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_roomAvatarImage-iPad-pseudo.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ec69becf9b8ca97645ef74fa5219282f9d499ba222f4d0b0dba4c9172072569 +size 94910 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_roomAvatarImage-iPhone-15-en-GB.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_roomAvatarImage-iPhone-15-en-GB.1.png new file mode 100644 index 000000000..e5b23e96a --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_roomAvatarImage-iPhone-15-en-GB.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5c9296206be65a3965ef5ad32a79dd19108e6ed844ba22264a175f5b282e461 +size 53615 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_roomAvatarImage-iPhone-15-pseudo.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_roomAvatarImage-iPhone-15-pseudo.1.png new file mode 100644 index 000000000..e5b23e96a --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_roomAvatarImage-iPhone-15-pseudo.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5c9296206be65a3965ef5ad32a79dd19108e6ed844ba22264a175f5b282e461 +size 53615 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_roomScreen-iPad-en-GB.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_roomScreen-iPad-en-GB.1.png index a24753542..2253f33ae 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_roomScreen-iPad-en-GB.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_roomScreen-iPad-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:681f42cd5c242b4564dc9fb97f47217eb05e02cb16b33ae0ea6675e4ba6dee80 -size 416815 +oid sha256:7a5840ed0428865293d7aed63ffcb9ae9016e695b3469e96b15e7ba7560eb549 +size 416817 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_roomScreen-iPad-pseudo.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_roomScreen-iPad-pseudo.1.png index ba44ae986..9f547749f 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_roomScreen-iPad-pseudo.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_roomScreen-iPad-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79cbf258b45844d0b29515aab55a3ac979f3aa6bdb7a21fef508c2025a428258 -size 427856 +oid sha256:565f5550e758f9bc8cbd83cfd88fef7395a3948c15b3d125bcd915d116cc877d +size 427888 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_roomScreen-iPhone-15-en-GB.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_roomScreen-iPhone-15-en-GB.1.png index b28a1fe9a..51fbfe9ee 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_roomScreen-iPhone-15-en-GB.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_roomScreen-iPhone-15-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a3471828f2b36c052dd0583628cb4cfaa0b3cc5cfc767bc0907f20b33d2b55b -size 270815 +oid sha256:6586a55668fd39a8eed3717061f6e881540551b0c31d65a7a6360e916cabff55 +size 270771 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_roomScreen-iPhone-15-pseudo.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_roomScreen-iPhone-15-pseudo.1.png index caf2559ea..c36e26903 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_roomScreen-iPhone-15-pseudo.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_roomScreen-iPhone-15-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32dca0d76ee5447d16211aa51b7d963502cfe3a5eb49daa216d06e9022782e0e -size 278037 +oid sha256:28c6e573a78d26c5c664d0c0f96299fe1158256ca786509f7fd3def44e66caa0 +size 278016 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_timelineView-iPad-en-GB.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_timelineView-iPad-en-GB.1.png index a24753542..2253f33ae 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_timelineView-iPad-en-GB.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_timelineView-iPad-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:681f42cd5c242b4564dc9fb97f47217eb05e02cb16b33ae0ea6675e4ba6dee80 -size 416815 +oid sha256:7a5840ed0428865293d7aed63ffcb9ae9016e695b3469e96b15e7ba7560eb549 +size 416817 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_timelineView-iPad-pseudo.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_timelineView-iPad-pseudo.1.png index ba44ae986..9f547749f 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_timelineView-iPad-pseudo.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_timelineView-iPad-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79cbf258b45844d0b29515aab55a3ac979f3aa6bdb7a21fef508c2025a428258 -size 427856 +oid sha256:565f5550e758f9bc8cbd83cfd88fef7395a3948c15b3d125bcd915d116cc877d +size 427888 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_timelineView-iPhone-15-en-GB.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_timelineView-iPhone-15-en-GB.1.png index b28a1fe9a..51fbfe9ee 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_timelineView-iPhone-15-en-GB.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_timelineView-iPhone-15-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a3471828f2b36c052dd0583628cb4cfaa0b3cc5cfc767bc0907f20b33d2b55b -size 270815 +oid sha256:6586a55668fd39a8eed3717061f6e881540551b0c31d65a7a6360e916cabff55 +size 270771 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_timelineView-iPhone-15-pseudo.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_timelineView-iPhone-15-pseudo.1.png index caf2559ea..c36e26903 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_timelineView-iPhone-15-pseudo.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_timelineView-iPhone-15-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32dca0d76ee5447d16211aa51b7d963502cfe3a5eb49daa216d06e9022782e0e -size 278037 +oid sha256:28c6e573a78d26c5c664d0c0f96299fe1158256ca786509f7fd3def44e66caa0 +size 278016 diff --git a/UnitTests/Sources/HomeScreenRoomTests.swift b/UnitTests/Sources/HomeScreenRoomTests.swift index 6c2f8ab3d..42d79aeb7 100644 --- a/UnitTests/Sources/HomeScreenRoomTests.swift +++ b/UnitTests/Sources/HomeScreenRoomTests.swift @@ -36,6 +36,7 @@ class HomeScreenRoomTests: XCTestCase { name: "Test room", isDirect: false, avatarURL: nil, + heroes: [], lastMessage: nil, lastMessageFormattedTimestamp: nil, unreadMessagesCount: unreadMessagesCount, diff --git a/UnitTests/Sources/LoggingTests.swift b/UnitTests/Sources/LoggingTests.swift index 31f1a0ccd..290b2bf51 100644 --- a/UnitTests/Sources/LoggingTests.swift +++ b/UnitTests/Sources/LoggingTests.swift @@ -86,12 +86,14 @@ class LoggingTests: XCTestCase { // Given a room summary that contains sensitive information let roomName = "Private Conversation" let lastMessage = "Secret information" + let heroName = "Pseudonym" let roomSummary = RoomSummaryDetails(id: "myroomid", isInvite: false, inviter: nil, name: roomName, isDirect: true, avatarURL: nil, + heroes: [.init(userID: "", displayName: heroName)], lastMessage: AttributedString(lastMessage), lastMessageFormattedTimestamp: "Now", unreadMessagesCount: 0, @@ -116,6 +118,7 @@ class LoggingTests: XCTestCase { XCTAssertTrue(content.contains(roomSummary.id)) XCTAssertFalse(content.contains(roomName)) XCTAssertFalse(content.contains(lastMessage)) + XCTAssertFalse(content.contains(heroName)) } func validateTimelineContentIsRedacted() throws { diff --git a/UnitTests/Sources/RoomSummaryDetailsTests.swift b/UnitTests/Sources/RoomSummaryDetailsTests.swift new file mode 100644 index 000000000..f958d9cf0 --- /dev/null +++ b/UnitTests/Sources/RoomSummaryDetailsTests.swift @@ -0,0 +1,84 @@ +// +// Copyright 2024 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import ElementX + +class RoomSummaryDetailsTests: XCTestCase { + // swiftlint:disable:next large_tuple + let roomDetails: (id: String, name: String, avatarURL: URL) = ("room_id", "Room Name", "mxc://hs.tld/room/avatar") + let heroes = [UserProfileProxy(userID: "hero_1", displayName: "Hero 1", avatarURL: "mxc://hs.tld/user/avatar")] + + func testRoomAvatar() { + let details = makeDetails(isDirect: false, hasRoomAvatar: true) + + switch details.avatar { + case .room(let id, let name, let avatarURL): + XCTAssertEqual(id, roomDetails.id) + XCTAssertEqual(name, roomDetails.name) + XCTAssertEqual(avatarURL, roomDetails.avatarURL) + case .heroes: + XCTFail("A room shouldn't use the heroes for its avatar.") + } + } + + func testDMAvatarSet() { + let details = makeDetails(isDirect: true, hasRoomAvatar: true) + + switch details.avatar { + case .room(let id, let name, let avatarURL): + XCTAssertEqual(id, roomDetails.id) + XCTAssertEqual(name, roomDetails.name) + XCTAssertEqual(avatarURL, roomDetails.avatarURL) + case .heroes: + XCTFail("A DM with an avatar set shouldn't use the heroes instead.") + } + } + + func testDMAvatarNotSet() { + let details = makeDetails(isDirect: true, hasRoomAvatar: false) + + switch details.avatar { + case .room: + XCTFail("A DM without an avatar should defer to the hero for the correct placeholder tint colour.") + case .heroes(let heroes): + XCTAssertEqual(heroes, self.heroes) + } + } + + // MARK: - Helpers + + func makeDetails(isDirect: Bool, hasRoomAvatar: Bool) -> RoomSummaryDetails { + RoomSummaryDetails(id: roomDetails.id, + isInvite: false, + inviter: nil, + name: roomDetails.name, + isDirect: isDirect, + avatarURL: hasRoomAvatar ? roomDetails.avatarURL : nil, + heroes: heroes, + lastMessage: nil, + lastMessageFormattedTimestamp: nil, + unreadMessagesCount: 0, + unreadMentionsCount: 0, + unreadNotificationsCount: 0, + notificationMode: nil, + canonicalAlias: nil, + hasOngoingCall: false, + isMarkedUnread: false, + isFavourite: false) + } +}