Custom Notification Tones (#5527)

* added foundation for custom notifications

* feat: add localizations for tone names

also, if available, include the pro sound in the element x sound list

* use a static file location to store the notification tone

and update the file at that location when selected

* if there's a problem previewing or setting the alert tone, toast the user with the error

* rename tone directory

* create NotificationToneManager to handle tone business logic

* conform to project formatting

* assign custom sound to actual notifications

* dry up notification generation in the nse

* add compilation condition for including notification alert tone and remove from share extension

* swift format had some opinions

* flatten the notification tone ui, removing system/elementx designations

* fix stored file url breaking due to sandbox path changes

* tone manager can import custom tones and provide a list of imported tones

* ui support for custom tones

* can delete custom imported tones

* sort the custom tones

* add additional preset sounds

* can choose caf files for custom sounds and fix location persistence issues

* store sound locations with relative links

this is a better solution than storing bookmarks since bookmark resolution can be variable and differ from freshly derived urls

* add docs for new notification features

* run tone conversion in background

* resolved several ai found issues

* added protocol and mocks for notification tone manager

* added tests for notification settings screen vm interactions with the tone manager

* add tests for notification tone manager

* rename element tone names per feedback

* move custom tone option to the top of the notification tone list

* rename Tone to Sound for consistency with Android

* remove additional bundled tones

* restore accidentally deleted Element Default sound file

* system alert tones are filtered by what's available on the system prior to finalizing the collection

* Add a couple more caf file conversion log statements

* Simplify the notification sound conversion logic

* Update notification sound strings in sync with Android

* update xcodeproj after rebase

* remove conditional compilation and include NotificationAlertSound in share extension

* Use AudioPlayer instead of custom tone previewer and remove the previewer

* rename NotificationTone

* Consolidate and move errors to top of NotificationToneManager

* tone manager no longer depends on userinterfacecontroller

* Move static Tone properties to NotificationToneManager

* resolve view model initializer feedback

* update xcodeproj

* fatalerror when tone manager cannot init

* Remove banner icon on alert tone error

* fixup review feedback

* Moved URL handling to NotificationToneManager
This commit is contained in:
Michael
2026-05-20 07:40:34 -05:00
committed by GitHub
parent cb1c89be9d
commit dd0de4d7b0
22 changed files with 1213 additions and 17 deletions
@@ -0,0 +1,25 @@
//
// 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 UserNotifications
extension CommonSettingsProtocol {
/// The sound name to use in outgoing notifications.
/// Falls back to the default Element X tone if no custom tone has been selected.
var notificationSoundName: UNNotificationSoundName {
if selectedNotificationTone == nil {
return UNNotificationSoundName("message.caf")
} else {
return UNNotificationSoundName(NotificationToneManager.selectedToneFilename)
}
}
/// A `UNNotificationSound` built from `notificationSoundName`, ready to attach to a notification content object.
var notificationSound: UNNotificationSound {
UNNotificationSound(named: notificationSoundName)
}
}
+2 -2
View File
@@ -35,9 +35,9 @@ class NotificationHandler {
let eventStringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()),
style: .plain)
notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: eventStringBuilder,
notificationSoundName: settings.notificationSoundName.publisher.value,
notificationSoundName: settings.notificationSoundName,
userSession: userSession)
}
@@ -231,8 +231,8 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
let content = UNMutableNotificationContent()
content.body = L10n.notificationReceivedWhileOfflineIos
content.badge = originalRequest.content.unreadCount as NSNumber?
content.sound = .init(named: settings.notificationSoundName.publisher.value)
content.sound = settings.notificationSound
let request = UNNotificationRequest(identifier: Self.receivedWhileOfflineNotificationID, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
}
+4 -1
View File
@@ -76,7 +76,7 @@ targets:
SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h
OTHER_SWIFT_FLAGS:
- "-DIS_NSE"
sources:
- path: ../Sources
- path: ../SupportingFiles
@@ -118,6 +118,9 @@ targets:
- path: ../../ElementX/Sources/Other/SwiftUI/Views/PlaceholderAvatarImage.swift
- path: ../../ElementX/Sources/Other/TestablePreview/TestablePreview.swift
- path: ../../ElementX/Sources/Other/UserAgentBuilder.swift
- path: ../../ElementX/Sources/Services/AlertTones/NotificationTone.swift
- path: ../../ElementX/Sources/Services/AlertTones/NotificationToneManager.swift
- path: ../../ElementX/Sources/Services/AlertTones/NotificationToneManagerProtocol.swift
- path: ../../ElementX/Sources/Services/ElementCall/ElementCallServiceConstants.swift
- path: ../../ElementX/Sources/Services/Keychain/KeychainController.swift
- path: ../../ElementX/Sources/Services/Keychain/KeychainControllerProtocol.swift