import Foundation import SwiftSignalKit import Postbox import TelegramCore import Contacts import AddressBook public struct PresentationDateTimeFormat: Equatable { let timeFormat: PresentationTimeFormat let dateFormat: PresentationDateFormat let dateSeparator: String } public struct PresentationVolumeControlStatusBarIcons: Equatable { let offIcon: UIImage let halfIcon: UIImage let fullIcon: UIImage public var images: (UIImage, UIImage, UIImage) { return (self.offIcon, self.halfIcon, self.fullIcon) } } public enum PresentationTimeFormat { case regular case military } public enum PresentationDateFormat { case monthFirst case dayFirst } public enum PresentationPersonNameOrder: Int32 { case firstLast = 0 case lastFirst = 1 } extension PresentationStrings : Equatable { public static func ==(lhs: PresentationStrings, rhs: PresentationStrings) -> Bool { return lhs === rhs } } public final class PresentationData: Equatable { public let strings: PresentationStrings public let theme: PresentationTheme public let chatWallpaper: TelegramWallpaper public let chatWallpaperMode: WallpaperPresentationOptions public let volumeControlStatusBarIcons: PresentationVolumeControlStatusBarIcons public let fontSize: PresentationFontSize public let dateTimeFormat: PresentationDateTimeFormat public let nameDisplayOrder: PresentationPersonNameOrder public let nameSortOrder: PresentationPersonNameOrder public let disableAnimations: Bool public init(strings: PresentationStrings, theme: PresentationTheme, chatWallpaper: TelegramWallpaper, chatWallpaperMode: WallpaperPresentationOptions, volumeControlStatusBarIcons: PresentationVolumeControlStatusBarIcons, fontSize: PresentationFontSize, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, nameSortOrder: PresentationPersonNameOrder, disableAnimations: Bool) { self.strings = strings self.theme = theme self.chatWallpaper = chatWallpaper self.chatWallpaperMode = chatWallpaperMode self.volumeControlStatusBarIcons = volumeControlStatusBarIcons self.fontSize = fontSize self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder self.nameSortOrder = nameSortOrder self.disableAnimations = disableAnimations } public static func ==(lhs: PresentationData, rhs: PresentationData) -> Bool { return lhs.strings === rhs.strings && lhs.theme === rhs.theme && lhs.chatWallpaper == rhs.chatWallpaper && lhs.chatWallpaperMode == rhs.chatWallpaperMode && lhs.volumeControlStatusBarIcons == rhs.volumeControlStatusBarIcons && lhs.fontSize == rhs.fontSize && lhs.dateTimeFormat == rhs.dateTimeFormat && lhs.disableAnimations == rhs.disableAnimations } } func dictFromLocalization(_ value: Localization) -> [String: String] { var dict: [String: String] = [:] for entry in value.entries { switch entry { case let .string(key, value): dict[key] = value case let .pluralizedString(key, zero, one, two, few, many, other): if let zero = zero { dict["\(key)_zero"] = zero } if let one = one { dict["\(key)_1"] = one } if let two = two { dict["\(key)_2"] = two } if let few = few { dict["\(key)_3_10"] = few } if let many = many { dict["\(key)_many"] = many } dict["\(key)_any"] = other } } return dict } private func volumeControlStatusBarIcons() -> PresentationVolumeControlStatusBarIcons { return PresentationVolumeControlStatusBarIcons(offIcon: UIImage(bundleImageName: "Components/Volume/VolumeOff")!, halfIcon: UIImage(bundleImageName: "Components/Volume/VolumeHalf")!, fullIcon: UIImage(bundleImageName: "Components/Volume/VolumeFull")!) } private func currentDateTimeFormat() -> PresentationDateTimeFormat { let locale = Locale.current let dateFormatter = DateFormatter() dateFormatter.locale = locale dateFormatter.dateStyle = .none dateFormatter.timeStyle = .medium dateFormatter.timeZone = TimeZone.current let dateString = dateFormatter.string(from: Date()) let timeFormat: PresentationTimeFormat if dateString.contains(dateFormatter.amSymbol) || dateString.contains(dateFormatter.pmSymbol) { timeFormat = .regular } else { timeFormat = .military } let dateFormat: PresentationDateFormat let dateSeparator: String if let dateString = DateFormatter.dateFormat(fromTemplate: "MdY", options: 0, locale: locale) { if dateString.contains(".") { dateSeparator = "." } else if dateString.contains("/") { dateSeparator = "/" } else if dateString.contains("-") { dateSeparator = "-" } else { dateSeparator = "/" } if dateString.contains("M\(dateSeparator)d") { dateFormat = .monthFirst } else { dateFormat = .dayFirst } } else { dateSeparator = "/" dateFormat = .dayFirst } return PresentationDateTimeFormat(timeFormat: timeFormat, dateFormat: dateFormat, dateSeparator: dateSeparator) } private func currentPersonNameSortOrder() -> PresentationPersonNameOrder { if #available(iOSApplicationExtension 9.0, *) { switch CNContactsUserDefaults.shared().sortOrder { case .givenName: return .firstLast default: return .lastFirst } } else { if ABPersonGetSortOrdering() == kABPersonSortByFirstName { return .firstLast } else { return .lastFirst } } } public final class InitialPresentationDataAndSettings { public let presentationData: PresentationData public let automaticMediaDownloadSettings: AutomaticMediaDownloadSettings public let callListSettings: CallListSettings public let inAppNotificationSettings: InAppNotificationSettings public let mediaInputSettings: MediaInputSettings public let experimentalUISettings: ExperimentalUISettings init(presentationData: PresentationData, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings, callListSettings: CallListSettings, inAppNotificationSettings: InAppNotificationSettings, mediaInputSettings: MediaInputSettings, experimentalUISettings: ExperimentalUISettings) { self.presentationData = presentationData self.automaticMediaDownloadSettings = automaticMediaDownloadSettings self.callListSettings = callListSettings self.inAppNotificationSettings = inAppNotificationSettings self.mediaInputSettings = mediaInputSettings self.experimentalUISettings = experimentalUISettings } } public func currentPresentationDataAndSettings(postbox: Postbox) -> Signal { return postbox.transaction { transaction -> (PresentationThemeSettings, LocalizationSettings?, AutomaticMediaDownloadSettings, CallListSettings, InAppNotificationSettings, MediaInputSettings, ExperimentalUISettings, ContactSynchronizationSettings) in let themeSettings: PresentationThemeSettings if let current = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.presentationThemeSettings) as? PresentationThemeSettings { themeSettings = current } else { themeSettings = PresentationThemeSettings.defaultSettings } let localizationSettings: LocalizationSettings? if let current = transaction.getPreferencesEntry(key: PreferencesKeys.localizationSettings) as? LocalizationSettings { localizationSettings = current } else { localizationSettings = nil } let automaticMediaDownloadSettings: AutomaticMediaDownloadSettings if let value = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings) as? AutomaticMediaDownloadSettings { automaticMediaDownloadSettings = value } else { automaticMediaDownloadSettings = AutomaticMediaDownloadSettings.defaultSettings } let callListSettings: CallListSettings if let value = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.callListSettings) as? CallListSettings { callListSettings = value } else { callListSettings = CallListSettings.defaultSettings } let inAppNotificationSettings: InAppNotificationSettings if let value = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.inAppNotificationSettings) as? InAppNotificationSettings { inAppNotificationSettings = value } else { inAppNotificationSettings = InAppNotificationSettings.defaultSettings } let mediaInputSettings: MediaInputSettings if let value = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.mediaInputSettings) as? MediaInputSettings { mediaInputSettings = value } else { mediaInputSettings = MediaInputSettings.defaultSettings } let experimentalUISettings: ExperimentalUISettings = (transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.experimentalUISettings) as? ExperimentalUISettings) ?? ExperimentalUISettings.defaultSettings let contactSettings: ContactSynchronizationSettings = (transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.contactSynchronizationSettings) as? ContactSynchronizationSettings) ?? ContactSynchronizationSettings.defaultSettings return (themeSettings, localizationSettings, automaticMediaDownloadSettings, callListSettings, inAppNotificationSettings, mediaInputSettings, experimentalUISettings, contactSettings) } |> map { (themeSettings, localizationSettings, automaticMediaDownloadSettings, callListSettings, inAppNotificationSettings, mediaInputSettings, experimentalUISettings, contactSettings) -> InitialPresentationDataAndSettings in let themeValue: PresentationTheme let effectiveTheme: PresentationThemeReference var effectiveChatWallpaper: TelegramWallpaper = themeSettings.chatWallpaper var effectiveChatWallpaperOptions: WallpaperPresentationOptions = themeSettings.chatWallpaperOptions if automaticThemeShouldSwitchNow(themeSettings.automaticThemeSwitchSetting, currentTheme: themeSettings.theme) { effectiveTheme = .builtin(themeSettings.automaticThemeSwitchSetting.theme) switch effectiveChatWallpaper { case .builtin, .color: switch themeSettings.automaticThemeSwitchSetting.theme { case .nightAccent: effectiveChatWallpaper = .color(0x18222d) effectiveChatWallpaperOptions = [] case .nightGrayscale: effectiveChatWallpaper = .color(0x000000) effectiveChatWallpaperOptions = [] default: break } default: break } } else { effectiveTheme = themeSettings.theme } switch effectiveTheme { case let .builtin(reference): switch reference { case .dayClassic: themeValue = defaultPresentationTheme case .nightGrayscale: themeValue = defaultDarkPresentationTheme case .nightAccent: themeValue = defaultDarkAccentPresentationTheme case .day: themeValue = makeDefaultDayPresentationTheme(accentColor: themeSettings.themeAccentColor ?? defaultDayAccentColor) } } let stringsValue: PresentationStrings if let localizationSettings = localizationSettings { stringsValue = PresentationStrings(primaryComponent: PresentationStringsComponent(languageCode: localizationSettings.primaryComponent.languageCode, localizedName: localizationSettings.primaryComponent.localizedName, pluralizationRulesCode: localizationSettings.primaryComponent.customPluralizationCode, dict: dictFromLocalization(localizationSettings.primaryComponent.localization)), secondaryComponent: localizationSettings.secondaryComponent.flatMap({ PresentationStringsComponent(languageCode: $0.languageCode, localizedName: $0.localizedName, pluralizationRulesCode: $0.customPluralizationCode, dict: dictFromLocalization($0.localization)) })) } else { stringsValue = defaultPresentationStrings } let dateTimeFormat = currentDateTimeFormat() let nameDisplayOrder = contactSettings.nameDisplayOrder let nameSortOrder = currentPersonNameSortOrder() return InitialPresentationDataAndSettings(presentationData: PresentationData(strings: stringsValue, theme: themeValue, chatWallpaper: effectiveChatWallpaper, chatWallpaperMode: effectiveChatWallpaperOptions, volumeControlStatusBarIcons: volumeControlStatusBarIcons(), fontSize: themeSettings.fontSize, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, nameSortOrder: nameSortOrder, disableAnimations: themeSettings.disableAnimations), automaticMediaDownloadSettings: automaticMediaDownloadSettings, callListSettings: callListSettings, inAppNotificationSettings: inAppNotificationSettings, mediaInputSettings: mediaInputSettings, experimentalUISettings: experimentalUISettings) } } private var first = true private func roundTimeToDay(_ timestamp: Int32) -> Int32 { let calendar = Calendar.current let offset = 0 let components = calendar.dateComponents([.hour, .minute, .second], from: Date(timeIntervalSince1970: Double(timestamp + Int32(offset)))) return Int32(components.hour! * 60 * 60 + components.minute! * 60 + components.second!) } private func automaticThemeShouldSwitchNow(_ settings: AutomaticThemeSwitchSetting, currentTheme: PresentationThemeReference) -> Bool { switch currentTheme { case let .builtin(builtin): switch builtin { case .nightAccent, .nightGrayscale: return false default: break } } switch settings.trigger { case .none: return false case let .timeBased(setting): let fromValue: Int32 let toValue: Int32 switch setting { case let .automatic(automatic): fromValue = automatic.sunset toValue = automatic.sunrise case let .manual(fromSeconds, toSeconds): fromValue = fromSeconds toValue = toSeconds } let roundedTimestamp = roundTimeToDay(Int32(Date().timeIntervalSince1970)) if roundedTimestamp >= fromValue || roundedTimestamp <= toValue { return true } else { return false } case let .brightness(threshold): return UIScreen.main.brightness <= CGFloat(threshold) } } private func automaticThemeShouldSwitch(_ settings: AutomaticThemeSwitchSetting, currentTheme: PresentationThemeReference) -> Signal { if case .none = settings.trigger { return .single(false) } else { return Signal { subscriber in subscriber.putNext(automaticThemeShouldSwitchNow(settings, currentTheme: currentTheme)) let timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { subscriber.putNext(automaticThemeShouldSwitchNow(settings, currentTheme: currentTheme)) }, queue: Queue.mainQueue()) timer.start() return ActionDisposable { timer.invalidate() } } |> runOn(Queue.mainQueue()) |> distinctUntilChanged } } public func updatedPresentationData(postbox: Postbox, applicationBindings: TelegramApplicationBindings) -> Signal { let preferencesKey = PostboxViewKey.preferences(keys: Set([ApplicationSpecificPreferencesKeys.presentationThemeSettings, PreferencesKeys.localizationSettings, ApplicationSpecificPreferencesKeys.contactSynchronizationSettings])) return postbox.combinedView(keys: [preferencesKey]) |> mapToSignal { view -> Signal in let themeSettings: PresentationThemeSettings if let current = (view.views[preferencesKey] as! PreferencesView).values[ApplicationSpecificPreferencesKeys.presentationThemeSettings] as? PresentationThemeSettings { themeSettings = current } else { themeSettings = PresentationThemeSettings.defaultSettings } let contactSettings: ContactSynchronizationSettings = (view.views[preferencesKey] as! PreferencesView).values[ApplicationSpecificPreferencesKeys.contactSynchronizationSettings] as? ContactSynchronizationSettings ?? ContactSynchronizationSettings.defaultSettings return (.single(UIColor(rgb: 0x000000, alpha: 0.3)) |> then(chatServiceBackgroundColor(wallpaper: themeSettings.chatWallpaper, postbox: postbox))) |> mapToSignal { serviceBackgroundColor in return applicationBindings.applicationInForeground |> mapToSignal({ inForeground -> Signal in if inForeground { return automaticThemeShouldSwitch(themeSettings.automaticThemeSwitchSetting, currentTheme: themeSettings.theme) |> distinctUntilChanged |> map { shouldSwitch in let themeValue: PresentationTheme let effectiveTheme: PresentationThemeReference var effectiveChatWallpaper: TelegramWallpaper = themeSettings.chatWallpaper var effectiveChatWallpaperOptions: WallpaperPresentationOptions = themeSettings.chatWallpaperOptions if shouldSwitch { effectiveTheme = .builtin(themeSettings.automaticThemeSwitchSetting.theme) switch effectiveChatWallpaper { case .builtin, .color: switch themeSettings.automaticThemeSwitchSetting.theme { case .nightAccent: effectiveChatWallpaper = .color(0x18222d) effectiveChatWallpaperOptions = [] case .nightGrayscale: effectiveChatWallpaper = .color(0x000000) effectiveChatWallpaperOptions = [] default: break } default: break } } else { effectiveTheme = themeSettings.theme } switch effectiveTheme { case let .builtin(reference): switch reference { case .dayClassic: themeValue = makeDefaultPresentationTheme(serviceBackgroundColor: serviceBackgroundColor) case .nightGrayscale: themeValue = defaultDarkPresentationTheme case .nightAccent: themeValue = defaultDarkAccentPresentationTheme case .day: themeValue = makeDefaultDayPresentationTheme(accentColor: themeSettings.themeAccentColor ?? defaultDayAccentColor) } } let localizationSettings: LocalizationSettings? if let current = (view.views[preferencesKey] as! PreferencesView).values[PreferencesKeys.localizationSettings] as? LocalizationSettings { localizationSettings = current } else { localizationSettings = nil } let stringsValue: PresentationStrings if let localizationSettings = localizationSettings { stringsValue = PresentationStrings(primaryComponent: PresentationStringsComponent(languageCode: localizationSettings.primaryComponent.languageCode, localizedName: localizationSettings.primaryComponent.localizedName, pluralizationRulesCode: localizationSettings.primaryComponent.customPluralizationCode, dict: dictFromLocalization(localizationSettings.primaryComponent.localization)), secondaryComponent: localizationSettings.secondaryComponent.flatMap({ PresentationStringsComponent(languageCode: $0.languageCode, localizedName: $0.localizedName, pluralizationRulesCode: $0.customPluralizationCode, dict: dictFromLocalization($0.localization)) })) } else { stringsValue = defaultPresentationStrings } let dateTimeFormat = currentDateTimeFormat() let nameDisplayOrder = contactSettings.nameDisplayOrder let nameSortOrder = currentPersonNameSortOrder() return PresentationData(strings: stringsValue, theme: themeValue, chatWallpaper: effectiveChatWallpaper, chatWallpaperMode: effectiveChatWallpaperOptions, volumeControlStatusBarIcons: volumeControlStatusBarIcons(), fontSize: themeSettings.fontSize, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, nameSortOrder: nameSortOrder, disableAnimations: themeSettings.disableAnimations) } } else { return .complete() } }) } } } public func defaultPresentationData() -> PresentationData { let dateTimeFormat = currentDateTimeFormat() let nameDisplayOrder: PresentationPersonNameOrder = .firstLast let nameSortOrder = currentPersonNameSortOrder() let themeSettings = PresentationThemeSettings.defaultSettings return PresentationData(strings: defaultPresentationStrings, theme: defaultPresentationTheme, chatWallpaper: .builtin, chatWallpaperMode: [], volumeControlStatusBarIcons: volumeControlStatusBarIcons(), fontSize: themeSettings.fontSize, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, nameSortOrder: nameSortOrder, disableAnimations: themeSettings.disableAnimations) }