// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation import AudioToolbox import GRDB import DifferenceKit import SessionUtilitiesKit public extension Setting.EnumKey { /// Controls how notifications should appear for the user (See `NotificationPreviewType` for the options) static let preferencesNotificationPreviewType: Setting.EnumKey = "preferencesNotificationPreviewType" /// Controls what the default sound for notifications is (See `Sound` for the options) static let defaultNotificationSound: Setting.EnumKey = "defaultNotificationSound" } public extension Setting.BoolKey { /// Controls whether typing indicators are enabled /// /// **Note:** Only works if both participants in a "contact" thread have this setting enabled static let areReadReceiptsEnabled: Setting.BoolKey = "areReadReceiptsEnabled" /// Controls whether typing indicators are enabled /// /// **Note:** Only works if both participants in a "contact" thread have this setting enabled static let typingIndicatorsEnabled: Setting.BoolKey = "typingIndicatorsEnabled" /// Controls whether the device will automatically lock the screen static let isScreenLockEnabled: Setting.BoolKey = "isScreenLockEnabled" /// Controls whether Link Previews (image & title URL metadata) will be downloaded when the user enters a URL /// /// **Note:** Link Previews are only enabled for HTTPS urls static let areLinkPreviewsEnabled: Setting.BoolKey = "areLinkPreviewsEnabled" /// Controls whether Giphy search is enabled /// /// **Note:** Link Previews are only enabled for HTTPS urls static let isGiphyEnabled: Setting.BoolKey = "isGiphyEnabled" /// Controls whether Calls are enabled static let areCallsEnabled: Setting.BoolKey = "areCallsEnabled" /// Controls whether open group messages older than 6 months should be deleted static let trimOpenGroupMessagesOlderThanSixMonths: Setting.BoolKey = "trimOpenGroupMessagesOlderThanSixMonths" /// Controls whether the message requests item has been hidden on the home screen static let hasHiddenMessageRequests: Setting.BoolKey = "hasHiddenMessageRequests" /// Controls whether the notification sound should play while the app is in the foreground static let playNotificationSoundInForeground: Setting.BoolKey = "playNotificationSoundInForeground" /// A flag indicating whether the user has ever viewed their seed static let hasViewedSeed: Setting.BoolKey = "hasViewedSeed" /// A flag indicating whether the user has ever saved a thread static let hasSavedThread: Setting.BoolKey = "hasSavedThread" /// A flag indicating whether the user has ever send a message static let hasSentAMessage: Setting.BoolKey = "hasSentAMessageKey" /// A flag indicating whether the app is ready for app extensions to run static let isReadyForAppExtensions: Setting.BoolKey = "isReadyForAppExtensions" /// Controls whether concurrent audio messages should automatically be played after the one the user starts /// playing finishes static let shouldAutoPlayConsecutiveAudioMessages: Setting.BoolKey = "shouldAutoPlayConsecutiveAudioMessages" /// Controls whether the device will poll for community message requests (SOGS `/inbox` endpoint) static let checkForCommunityMessageRequests: Setting.BoolKey = "checkForCommunityMessageRequests" } public extension Setting.StringKey { /// This is the most recently recorded Push Notifications token static let lastRecordedPushToken: Setting.StringKey = "lastRecordedPushToken" /// This is the most recently recorded Voip token static let lastRecordedVoipToken: Setting.StringKey = "lastRecordedVoipToken" /// This is the last six emoji used by the user static let recentReactionEmoji: Setting.StringKey = "recentReactionEmoji" /// This is the preferred skin tones preference for the given emoji static func emojiPreferredSkinTones(emoji: String) -> Setting.StringKey { return Setting.StringKey("preferredSkinTones-\(emoji)") } } public extension Setting.DoubleKey { /// The duration of the timeout for screen lock in seconds @available(*, unavailable, message: "Screen Lock should always be instant now") static let screenLockTimeoutSeconds: Setting.DoubleKey = "screenLockTimeoutSeconds" } public extension Setting.IntKey { /// This is the number of times the app has successfully become active, it's not actually used for anything but allows us to make /// a database change on launch so the database will output an error if it fails to write static let activeCounter: Setting.IntKey = "activeCounter" } public enum Preferences { public enum NotificationPreviewType: Int, CaseIterable, EnumIntSetting, Differentiable { public static var defaultPreviewType: NotificationPreviewType = .nameAndPreview /// Notifications should include both the sender name and a preview of the message content case nameAndPreview /// Notifications should include the sender name but no preview case nameNoPreview /// Notifications should be a generic message case noNameNoPreview public var name: String { switch self { case .nameAndPreview: return "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT".localized() case .nameNoPreview: return "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY".localized() case .noNameNoPreview: return "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT".localized() } } } public enum Sound: Int, Codable, DatabaseValueConvertible, EnumIntSetting, Differentiable { public static var defaultiOSIncomingRingtone: Sound = .opening public static var defaultNotificationSound: Sound = .note // Don't store too many sounds in memory (Most users will only use 1 or 2 sounds anyway) private static let maxCachedSounds: Int = 4 private static var cachedSystemSounds: Atomic<[String: (url: URL?, soundId: SystemSoundID)]> = Atomic([:]) private static var cachedSystemSoundOrder: Atomic<[String]> = Atomic([]) // Values case `default` // Notification Sounds case aurora = 1000 case bamboo case chord case circles case complete case hello case input case keys case note case popcorn case pulse case synth case signalClassic // Ringtone Sounds case opening = 2000 // Calls case callConnecting = 3000 case callOutboundRinging case callBusy case callFailure // Other case messageSent = 4000 case none public static var notificationSounds: [Sound] { return [ // None and Note (default) should be first. .none, .note, .aurora, .bamboo, .chord, .circles, .complete, .hello, .input, .keys, .popcorn, .pulse, .synth ] } public var displayName: String { // TODO: Should we localize these sound names? switch self { case .`default`: return "" // Notification Sounds case .aurora: return "Aurora" case .bamboo: return "Bamboo" case .chord: return "Chord" case .circles: return "Circles" case .complete: return "Complete" case .hello: return "Hello" case .input: return "Input" case .keys: return "Keys" case .note: return "Note" case .popcorn: return "Popcorn" case .pulse: return "Pulse" case .synth: return "Synth" case .signalClassic: return "Signal Classic" // Ringtone Sounds case .opening: return "Opening" // Calls case .callConnecting: return "Call Connecting" case .callOutboundRinging: return "Call Outboung Ringing" case .callBusy: return "Call Busy" case .callFailure: return "Call Failure" // Other case .messageSent: return "Message Sent" case .none: return "SOUNDS_NONE".localized() } } // MARK: - Functions public func filename(quiet: Bool = false) -> String? { switch self { case .`default`: return "" // Notification Sounds case .aurora: return (quiet ? "aurora-quiet.aifc" : "aurora.aifc") case .bamboo: return (quiet ? "bamboo-quiet.aifc" : "bamboo.aifc") case .chord: return (quiet ? "chord-quiet.aifc" : "chord.aifc") case .circles: return (quiet ? "circles-quiet.aifc" : "circles.aifc") case .complete: return (quiet ? "complete-quiet.aifc" : "complete.aifc") case .hello: return (quiet ? "hello-quiet.aifc" : "hello.aifc") case .input: return (quiet ? "input-quiet.aifc" : "input.aifc") case .keys: return (quiet ? "keys-quiet.aifc" : "keys.aifc") case .note: return (quiet ? "note-quiet.aifc" : "note.aifc") case .popcorn: return (quiet ? "popcorn-quiet.aifc" : "popcorn.aifc") case .pulse: return (quiet ? "pulse-quiet.aifc" : "pulse.aifc") case .synth: return (quiet ? "synth-quiet.aifc" : "synth.aifc") case .signalClassic: return (quiet ? "classic-quiet.aifc" : "classic.aifc") // Ringtone Sounds case .opening: return "Opening.m4r" // Calls case .callConnecting: return "ringback_tone_ansi.caf" case .callOutboundRinging: return "ringback_tone_ansi.caf" case .callBusy: return "busy_tone_ansi.caf" case .callFailure: return "end_call_tone_cept.caf" // Other case .messageSent: return "message_sent.aiff" case .none: return "silence.aiff" } } public func soundUrl(quiet: Bool = false) -> URL? { guard let filename: String = filename(quiet: quiet) else { return nil } let url: URL = URL(fileURLWithPath: filename) return Bundle.main.url( forResource: url.deletingPathExtension().path, withExtension: url.pathExtension ) } public func notificationSound(isQuiet: Bool) -> UNNotificationSound { guard let filename: String = filename(quiet: isQuiet) else { SNLog("[Preferences.Sound] filename was unexpectedly nil") return UNNotificationSound.default } return UNNotificationSound(named: UNNotificationSoundName(rawValue: filename)) } public static func systemSoundId(for sound: Sound, quiet: Bool) -> SystemSoundID { let cacheKey: String = "\(sound.rawValue):\(quiet ? 1 : 0)" if let cachedSound: SystemSoundID = cachedSystemSounds.wrappedValue[cacheKey]?.soundId { return cachedSound } let systemSound: (url: URL?, soundId: SystemSoundID) = ( url: sound.soundUrl(quiet: quiet), soundId: SystemSoundID() ) cachedSystemSounds.mutate { cache in cachedSystemSoundOrder.mutate { order in if order.count > Sound.maxCachedSounds { cache.removeValue(forKey: order[0]) order.remove(at: 0) } order.append(cacheKey) } cache[cacheKey] = systemSound } return systemSound.soundId } // MARK: - AudioPlayer public static func audioPlayer(for sound: Sound, behavior: OWSAudioBehavior) -> OWSAudioPlayer? { guard let soundUrl: URL = sound.soundUrl(quiet: false) else { return nil } let player = OWSAudioPlayer(mediaUrl: soundUrl, audioBehavior: behavior) // These two cases should loop if sound == .callConnecting || sound == .callOutboundRinging { player.isLooping = true } return player } } public static var isCallKitSupported: Bool { #if targetEnvironment(simulator) /// The iOS simulator doesn't support CallKit, when receiving a call on the simulator and routing it via CallKit it /// will immediately trigger a hangup making it difficult to test - instead we just should just avoid using CallKit /// entirely on the simulator return false #else guard let regionCode: String = NSLocale.current.regionCode else { return false } guard !regionCode.contains("CN") && !regionCode.contains("CHN") else { return false } return true #endif } }