Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import Foundation
import NeedleFoundation
import WireDataModel
import WireFoundation
import WireLogging
import WireNetwork

Expand Down Expand Up @@ -658,7 +659,8 @@ final class NSEClientScope: Component<NSEClientScopeDependency> {
conversationLocalStore: conversationLocalStore,
selectedAccount: dependency.account,
accountManager: dependency.accountManager,
databaseSaver: databaseSaver
databaseSaver: databaseSaver,
notificationSoundPreference: .stored(in: dependency.sharedUserDefaults)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import CallKit
import Foundation
import UserNotifications
import WireDataModel
import WireFoundation
import WireLogging

protocol ShowNotificationUseCaseProtocol {
Expand All @@ -35,19 +36,22 @@ struct ShowNotificationUseCase: ShowNotificationUseCaseProtocol {
private let selectedAccount: Account
private let accountManager: AccountManager
private let databaseSaver: any DatabaseSaverProtocol
private let notificationSoundPreference: NotificationSoundPreference

init(
contentHandler: @escaping (UNNotificationContent) -> Void,
conversationLocalStore: any ConversationLocalStoreProtocol,
selectedAccount: Account,
accountManager: AccountManager,
databaseSaver: any DatabaseSaverProtocol
databaseSaver: any DatabaseSaverProtocol,
notificationSoundPreference: NotificationSoundPreference = .defaultValue
) {
self.contentHandler = contentHandler
self.conversationLocalStore = conversationLocalStore
self.selectedAccount = selectedAccount
self.accountManager = accountManager
self.databaseSaver = databaseSaver
self.notificationSoundPreference = notificationSoundPreference
}

func invoke(
Expand Down Expand Up @@ -100,6 +104,12 @@ struct ShowNotificationUseCase: ShowNotificationUseCaseProtocol {
notification.interruptionLevel = .timeSensitive
notification.badge = try await getNotificationBadge()

if !notifications.isEmpty, shouldApplyNotificationSoundPreference(to: notification) {
notification.sound = UNNotificationSound(
named: UNNotificationSoundName(notificationSoundPreference.notificationSoundName)
)
}

WireLogger.notifications.info(
"Showing notification to the user",
attributes: .newNSE, .safePublic
Expand All @@ -109,6 +119,19 @@ struct ShowNotificationUseCase: ShowNotificationUseCaseProtocol {
contentHandler(notification)
}

private func shouldApplyNotificationSoundPreference(to notification: UNNotificationContent) -> Bool {
guard let sound = notification.sound else { return true }

let soundsWithDedicatedSettings = [
NotificationSound.call,
NotificationSound.ping
].map {
UNNotificationSound(named: UNNotificationSoundName($0.rawValue))
}

return !soundsWithDedicatedSettings.contains(sound)
}

private func getNotificationBadge() async throws -> NSNumber {
// Ensures unread conversations count is up-to-date.
try await databaseSaver.save()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//

import WireDataModel
import WireFoundation
import WireNetworkSupport
import XCTest
@testable import WireDomain
Expand All @@ -28,6 +29,7 @@ final class ShowNotificationUseCaseTests: XCTestCase {
private var conversationLocalStore: MockConversationLocalStoreProtocol!
private var databaseSaver: MockDatabaseSaverProtocol!
private var didDisplayNotification = false
private var displayedNotification: UNNotificationContent?

override func setUp() async throws {
conversationLocalStore = MockConversationLocalStoreProtocol()
Expand All @@ -39,7 +41,10 @@ final class ShowNotificationUseCaseTests: XCTestCase {
)

sut = ShowNotificationUseCase(
contentHandler: { _ in self.didDisplayNotification = true },
contentHandler: {
self.didDisplayNotification = true
self.displayedNotification = $0
},
conversationLocalStore: conversationLocalStore,
selectedAccount: Account(userName: .init(), userIdentifier: .mockID1),
accountManager: try AccountManager(
Expand All @@ -55,16 +60,17 @@ final class ShowNotificationUseCaseTests: XCTestCase {
sut = nil
conversationLocalStore = nil
didDisplayNotification = false
displayedNotification = nil
databaseSaver = nil
}

func testProcess_It_Invokes_Notification_Content_Handler() async throws {

// Mock

let userNotifications: [UserNotification] = [
.text(UNMutableNotificationContent())
]
let content = UNMutableNotificationContent()
content.sound = UNNotificationSound(named: .init("default"))
let userNotifications: [UserNotification] = [.text(content)]

conversationLocalStore.unreadConversationCount_MockValue = 1
databaseSaver.save_MockMethod = {}
Expand All @@ -78,6 +84,42 @@ final class ShowNotificationUseCaseTests: XCTestCase {
XCTAssertEqual(didDisplayNotification, true)
XCTAssertEqual(databaseSaver.save_Invocations.count, 1)
XCTAssertEqual(conversationLocalStore.unreadConversationCount_Invocations.count, 1)
XCTAssertEqual(displayedNotification?.sound, UNNotificationSound(named: .init("new_message.caf")))
}

func testProcessUsesSelectedSystemDefaultSound() async throws {
sut = ShowNotificationUseCase(
contentHandler: { self.displayedNotification = $0 },
conversationLocalStore: conversationLocalStore,
selectedAccount: Account(userName: .init(), userIdentifier: .mockID1),
accountManager: try AccountManager(
currentAppVersion: "1.0.0",
directory: FileManager.default.temporaryDirectory,
defaults: .temporary()
),
databaseSaver: databaseSaver,
notificationSoundPreference: .systemDefault
)
conversationLocalStore.unreadConversationCount_MockValue = 1
databaseSaver.save_MockMethod = {}

try await sut.invoke(userNotifications: [.text(UNMutableNotificationContent())])

XCTAssertEqual(displayedNotification?.sound, UNNotificationSound.default)
}

func testProcessPreservesDedicatedPingSound() async throws {
let content = UNMutableNotificationContent()
content.sound = UNNotificationSound(named: .init(NotificationSound.ping.rawValue))
conversationLocalStore.unreadConversationCount_MockValue = 1
databaseSaver.save_MockMethod = {}

try await sut.invoke(userNotifications: [.text(content)])

XCTAssertEqual(
displayedNotification?.sound,
UNNotificationSound(named: .init(NotificationSound.ping.rawValue))
)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// Wire
// Copyright (C) 2026 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

public import Foundation

public enum NotificationSoundPreference: String, CaseIterable, Sendable {

case systemDefault
case wire

public static let storageKey = "NotificationSoundPreference"
public static let defaultValue = NotificationSoundPreference.wire

public static func stored(in userDefaults: UserDefaults) -> NotificationSoundPreference {
userDefaults.string(forKey: storageKey).flatMap(NotificationSoundPreference.init(rawValue:)) ?? defaultValue
}

public func store(in userDefaults: UserDefaults) {
userDefaults.set(rawValue, forKey: Self.storageKey)
}

public var notificationSoundName: String {
switch self {
case .systemDefault:
"default"
case .wire:
"new_message.caf"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// Wire
// Copyright (C) 2026 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import XCTest

@testable import WireFoundation

final class NotificationSoundPreferenceTests: XCTestCase {

private var userDefaults: UserDefaults!
private var suiteName: String!

override func setUp() {
super.setUp()
suiteName = UUID().uuidString
userDefaults = UserDefaults(suiteName: suiteName)
}

override func tearDown() {
userDefaults.removePersistentDomain(forName: suiteName)
userDefaults = nil
suiteName = nil
super.tearDown()
}

func testDefaultPreferenceIsWireSound() {
XCTAssertEqual(NotificationSoundPreference.stored(in: userDefaults), .wire)
XCTAssertEqual(NotificationSoundPreference.wire.notificationSoundName, "new_message.caf")
}

func testStoresSystemDefaultPreference() {
NotificationSoundPreference.systemDefault.store(in: userDefaults)

XCTAssertEqual(NotificationSoundPreference.stored(in: userDefaults), .systemDefault)
XCTAssertEqual(NotificationSoundPreference.systemDefault.notificationSoundName, "default")
}
}
3 changes: 3 additions & 0 deletions WireUI/Sources/WireLocators/Locators.swift
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,9 @@ public enum Locators {

case theme = "Theme"
case themeCell
case notificationSoundCell
case systemNotificationSound
case wireNotificationSound
case lockWithPasscode = "Lock With Passcode"
case createLinkPreviews = "Create Link Previews"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//

import Foundation
import WireFoundation

/// Represents the sound for types of notifications.
public enum NotificationSound {
Expand All @@ -31,7 +32,12 @@ public enum NotificationSound {

/// The name of the song.
public var name: String {
defaultFileName
switch self {
case .newMessage:
NotificationSoundPreference.stored(in: Self.storage).notificationSoundName
case .call, .ping:
defaultFileName
}
}

// MARK: - Utilities
Expand All @@ -44,12 +50,4 @@ public enum NotificationSound {
}
}

// Unused - leaving this here in case we need to support custom sounds again in the future.
private var preferenceKey: String {
switch self {
case .call: "ZMCallSoundName"
case .ping: "ZMPingSoundName"
case .newMessage: "ZMMessageSoundName"
}
}
}
25 changes: 25 additions & 0 deletions wire-ios/Wire-iOS Tests/Settings/SettingsPropertyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import WireFoundation
import XCTest

@testable import Wire
Expand Down Expand Up @@ -141,6 +142,30 @@ final class SettingsPropertyTests: XCTestCase {
try! saveAndCheck(property, value: 1)
}

func testThatNotificationSoundPropertySetsValueInSharedDefaults() throws {
let suiteName = UUID().uuidString
let sharedUserDefaults = try XCTUnwrap(UserDefaults(suiteName: suiteName))
defer { sharedUserDefaults.removePersistentDomain(forName: suiteName) }
let factory = SettingsPropertyFactory(
userDefaults: userDefaults,
sharedUserDefaults: sharedUserDefaults,
mediaManager: ZMMockAVSMediaManager(),
userSession: userSession,
selfUser: MockZMEditableUser(),
trackingManager: MockTrackingInterface()
)
let property = factory.property(.notificationSound)

XCTAssertEqual(property.rawValue() as? String, NotificationSoundPreference.wire.rawValue)

try property.set(
newValue: .string(value: NotificationSoundPreference.systemDefault.rawValue),
resultHandler: { _ in }
)

XCTAssertEqual(NotificationSoundPreference.stored(in: sharedUserDefaults), .systemDefault)
}

func testThatIntegerBlockSettingSave() {
// given
let selfUser = MockZMEditableUser()
Expand Down
14 changes: 14 additions & 0 deletions wire-ios/Wire-iOS/Generated/Strings+Generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6042,6 +6042,20 @@ internal enum L10n {
/// Message Previews
internal static let toogle = L10n.tr("Localizable", "self.settings.notifications.push_notification.toogle", fallback: "Message Previews")
}
internal enum Sound {
/// System Default uses the alert sound provided by iOS. Apple does not make its other built-in alert tones available to apps.
internal static let footer = L10n.tr("Localizable", "self.settings.notifications.sound.footer", fallback: "System Default uses the alert sound provided by iOS. Apple does not make its other built-in alert tones available to apps.")
/// Notification Sound
internal static let title = L10n.tr("Localizable", "self.settings.notifications.sound.title", fallback: "Notification Sound")
internal enum SystemDefault {
/// System Default
internal static let title = L10n.tr("Localizable", "self.settings.notifications.sound.system_default.title", fallback: "System Default")
}
internal enum Wire {
/// Wire
internal static let title = L10n.tr("Localizable", "self.settings.notifications.sound.wire.title", fallback: "Wire")
}
}
}
internal enum OptionsMenu {
/// Options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,10 @@
"self.settings.notifications.push_notification.title" = "Notifications";
"self.settings.notifications.push_notification.toogle" = "Message Previews";
"self.settings.notifications.push_notification.footer" = "Sender name and message on the lock screen and in Notification Center.";
"self.settings.notifications.sound.title" = "Notification Sound";
"self.settings.notifications.sound.footer" = "System Default uses the alert sound provided by iOS. Apple does not make its other built-in alert tones available to apps.";
"self.settings.notifications.sound.system_default.title" = "System Default";
"self.settings.notifications.sound.wire.title" = "Wire";

"self.settings.notifications.chat_alerts.toggle" = "Message Banners";
"self.settings.notifications.chat_alerts.footer" = "New messages in other conversations.";
Expand Down
Loading
Loading