diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 7695329dee..7d0f954ae0 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -544,52 +544,55 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi private func initializeContent() { switch self.source { case let .reference(source): - /*let transitionInfo = source.transitionInfo() - if let transitionInfo = transitionInfo { - let referenceView = transitionInfo.referenceView - self.contentContainerNode.contentNode = .reference(view: referenceView) - self.contentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace - self.customPosition = transitionInfo.customPosition - var projectedFrame = convertFrame(referenceView.bounds, from: referenceView, to: self.view) - projectedFrame.origin.x += transitionInfo.insets.left - projectedFrame.size.width -= transitionInfo.insets.left + transitionInfo.insets.right - projectedFrame.origin.y += transitionInfo.insets.top - projectedFrame.size.width -= transitionInfo.insets.top + transitionInfo.insets.bottom - self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) - }*/ - let presentationNode = ContextControllerExtractedPresentationNode( - getController: { [weak self] in - return self?.getController() - }, - requestUpdate: { [weak self] transition in - guard let strongSelf = self else { - return - } - if let validLayout = strongSelf.validLayout { - strongSelf.updateLayout( - layout: validLayout, - transition: transition, - previousActionsContainerNode: nil - ) - } - }, - requestDismiss: { [weak self] result in - guard let strongSelf = self else { - return - } - strongSelf.dismissedForCancel?() - strongSelf.beginDismiss(result) - }, - requestAnimateOut: { [weak self] result, completion in - guard let strongSelf = self else { - return - } - strongSelf.animateOut(result: result, completion: completion) - }, - source: .reference(source) - ) - self.presentationNode = presentationNode - self.addSubnode(presentationNode) + if let controller = self.getController() as? ContextController, controller.workaroundUseLegacyImplementation { + let transitionInfo = source.transitionInfo() + if let transitionInfo = transitionInfo { + let referenceView = transitionInfo.referenceView + self.contentContainerNode.contentNode = .reference(view: referenceView) + self.contentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace + self.customPosition = transitionInfo.customPosition + var projectedFrame = convertFrame(referenceView.bounds, from: referenceView, to: self.view) + projectedFrame.origin.x += transitionInfo.insets.left + projectedFrame.size.width -= transitionInfo.insets.left + transitionInfo.insets.right + projectedFrame.origin.y += transitionInfo.insets.top + projectedFrame.size.width -= transitionInfo.insets.top + transitionInfo.insets.bottom + self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) + } + } else { + let presentationNode = ContextControllerExtractedPresentationNode( + getController: { [weak self] in + return self?.getController() + }, + requestUpdate: { [weak self] transition in + guard let strongSelf = self else { + return + } + if let validLayout = strongSelf.validLayout { + strongSelf.updateLayout( + layout: validLayout, + transition: transition, + previousActionsContainerNode: nil + ) + } + }, + requestDismiss: { [weak self] result in + guard let strongSelf = self else { + return + } + strongSelf.dismissedForCancel?() + strongSelf.beginDismiss(result) + }, + requestAnimateOut: { [weak self] result, completion in + guard let strongSelf = self else { + return + } + strongSelf.animateOut(result: result, completion: completion) + }, + source: .reference(source) + ) + self.presentationNode = presentationNode + self.addSubnode(presentationNode) + } case let .extracted(source): let presentationNode = ContextControllerExtractedPresentationNode( getController: { [weak self] in @@ -2251,6 +2254,7 @@ public final class ContextController: ViewController, StandalonePresentableContr public var useComplexItemsTransitionAnimation = false public var immediateItemsTransitionAnimation = false + public var workaroundUseLegacyImplementation = false public enum HandledTouchEvent { case ignore diff --git a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift index 37f0cb61af..86f51a4c6c 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift @@ -32,8 +32,9 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem { let zeroSeparatorInsets: Bool public let sectionId: ItemListSectionId let action: () -> Void + let deleteAction: (() -> Void)? - public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, iconSize: CGSize? = nil, iconPlacement: IconPlacement = .default, title: String, style: ItemListCheckboxItemStyle, color: ItemListCheckboxItemColor = .accent, checked: Bool, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { + public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, iconSize: CGSize? = nil, iconPlacement: IconPlacement = .default, title: String, style: ItemListCheckboxItemStyle, color: ItemListCheckboxItemColor = .accent, checked: Bool, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void, deleteAction: (() -> Void)? = nil) { self.presentationData = presentationData self.icon = icon self.iconSize = iconSize @@ -45,6 +46,7 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem { self.zeroSeparatorInsets = zeroSeparatorInsets self.sectionId = sectionId self.action = action + self.deleteAction = deleteAction } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -88,7 +90,7 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem { } } -public class ItemListCheckboxItemNode: ListViewItemNode { +public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode @@ -97,6 +99,7 @@ public class ItemListCheckboxItemNode: ListViewItemNode { private let activateArea: AccessibilityAreaNode + private let contentContainerNode: ASDisplayNode private let imageNode: ASImageNode private let iconNode: ASImageNode private let titleNode: TextNode @@ -115,6 +118,8 @@ public class ItemListCheckboxItemNode: ListViewItemNode { self.maskNode = ASImageNode() + self.contentContainerNode = ASDisplayNode() + self.imageNode = ASImageNode() self.imageNode.isLayerBacked = true self.imageNode.displayWithoutProcessing = true @@ -135,11 +140,12 @@ public class ItemListCheckboxItemNode: ListViewItemNode { self.activateArea = AccessibilityAreaNode() - super.init(layerBacked: false, dynamicBounce: false) + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) - self.addSubnode(self.imageNode) - self.addSubnode(self.iconNode) - self.addSubnode(self.titleNode) + self.addSubnode(self.contentContainerNode) + self.contentContainerNode.addSubnode(self.imageNode) + self.contentContainerNode.addSubnode(self.iconNode) + self.contentContainerNode.addSubnode(self.titleNode) self.addSubnode(self.activateArea) self.activateArea.activate = { [weak self] in @@ -204,6 +210,8 @@ public class ItemListCheckboxItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item + strongSelf.contentContainerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: layout.contentSize.height)) + strongSelf.activateArea.accessibilityLabel = item.title if item.checked { strongSelf.activateArea.accessibilityValue = "Selected" @@ -297,6 +305,14 @@ public class ItemListCheckboxItemNode: ListViewItemNode { } strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel)) + + strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) + + if item.deleteAction != nil { + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)])) + } else { + strongSelf.setRevealOptions((left: [], right: [])) + } } }) } @@ -347,4 +363,61 @@ public class ItemListCheckboxItemNode: ListViewItemNode { override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } + + override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + super.updateRevealOffset(offset: offset, transition: transition) + + transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: CGPoint(x: offset, y: self.contentContainerNode.frame.minY), size: self.contentContainerNode.bounds.size)) + + /*if let (item, params, _, _, _) = self.layoutParams { + let revealOffset = offset + + let editingOffset: CGFloat + if let editableControlNode = self.editableControlNode { + editingOffset = editableControlNode.bounds.size.width + var editableControlFrame = editableControlNode.frame + editableControlFrame.origin.x = params.leftInset + offset + transition.updateFrame(node: editableControlNode, frame: editableControlFrame) + } else { + editingOffset = 0.0 + } + + let leftInset: CGFloat = 86.0 + params.leftInset + editingOffset + let rightInset: CGFloat = 13.0 + params.rightInset + var infoIconRightInset: CGFloat = rightInset - 1.0 + + var dateRightInset: CGFloat = 46.0 + params.rightInset + if item.editing { + dateRightInset += 5.0 + infoIconRightInset -= 36.0 + } + + var avatarFrame = self.avatarNode.frame + avatarFrame.origin.x = revealOffset + leftInset - 52.0 + transition.updateFrameAdditive(node: self.avatarNode, frame: avatarFrame) + + transition.updateFrameAdditive(node: self.titleNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size)) + + transition.updateFrameAdditive(node: self.statusNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: self.statusNode.frame.minY), size: self.statusNode.bounds.size)) + + transition.updateFrameAdditive(node: self.dateNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + self.bounds.size.width - dateRightInset - self.dateNode.bounds.size.width, y: self.dateNode.frame.minY), size: self.dateNode.bounds.size)) + + transition.updateFrameAdditive(node: self.typeIconNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 81.0, y: self.typeIconNode.frame.minY), size: self.typeIconNode.bounds.size)) + + transition.updateFrameAdditive(node: self.infoButtonNode, frame: CGRect(origin: CGPoint(x: revealOffset + self.bounds.size.width - infoIconRightInset - self.infoButtonNode.bounds.width, y: self.infoButtonNode.frame.minY), size: self.infoButtonNode.bounds.size)) + }*/ + } + + override public func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { + self.setRevealOptionsOpened(false, animated: true) + self.revealOptionsInteractivelyClosed() + + if let item = self.item { + item.deleteAction?() + } + + /*if let item = self.layoutParams?.0 { + item.interaction.delete(item.messages.map { $0.id }) + }*/ + } } diff --git a/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift b/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift index a3e43760a5..bd8c5ee19c 100644 --- a/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift +++ b/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift @@ -21,6 +21,7 @@ private struct NotificationSoundSelectionArguments { let complete: () -> Void let cancel: () -> Void let upload: () -> Void + let deleteSound: (PeerMessageSound) -> Void } private enum NotificationSoundSelectionSection: Int32 { @@ -31,9 +32,15 @@ private enum NotificationSoundSelectionSection: Int32 { private struct NotificationSoundSelectionState: Equatable { var selectedSound: PeerMessageSound + var removedSounds: [PeerMessageSound] } private enum NotificationSoundSelectionEntry: ItemListNodeEntry { + enum StableId: Hashable { + case index(Int32) + case sound(PeerMessageSound.Id) + } + case cloudHeader(String) case uploadSound(String) case cloudInfo(String) @@ -42,7 +49,7 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry { case classicHeader(PresentationTheme, String) case none(section: NotificationSoundSelectionSection, theme: PresentationTheme, text: String, selected: Bool) case `default`(section: NotificationSoundSelectionSection, theme: PresentationTheme, text: String, selected: Bool) - case sound(section: NotificationSoundSelectionSection, index: Int32, theme: PresentationTheme, text: String, sound: PeerMessageSound, selected: Bool) + case sound(section: NotificationSoundSelectionSection, index: Int32, theme: PresentationTheme, text: String, sound: PeerMessageSound, selected: Bool, canBeDeleted: Bool) var section: ItemListSectionId { switch self { @@ -56,7 +63,7 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry { return section.rawValue case let .default(section, _, _, _): return section.rawValue - case let .sound(section, _, _, _, _, _): + case let .sound(section, _, _, _, _, _, _): return section.rawValue } } @@ -91,7 +98,7 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry { case .classic: return 2002 } - case let .sound(section, index, _, _, _, _): + case let .sound(section, index, _, _, _, _, _): switch section { case .cloud: return 3 + index @@ -103,8 +110,13 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry { } } - var stableId: Int32 { - return self.sortId + var stableId: StableId { + switch self { + case let .sound(_ , _, _, _, sound, _, _): + return .sound(sound.id) + default: + return .index(self.sortId) + } } static func ==(lhs: NotificationSoundSelectionEntry, rhs: NotificationSoundSelectionEntry) -> Bool { @@ -151,8 +163,8 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry { } else { return false } - case let .sound(lhsSection, lhsIndex, lhsTheme, lhsText, lhsSound, lhsSelected): - if case let .sound(rhsSection, rhsIndex, rhsTheme, rhsText, rhsSound, rhsSelected) = rhs, lhsSection == rhsSection, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsText == rhsText, lhsSound == rhsSound, lhsSelected == rhsSelected { + case let .sound(lhsSection, lhsIndex, lhsTheme, lhsText, lhsSound, lhsSelected, lhsCanBeDeleted): + if case let .sound(rhsSection, rhsIndex, rhsTheme, rhsText, rhsSound, rhsSelected, rhsCanBeDeleted) = rhs, lhsSection == rhsSection, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsText == rhsText, lhsSound == rhsSound, lhsSelected == rhsSelected, lhsCanBeDeleted == rhsCanBeDeleted { return true } else { return false @@ -161,7 +173,7 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry { } static func <(lhs: NotificationSoundSelectionEntry, rhs: NotificationSoundSelectionEntry) -> Bool { - return lhs.stableId < rhs.stableId + return lhs.sortId < rhs.sortId } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { @@ -188,23 +200,30 @@ private enum NotificationSoundSelectionEntry: 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 notificationsAndSoundsEntries(presentationData: PresentationData, defaultSound: PeerMessageSound?, state: NotificationSoundSelectionState, notificationSoundList: NotificationSoundList?) -> [NotificationSoundSelectionEntry] { + let selectedSound = resolvedNotificationSound(sound: state.selectedSound, notificationSoundList: notificationSoundList) + var entries: [NotificationSoundSelectionEntry] = [] entries.append(.cloudHeader(presentationData.strings.Notifications_TelegramTones)) - //entries.append(.none(section: .cloud, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: .none), selected: state.selectedSound == .none)) + //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) - entries.append(.sound(section: .cloud, index: Int32(entries.count), theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: sound), sound: sound, selected: state.selectedSound.id == sound.id)) + if state.removedSounds.contains(where: { $0.id == sound.id }) { + continue + } + entries.append(.sound(section: .cloud, index: Int32(entries.count), theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: sound), sound: sound, selected: selectedSound.id == sound.id, canBeDeleted: true)) } } entries.append(.uploadSound(presentationData.strings.Notifications_UploadSound)) @@ -212,18 +231,18 @@ private func notificationsAndSoundsEntries(presentationData: PresentationData, d entries.append(.modernHeader(presentationData.theme, presentationData.strings.Notifications_AlertTones)) if let defaultSound = defaultSound { - entries.append(.default(section: .modern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: .default, default: defaultSound), selected: state.selectedSound.id == .default)) + entries.append(.default(section: .modern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: .default, default: defaultSound), selected: selectedSound.id == .default)) } - entries.append(.none(section: .modern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: .none), selected: state.selectedSound.id == .none)) + entries.append(.none(section: .modern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: .none), selected: selectedSound.id == .none)) for i in 0 ..< 12 { let sound: PeerMessageSound = .bundledModern(id: Int32(i)) - entries.append(.sound(section: .modern, index: Int32(i), theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: sound), sound: sound, selected: sound.id == state.selectedSound.id)) + entries.append(.sound(section: .modern, index: Int32(i), theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: sound), sound: sound, selected: sound.id == selectedSound.id, canBeDeleted: false)) } entries.append(.classicHeader(presentationData.theme, presentationData.strings.Notifications_ClassicTones)) for i in 0 ..< 8 { let sound: PeerMessageSound = .bundledClassic(id: Int32(i)) - entries.append(.sound(section: .classic, index: Int32(i), theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: sound), sound: sound, selected: sound.id == state.selectedSound.id)) + entries.append(.sound(section: .classic, index: Int32(i), theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: sound), sound: sound, selected: sound.id == selectedSound.id, canBeDeleted: false)) } return entries @@ -340,8 +359,8 @@ public func playSound(context: AccountContext, notificationSoundList: Notificati } public func notificationSoundSelectionController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, isModal: Bool, currentSound: PeerMessageSound, defaultSound: PeerMessageSound?, completion: @escaping (PeerMessageSound) -> Void) -> ViewController { - let statePromise = ValuePromise(NotificationSoundSelectionState(selectedSound: currentSound), ignoreRepeated: true) - let stateValue = Atomic(value: NotificationSoundSelectionState(selectedSound: currentSound)) + let statePromise = ValuePromise(NotificationSoundSelectionState(selectedSound: currentSound, removedSounds: []), ignoreRepeated: true) + let stateValue = Atomic(value: NotificationSoundSelectionState(selectedSound: currentSound, removedSounds: [])) let updateState: ((NotificationSoundSelectionState) -> NotificationSoundSelectionState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } @@ -355,7 +374,11 @@ public func notificationSoundSelectionController(context: AccountContext, update let arguments = NotificationSoundSelectionArguments(account: context.account, selectSound: { sound in updateState { state in - return NotificationSoundSelectionState(selectedSound: sound) + var state = state + + state.selectedSound = sound + + return state } let _ = (context.engine.peers.notificationSoundList() @@ -369,6 +392,25 @@ public func notificationSoundSelectionController(context: AccountContext, update 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 + } }) let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData @@ -432,20 +474,32 @@ public func notificationSoundSelectionController(context: AccountContext, update return } if data.count > 200 * 1024 { - presentUndo?(.info(title: "Audio is too large", text: "The file is larger than 200 KB.")) + //TODO:localize + presentUndo?(.info(title: "Audio is too large", text: "The file is over 200 KB.")) return } - asset.loadValuesAsynchronously(forKeys: ["duration"], completionHandler: { - let duration = asset.duration.seconds - if duration > 5.0 { - //TODO:localize - presentUndo?(.info(title: "Audio 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 - }, error: { _ in - })) + 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 + })) + } } }) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationSoundList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationSoundList.swift index 856e34aa77..9d400902f5 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationSoundList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationSoundList.swift @@ -279,3 +279,59 @@ func _internal_uploadNotificationSound(account: Account, title: String, data: Da } } } + +public enum DeleteNotificationSoundError { + case generic +} + +func _internal_deleteNotificationSound(account: Account, fileId: Int64) -> Signal { + return account.postbox.transaction { transaction -> NotificationSoundList.NotificationSound? in + return _internal_cachedNotificationSoundList(transaction: transaction).flatMap { list -> NotificationSoundList.NotificationSound? in + return list.sounds.first(where: { $0.file.fileId.id == fileId }) + } + } + |> castError(DeleteNotificationSoundError.self) + |> mapToSignal { sound -> Signal in + guard let sound = sound else { + return .fail(.generic) + } + guard let resource = sound.file.resource as? CloudDocumentMediaResource else { + return .fail(.generic) + } + + return account.network.request(Api.functions.account.saveRingtone(id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), unsave: .boolTrue)) + |> mapError { _ -> DeleteNotificationSoundError in + return .generic + } + |> mapToSignal { _ -> Signal in + return account.postbox.transaction { transaction -> Void in + if let notificationSoundList = _internal_cachedNotificationSoundList(transaction: transaction) { + let updatedNotificationSoundList = NotificationSoundList(hash: notificationSoundList.hash, sounds: notificationSoundList.sounds.filter { item in + return item.file.fileId.id != fileId + }) + _internal_setCachedNotificationSoundList(transaction: transaction, notificationSoundList: updatedNotificationSoundList) + } + } + |> castError(DeleteNotificationSoundError.self) + |> ignoreValues + } + } +} + +public func resolvedNotificationSound(sound: PeerMessageSound, notificationSoundList: NotificationSoundList?) -> PeerMessageSound { + switch sound { + case let .cloud(fileId): + if let notificationSoundList = notificationSoundList { + for listSound in notificationSoundList.sounds { + if listSound.file.fileId.id == fileId { + return sound + } + } + return .bundledModern(id: 0) + } else { + return .default + } + default: + return sound + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index a24028a353..f3fba7957d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -703,6 +703,10 @@ public extension TelegramEngine { public func uploadNotificationSound(title: String, data: Data) -> Signal { return _internal_uploadNotificationSound(account: self.account, title: title, data: data) } + + public func deleteNotificationSound(fileId: Int64) -> Signal { + return _internal_deleteNotificationSound(account: self.account, fileId: fileId) + } } } diff --git a/submodules/TelegramStringFormatting/Sources/PeerNotificationSoundStrings.swift b/submodules/TelegramStringFormatting/Sources/PeerNotificationSoundStrings.swift index dbf3cfcd02..6cd9a7222e 100644 --- a/submodules/TelegramStringFormatting/Sources/PeerNotificationSoundStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/PeerNotificationSoundStrings.swift @@ -51,7 +51,30 @@ private func soundName(strings: PresentationStrings, sound: PeerMessageSound, no } for sound in notificationSoundList.sounds { if sound.file.fileId.id == fileId { - return sound.file.fileName ?? "Cloud Tone" + for attribute in sound.file.attributes { + switch attribute { + case let .Audio(_, _, title, performer, _): + if let title = title, !title.isEmpty, let performer = performer, !performer.isEmpty { + return "\(title) - \(performer)" + } else if let title = title, !title.isEmpty { + return title + } else if let performer = performer, !performer.isEmpty { + return performer + } + default: + break + } + } + + if let fileName = sound.file.fileName, !fileName.isEmpty { + if let range = fileName.range(of: ".", options: .backwards) { + return String(fileName[fileName.startIndex ..< range.lowerBound]) + } else { + return fileName + } + } + + return "Cloud Tone" } } return "" @@ -71,9 +94,16 @@ public func localizedPeerNotificationSoundString(strings: PresentationStrings, n } return strings.UserInfo_NotificationsDefaultSound(actualName).string } else { - return strings.UserInfo_NotificationsDefault + let name = soundName(strings: strings, sound: .bundledModern(id: 0), notificationSoundList: notificationSoundList) + return name + //return strings.UserInfo_NotificationsDefault } default: - return soundName(strings: strings, sound: sound, notificationSoundList: notificationSoundList) + let name = soundName(strings: strings, sound: sound, notificationSoundList: notificationSoundList) + if name.isEmpty { + return localizedPeerNotificationSoundString(strings: strings, notificationSoundList: notificationSoundList, sound: .default, default: `default`) + } else { + return name + } } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 5511c0a3b4..f6f02a2b2e 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -8222,6 +8222,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 contextController.dismissed = { [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(interactive: true, { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 030e1d656e..d3323845fd 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -875,7 +875,8 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState 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)).start(completed: { + 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)) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 72242fba8e..0c2db0c79b 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -766,7 +766,9 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in + return ("URL", contents) + }), textAlignment: .natural) self.textNode.attributedText = attributedText self.textNode.maximumNumberOfLines = 5 @@ -774,6 +776,13 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.originalRemainingSeconds = 5 if let action = action { + self.textNode.highlightAttributeAction = { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + } self.textNode.tapAttributeAction = { attributes, _ in if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { action() @@ -820,15 +829,33 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { super.init() switch content { - case .removedChat: - self.panelWrapperNode.addSubnode(self.timerTextNode) - case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .sticker, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded: - break - case .dice: - self.panelWrapperNode.clipsToBounds = true - case .info: + case .removedChat: + self.panelWrapperNode.addSubnode(self.timerTextNode) + case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .sticker, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded: + if self.textNode.tapAttributeAction != nil { + self.isUserInteractionEnabled = true + } else { self.isUserInteractionEnabled = false + } + case .dice: + self.panelWrapperNode.clipsToBounds = true + case .info: + if self.textNode.tapAttributeAction != nil { + self.isUserInteractionEnabled = true + } else { + self.isUserInteractionEnabled = false + } } + + self.titleNode.isUserInteractionEnabled = false + self.textNode.isUserInteractionEnabled = self.textNode.tapAttributeAction != nil + self.iconNode?.isUserInteractionEnabled = false + self.animationNode?.isUserInteractionEnabled = false + self.iconCheckNode?.isUserInteractionEnabled = false + self.avatarNode?.isUserInteractionEnabled = false + self.slotMachineNode?.isUserInteractionEnabled = false + self.animatedStickerNode?.isUserInteractionEnabled = false + self.statusNode.flatMap(self.panelWrapperNode.addSubnode) self.iconNode.flatMap(self.panelWrapperNode.addSubnode) self.iconCheckNode.flatMap(self.panelWrapperNode.addSubnode) @@ -837,9 +864,9 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.animatedStickerNode.flatMap(self.panelWrapperNode.addSubnode) self.slotMachineNode.flatMap(self.panelWrapperNode.addSubnode) self.avatarNode.flatMap(self.panelWrapperNode.addSubnode) + self.panelWrapperNode.addSubnode(self.buttonNode) self.panelWrapperNode.addSubnode(self.titleNode) self.panelWrapperNode.addSubnode(self.textNode) - self.panelWrapperNode.addSubnode(self.buttonNode) if displayUndo { self.panelWrapperNode.addSubnode(self.undoButtonTextNode) self.panelWrapperNode.addSubnode(self.undoButtonNode)