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