Notification sounds update

This commit is contained in:
Ali 2022-03-31 01:19:30 +04:00
parent c278a5af7d
commit f45de2d44f
9 changed files with 345 additions and 95 deletions

View File

@ -544,52 +544,55 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
private func initializeContent() { private func initializeContent() {
switch self.source { switch self.source {
case let .reference(source): case let .reference(source):
/*let transitionInfo = source.transitionInfo() if let controller = self.getController() as? ContextController, controller.workaroundUseLegacyImplementation {
if let transitionInfo = transitionInfo { let transitionInfo = source.transitionInfo()
let referenceView = transitionInfo.referenceView if let transitionInfo = transitionInfo {
self.contentContainerNode.contentNode = .reference(view: referenceView) let referenceView = transitionInfo.referenceView
self.contentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace self.contentContainerNode.contentNode = .reference(view: referenceView)
self.customPosition = transitionInfo.customPosition self.contentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace
var projectedFrame = convertFrame(referenceView.bounds, from: referenceView, to: self.view) self.customPosition = transitionInfo.customPosition
projectedFrame.origin.x += transitionInfo.insets.left var projectedFrame = convertFrame(referenceView.bounds, from: referenceView, to: self.view)
projectedFrame.size.width -= transitionInfo.insets.left + transitionInfo.insets.right projectedFrame.origin.x += transitionInfo.insets.left
projectedFrame.origin.y += transitionInfo.insets.top projectedFrame.size.width -= transitionInfo.insets.left + transitionInfo.insets.right
projectedFrame.size.width -= transitionInfo.insets.top + transitionInfo.insets.bottom projectedFrame.origin.y += transitionInfo.insets.top
self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) projectedFrame.size.width -= transitionInfo.insets.top + transitionInfo.insets.bottom
}*/ self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame)
let presentationNode = ContextControllerExtractedPresentationNode( }
getController: { [weak self] in } else {
return self?.getController() let presentationNode = ContextControllerExtractedPresentationNode(
}, getController: { [weak self] in
requestUpdate: { [weak self] transition in return self?.getController()
guard let strongSelf = self else { },
return requestUpdate: { [weak self] transition in
} guard let strongSelf = self else {
if let validLayout = strongSelf.validLayout { return
strongSelf.updateLayout( }
layout: validLayout, if let validLayout = strongSelf.validLayout {
transition: transition, strongSelf.updateLayout(
previousActionsContainerNode: nil layout: validLayout,
) transition: transition,
} previousActionsContainerNode: nil
}, )
requestDismiss: { [weak self] result in }
guard let strongSelf = self else { },
return requestDismiss: { [weak self] result in
} guard let strongSelf = self else {
strongSelf.dismissedForCancel?() return
strongSelf.beginDismiss(result) }
}, strongSelf.dismissedForCancel?()
requestAnimateOut: { [weak self] result, completion in strongSelf.beginDismiss(result)
guard let strongSelf = self else { },
return requestAnimateOut: { [weak self] result, completion in
} guard let strongSelf = self else {
strongSelf.animateOut(result: result, completion: completion) return
}, }
source: .reference(source) strongSelf.animateOut(result: result, completion: completion)
) },
self.presentationNode = presentationNode source: .reference(source)
self.addSubnode(presentationNode) )
self.presentationNode = presentationNode
self.addSubnode(presentationNode)
}
case let .extracted(source): case let .extracted(source):
let presentationNode = ContextControllerExtractedPresentationNode( let presentationNode = ContextControllerExtractedPresentationNode(
getController: { [weak self] in getController: { [weak self] in
@ -2251,6 +2254,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
public var useComplexItemsTransitionAnimation = false public var useComplexItemsTransitionAnimation = false
public var immediateItemsTransitionAnimation = false public var immediateItemsTransitionAnimation = false
public var workaroundUseLegacyImplementation = false
public enum HandledTouchEvent { public enum HandledTouchEvent {
case ignore case ignore

View File

@ -32,8 +32,9 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem {
let zeroSeparatorInsets: Bool let zeroSeparatorInsets: Bool
public let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
let action: () -> Void 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.presentationData = presentationData
self.icon = icon self.icon = icon
self.iconSize = iconSize self.iconSize = iconSize
@ -45,6 +46,7 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem {
self.zeroSeparatorInsets = zeroSeparatorInsets self.zeroSeparatorInsets = zeroSeparatorInsets
self.sectionId = sectionId self.sectionId = sectionId
self.action = action 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<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) { public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (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 backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode
@ -97,6 +99,7 @@ public class ItemListCheckboxItemNode: ListViewItemNode {
private let activateArea: AccessibilityAreaNode private let activateArea: AccessibilityAreaNode
private let contentContainerNode: ASDisplayNode
private let imageNode: ASImageNode private let imageNode: ASImageNode
private let iconNode: ASImageNode private let iconNode: ASImageNode
private let titleNode: TextNode private let titleNode: TextNode
@ -115,6 +118,8 @@ public class ItemListCheckboxItemNode: ListViewItemNode {
self.maskNode = ASImageNode() self.maskNode = ASImageNode()
self.contentContainerNode = ASDisplayNode()
self.imageNode = ASImageNode() self.imageNode = ASImageNode()
self.imageNode.isLayerBacked = true self.imageNode.isLayerBacked = true
self.imageNode.displayWithoutProcessing = true self.imageNode.displayWithoutProcessing = true
@ -135,11 +140,12 @@ public class ItemListCheckboxItemNode: ListViewItemNode {
self.activateArea = AccessibilityAreaNode() 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.contentContainerNode)
self.addSubnode(self.iconNode) self.contentContainerNode.addSubnode(self.imageNode)
self.addSubnode(self.titleNode) self.contentContainerNode.addSubnode(self.iconNode)
self.contentContainerNode.addSubnode(self.titleNode)
self.addSubnode(self.activateArea) self.addSubnode(self.activateArea)
self.activateArea.activate = { [weak self] in self.activateArea.activate = { [weak self] in
@ -204,6 +210,8 @@ public class ItemListCheckboxItemNode: ListViewItemNode {
if let strongSelf = self { if let strongSelf = self {
strongSelf.item = item strongSelf.item = item
strongSelf.contentContainerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = item.title strongSelf.activateArea.accessibilityLabel = item.title
if item.checked { if item.checked {
strongSelf.activateArea.accessibilityValue = "Selected" 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.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) { override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) 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 })
}*/
}
} }

View File

@ -21,6 +21,7 @@ private struct NotificationSoundSelectionArguments {
let complete: () -> Void let complete: () -> Void
let cancel: () -> Void let cancel: () -> Void
let upload: () -> Void let upload: () -> Void
let deleteSound: (PeerMessageSound) -> Void
} }
private enum NotificationSoundSelectionSection: Int32 { private enum NotificationSoundSelectionSection: Int32 {
@ -31,9 +32,15 @@ private enum NotificationSoundSelectionSection: Int32 {
private struct NotificationSoundSelectionState: Equatable { private struct NotificationSoundSelectionState: Equatable {
var selectedSound: PeerMessageSound var selectedSound: PeerMessageSound
var removedSounds: [PeerMessageSound]
} }
private enum NotificationSoundSelectionEntry: ItemListNodeEntry { private enum NotificationSoundSelectionEntry: ItemListNodeEntry {
enum StableId: Hashable {
case index(Int32)
case sound(PeerMessageSound.Id)
}
case cloudHeader(String) case cloudHeader(String)
case uploadSound(String) case uploadSound(String)
case cloudInfo(String) case cloudInfo(String)
@ -42,7 +49,7 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry {
case classicHeader(PresentationTheme, String) case classicHeader(PresentationTheme, String)
case none(section: NotificationSoundSelectionSection, theme: PresentationTheme, text: String, selected: Bool) case none(section: NotificationSoundSelectionSection, theme: PresentationTheme, text: String, selected: Bool)
case `default`(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 { var section: ItemListSectionId {
switch self { switch self {
@ -56,7 +63,7 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry {
return section.rawValue return section.rawValue
case let .default(section, _, _, _): case let .default(section, _, _, _):
return section.rawValue return section.rawValue
case let .sound(section, _, _, _, _, _): case let .sound(section, _, _, _, _, _, _):
return section.rawValue return section.rawValue
} }
} }
@ -91,7 +98,7 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry {
case .classic: case .classic:
return 2002 return 2002
} }
case let .sound(section, index, _, _, _, _): case let .sound(section, index, _, _, _, _, _):
switch section { switch section {
case .cloud: case .cloud:
return 3 + index return 3 + index
@ -103,8 +110,13 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry {
} }
} }
var stableId: Int32 { var stableId: StableId {
return self.sortId switch self {
case let .sound(_ , _, _, _, sound, _, _):
return .sound(sound.id)
default:
return .index(self.sortId)
}
} }
static func ==(lhs: NotificationSoundSelectionEntry, rhs: NotificationSoundSelectionEntry) -> Bool { static func ==(lhs: NotificationSoundSelectionEntry, rhs: NotificationSoundSelectionEntry) -> Bool {
@ -151,8 +163,8 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .sound(lhsSection, lhsIndex, lhsTheme, lhsText, lhsSound, lhsSelected): case let .sound(lhsSection, lhsIndex, lhsTheme, lhsText, lhsSound, lhsSelected, lhsCanBeDeleted):
if case let .sound(rhsSection, rhsIndex, rhsTheme, rhsText, rhsSound, rhsSelected) = rhs, lhsSection == rhsSection, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsText == rhsText, lhsSound == rhsSound, lhsSelected == rhsSelected { 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 return true
} else { } else {
return false return false
@ -161,7 +173,7 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry {
} }
static func <(lhs: NotificationSoundSelectionEntry, rhs: NotificationSoundSelectionEntry) -> Bool { static func <(lhs: NotificationSoundSelectionEntry, rhs: NotificationSoundSelectionEntry) -> Bool {
return lhs.stableId < rhs.stableId return lhs.sortId < rhs.sortId
} }
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { 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: { return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.selectSound(.default) 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: { return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.selectSound(sound) arguments.selectSound(sound)
}) }, deleteAction: canBeDeleted ? {
arguments.deleteSound(sound)
} : nil)
} }
} }
} }
private func notificationsAndSoundsEntries(presentationData: PresentationData, defaultSound: PeerMessageSound?, state: NotificationSoundSelectionState, notificationSoundList: NotificationSoundList?) -> [NotificationSoundSelectionEntry] { private func notificationsAndSoundsEntries(presentationData: PresentationData, defaultSound: PeerMessageSound?, state: NotificationSoundSelectionState, notificationSoundList: NotificationSoundList?) -> [NotificationSoundSelectionEntry] {
let selectedSound = resolvedNotificationSound(sound: state.selectedSound, notificationSoundList: notificationSoundList)
var entries: [NotificationSoundSelectionEntry] = [] var entries: [NotificationSoundSelectionEntry] = []
entries.append(.cloudHeader(presentationData.strings.Notifications_TelegramTones)) 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 { if let notificationSoundList = notificationSoundList {
for listSound in notificationSoundList.sounds { for listSound in notificationSoundList.sounds {
let sound: PeerMessageSound = .cloud(fileId: listSound.file.fileId.id) 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)) 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)) entries.append(.modernHeader(presentationData.theme, presentationData.strings.Notifications_AlertTones))
if let defaultSound = defaultSound { 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 { for i in 0 ..< 12 {
let sound: PeerMessageSound = .bundledModern(id: Int32(i)) 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)) entries.append(.classicHeader(presentationData.theme, presentationData.strings.Notifications_ClassicTones))
for i in 0 ..< 8 { for i in 0 ..< 8 {
let sound: PeerMessageSound = .bundledClassic(id: Int32(i)) 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 return entries
@ -340,8 +359,8 @@ public func playSound(context: AccountContext, notificationSoundList: Notificati
} }
public func notificationSoundSelectionController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, isModal: Bool, currentSound: PeerMessageSound, defaultSound: PeerMessageSound?, completion: @escaping (PeerMessageSound) -> Void) -> ViewController { public func notificationSoundSelectionController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, isModal: Bool, currentSound: PeerMessageSound, defaultSound: PeerMessageSound?, completion: @escaping (PeerMessageSound) -> Void) -> ViewController {
let statePromise = ValuePromise(NotificationSoundSelectionState(selectedSound: currentSound), ignoreRepeated: true) let statePromise = ValuePromise(NotificationSoundSelectionState(selectedSound: currentSound, removedSounds: []), ignoreRepeated: true)
let stateValue = Atomic(value: NotificationSoundSelectionState(selectedSound: currentSound)) let stateValue = Atomic(value: NotificationSoundSelectionState(selectedSound: currentSound, removedSounds: []))
let updateState: ((NotificationSoundSelectionState) -> NotificationSoundSelectionState) -> Void = { f in let updateState: ((NotificationSoundSelectionState) -> NotificationSoundSelectionState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) }) 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 let arguments = NotificationSoundSelectionArguments(account: context.account, selectSound: { sound in
updateState { state in updateState { state in
return NotificationSoundSelectionState(selectedSound: sound) var state = state
state.selectedSound = sound
return state
} }
let _ = (context.engine.peers.notificationSoundList() let _ = (context.engine.peers.notificationSoundList()
@ -369,6 +392,25 @@ public func notificationSoundSelectionController(context: AccountContext, update
cancelImpl?() cancelImpl?()
}, upload: { }, upload: {
presentFilePicker?() 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 let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
@ -432,20 +474,32 @@ public func notificationSoundSelectionController(context: AccountContext, update
return return
} }
if data.count > 200 * 1024 { 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 return
} }
asset.loadValuesAsynchronously(forKeys: ["duration"], completionHandler: { asset.loadValuesAsynchronously(forKeys: ["tracks", "duration"], completionHandler: {
let duration = asset.duration.seconds if asset.statusOfValue(forKey: "duration", error: nil) != .loaded {
if duration > 5.0 { return
//TODO:localize }
presentUndo?(.info(title: "Audio is too long", text: "The duration is longer than 5 seconds.")) guard let track = asset.tracks(withMediaType: .audio).first else {
} else { return
soundActionDisposable.set((context.engine.peers.uploadNotificationSound(title: url.lastPathComponent, data: data) }
|> deliverOnMainQueue).start(next: { _ in let duration = track.timeRange.duration.seconds
}, error: { _ in
})) 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
}))
}
} }
}) })
} }

View File

@ -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<Never, DeleteNotificationSoundError> {
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<Never, DeleteNotificationSoundError> 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<Never, DeleteNotificationSoundError> 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
}
}

View File

@ -703,6 +703,10 @@ public extension TelegramEngine {
public func uploadNotificationSound(title: String, data: Data) -> Signal<NotificationSoundList.NotificationSound, UploadNotificationSoundError> { public func uploadNotificationSound(title: String, data: Data) -> Signal<NotificationSoundList.NotificationSound, UploadNotificationSoundError> {
return _internal_uploadNotificationSound(account: self.account, title: title, data: data) return _internal_uploadNotificationSound(account: self.account, title: title, data: data)
} }
public func deleteNotificationSound(fileId: Int64) -> Signal<Never, DeleteNotificationSoundError> {
return _internal_deleteNotificationSound(account: self.account, fileId: fileId)
}
} }
} }

View File

@ -51,7 +51,30 @@ private func soundName(strings: PresentationStrings, sound: PeerMessageSound, no
} }
for sound in notificationSoundList.sounds { for sound in notificationSoundList.sounds {
if sound.file.fileId.id == fileId { 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 "" return ""
@ -71,9 +94,16 @@ public func localizedPeerNotificationSoundString(strings: PresentationStrings, n
} }
return strings.UserInfo_NotificationsDefaultSound(actualName).string return strings.UserInfo_NotificationsDefaultSound(actualName).string
} else { } else {
return strings.UserInfo_NotificationsDefault let name = soundName(strings: strings, sound: .bundledModern(id: 0), notificationSoundList: notificationSoundList)
return name
//return strings.UserInfo_NotificationsDefault
} }
default: 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
}
} }
} }

View File

@ -8222,6 +8222,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() 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) 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 contextController.dismissed = { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(interactive: true, { strongSelf.updateChatPresentationInterfaceState(interactive: true, {

View File

@ -875,7 +875,8 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
actions.append(.action(ContextMenuActionItem(text: "Save for Notifications", icon: { theme in actions.append(.action(ContextMenuActionItem(text: "Save for Notifications", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/DownloadTone"), color: theme.actionSheet.primaryTextColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/DownloadTone"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in }, 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 //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.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)) controllerInteraction.navigationController()?.pushViewController(notificationsAndSoundsController(context: context, exceptionsList: nil))

View File

@ -766,7 +766,9 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold = MarkdownAttributeSet(font: Font.semibold(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 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.attributedText = attributedText
self.textNode.maximumNumberOfLines = 5 self.textNode.maximumNumberOfLines = 5
@ -774,6 +776,13 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.originalRemainingSeconds = 5 self.originalRemainingSeconds = 5
if let action = action { 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 self.textNode.tapAttributeAction = { attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
action() action()
@ -820,15 +829,33 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
super.init() super.init()
switch content { switch content {
case .removedChat: case .removedChat:
self.panelWrapperNode.addSubnode(self.timerTextNode) 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: 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 if self.textNode.tapAttributeAction != nil {
case .dice: self.isUserInteractionEnabled = true
self.panelWrapperNode.clipsToBounds = true } else {
case .info:
self.isUserInteractionEnabled = false 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.statusNode.flatMap(self.panelWrapperNode.addSubnode)
self.iconNode.flatMap(self.panelWrapperNode.addSubnode) self.iconNode.flatMap(self.panelWrapperNode.addSubnode)
self.iconCheckNode.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.animatedStickerNode.flatMap(self.panelWrapperNode.addSubnode)
self.slotMachineNode.flatMap(self.panelWrapperNode.addSubnode) self.slotMachineNode.flatMap(self.panelWrapperNode.addSubnode)
self.avatarNode.flatMap(self.panelWrapperNode.addSubnode) self.avatarNode.flatMap(self.panelWrapperNode.addSubnode)
self.panelWrapperNode.addSubnode(self.buttonNode)
self.panelWrapperNode.addSubnode(self.titleNode) self.panelWrapperNode.addSubnode(self.titleNode)
self.panelWrapperNode.addSubnode(self.textNode) self.panelWrapperNode.addSubnode(self.textNode)
self.panelWrapperNode.addSubnode(self.buttonNode)
if displayUndo { if displayUndo {
self.panelWrapperNode.addSubnode(self.undoButtonTextNode) self.panelWrapperNode.addSubnode(self.undoButtonTextNode)
self.panelWrapperNode.addSubnode(self.undoButtonNode) self.panelWrapperNode.addSubnode(self.undoButtonNode)