mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Notification sounds update
This commit is contained in:
parent
c278a5af7d
commit
f45de2d44f
@ -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
|
||||
|
@ -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<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 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 })
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
@ -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<PresentationData, NoError>)? = 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
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -703,6 +703,10 @@ public extension TelegramEngine {
|
||||
public func uploadNotificationSound(title: String, data: Data) -> Signal<NotificationSoundList.NotificationSound, UploadNotificationSoundError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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, {
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user