From 7cc6fa3718e14b61143fe648a7df750980eb570a Mon Sep 17 00:00:00 2001 From: Ali <> Date: Thu, 31 Mar 2022 18:15:24 +0400 Subject: [PATCH 1/3] Update tgcalls --- submodules/TgVoipWebrtc/tgcalls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 95369c01df..96b2bbf3a7 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 95369c01dfc8639b89dd91c4bc6e9da2ae2ed223 +Subproject commit 96b2bbf3a7b352924998e93c649ef30174a1a433 From ed012a56cbcb1fdfc9435eac947d2fffd6863591 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Thu, 31 Mar 2022 19:10:12 +0400 Subject: [PATCH 2/3] Fix context menu workaround --- submodules/ContextUI/Sources/ContextController.swift | 5 +++-- submodules/TelegramUI/Sources/ChatController.swift | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 7d0f954ae0..78a9855c95 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -2254,7 +2254,7 @@ public final class ContextController: ViewController, StandalonePresentableContr public var useComplexItemsTransitionAnimation = false public var immediateItemsTransitionAnimation = false - public var workaroundUseLegacyImplementation = false + let workaroundUseLegacyImplementation: Bool public enum HandledTouchEvent { case ignore @@ -2267,13 +2267,14 @@ public final class ContextController: ViewController, StandalonePresentableContr public var reactionSelected: ((ReactionContextItem, Bool) -> Void)? - public init(account: Account, presentationData: PresentationData, source: ContextContentSource, items: Signal, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil) { + public init(account: Account, presentationData: PresentationData, source: ContextContentSource, items: Signal, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, workaroundUseLegacyImplementation: Bool = false) { self.account = account self.presentationData = presentationData self.source = source self.items = items self.recognizer = recognizer self.gesture = gesture + self.workaroundUseLegacyImplementation = workaroundUseLegacyImplementation super.init(navigationBarPresentationData: nil) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 96dfc876b1..9dc1c58fb2 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -8216,8 +8216,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(ChatControllerContextReferenceContentSource(controller: strongSelf, sourceNode: node, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) - contextController.workaroundUseLegacyImplementation = true + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(ChatControllerContextReferenceContentSource(controller: strongSelf, sourceNode: node, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture, workaroundUseLegacyImplementation: true) contextController.dismissed = { [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(interactive: true, { From eb9ff7bedad348aef7ae8b3e909aec585c29cb63 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Thu, 31 Mar 2022 23:49:52 +0400 Subject: [PATCH 3/3] Notification sound updates --- submodules/Markdown/Source/Markdown.swift | 6 +- .../Sources/NotificationSoundSelection.swift | 120 +++++----- ...ificationExceptionSettingsController.swift | 224 +++++++++++++----- .../ChatInterfaceStateContextMenus.swift | 29 ++- 4 files changed, 251 insertions(+), 128 deletions(-) diff --git a/submodules/Markdown/Source/Markdown.swift b/submodules/Markdown/Source/Markdown.swift index a82f522b81..73a3334ca5 100644 --- a/submodules/Markdown/Source/Markdown.swift +++ b/submodules/Markdown/Source/Markdown.swift @@ -108,8 +108,10 @@ public func parseMarkdownIntoAttributedString(_ string: String, attributes: Mark } result.append(NSAttributedString(string: bold, attributes: boldAttributes)) } else { - result.append(NSAttributedString(string: nsString.substring(with: NSMakeRange(remainingRange.location, 1)), attributes: bodyAttributes)) - remainingRange = NSMakeRange(range.location + 1, remainingRange.length - 1) + if remainingRange.length != 0 { + result.append(NSAttributedString(string: nsString.substring(with: NSMakeRange(remainingRange.location, 1)), attributes: bodyAttributes)) + remainingRange = NSMakeRange(range.location + 1, remainingRange.length - 1) + } } } else { if result.string.hasSuffix("\\") { diff --git a/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift b/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift index bd8c5ee19c..5becf0d648 100644 --- a/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift +++ b/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift @@ -451,69 +451,73 @@ public func notificationSoundSelectionController(context: AccountContext, update controller?.dismiss() } - var presentUndo: ((UndoOverlayContent) -> Void)? - - presentFilePicker = { [weak controller] in guard let controller = controller else { return } - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - controller.present(legacyICloudFilePicker(theme: presentationData.theme, documentTypes: ["public.mp3"], completion: { urls in - guard !urls.isEmpty, let url = urls.first else { - return - } - - let coordinator = NSFileCoordinator(filePresenter: nil) - var error: NSError? - coordinator.coordinate(readingItemAt: url, options: .forUploading, error: &error, byAccessor: { url in - Queue.mainQueue().async { - let asset = AVAsset(url: url) - - guard let data = try? Data(contentsOf: url) else { - return - } - if data.count > 200 * 1024 { - //TODO:localize - presentUndo?(.info(title: "Audio is too large", text: "The file is over 200 KB.")) - return - } - - asset.loadValuesAsynchronously(forKeys: ["tracks", "duration"], completionHandler: { - if asset.statusOfValue(forKey: "duration", error: nil) != .loaded { - return - } - guard let track = asset.tracks(withMediaType: .audio).first else { - return - } - let duration = track.timeRange.duration.seconds - - Queue.mainQueue().async { - if duration > 5.0 { - //TODO:localize - presentUndo?(.info(title: "\(url.lastPathComponent) is too long", text: "The duration is longer than 5 seconds.")) - } else { - soundActionDisposable.set((context.engine.peers.uploadNotificationSound(title: url.lastPathComponent, data: data) - |> deliverOnMainQueue).start(next: { _ in - //TODO:localize - presentUndo?(.notificationSoundAdded(title: "Sound Added", text: "The sound **\(url.deletingPathExtension().lastPathComponent)** was added to your Telegram tones.**", action: nil)) - }, error: { _ in - })) - } - } - }) - } - }) - }), in: .window(.root)) - } - - presentUndo = { [weak controller] content in - guard let controller = controller else { - return - } - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - controller.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + presentCustomNotificationSoundFilePicker(context: context, controller: controller, disposable: soundActionDisposable) } return controller } + +public func presentCustomNotificationSoundFilePicker(context: AccountContext, controller: ViewController, disposable: MetaDisposable) { + let presentUndo: (UndoOverlayContent) -> Void = { [weak controller] content in + guard let controller = controller else { + return + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + } + + let settings = NotificationSoundSettings.extract(from: context.currentAppConfiguration.with({ $0 })) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + controller.present(legacyICloudFilePicker(theme: presentationData.theme, documentTypes: ["public.mp3"], completion: { urls in + guard !urls.isEmpty, let url = urls.first else { + return + } + + let coordinator = NSFileCoordinator(filePresenter: nil) + var error: NSError? + coordinator.coordinate(readingItemAt: url, options: .forUploading, error: &error, byAccessor: { url in + Queue.mainQueue().async { + let asset = AVAsset(url: url) + + guard let data = try? Data(contentsOf: url) else { + return + } + if data.count > settings.maxSize { + //TODO:localize + presentUndo(.info(title: "Audio is too large", text: "The file is over \(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).")) + return + } + + asset.loadValuesAsynchronously(forKeys: ["tracks", "duration"], completionHandler: { + if asset.statusOfValue(forKey: "duration", error: nil) != .loaded { + return + } + guard let track = asset.tracks(withMediaType: .audio).first else { + return + } + let duration = track.timeRange.duration.seconds + + Queue.mainQueue().async { + if duration > Double(settings.maxDuration) { + //TODO:localize + presentUndo(.info(title: "\(url.lastPathComponent) is too long", text: "The duration is longer than \(stringForDuration(Int32(settings.maxDuration))).")) + } else { + disposable.set((context.engine.peers.uploadNotificationSound(title: url.lastPathComponent, data: data) + |> deliverOnMainQueue).start(next: { _ in + //TODO:localize + presentUndo(.notificationSoundAdded(title: "Sound Added", text: "The sound **\(url.deletingPathExtension().lastPathComponent)** was added to your Telegram tones.**", action: nil)) + }, error: { _ in + })) + } + } + }) + } + }) + }), in: .window(.root)) +} diff --git a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionSettingsController.swift b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionSettingsController.swift index 40b8161a6a..88f6699b49 100644 --- a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionSettingsController.swift +++ b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionSettingsController.swift @@ -17,30 +17,30 @@ private enum NotificationPeerExceptionSection: Int32 { case remove case switcher case displayPreviews + case soundCloud case soundModern case soundClassic } -private enum NotificationPeerExceptionSwitcher : Equatable { +private enum NotificationPeerExceptionSwitcher: Hashable { case alwaysOn case alwaysOff } -private enum NotificationPeerExceptionEntryId : Hashable { +private enum NotificationPeerExceptionEntryId: Hashable { case remove case switcher(NotificationPeerExceptionSwitcher) - case sound(PeerMessageSound) + case sound(PeerMessageSound.Id) case switcherHeader case displayPreviews(NotificationPeerExceptionSwitcher) case displayPreviewsHeader case soundModernHeader case soundClassicHeader case none + case uploadSound + case cloudHeader + case cloudInfo case `default` - - func hash(into hasher: inout Hasher) { - hasher.combine(0) - } } private final class NotificationPeerExceptionArguments { @@ -52,8 +52,10 @@ private final class NotificationPeerExceptionArguments { let removeFromExceptions: () -> Void let complete: () -> Void let cancel: () -> Void + let upload: () -> Void + let deleteSound: (PeerMessageSound) -> Void - init(account: Account, selectSound: @escaping(PeerMessageSound) -> Void, selectMode: @escaping(NotificationPeerExceptionSwitcher) -> Void, selectDisplayPreviews: @escaping (NotificationPeerExceptionSwitcher) -> Void, removeFromExceptions: @escaping () -> Void, complete: @escaping()->Void, cancel: @escaping() -> Void) { + init(account: Account, selectSound: @escaping(PeerMessageSound) -> Void, selectMode: @escaping(NotificationPeerExceptionSwitcher) -> Void, selectDisplayPreviews: @escaping (NotificationPeerExceptionSwitcher) -> Void, removeFromExceptions: @escaping () -> Void, complete: @escaping()->Void, cancel: @escaping() -> Void, upload: @escaping () -> Void, deleteSound: @escaping (PeerMessageSound) -> Void) { self.account = account self.selectSound = selectSound self.selectMode = selectMode @@ -61,6 +63,8 @@ private final class NotificationPeerExceptionArguments { self.removeFromExceptions = removeFromExceptions self.complete = complete self.cancel = cancel + self.upload = upload + self.deleteSound = deleteSound } } @@ -77,8 +81,10 @@ private enum NotificationPeerExceptionEntry: ItemListNodeEntry { case soundClassicHeader(index:Int32, theme: PresentationTheme, title: String) case none(index:Int32, section: NotificationPeerExceptionSection, theme: PresentationTheme, text: String, selected: Bool) case `default`(index:Int32, section: NotificationPeerExceptionSection, theme: PresentationTheme, text: String, selected: Bool) - case sound(index:Int32, section: NotificationPeerExceptionSection, theme: PresentationTheme, text: String, sound: PeerMessageSound, selected: Bool) - + case sound(index:Int32, section: NotificationPeerExceptionSection, theme: PresentationTheme, text: String, sound: PeerMessageSound, selected: Bool, canBeDeleted: Bool) + case cloudHeader(index: Int32, text: String) + case uploadSound(index: Int32, text: String) + case cloudInfo(index: Int32, text: String) var index: Int32 { switch self { @@ -100,7 +106,13 @@ private enum NotificationPeerExceptionEntry: ItemListNodeEntry { return index case let .default(index, _, _, _, _): return index - case let .sound(index, _, _, _, _, _): + case let .sound(index, _, _, _, _, _, _): + return index + case let .cloudHeader(index, _): + return index + case let .cloudInfo(index, _): + return index + case let .uploadSound(index, _): return index } } @@ -113,6 +125,8 @@ private enum NotificationPeerExceptionEntry: ItemListNodeEntry { return NotificationPeerExceptionSection.switcher.rawValue case .displayPreviews, .displayPreviewsHeader: return NotificationPeerExceptionSection.displayPreviews.rawValue + case .cloudInfo, .cloudHeader, .uploadSound: + return NotificationPeerExceptionSection.soundCloud.rawValue case .soundModernHeader: return NotificationPeerExceptionSection.soundModern.rawValue case .soundClassicHeader: @@ -121,7 +135,7 @@ private enum NotificationPeerExceptionEntry: ItemListNodeEntry { return section.rawValue case let .default(_, section, _, _, _): return section.rawValue - case let .sound(_, section, _, _, _, _): + case let .sound(_, section, _, _, _, _, _): return section.rawValue } } @@ -146,8 +160,14 @@ private enum NotificationPeerExceptionEntry: ItemListNodeEntry { return .none case .default: return .default - case let .sound(_, _, _, _, sound, _): - return .sound(sound) + case let .sound(_, _, _, _, sound, _, _): + return .sound(sound.id) + case .uploadSound: + return .uploadSound + case .cloudHeader: + return .cloudHeader + case .cloudInfo: + return .cloudInfo } } @@ -186,6 +206,15 @@ private enum NotificationPeerExceptionEntry: ItemListNodeEntry { return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.selectDisplayPreviews(value) }) + case let .cloudHeader(_, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .cloudInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .uploadSound(_, text): + let icon = PresentationResourcesItemList.uploadToneIcon(presentationData.theme) + return ItemListCheckboxItem(presentationData: presentationData, icon: icon, iconSize: nil, iconPlacement: .check, title: text, style: .left, checked: false, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.upload() + }) case let .displayPreviewsHeader(_, _, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .soundModernHeader(_, _, text): @@ -200,17 +229,21 @@ private enum NotificationPeerExceptionEntry: ItemListNodeEntry { return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.selectSound(.default) }) - case let .sound(_, _, _, text, sound, selected): + case let .sound(_, _, _, text, sound, selected, canBeDeleted): return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.selectSound(sound) - }) + }, deleteAction: canBeDeleted ? { + arguments.deleteSound(sound) + } : nil) } } } private func notificationPeerExceptionEntries(presentationData: PresentationData, notificationSoundList: NotificationSoundList?, state: NotificationExceptionPeerState) -> [NotificationPeerExceptionEntry] { - var entries:[NotificationPeerExceptionEntry] = [] + let selectedSound = resolvedNotificationSound(sound: state.selectedSound, notificationSoundList: notificationSoundList) + + var entries: [NotificationPeerExceptionEntry] = [] var index: Int32 = 0 @@ -236,18 +269,43 @@ private func notificationPeerExceptionEntries(presentationData: PresentationData entries.append(.displayPreviews(index: index, theme: presentationData.theme, strings: presentationData.strings, value: .alwaysOff, selected: state.displayPreviews == .alwaysOff)) index += 1 - entries.append(.soundModernHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notifications_AlertTones)) + entries.append(.cloudHeader(index: index, text: presentationData.strings.Notifications_TelegramTones)) index += 1 - entries.append(.default(index: index, section: .soundModern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: .default, default: state.defaultSound), selected: state.selectedSound == .default)) + index = 1000 + + //entries.append(.none(section: .cloud, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: .none), selected: selectedSound == .none)) + if let notificationSoundList = notificationSoundList { + for listSound in notificationSoundList.sounds { + let sound: PeerMessageSound = .cloud(fileId: listSound.file.fileId.id) + if state.removedSounds.contains(where: { $0.id == sound.id }) { + continue + } + entries.append(.sound(index: index, section: .soundCloud, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: sound), sound: sound, selected: selectedSound.id == sound.id, canBeDeleted: true)) + index += 1 + } + } + + index = 2000 + + entries.append(.uploadSound(index: index, text: presentationData.strings.Notifications_UploadSound)) + index += 1 + entries.append(.cloudInfo(index: index, text: presentationData.strings.Notifications_MessageSoundInfo)) + index += 1 + + entries.append(.soundModernHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notifications_AlertTones)) + + index = 3000 + + entries.append(.default(index: index, section: .soundModern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: .default, default: state.defaultSound), selected: selectedSound == .default)) index += 1 - entries.append(.none(index: index, section: .soundModern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: .none), selected: state.selectedSound == .none)) + entries.append(.none(index: index, section: .soundModern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: .none), selected: selectedSound == .none)) index += 1 for i in 0 ..< 12 { let sound: PeerMessageSound = .bundledModern(id: Int32(i)) - entries.append(.sound(index: index, section: .soundModern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: sound), sound: sound, selected: sound == state.selectedSound)) + entries.append(.sound(index: index, section: .soundModern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: sound), sound: sound, selected: sound.id == selectedSound.id, canBeDeleted: false)) index += 1 } @@ -256,7 +314,7 @@ private func notificationPeerExceptionEntries(presentationData: PresentationData for i in 0 ..< 8 { let sound: PeerMessageSound = .bundledClassic(id: Int32(i)) - entries.append(.sound(index: index, section: .soundClassic, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: sound), sound: sound, selected: sound == state.selectedSound)) + entries.append(.sound(index: index, section: .soundClassic, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: sound), sound: sound, selected: sound.id == selectedSound.id, canBeDeleted: false)) index += 1 } } @@ -265,11 +323,12 @@ private func notificationPeerExceptionEntries(presentationData: PresentationData } private struct NotificationExceptionPeerState : Equatable { - let canRemove: Bool - let selectedSound: PeerMessageSound - let mode: NotificationPeerExceptionSwitcher - let defaultSound: PeerMessageSound - let displayPreviews: NotificationPeerExceptionSwitcher + var canRemove: Bool + var selectedSound: PeerMessageSound + var mode: NotificationPeerExceptionSwitcher + var defaultSound: PeerMessageSound + var displayPreviews: NotificationPeerExceptionSwitcher + var removedSounds: [PeerMessageSound] init(canRemove: Bool, notifications: TelegramPeerNotificationSettings? = nil) { self.canRemove = canRemove @@ -290,31 +349,19 @@ private struct NotificationExceptionPeerState : Equatable { } self.defaultSound = .default + self.removedSounds = [] } - init(canRemove: Bool, selectedSound: PeerMessageSound, mode: NotificationPeerExceptionSwitcher, defaultSound: PeerMessageSound, displayPreviews: NotificationPeerExceptionSwitcher) { + init(canRemove: Bool, selectedSound: PeerMessageSound, mode: NotificationPeerExceptionSwitcher, defaultSound: PeerMessageSound, displayPreviews: NotificationPeerExceptionSwitcher, removedSounds: [PeerMessageSound]) { self.canRemove = canRemove self.selectedSound = selectedSound self.mode = mode self.defaultSound = defaultSound self.displayPreviews = displayPreviews - } - - func withUpdatedDefaultSound(_ defaultSound: PeerMessageSound) -> NotificationExceptionPeerState { - return NotificationExceptionPeerState(canRemove: self.canRemove, selectedSound: self.selectedSound, mode: self.mode, defaultSound: defaultSound, displayPreviews: self.displayPreviews) - } - func withUpdatedSound(_ selectedSound: PeerMessageSound) -> NotificationExceptionPeerState { - return NotificationExceptionPeerState(canRemove: self.canRemove, selectedSound: selectedSound, mode: self.mode, defaultSound: self.defaultSound, displayPreviews: self.displayPreviews) - } - func withUpdatedMode(_ mode: NotificationPeerExceptionSwitcher) -> NotificationExceptionPeerState { - return NotificationExceptionPeerState(canRemove: self.canRemove, selectedSound: self.selectedSound, mode: mode, defaultSound: self.defaultSound, displayPreviews: self.displayPreviews) - } - func withUpdatedDisplayPreviews(_ displayPreviews: NotificationPeerExceptionSwitcher) -> NotificationExceptionPeerState { - return NotificationExceptionPeerState(canRemove: self.canRemove, selectedSound: self.selectedSound, mode: self.mode, defaultSound: self.defaultSound, displayPreviews: displayPreviews) + self.removedSounds = removedSounds } } - public func notificationPeerExceptionController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: Peer, mode: NotificationExceptionMode, edit: Bool = false, updatePeerSound: @escaping(PeerId, PeerMessageSound) -> Void, updatePeerNotificationInterval: @escaping(PeerId, Int32?) -> Void, updatePeerDisplayPreviews: @escaping(PeerId, PeerNotificationDisplayPreviews) -> Void, removePeerFromExceptions: @escaping () -> Void, modifiedPeer: @escaping () -> Void) -> ViewController { let initialState = NotificationExceptionPeerState(canRemove: false) let statePromise = Promise(initialState) @@ -327,6 +374,9 @@ public func notificationPeerExceptionController(context: AccountContext, updated var removeFromExceptionsImpl: (() -> Void)? var cancelImpl: (() -> Void)? let playSoundDisposable = MetaDisposable() + var presentFilePicker: (() -> Void)? + + let soundActionDisposable = MetaDisposable() let arguments = NotificationPeerExceptionArguments(account: context.account, selectSound: { sound in updateState { state in @@ -336,15 +386,21 @@ public func notificationPeerExceptionController(context: AccountContext, updated playSoundDisposable.set(playSound(context: context, notificationSoundList: notificationSoundList, sound: sound, defaultSound: state.defaultSound).start()) }) - return state.withUpdatedSound(sound) + var state = state + state.selectedSound = sound + return state } }, selectMode: { mode in updateState { state in - return state.withUpdatedMode(mode) + var state = state + state.mode = mode + return state } }, selectDisplayPreviews: { value in updateState { state in - return state.withUpdatedDisplayPreviews(value) + var state = state + state.displayPreviews = value + return state } }, removeFromExceptions: { removeFromExceptionsImpl?() @@ -352,23 +408,45 @@ public func notificationPeerExceptionController(context: AccountContext, updated completeImpl?() }, cancel: { cancelImpl?() + }, upload: { + presentFilePicker?() + }, deleteSound: { sound in + updateState { state in + var state = state + + state.removedSounds.append(sound) + if state.selectedSound.id == sound.id { + state.selectedSound = .bundledModern(id: 0) + } + + return state + } + switch sound { + case let .cloud(id): + soundActionDisposable.set((context.engine.peers.deleteNotificationSound(fileId: id) + |> deliverOnMainQueue).start(completed: { + })) + default: + break + } }) statePromise.set(context.account.postbox.transaction { transaction -> NotificationExceptionPeerState in var state = NotificationExceptionPeerState(canRemove: mode.peerIds.contains(peer.id), notifications: transaction.getPeerNotificationSettings(peer.id) as? TelegramPeerNotificationSettings) let globalSettings: GlobalNotificationSettings = transaction.getPreferencesEntry(key: PreferencesKeys.globalNotifications)?.get(GlobalNotificationSettings.self) ?? GlobalNotificationSettings.defaultSettings switch mode { - case .channels: - state = state.withUpdatedDefaultSound(globalSettings.effective.channels.sound) - case .groups: - state = state.withUpdatedDefaultSound(globalSettings.effective.groupChats.sound) - case .users: - state = state.withUpdatedDefaultSound(globalSettings.effective.privateChats.sound) + case .channels: + state.defaultSound = globalSettings.effective.channels.sound + case .groups: + state.defaultSound = globalSettings.effective.groupChats.sound + case .users: + state.defaultSound = globalSettings.effective.privateChats.sound } - _ = stateValue.swap(state) + let _ = stateValue.swap(state) return state }) + let previousSoundIds = Atomic>(value: Set()) let signal = combineLatest(queue: .mainQueue(), (updatedPresentationData?.signal ?? context.sharedContext.presentationData), context.engine.peers.notificationSoundList(), statePromise.get() |> distinctUntilChanged) |> map { presentationData, notificationSoundList, state -> (ItemListControllerState, (ItemListNodeState, Any)) in @@ -380,14 +458,30 @@ public func notificationPeerExceptionController(context: AccountContext, updated arguments.complete() }) + var updatedSoundIds = Set() + if let notificationSoundList = notificationSoundList { + for sound in notificationSoundList.sounds { + if state.removedSounds.contains(.cloud(fileId: sound.file.fileId.id)) { + continue + } + updatedSoundIds.insert(sound.file.fileId.id) + } + } + + var animated = false + if previousSoundIds.swap(updatedSoundIds) != updatedSoundIds { + animated = true + } + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: notificationPeerExceptionEntries(presentationData: presentationData, notificationSoundList: notificationSoundList, state: state), style: .blocks, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: notificationPeerExceptionEntries(presentationData: presentationData, notificationSoundList: notificationSoundList, state: state), style: .blocks, animateChanges: animated) return (controllerState, (listState, arguments)) } let controller = ItemListController(context: context, state: signal |> afterDisposed { playSoundDisposable.dispose() + soundActionDisposable.dispose() }) controller.enableInteractiveDismiss = true @@ -395,12 +489,17 @@ public func notificationPeerExceptionController(context: AccountContext, updated completeImpl = { [weak controller] in controller?.dismiss() modifiedPeer() - updateState { state in - updatePeerSound(peer.id, state.selectedSound) - updatePeerNotificationInterval(peer.id, state.mode == .alwaysOn ? 0 : Int32.max) - updatePeerDisplayPreviews(peer.id, state.displayPreviews == .alwaysOn ? .show : .hide) - return state - } + + let _ = (context.engine.peers.notificationSoundList() + |> take(1) + |> deliverOnMainQueue).start(next: { notificationSoundList in + updateState { state in + updatePeerSound(peer.id, resolvedNotificationSound(sound: state.selectedSound, notificationSoundList: notificationSoundList)) + updatePeerNotificationInterval(peer.id, state.mode == .alwaysOn ? 0 : Int32.max) + updatePeerDisplayPreviews(peer.id, state.displayPreviews == .alwaysOn ? .show : .hide) + return state + } + }) } removeFromExceptionsImpl = { [weak controller] in @@ -411,6 +510,13 @@ public func notificationPeerExceptionController(context: AccountContext, updated cancelImpl = { [weak controller] in controller?.dismiss() } + + presentFilePicker = { [weak controller] in + guard let controller = controller else { + return + } + presentCustomNotificationSoundFilePicker(context: context, controller: controller, disposable: soundActionDisposable) + } return controller } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index d3323845fd..2e509de832 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -871,19 +871,30 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } for media in message.media { - if let file = media as? TelegramMediaFile, let size = file.size, size < 200 * 1024, (["audio/mpeg", "audio/mp3", "audio/mpeg3"] as [String]).contains(file.mimeType.lowercased()) { + if let file = media as? TelegramMediaFile, let size = file.size, let duration = file.duration, (["audio/mpeg", "audio/mp3", "audio/mpeg3"] as [String]).contains(file.mimeType.lowercased()) { actions.append(.action(ContextMenuActionItem(text: "Save for Notifications", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/DownloadTone"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in - let _ = (context.engine.peers.saveNotificationSound(file: .message(message: MessageReference(message), media: file)) - |> deliverOnMainQueue).start(completed: { - //TODO:localize - controllerInteraction.displayUndo(.notificationSoundAdded(title: "Sound added", text: "You can now use this sound as a notification tone in your [custom notification settings]().", action: { - controllerInteraction.navigationController()?.pushViewController(notificationsAndSoundsController(context: context, exceptionsList: nil)) - })) - }) - f(.default) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let settings = NotificationSoundSettings.extract(from: context.currentAppConfiguration.with({ $0 })) + if size > settings.maxSize { + //TODO:localize + controllerInteraction.displayUndo(.info(title: "Audio is too large", text: "The file is over \(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).")) + } else if Double(duration) > Double(settings.maxDuration) { + //TODO:localize + controllerInteraction.displayUndo(.info(title: "Audio is too long", text: "The duration is longer than \(stringForDuration(Int32(settings.maxDuration))).")) + } else { + let _ = (context.engine.peers.saveNotificationSound(file: .message(message: MessageReference(message), media: file)) + |> deliverOnMainQueue).start(completed: { + //TODO:localize + controllerInteraction.displayUndo(.notificationSoundAdded(title: "Sound added", text: "You can now use this sound as a notification tone in your [custom notification settings]().", action: { + controllerInteraction.navigationController()?.pushViewController(notificationsAndSoundsController(context: context, exceptionsList: nil)) + })) + }) + } }))) /*