From 61fe2a8158bd4e47347df6965b3f312d55537a15 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 8 Oct 2026 14:30:20 +0300 Subject: [PATCH] Add a shared NotificationCountBadge view Extract the unread notification count capsule from the room list cell into its own view so it can be reused elsewhere. It caps the displayed value at 99+ and comes in a medium size (used by the room list) and a small size. --- .../Sources/GeneratedAccessibilityTests.swift | 4 ++ ElementX.xcodeproj/project.pbxproj | 4 ++ .../Views/NotificationCountBadge.swift | 48 +++++++++++++++++++ .../TestablePreviewsDictionary.swift | 1 + .../HomeScreen/View/HomeScreenRoomCell.swift | 12 +---- .../Sources/GeneratedPreviewTests.swift | 7 +++ .../notificationCountBadge.iPad-en-GB-0.png | 3 ++ .../notificationCountBadge.iPad-pseudo-0.png | 3 ++ .../notificationCountBadge.iPhone-en-GB-0.png | 3 ++ ...notificationCountBadge.iPhone-pseudo-0.png | 3 ++ 10 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 ElementX/Sources/Other/SwiftUI/Views/NotificationCountBadge.swift create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/notificationCountBadge.iPad-en-GB-0.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/notificationCountBadge.iPad-pseudo-0.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/notificationCountBadge.iPhone-en-GB-0.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/notificationCountBadge.iPhone-pseudo-0.png diff --git a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift index 8ecded246..7aca4abe4 100644 --- a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift +++ b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift @@ -407,6 +407,10 @@ extension AccessibilityTests { try await performAccessibilityAudit(named: "NoticeRoomTimelineView_Previews") } + func testNotificationCountBadge() async throws { + try await performAccessibilityAudit(named: "NotificationCountBadge_Previews") + } + func testNotificationPermissionsScreen() async throws { try await performAccessibilityAudit(named: "NotificationPermissionsScreen_Previews") } diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 9bd6e56b1..ec53c8ddc 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -266,6 +266,7 @@ 288408E6151D7BD3EBAA073A /* RoomScreenHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = B343C5255FB408DDE853CFDF /* RoomScreenHook.swift */; }; 28E8C44DD6E39BEB2A1599C8 /* AccessibilityTestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CABD320DE5566D133890B24 /* AccessibilityTestsAppCoordinator.swift */; }; 291A3E0EECD496D0FCC2ADAA /* TimelineScrollButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4E14E3688FF3532456A656 /* TimelineScrollButton.swift */; }; + 293179E5CE24DDF1FE82A110 /* NotificationCountBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = E124365DC2141340F6F81A72 /* NotificationCountBadge.swift */; }; 29491EE7AE37E239E839C5A3 /* LocationSharingScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BEBF0E59F25E842EDB6FD11 /* LocationSharingScreenModels.swift */; }; 2955F4C160CFD7794D819C64 /* EffectsScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024F7398C5FC12586FB10E9D /* EffectsScene.swift */; }; 298F9EC30E918F12AB7F1EE8 /* TypingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81F0325E252B057FAEEE1B2D /* TypingIndicatorView.swift */; }; @@ -3135,6 +3136,7 @@ E0FCA0957FAA0E15A9F5579D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Untranslated.stringsdict; sourceTree = ""; }; E0FF9CB3EFA753277291F609 /* EncryptionResetScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetScreenCoordinator.swift; sourceTree = ""; }; E10DA51DBC8C7E1460DBCCBD /* UserProfileListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileListRow.swift; sourceTree = ""; }; + E124365DC2141340F6F81A72 /* NotificationCountBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCountBadge.swift; sourceTree = ""; }; E157152B11E347F735C3FD6E /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = tr; path = tr.lproj/Localizable.stringsdict; sourceTree = ""; }; E1573D28C8A9FB6399D0EEFB /* SecureBackupLogoutConfirmationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenCoordinator.swift; sourceTree = ""; }; E1714DD78BFE27ADD40A6F89 /* LinkNewDeviceScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceScreenViewModelTests.swift; sourceTree = ""; }; @@ -4191,6 +4193,7 @@ C352359663A0E52BA20761EE /* LoadableImage.swift */, FFECCE59967018204876D0A5 /* LocationMarkerView.swift */, 50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */, + E124365DC2141340F6F81A72 /* NotificationCountBadge.swift */, 648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */, C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */, 95547A6C69606B5282C765D3 /* RemoveItemButton.swift */, @@ -9245,6 +9248,7 @@ 7F825CBD857D65DC986087BA /* NoticeRoomTimelineView.swift in Sources */, 3F70E237CE4C3FAB02FC227F /* NotificationConstants.swift in Sources */, 5500904C40D67D131E7E9A26 /* NotificationContentBuilder.swift in Sources */, + 293179E5CE24DDF1FE82A110 /* NotificationCountBadge.swift in Sources */, 28426806399E3659BEBC5918 /* NotificationHandler.swift in Sources */, E6A2F6E4795C1054025AACFF /* NotificationItemProxy.swift in Sources */, 9312F5A17AE59A9E910C51D6 /* NotificationItemProxyMock.swift in Sources */, diff --git a/ElementX/Sources/Other/SwiftUI/Views/NotificationCountBadge.swift b/ElementX/Sources/Other/SwiftUI/Views/NotificationCountBadge.swift new file mode 100644 index 000000000..c2d776cac --- /dev/null +++ b/ElementX/Sources/Other/SwiftUI/Views/NotificationCountBadge.swift @@ -0,0 +1,48 @@ +// +// Copyright 2026 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Compound +import SwiftUI + +/// A capsule showing an unread notification count, capped at 99+. +struct NotificationCountBadge: View { + enum Size { + case small + case medium + } + + let count: Int + var size: Size = .medium + + var body: some View { + Text(count > 99 ? "99+" : "\(count)") + .font(size == .small ? .compound.bodyXSSemibold : .compound.bodySMSemibold) + .foregroundStyle(.compound.textOnSolidPrimary) + .lineLimit(1) + .padding(.horizontal, size == .small ? 4 : 6) + .frame(minWidth: size == .small ? 16 : 20, minHeight: size == .small ? 16 : 20) + .background(.compound.iconAccentPrimary, in: .capsule) + } +} + +// MARK: - Previews + +struct NotificationCountBadge_Previews: PreviewProvider, TestablePreview { + static var previews: some View { + VStack(spacing: 8) { + ForEach([Size.small, .medium], id: \.self) { size in + HStack(spacing: 8) { + NotificationCountBadge(count: 1, size: size) + NotificationCountBadge(count: 42, size: size) + NotificationCountBadge(count: 100, size: size) + } + } + } + } + + private typealias Size = NotificationCountBadge.Size +} diff --git a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift index fa5a0c4e5..c1a2d035a 100644 --- a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift +++ b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift @@ -106,6 +106,7 @@ enum TestablePreviewsDictionary { "ModerationAndSafetySettingsScreen_Previews" : ModerationAndSafetySettingsScreen_Previews.self, "MultiAccountAnnouncementView_Previews" : MultiAccountAnnouncementView_Previews.self, "NoticeRoomTimelineView_Previews" : NoticeRoomTimelineView_Previews.self, + "NotificationCountBadge_Previews" : NotificationCountBadge_Previews.self, "NotificationPermissionsScreen_Previews" : NotificationPermissionsScreen_Previews.self, "NotificationSettingsEditScreenRoomCell_Previews" : NotificationSettingsEditScreenRoomCell_Previews.self, "NotificationSettingsEditScreen_Previews" : NotificationSettingsEditScreen_Previews.self, diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift index 90830d19d..265249877 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift @@ -146,13 +146,7 @@ struct HomeScreenRoomCell: View { if room.badges.isDotShown { if room.isHighlighted, room.badges.notificationCount > 0 { - Text(formattedNotificationCount) - .font(.compound.bodySMSemibold) - .foregroundColor(.compound.textOnSolidPrimary) - .lineLimit(1) - .padding(.horizontal, 6) - .frame(minWidth: 20, minHeight: 20) - .background(.compound.iconAccentPrimary, in: .capsule) + NotificationCountBadge(count: Int(room.badges.notificationCount)) .accessibilityLabel(L10n.a11yNotificationsNewMessages) } else { Circle() @@ -170,10 +164,6 @@ struct HomeScreenRoomCell: View { .accessibilityLabel(L10n.a11yNotificationsNewMentions) } - private var formattedNotificationCount: String { - room.badges.notificationCount > 99 ? "99+" : "\(room.badges.notificationCount)" - } - @ViewBuilder private var lastMessage: some View { if let displayedLastMessage = room.displayedLastMessage { diff --git a/PreviewTests/Sources/GeneratedPreviewTests.swift b/PreviewTests/Sources/GeneratedPreviewTests.swift index 332dff215..f35458d24 100644 --- a/PreviewTests/Sources/GeneratedPreviewTests.swift +++ b/PreviewTests/Sources/GeneratedPreviewTests.swift @@ -711,6 +711,13 @@ extension PreviewTests { } } + @Test + func notificationCountBadge() async throws { + for (index, preview) in NotificationCountBadge_Previews._allPreviews.enumerated() { + try await assertSnapshots(matching: preview, step: index) + } + } + @Test func notificationPermissionsScreen() async throws { for (index, preview) in NotificationPermissionsScreen_Previews._allPreviews.enumerated() { diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/notificationCountBadge.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/notificationCountBadge.iPad-en-GB-0.png new file mode 100644 index 000000000..e448e70e1 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/notificationCountBadge.iPad-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1b9fdaf8ac1653153e78faa59162c930af5e5f6d647293ea907840f436f97b8 +size 73171 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/notificationCountBadge.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/notificationCountBadge.iPad-pseudo-0.png new file mode 100644 index 000000000..3468cf0c4 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/notificationCountBadge.iPad-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdd5c07929cc8ac34ba855e4143f2ab1c23bde0c0780acfdf885e50c486733dc +size 74808 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/notificationCountBadge.iPhone-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/notificationCountBadge.iPhone-en-GB-0.png new file mode 100644 index 000000000..709d6b04e --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/notificationCountBadge.iPhone-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09a997528760f172b9b41994487e309e8958e7dd2404585bc7535a9f0f0b160a +size 33306 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/notificationCountBadge.iPhone-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/notificationCountBadge.iPhone-pseudo-0.png new file mode 100644 index 000000000..830736b8d --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/notificationCountBadge.iPhone-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c058780bc218c210e7b1b12efd2838f332091103246cdebfb7a2f717dbfb2d9c +size 34183