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,64 @@
//
// 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 Foundation
/// Represents a notification alert tone and its backing audio file location.
struct NotificationTone: Hashable, Comparable, Codable {
/// Display name for the tone, falling back to the filename stem.
var label: String {
labelOverride ?? URL(filePath: "/\(relativePath.last, default: "")").deletingPathExtension().lastPathComponent
}
private let labelOverride: String?
let storageLocationRoot: StorageLocation
let relativePath: [String]
/// The audio filename including its extension.
var filename: String {
relativePath.last ?? ""
}
/// - Parameters:
/// - labelOverride: Optional custom display name. If `nil`, the filename stem is used.
/// - storageLocationRoot: Where the file lives (system, bundle, or library).
/// - relativePath: Path components relative to the storage root, e.g. `["New", "Bloom.caf"]`.
init(labelOverride: String?, storageLocationRoot: StorageLocation, relativePath: [String]) {
self.labelOverride = labelOverride
self.storageLocationRoot = storageLocationRoot
self.relativePath = relativePath
}
/// Indicates which storage root backs the tone's audio file.
enum StorageLocation: Codable, Hashable {
/// The device's system sounds directory.
case system
/// The app's main bundle resources.
case appBundle
/// The app's Library directory (user-imported tones).
case appLibrary
}
/// Creates a tone backed by a file in the system sounds directory.
static func createSystemSound(label: String?, filename: String, systemSoundsSubdirectory: [String] = []) -> NotificationTone {
NotificationTone(labelOverride: label, storageLocationRoot: .system, relativePath: systemSoundsSubdirectory + [filename])
}
/// Creates a tone backed by a file in the app bundle.
static func createBundledSound(label: String?, filename: String) -> NotificationTone {
NotificationTone(labelOverride: label, storageLocationRoot: .appBundle, relativePath: [filename])
}
/// Creates a tone backed by a user-imported file in the app library.
static func createCustomUserSound(filename: String) -> NotificationTone {
NotificationTone(labelOverride: nil, storageLocationRoot: .appLibrary, relativePath: [filename])
}
static func < (lhs: NotificationTone, rhs: NotificationTone) -> Bool {
lhs.label < rhs.label
}
}