Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2024-04-08 20:34:08 +04:00
commit ff5307a254
11 changed files with 813 additions and 59 deletions

View File

@ -232,7 +232,7 @@ public final class ItemListControllerNodeView: UITracingLayerView {
weak var controller: ItemListController? weak var controller: ItemListController?
} }
open class ItemListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { open class ItemListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
private weak var controller: ItemListController? private weak var controller: ItemListController?
private var _ready = ValuePromise<Bool>() private var _ready = ValuePromise<Bool>()
@ -495,7 +495,7 @@ open class ItemListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
} }
return directions return directions
}, edgeWidth: .widthMultiplier(factor: 1.0 / 6.0, min: 22.0, max: 80.0)) }, edgeWidth: .widthMultiplier(factor: 1.0 / 6.0, min: 22.0, max: 80.0))
panRecognizer.delegate = self panRecognizer.delegate = self.wrappedGestureRecognizerDelegate
panRecognizer.delaysTouchesBegan = false panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true panRecognizer.cancelsTouchesInView = true
self.panRecognizer = panRecognizer self.panRecognizer = panRecognizer

View File

@ -14,10 +14,16 @@ public enum ItemListSwitchItemNodeType {
} }
public class ItemListSwitchItem: ListViewItem, ItemListItem { public class ItemListSwitchItem: ListViewItem, ItemListItem {
public enum TextColor {
case primary
case accent
}
let presentationData: ItemListPresentationData let presentationData: ItemListPresentationData
let icon: UIImage? let icon: UIImage?
let title: String let title: String
let text: String? let text: String?
let textColor: TextColor
let titleBadgeComponent: AnyComponent<Empty>? let titleBadgeComponent: AnyComponent<Empty>?
let value: Bool let value: Bool
let type: ItemListSwitchItemNodeType let type: ItemListSwitchItemNodeType
@ -31,13 +37,15 @@ public class ItemListSwitchItem: ListViewItem, ItemListItem {
let style: ItemListStyle let style: ItemListStyle
let updated: (Bool) -> Void let updated: (Bool) -> Void
let activatedWhileDisabled: () -> Void let activatedWhileDisabled: () -> Void
let action: (() -> Void)?
public let tag: ItemListItemTag? public let tag: ItemListItemTag?
public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, title: String, text: String? = nil, titleBadgeComponent: AnyComponent<Empty>? = nil, value: Bool, type: ItemListSwitchItemNodeType = .regular, enableInteractiveChanges: Bool = true, enabled: Bool = true, displayLocked: Bool = false, disableLeadingInset: Bool = false, maximumNumberOfLines: Int = 1, noCorners: Bool = false, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void, activatedWhileDisabled: @escaping () -> Void = {}, tag: ItemListItemTag? = nil) { public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, title: String, text: String? = nil, textColor: TextColor = .primary, titleBadgeComponent: AnyComponent<Empty>? = nil, value: Bool, type: ItemListSwitchItemNodeType = .regular, enableInteractiveChanges: Bool = true, enabled: Bool = true, displayLocked: Bool = false, disableLeadingInset: Bool = false, maximumNumberOfLines: Int = 1, noCorners: Bool = false, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void, activatedWhileDisabled: @escaping () -> Void = {}, action: (() -> Void)? = nil, tag: ItemListItemTag? = nil) {
self.presentationData = presentationData self.presentationData = presentationData
self.icon = icon self.icon = icon
self.title = title self.title = title
self.text = text self.text = text
self.textColor = textColor
self.titleBadgeComponent = titleBadgeComponent self.titleBadgeComponent = titleBadgeComponent
self.value = value self.value = value
self.type = type self.type = type
@ -51,6 +59,7 @@ public class ItemListSwitchItem: ListViewItem, ItemListItem {
self.style = style self.style = style
self.updated = updated self.updated = updated
self.activatedWhileDisabled = activatedWhileDisabled self.activatedWhileDisabled = activatedWhileDisabled
self.action = action
self.tag = tag self.tag = tag
} }
@ -90,6 +99,17 @@ public class ItemListSwitchItem: ListViewItem, ItemListItem {
} }
} }
} }
public var selectable: Bool {
return self.action != nil && self.enabled
}
public func selected(listView: ListView){
listView.clearHighlightAnimated(true)
if self.enabled {
self.action?()
}
}
} }
protocol ItemListSwitchNodeImpl { protocol ItemListSwitchNodeImpl {
@ -132,7 +152,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
private let iconNode: ASImageNode private let iconNode: ASImageNode
private let titleNode: TextNode private let titleNode: TextNode
private let textNode: TextNode private var textNode: TextNode?
private var switchNode: ASDisplayNode & ItemListSwitchNodeImpl private var switchNode: ASDisplayNode & ItemListSwitchNodeImpl
private let switchGestureNode: ASDisplayNode private let switchGestureNode: ASDisplayNode
private var disabledOverlayNode: ASDisplayNode? private var disabledOverlayNode: ASDisplayNode?
@ -168,11 +188,9 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
self.iconNode.displaysAsynchronously = false self.iconNode.displaysAsynchronously = false
self.titleNode = TextNode() self.titleNode = TextNode()
self.titleNode.anchorPoint = CGPoint()
self.titleNode.isUserInteractionEnabled = false self.titleNode.isUserInteractionEnabled = false
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
switch type { switch type {
case .regular: case .regular:
self.switchNode = SwitchNode() self.switchNode = SwitchNode()
@ -190,7 +208,6 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
super.init(layerBacked: false, dynamicBounce: false) super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode) self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.switchNode) self.addSubnode(self.switchNode)
self.addSubnode(self.switchGestureNode) self.addSubnode(self.switchGestureNode)
self.addSubnode(self.activateArea) self.addSubnode(self.activateArea)
@ -230,7 +247,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
let itemSeparatorColor: UIColor let itemSeparatorColor: UIColor
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let textFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0) let textFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)
var updatedTheme: PresentationTheme? var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme { if currentItem?.presentationData.theme !== item.presentationData.theme {
@ -274,11 +291,21 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: item.maximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - params.rightInset - 64.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: item.maximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - params.rightInset - 64.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text ?? "", font: textFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - params.rightInset - 84.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
contentSize.height = max(contentSize.height, titleLayout.size.height + topInset * 2.0) contentSize.height = max(contentSize.height, titleLayout.size.height + topInset * 2.0)
if item.text != nil {
var textLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let text = item.text {
let textColor: UIColor
switch item.textColor {
case .primary:
textColor = item.presentationData.theme.list.itemSecondaryTextColor
case .accent:
textColor = item.presentationData.theme.list.itemAccentColor
}
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - params.rightInset - 84.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
contentSize.height += -1.0 + textLayout.size.height contentSize.height += -1.0 + textLayout.size.height
textLayoutAndApply = (textLayout, textApply)
} }
if !item.enabled { if !item.enabled {
@ -294,6 +321,13 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] animated in return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] animated in
if let strongSelf = self { if let strongSelf = self {
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
strongSelf.item = item strongSelf.item = item
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
@ -309,7 +343,9 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.activateArea.accessibilityTraits = accessibilityTraits strongSelf.activateArea.accessibilityTraits = accessibilityTraits
if let icon = item.icon { if let icon = item.icon {
var iconTransition = transition
if strongSelf.iconNode.supernode == nil { if strongSelf.iconNode.supernode == nil {
iconTransition = .immediate
strongSelf.addSubnode(strongSelf.iconNode) strongSelf.addSubnode(strongSelf.iconNode)
} }
if updateIcon { if updateIcon {
@ -321,19 +357,12 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
} else { } else {
iconY = max(0.0, floor(topInset + titleLayout.size.height + 1.0 - icon.size.height * 0.5)) iconY = max(0.0, floor(topInset + titleLayout.size.height + 1.0 - icon.size.height * 0.5))
} }
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - icon.size.width) / 2.0), y: iconY), size: icon.size) iconTransition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - icon.size.width) / 2.0), y: iconY), size: icon.size))
} else if strongSelf.iconNode.supernode != nil { } else if strongSelf.iconNode.supernode != nil {
strongSelf.iconNode.image = nil strongSelf.iconNode.image = nil
strongSelf.iconNode.removeFromSupernode() strongSelf.iconNode.removeFromSupernode()
} }
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
if let currentDisabledOverlayNode = currentDisabledOverlayNode { if let currentDisabledOverlayNode = currentDisabledOverlayNode {
if currentDisabledOverlayNode != strongSelf.disabledOverlayNode { if currentDisabledOverlayNode != strongSelf.disabledOverlayNode {
strongSelf.disabledOverlayNode = currentDisabledOverlayNode strongSelf.disabledOverlayNode = currentDisabledOverlayNode
@ -367,7 +396,6 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
} }
let _ = titleApply() let _ = titleApply()
let _ = textApply()
switch item.style { switch item.style {
case .plain: case .plain:
@ -383,7 +411,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
if strongSelf.maskNode.supernode != nil { if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode() strongSelf.maskNode.removeFromSupernode()
} }
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)))
case .blocks: case .blocks:
if strongSelf.backgroundNode.supernode == nil { if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
@ -421,15 +449,39 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) transition.updateFrame(node: strongSelf.maskNode, frame: strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0))
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)))
} }
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: topInset), size: titleLayout.size) let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: topInset), size: titleLayout.size)
transition.updatePosition(node: strongSelf.titleNode, position: titleFrame.origin)
strongSelf.titleNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + 2.0), size: textLayout.size) if let (textLayout, textApply) = textLayoutAndApply {
let textNode = textApply()
if strongSelf.textNode !== textNode {
strongSelf.textNode?.removeFromSupernode()
strongSelf.textNode = textNode
textNode.isUserInteractionEnabled = false
strongSelf.addSubnode(textNode)
if transition.isAnimated {
textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + 2.0), size: textLayout.size)
} else if let textNode = strongSelf.textNode {
strongSelf.textNode = nil
if transition.isAnimated {
textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textNode] _ in
textNode?.removeFromSupernode()
})
} else {
textNode.removeFromSupernode()
}
}
if let switchView = strongSelf.switchNode.view as? UISwitch { if let switchView = strongSelf.switchNode.view as? UISwitch {
if strongSelf.switchNode.bounds.size.width.isZero { if strongSelf.switchNode.bounds.size.width.isZero {
@ -437,7 +489,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
} }
let switchSize = switchView.bounds.size let switchSize = switchView.bounds.size
strongSelf.switchNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - switchSize.width - 15.0, y: floor((contentSize.height - switchSize.height) / 2.0)), size: switchSize) transition.updateFrame(node: strongSelf.switchNode, frame: CGRect(origin: CGPoint(x: params.width - params.rightInset - switchSize.width - 15.0, y: floor((contentSize.height - switchSize.height) / 2.0)), size: switchSize))
strongSelf.switchGestureNode.frame = strongSelf.switchNode.frame strongSelf.switchGestureNode.frame = strongSelf.switchNode.frame
if switchView.isOn != item.value { if switchView.isOn != item.value {
switchView.setOn(item.value, animated: animated) switchView.setOn(item.value, animated: animated)
@ -447,6 +499,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.switchGestureNode.isHidden = item.enableInteractiveChanges && item.enabled strongSelf.switchGestureNode.isHidden = item.enableInteractiveChanges && item.enabled
if item.displayLocked { if item.displayLocked {
var lockedIconTransition = transition
var updateLockedIconImage = false var updateLockedIconImage = false
if let _ = updatedTheme { if let _ = updatedTheme {
updateLockedIconImage = true updateLockedIconImage = true
@ -456,6 +509,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
if let current = strongSelf.lockedIconNode { if let current = strongSelf.lockedIconNode {
lockedIconNode = current lockedIconNode = current
} else { } else {
lockedIconTransition = .immediate
updateLockedIconImage = true updateLockedIconImage = true
lockedIconNode = ASImageNode() lockedIconNode = ASImageNode()
strongSelf.lockedIconNode = lockedIconNode strongSelf.lockedIconNode = lockedIconNode
@ -469,7 +523,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
let switchFrame = strongSelf.switchNode.frame let switchFrame = strongSelf.switchNode.frame
if let icon = lockedIconNode.image { if let icon = lockedIconNode.image {
lockedIconNode.frame = CGRect(origin: CGPoint(x: switchFrame.minX + 10.0 + UIScreenPixel, y: switchFrame.minY + 9.0), size: icon.size) lockedIconTransition.updateFrame(node: lockedIconNode, frame: CGRect(origin: CGPoint(x: switchFrame.minX + 10.0 + UIScreenPixel, y: switchFrame.minY + 9.0), size: icon.size))
} }
} else if let lockedIconNode = strongSelf.lockedIconNode { } else if let lockedIconNode = strongSelf.lockedIconNode {
strongSelf.lockedIconNode = nil strongSelf.lockedIconNode = nil
@ -492,17 +546,19 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
containerSize: contentSize containerSize: contentSize
) )
if let view = componentView.view { if let view = componentView.view {
var titleBadgeTransition = transition
if view.superview == nil { if view.superview == nil {
titleBadgeTransition = .immediate
strongSelf.view.addSubview(view) strongSelf.view.addSubview(view)
} }
view.frame = CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + 7.0, y: floor((contentSize.height - badgeSize.height) / 2.0)), size: badgeSize) titleBadgeTransition.updateFrame(view: view, frame: CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + 7.0, y: floor((contentSize.height - badgeSize.height) / 2.0)), size: badgeSize))
} }
} else if let componentView = strongSelf.titleBadgeComponentView { } else if let componentView = strongSelf.titleBadgeComponentView {
strongSelf.titleBadgeComponentView = nil strongSelf.titleBadgeComponentView = nil
componentView.view?.removeFromSuperview() componentView.view?.removeFromSuperview()
} }
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel)) transition.updateFrame(node: strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layoutSize.height + UIScreenPixel + UIScreenPixel)))
} }
}) })
} }

View File

@ -63,6 +63,7 @@ private final class NotificationsAndSoundsArguments {
let suppressWarning: () -> Void let suppressWarning: () -> Void
let openPeerCategory: (NotificationsPeerCategory) -> Void let openPeerCategory: (NotificationsPeerCategory) -> Void
let openReactions: () -> Void
let updateInAppSounds: (Bool) -> Void let updateInAppSounds: (Bool) -> Void
let updateInAppVibration: (Bool) -> Void let updateInAppVibration: (Bool) -> Void
@ -80,7 +81,7 @@ private final class NotificationsAndSoundsArguments {
let updateNotificationsFromAllAccounts: (Bool) -> Void let updateNotificationsFromAllAccounts: (Bool) -> Void
init(context: AccountContext, presentController: @escaping (ViewController, ViewControllerPresentationArguments?) -> Void, pushController: @escaping(ViewController)->Void, soundSelectionDisposable: MetaDisposable, authorizeNotifications: @escaping () -> Void, suppressWarning: @escaping () -> Void, openPeerCategory: @escaping (NotificationsPeerCategory) -> Void, updateInAppSounds: @escaping (Bool) -> Void, updateInAppVibration: @escaping (Bool) -> Void, updateInAppPreviews: @escaping (Bool) -> Void, updateDisplayNameOnLockscreen: @escaping (Bool) -> Void, updateIncludeTag: @escaping (CounterTagSettings, Bool) -> Void, updateTotalUnreadCountCategory: @escaping (Bool) -> Void, resetNotifications: @escaping () -> Void, openAppSettings: @escaping () -> Void, updateJoinedNotifications: @escaping (Bool) -> Void, updateNotificationsFromAllAccounts: @escaping (Bool) -> Void) { init(context: AccountContext, presentController: @escaping (ViewController, ViewControllerPresentationArguments?) -> Void, pushController: @escaping(ViewController)->Void, soundSelectionDisposable: MetaDisposable, authorizeNotifications: @escaping () -> Void, suppressWarning: @escaping () -> Void, openPeerCategory: @escaping (NotificationsPeerCategory) -> Void, openReactions: @escaping () -> Void, updateInAppSounds: @escaping (Bool) -> Void, updateInAppVibration: @escaping (Bool) -> Void, updateInAppPreviews: @escaping (Bool) -> Void, updateDisplayNameOnLockscreen: @escaping (Bool) -> Void, updateIncludeTag: @escaping (CounterTagSettings, Bool) -> Void, updateTotalUnreadCountCategory: @escaping (Bool) -> Void, resetNotifications: @escaping () -> Void, openAppSettings: @escaping () -> Void, updateJoinedNotifications: @escaping (Bool) -> Void, updateNotificationsFromAllAccounts: @escaping (Bool) -> Void) {
self.context = context self.context = context
self.presentController = presentController self.presentController = presentController
self.pushController = pushController self.pushController = pushController
@ -88,6 +89,7 @@ private final class NotificationsAndSoundsArguments {
self.authorizeNotifications = authorizeNotifications self.authorizeNotifications = authorizeNotifications
self.suppressWarning = suppressWarning self.suppressWarning = suppressWarning
self.openPeerCategory = openPeerCategory self.openPeerCategory = openPeerCategory
self.openReactions = openReactions
self.updateInAppSounds = updateInAppSounds self.updateInAppSounds = updateInAppSounds
self.updateInAppVibration = updateInAppVibration self.updateInAppVibration = updateInAppVibration
self.updateInAppPreviews = updateInAppPreviews self.updateInAppPreviews = updateInAppPreviews
@ -145,6 +147,7 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
case groupChats(PresentationTheme, String, String, String) case groupChats(PresentationTheme, String, String, String)
case channels(PresentationTheme, String, String, String) case channels(PresentationTheme, String, String, String)
case stories(PresentationTheme, String, String, String) case stories(PresentationTheme, String, String, String)
case reactions(PresentationTheme, String, String, String)
case inAppHeader(PresentationTheme, String) case inAppHeader(PresentationTheme, String)
case inAppSounds(PresentationTheme, String, Bool) case inAppSounds(PresentationTheme, String, Bool)
@ -171,7 +174,7 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
return NotificationsAndSoundsSection.accounts.rawValue return NotificationsAndSoundsSection.accounts.rawValue
case .permissionInfo, .permissionEnable: case .permissionInfo, .permissionEnable:
return NotificationsAndSoundsSection.permission.rawValue return NotificationsAndSoundsSection.permission.rawValue
case .categoriesHeader, .privateChats, .groupChats, .channels, .stories: case .categoriesHeader, .privateChats, .groupChats, .channels, .stories, .reactions:
return NotificationsAndSoundsSection.categories.rawValue return NotificationsAndSoundsSection.categories.rawValue
case .inAppHeader, .inAppSounds, .inAppVibrate, .inAppPreviews: case .inAppHeader, .inAppSounds, .inAppVibrate, .inAppPreviews:
return NotificationsAndSoundsSection.inApp.rawValue return NotificationsAndSoundsSection.inApp.rawValue
@ -208,6 +211,8 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
return 8 return 8
case .stories: case .stories:
return 9 return 9
case .reactions:
return 10
case .inAppHeader: case .inAppHeader:
return 14 return 14
case .inAppSounds: case .inAppSounds:
@ -326,6 +331,12 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .reactions(lhsTheme, lhsTitle, lhsSubtitle, lhsLabel):
if case let .reactions(rhsTheme, rhsTitle, rhsSubtitle, rhsLabel) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsLabel == rhsLabel {
return true
} else {
return false
}
case let .inAppHeader(lhsTheme, lhsText): case let .inAppHeader(lhsTheme, lhsText):
if case let .inAppHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { if case let .inAppHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true return true
@ -451,9 +462,13 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
arguments.openPeerCategory(.channel) arguments.openPeerCategory(.channel)
}) })
case let .stories(_, title, subtitle, label): case let .stories(_, title, subtitle, label):
return NotificationsCategoryItemListItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Stories"), title: title, subtitle: subtitle, label: label, sectionId: self.section, style: .blocks, action: { return NotificationsCategoryItemListItem(presentationData: presentationData, icon: PresentationResourcesSettings.stories, title: title, subtitle: subtitle, label: label, sectionId: self.section, style: .blocks, action: {
arguments.openPeerCategory(.stories) arguments.openPeerCategory(.stories)
}) })
case let .reactions(_, title, subtitle, label):
return NotificationsCategoryItemListItem(presentationData: presentationData, icon: PresentationResourcesSettings.reactions, title: title, subtitle: subtitle, label: label, sectionId: self.section, style: .blocks, action: {
arguments.openReactions()
})
case let .inAppHeader(_, text): case let .inAppHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .inAppSounds(_, text, value): case let .inAppSounds(_, text, value):
@ -564,6 +579,31 @@ private func notificationsAndSoundsEntries(authorizationStatus: AccessType, warn
entries.append(.stories(presentationData.theme, presentationData.strings.Notifications_Stories, !exceptions.stories.isEmpty ? presentationData.strings.Notifications_CategoryExceptions(Int32(exceptions.stories.peerIds.count)) : "", storiesValue)) entries.append(.stories(presentationData.theme, presentationData.strings.Notifications_Stories, !exceptions.stories.isEmpty ? presentationData.strings.Notifications_CategoryExceptions(Int32(exceptions.stories.peerIds.count)) : "", storiesValue))
//TODO:localize
var reactionsValue: String = ""
var hasReactionNotifications = false
switch globalSettings.reactionSettings.messages {
case .nobody:
break
default:
if !reactionsValue.isEmpty {
reactionsValue.append(", ")
}
hasReactionNotifications = true
reactionsValue.append("Messages")
}
switch globalSettings.reactionSettings.stories {
case .nobody:
break
default:
if !reactionsValue.isEmpty {
reactionsValue.append(", ")
}
hasReactionNotifications = true
reactionsValue.append("Stories")
}
entries.append(.reactions(presentationData.theme, "Reactions", reactionsValue, hasReactionNotifications ? "On" : "Off"))
entries.append(.inAppHeader(presentationData.theme, presentationData.strings.Notifications_InAppNotifications.uppercased())) entries.append(.inAppHeader(presentationData.theme, presentationData.strings.Notifications_InAppNotifications.uppercased()))
entries.append(.inAppSounds(presentationData.theme, presentationData.strings.Notifications_InAppNotificationsSounds, inAppSettings.playSounds)) entries.append(.inAppSounds(presentationData.theme, presentationData.strings.Notifications_InAppNotificationsSounds, inAppSettings.playSounds))
entries.append(.inAppVibrate(presentationData.theme, presentationData.strings.Notifications_InAppNotificationsVibrate, inAppSettings.vibrate)) entries.append(.inAppVibrate(presentationData.theme, presentationData.strings.Notifications_InAppNotificationsVibrate, inAppSettings.vibrate))
@ -655,6 +695,10 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions
}) })
}, focusOnItemTag: nil)) }, focusOnItemTag: nil))
}) })
}, openReactions: {
pushControllerImpl?(reactionNotificationSettingsController(
context: context
))
}, updateInAppSounds: { value in }, updateInAppSounds: { value in
let _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in let _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
var settings = settings var settings = settings

View File

@ -92,6 +92,7 @@ public enum NotificationsPeerCategoryEntryTag: ItemListItemTag {
private enum NotificationsPeerCategoryEntry: ItemListNodeEntry { private enum NotificationsPeerCategoryEntry: ItemListNodeEntry {
enum StableId: Hashable { enum StableId: Hashable {
case enableHeader
case enable case enable
case enableImportant case enableImportant
case importantInfo case importantInfo
@ -104,6 +105,7 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry {
case removeAllExceptions case removeAllExceptions
} }
case enableHeader(String)
case enable(PresentationTheme, String, Bool) case enable(PresentationTheme, String, Bool)
case enableImportant(PresentationTheme, String, Bool) case enableImportant(PresentationTheme, String, Bool)
case importantInfo(PresentationTheme, String) case importantInfo(PresentationTheme, String)
@ -118,7 +120,7 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry {
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
case .enable, .enableImportant, .importantInfo: case .enableHeader, .enable, .enableImportant, .importantInfo:
return NotificationsPeerCategorySection.enable.rawValue return NotificationsPeerCategorySection.enable.rawValue
case .optionsHeader, .previews, .sound: case .optionsHeader, .previews, .sound:
return NotificationsPeerCategorySection.options.rawValue return NotificationsPeerCategorySection.options.rawValue
@ -129,6 +131,8 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry {
var stableId: StableId { var stableId: StableId {
switch self { switch self {
case .enableHeader:
return .enableHeader
case .enable: case .enable:
return .enable return .enable
case .enableImportant: case .enableImportant:
@ -154,6 +158,8 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry {
var sortIndex: Int32 { var sortIndex: Int32 {
switch self { switch self {
case .enableHeader:
return -1
case .enable: case .enable:
return 0 return 0
case .enableImportant: case .enableImportant:
@ -192,6 +198,12 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry {
static func ==(lhs: NotificationsPeerCategoryEntry, rhs: NotificationsPeerCategoryEntry) -> Bool { static func ==(lhs: NotificationsPeerCategoryEntry, rhs: NotificationsPeerCategoryEntry) -> Bool {
switch lhs { switch lhs {
case let .enableHeader(text):
if case .enableHeader(text) = rhs {
return true
} else {
return false
}
case let .enable(lhsTheme, lhsText, lhsValue): case let .enable(lhsTheme, lhsText, lhsValue):
if case let .enable(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { if case let .enable(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true return true
@ -263,6 +275,8 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! NotificationsPeerCategoryControllerArguments let arguments = arguments as! NotificationsPeerCategoryControllerArguments
switch self { switch self {
case let .enableHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .enable(_, text, value): case let .enable(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateEnabled(updatedValue) arguments.updateEnabled(updatedValue)
@ -330,6 +344,9 @@ private func notificationsPeerCategoryEntries(category: NotificationsPeerCategor
} }
if case .stories = category { if case .stories = category {
//TODO:localize
entries.append(.enableHeader("NOTIFY ME ABOUT..."))
var allEnabled = false var allEnabled = false
var importantEnabled = false var importantEnabled = false
@ -345,16 +362,16 @@ private func notificationsPeerCategoryEntries(category: NotificationsPeerCategor
importantEnabled = true importantEnabled = true
} }
entries.append(.enable(presentationData.theme, presentationData.strings.NotificationSettings_Stories_ShowAll, allEnabled)) entries.append(.enable(presentationData.theme, "New Stories", allEnabled))
if !allEnabled { if !allEnabled {
entries.append(.enableImportant(presentationData.theme, presentationData.strings.NotificationSettings_Stories_ShowImportant, importantEnabled)) entries.append(.enableImportant(presentationData.theme, "Important Stories", importantEnabled))
entries.append(.importantInfo(presentationData.theme, presentationData.strings.NotificationSettings_Stories_ShowImportantFooter)) entries.append(.importantInfo(presentationData.theme, presentationData.strings.NotificationSettings_Stories_ShowImportantFooter))
} }
if notificationSettings.enabled || !notificationExceptions.isEmpty { if notificationSettings.enabled || !notificationExceptions.isEmpty {
entries.append(.optionsHeader(presentationData.theme, presentationData.strings.Notifications_Options.uppercased())) entries.append(.optionsHeader(presentationData.theme, presentationData.strings.Notifications_Options.uppercased()))
entries.append(.previews(presentationData.theme, presentationData.strings.NotificationSettings_Stories_DisplayAuthorName, notificationSettings.storySettings.hideSender != .hide)) entries.append(.previews(presentationData.theme, "Show Sender's Name", notificationSettings.storySettings.hideSender != .hide))
entries.append(.sound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: filteredGlobalSound(notificationSettings.storySettings.sound)), filteredGlobalSound(notificationSettings.storySettings.sound))) entries.append(.sound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: filteredGlobalSound(notificationSettings.storySettings.sound)), filteredGlobalSound(notificationSettings.storySettings.sound)))
} }
} else { } else {

View File

@ -0,0 +1,418 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import DeviceAccess
import ItemListUI
import PresentationDataUtils
import AccountContext
import AlertUI
import PresentationDataUtils
import TelegramNotices
import NotificationSoundSelectionUI
import TelegramStringFormatting
private final class ReactionNotificationSettingsControllerArguments {
let context: AccountContext
let soundSelectionDisposable: MetaDisposable
let openMessages: () -> Void
let openStories: () -> Void
let toggleMessages: (Bool) -> Void
let toggleStories: (Bool) -> Void
let updatePreviews: (Bool) -> Void
let openSound: (PeerMessageSound) -> Void
init(
context: AccountContext,
soundSelectionDisposable: MetaDisposable,
openMessages: @escaping () -> Void,
openStories: @escaping () -> Void,
toggleMessages: @escaping (Bool) -> Void,
toggleStories: @escaping (Bool) -> Void,
updatePreviews: @escaping (Bool) -> Void,
openSound: @escaping (PeerMessageSound) -> Void
) {
self.context = context
self.soundSelectionDisposable = soundSelectionDisposable
self.openMessages = openMessages
self.openStories = openStories
self.toggleMessages = toggleMessages
self.toggleStories = toggleStories
self.updatePreviews = updatePreviews
self.openSound = openSound
}
}
private enum ReactionNotificationSettingsSection: Int32 {
case categories
case options
}
private enum ReactionNotificationSettingsEntry: ItemListNodeEntry {
enum StableId: Hashable {
case categoriesHeader
case messages
case stories
case optionsHeader
case previews
case sound
}
case categoriesHeader(String)
case messages(title: String, text: String?, value: Bool)
case stories(title: String, text: String?, value: Bool)
case optionsHeader(String)
case previews(String, Bool)
case sound(String, String, PeerMessageSound)
var section: ItemListSectionId {
switch self {
case .categoriesHeader, .messages, .stories:
return ReactionNotificationSettingsSection.categories.rawValue
case .optionsHeader, .previews, .sound:
return ReactionNotificationSettingsSection.options.rawValue
}
}
var stableId: StableId {
switch self {
case .categoriesHeader:
return .categoriesHeader
case .messages:
return .messages
case .stories:
return .stories
case .optionsHeader:
return .optionsHeader
case .previews:
return .previews
case .sound:
return .sound
}
}
var sortIndex: Int32 {
switch self {
case .categoriesHeader:
return 0
case .messages:
return 1
case .stories:
return 2
case .optionsHeader:
return 3
case .previews:
return 4
case .sound:
return 5
}
}
static func ==(lhs: ReactionNotificationSettingsEntry, rhs: ReactionNotificationSettingsEntry) -> Bool {
switch lhs {
case let .categoriesHeader(lhsText):
if case let .categoriesHeader(rhsText) = rhs, lhsText == rhsText {
return true
} else {
return false
}
case let .messages(title, text, value):
if case .messages(title, text, value) = rhs {
return true
} else {
return false
}
case let .stories(title, text, value):
if case .stories(title, text, value) = rhs {
return true
} else {
return false
}
case let .optionsHeader(lhsText):
if case let .optionsHeader(rhsText) = rhs, lhsText == rhsText {
return true
} else {
return false
}
case let .previews(lhsText, lhsValue):
if case let .previews(rhsText, rhsValue) = rhs, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .sound(lhsText, lhsValue, lhsSound):
if case let .sound(rhsText, rhsValue, rhsSound) = rhs, lhsText == rhsText, lhsValue == rhsValue, lhsSound == rhsSound {
return true
} else {
return false
}
}
}
static func <(lhs: ReactionNotificationSettingsEntry, rhs: ReactionNotificationSettingsEntry) -> Bool {
return lhs.sortIndex < rhs.sortIndex
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ReactionNotificationSettingsControllerArguments
switch self {
case let .categoriesHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .messages(title, text, value):
return ItemListSwitchItem(presentationData: presentationData, title: title, text: text, textColor: .accent, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleMessages(value)
}, action: {
arguments.openMessages()
})
case let .stories(title, text, value):
return ItemListSwitchItem(presentationData: presentationData, title: title, text: text, textColor: .accent, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleStories(value)
}, action: {
arguments.openStories()
})
case let .optionsHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .previews(text, value):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updatePreviews(value)
})
case let .sound(text, value, sound):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openSound(sound)
}, tag: self.tag)
}
}
}
private func filteredGlobalSound(_ sound: PeerMessageSound) -> PeerMessageSound {
if case .default = sound {
return defaultCloudPeerNotificationSound
} else {
return sound
}
}
private func reactionNotificationSettingsEntries(
globalSettings: GlobalNotificationSettingsSet,
state: ReactionNotificationSettingsState,
presentationData: PresentationData,
notificationSoundList: NotificationSoundList?
) -> [ReactionNotificationSettingsEntry] {
var entries: [ReactionNotificationSettingsEntry] = []
//TODO:localize
entries.append(.categoriesHeader("NOTIFY ME ABOUT..."))
let messagesText: String?
let messagesValue: Bool
switch globalSettings.reactionSettings.messages {
case .nobody:
messagesText = nil
messagesValue = false
case .contacts:
messagesText = "From My Contacts"
messagesValue = true
case .everyone:
messagesText = "From Everyone"
messagesValue = true
}
let storiesText: String?
let storiesValue: Bool
switch globalSettings.reactionSettings.stories {
case .nobody:
storiesText = nil
storiesValue = false
case .contacts:
storiesText = "From My Contacts"
storiesValue = true
case .everyone:
storiesText = "From Everyone"
storiesValue = true
}
entries.append(.messages(title: "Reactions to my Messages", text: messagesText, value: messagesValue))
entries.append(.stories(title: "Reactions to my Stories", text: storiesText, value: storiesValue))
if messagesValue || storiesValue {
entries.append(.optionsHeader(presentationData.strings.Notifications_Options.uppercased()))
//TODO:localize
entries.append(.previews("Show Sender's Name", globalSettings.reactionSettings.hideSender != .hide))
entries.append(.sound(presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: filteredGlobalSound(globalSettings.reactionSettings.sound)), filteredGlobalSound(globalSettings.reactionSettings.sound)))
}
return entries
}
private struct ReactionNotificationSettingsState: Equatable {
init() {
}
}
public func reactionNotificationSettingsController(
context: AccountContext
) -> ViewController {
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
let stateValue = Atomic<ReactionNotificationSettingsState>(value: ReactionNotificationSettingsState())
let statePromise: ValuePromise<ReactionNotificationSettingsState> = ValuePromise(ignoreRepeated: true)
statePromise.set(stateValue.with { $0 })
let updateState: ((ReactionNotificationSettingsState) -> ReactionNotificationSettingsState) -> Void = { f in
let result = stateValue.modify { f($0) }
statePromise.set(result)
}
let _ = updateState
let openCategory: (Bool) -> Void = { isMessages in
//TODO:localize
let text: String
if isMessages {
text = "Notify about reactions to my messages from"
} else {
text = "Notify about reactions to my stories from"
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: text),
ActionSheetButtonItem(title: "Everyone", color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
if isMessages {
settings.reactionSettings.messages = .everyone
} else {
settings.reactionSettings.stories = .everyone
}
return settings
}).start()
}),
ActionSheetButtonItem(title: "My Contacts", color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
if isMessages {
settings.reactionSettings.messages = .contacts
} else {
settings.reactionSettings.stories = .contacts
}
return settings
}).start()
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet, nil)
}
let arguments = ReactionNotificationSettingsControllerArguments(
context: context,
soundSelectionDisposable: MetaDisposable(),
openMessages: {
openCategory(true)
},
openStories: {
openCategory(false)
},
toggleMessages: { value in
let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
if value {
settings.reactionSettings.messages = .contacts
} else {
settings.reactionSettings.messages = .nobody
}
return settings
}).start()
},
toggleStories: { value in
let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
if value {
settings.reactionSettings.stories = .contacts
} else {
settings.reactionSettings.stories = .nobody
}
return settings
}).start()
},
updatePreviews: { value in
let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
settings.reactionSettings.hideSender = value ? .show : .hide
return settings
}).start()
}, openSound: { sound in
let controller = notificationSoundSelectionController(context: context, isModal: true, currentSound: sound, defaultSound: nil, completion: { value in
let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
settings.reactionSettings.sound = value
return settings
}).start()
})
pushControllerImpl?(controller)
}
)
let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications])
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
context.engine.peers.notificationSoundList(),
preferences,
statePromise.get()
)
|> map { presentationData, notificationSoundList, preferencesView, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let viewSettings: GlobalNotificationSettingsSet
if let settings = preferencesView.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) {
viewSettings = settings.effective
} else {
viewSettings = GlobalNotificationSettingsSet.defaultSettings
}
let entries = reactionNotificationSettingsEntries(
globalSettings: viewSettings,
state: state,
presentationData: presentationData,
notificationSoundList: notificationSoundList
)
let leftNavigationButton: ItemListNavigationButton?
let rightNavigationButton: ItemListNavigationButton?
leftNavigationButton = nil
rightNavigationButton = nil
//TODO:localize
let title: String = "Reactions"
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
pushControllerImpl = { [weak controller] c in
(controller?.navigationController as? NavigationController)?.pushViewController(c)
}
return controller
}

View File

@ -270,7 +270,9 @@ private func fetchedNotificationSettings(network: Network) -> Signal<GlobalNotif
) )
} }
return GlobalNotificationSettingsSet(privateChats: userSettings, groupChats: chatsSettings, channels: channelSettings, contactsJoined: contactsJoinedMuted == .boolFalse) let reactionSettings: PeerReactionNotificationSettings = .default
return GlobalNotificationSettingsSet(privateChats: userSettings, groupChats: chatsSettings, channels: channelSettings, reactionSettings: reactionSettings, contactsJoined: contactsJoinedMuted == .boolFalse)
} }
} }

View File

@ -42,16 +42,18 @@ public struct GlobalNotificationSettingsSet: Codable, Equatable {
public var privateChats: MessageNotificationSettings public var privateChats: MessageNotificationSettings
public var groupChats: MessageNotificationSettings public var groupChats: MessageNotificationSettings
public var channels: MessageNotificationSettings public var channels: MessageNotificationSettings
public var reactionSettings: PeerReactionNotificationSettings
public var contactsJoined: Bool public var contactsJoined: Bool
public static var defaultSettings: GlobalNotificationSettingsSet { public static var defaultSettings: GlobalNotificationSettingsSet {
return GlobalNotificationSettingsSet(privateChats: MessageNotificationSettings.defaultSettings, groupChats: .defaultSettings, channels: .defaultSettings, contactsJoined: true) return GlobalNotificationSettingsSet(privateChats: MessageNotificationSettings.defaultSettings, groupChats: .defaultSettings, channels: .defaultSettings, reactionSettings: .default, contactsJoined: true)
} }
public init(privateChats: MessageNotificationSettings, groupChats: MessageNotificationSettings, channels: MessageNotificationSettings, contactsJoined: Bool) { public init(privateChats: MessageNotificationSettings, groupChats: MessageNotificationSettings, channels: MessageNotificationSettings, reactionSettings: PeerReactionNotificationSettings, contactsJoined: Bool) {
self.privateChats = privateChats self.privateChats = privateChats
self.groupChats = groupChats self.groupChats = groupChats
self.channels = channels self.channels = channels
self.reactionSettings = reactionSettings
self.contactsJoined = contactsJoined self.contactsJoined = contactsJoined
} }
@ -61,6 +63,7 @@ public struct GlobalNotificationSettingsSet: Codable, Equatable {
self.privateChats = try container.decode(MessageNotificationSettings.self, forKey: "p") self.privateChats = try container.decode(MessageNotificationSettings.self, forKey: "p")
self.groupChats = try container.decode(MessageNotificationSettings.self, forKey: "g") self.groupChats = try container.decode(MessageNotificationSettings.self, forKey: "g")
self.channels = try container.decode(MessageNotificationSettings.self, forKey: "c") self.channels = try container.decode(MessageNotificationSettings.self, forKey: "c")
self.reactionSettings = try container.decodeIfPresent(PeerReactionNotificationSettings.self, forKey: "reactionSettings") ?? PeerReactionNotificationSettings.default
self.contactsJoined = (try container.decode(Int32.self, forKey: "contactsJoined")) != 0 self.contactsJoined = (try container.decode(Int32.self, forKey: "contactsJoined")) != 0
} }
@ -71,6 +74,7 @@ public struct GlobalNotificationSettingsSet: Codable, Equatable {
try container.encode(self.privateChats, forKey: "p") try container.encode(self.privateChats, forKey: "p")
try container.encode(self.groupChats, forKey: "g") try container.encode(self.groupChats, forKey: "g")
try container.encode(self.channels, forKey: "c") try container.encode(self.channels, forKey: "c")
try container.encode(self.reactionSettings, forKey: "reactionSettings")
try container.encode((self.contactsJoined ? 1 : 0) as Int32, forKey: "contactsJoined") try container.encode((self.contactsJoined ? 1 : 0) as Int32, forKey: "contactsJoined")
} }
} }

View File

@ -454,6 +454,93 @@ public struct PeerStoryNotificationSettings: Codable, Equatable {
} }
} }
public struct PeerReactionNotificationSettings: Codable, Equatable {
public enum CodingError: Error {
case generic
}
public static var `default`: PeerReactionNotificationSettings {
return PeerReactionNotificationSettings(
messages: .contacts,
stories: .contacts,
hideSender: .show,
sound: .default
)
}
private enum CodingKeys: String, CodingKey {
case messages
case stories
case hideSender
case sound
}
public enum Sources: Int32, Codable {
case nobody = 0
case contacts = 1
case everyone = 2
}
public enum Mute: Int32, Codable {
case `default` = 0
case unmuted = 1
case muted = 2
}
public enum HideSender: Int32, Codable {
case `default` = 0
case hide = 1
case show = 2
}
public var messages: Sources
public var stories: Sources
public var hideSender: HideSender
public var sound: PeerMessageSound
public init(
messages: Sources,
stories: Sources,
hideSender: HideSender,
sound: PeerMessageSound
) {
self.messages = messages
self.stories = stories
self.hideSender = hideSender
self.sound = sound
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let messages = Sources(rawValue: try container.decode(Int32.self, forKey: .messages)) {
self.messages = messages
} else {
throw CodingError.generic
}
if let stories = Sources(rawValue: try container.decode(Int32.self, forKey: .stories)) {
self.stories = stories
} else {
throw CodingError.generic
}
if let hideSender = HideSender(rawValue: try container.decode(Int32.self, forKey: .hideSender)) {
self.hideSender = hideSender
} else {
throw CodingError.generic
}
self.sound = try container.decode(PeerMessageSound.self, forKey: .sound)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.messages.rawValue, forKey: .messages)
try container.encode(self.stories.rawValue, forKey: .stories)
try container.encode(self.hideSender.rawValue, forKey: .hideSender)
try container.encode(self.sound, forKey: .sound)
}
}
public final class TelegramPeerNotificationSettings: PeerNotificationSettings, Codable, Equatable { public final class TelegramPeerNotificationSettings: PeerNotificationSettings, Codable, Equatable {
public let muteState: PeerMuteState public let muteState: PeerMuteState
public let messageSound: PeerMessageSound public let messageSound: PeerMessageSound

View File

@ -225,20 +225,37 @@ public struct EngineGlobalNotificationSettings: Equatable {
} }
} }
public struct ReactionSettings: Equatable {
public var messages: PeerReactionNotificationSettings.Sources
public var stories: PeerReactionNotificationSettings.Sources
public var hideSender: PeerReactionNotificationSettings.HideSender
public var sound: EnginePeer.NotificationSettings.MessageSound
public init(messages: PeerReactionNotificationSettings.Sources, stories: PeerReactionNotificationSettings.Sources, hideSender: PeerReactionNotificationSettings.HideSender, sound: EnginePeer.NotificationSettings.MessageSound) {
self.messages = messages
self.stories = stories
self.hideSender = hideSender
self.sound = sound
}
}
public var privateChats: CategorySettings public var privateChats: CategorySettings
public var groupChats: CategorySettings public var groupChats: CategorySettings
public var channels: CategorySettings public var channels: CategorySettings
public var reactionSettings: ReactionSettings
public var contactsJoined: Bool public var contactsJoined: Bool
public init( public init(
privateChats: CategorySettings, privateChats: CategorySettings,
groupChats: CategorySettings, groupChats: CategorySettings,
channels: CategorySettings, channels: CategorySettings,
reactionSettings: ReactionSettings,
contactsJoined: Bool contactsJoined: Bool
) { ) {
self.privateChats = privateChats self.privateChats = privateChats
self.groupChats = groupChats self.groupChats = groupChats
self.channels = channels self.channels = channels
self.reactionSettings = reactionSettings
self.contactsJoined = contactsJoined self.contactsJoined = contactsJoined
} }
} }
@ -632,12 +649,33 @@ public extension EngineGlobalNotificationSettings.CategorySettings {
} }
} }
public extension EngineGlobalNotificationSettings.ReactionSettings {
init(_ reactionSettings: PeerReactionNotificationSettings) {
self.init(
messages: reactionSettings.messages,
stories: reactionSettings.stories,
hideSender: reactionSettings.hideSender,
sound: EnginePeer.NotificationSettings.MessageSound(reactionSettings.sound)
)
}
func _asReactionSettings() -> PeerReactionNotificationSettings {
return PeerReactionNotificationSettings(
messages: self.messages,
stories: self.stories,
hideSender: self.hideSender,
sound: self.sound._asMessageSound()
)
}
}
public extension EngineGlobalNotificationSettings { public extension EngineGlobalNotificationSettings {
init(_ globalNotificationSettings: GlobalNotificationSettingsSet) { init(_ globalNotificationSettings: GlobalNotificationSettingsSet) {
self.init( self.init(
privateChats: CategorySettings(globalNotificationSettings.privateChats), privateChats: CategorySettings(globalNotificationSettings.privateChats),
groupChats: CategorySettings(globalNotificationSettings.groupChats), groupChats: CategorySettings(globalNotificationSettings.groupChats),
channels: CategorySettings(globalNotificationSettings.channels), channels: CategorySettings(globalNotificationSettings.channels),
reactionSettings: ReactionSettings(globalNotificationSettings.reactionSettings),
contactsJoined: globalNotificationSettings.contactsJoined contactsJoined: globalNotificationSettings.contactsJoined
) )
} }
@ -647,6 +685,7 @@ public extension EngineGlobalNotificationSettings {
privateChats: self.privateChats._asMessageNotificationSettings(), privateChats: self.privateChats._asMessageNotificationSettings(),
groupChats: self.groupChats._asMessageNotificationSettings(), groupChats: self.groupChats._asMessageNotificationSettings(),
channels: self.channels._asMessageNotificationSettings(), channels: self.channels._asMessageNotificationSettings(),
reactionSettings: self.reactionSettings._asReactionSettings(),
contactsJoined: self.contactsJoined contactsJoined: self.contactsJoined
) )
} }

View File

@ -26,7 +26,7 @@ private func addRoundedRectPath(context: CGContext, rect: CGRect, radius: CGFloa
context.restoreGState() context.restoreGState()
} }
private func renderIcon(name: String, backgroundColors: [UIColor]? = nil) -> UIImage? { private func renderIcon(name: String, scaleFactor: CGFloat = 1.0, backgroundColors: [UIColor]? = nil) -> UIImage? {
return generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in return generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size) let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds) context.clear(bounds)
@ -44,12 +44,14 @@ private func renderIcon(name: String, backgroundColors: [UIColor]? = nil) -> UII
context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: size.height), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions()) context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: size.height), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions())
context.resetClip() context.resetClip()
if let image = generateTintedImage(image: UIImage(bundleImageName: name), color: .white)?.cgImage { if let image = generateTintedImage(image: UIImage(bundleImageName: name), color: .white), let cgImage = image.cgImage {
context.draw(image, in: bounds) let imageSize = CGSize(width: image.size.width * scaleFactor, height: image.size.height * scaleFactor)
context.draw(cgImage, in: CGRect(origin: CGPoint(x: (bounds.width - imageSize.width) * 0.5, y: (bounds.height - imageSize.height) * 0.5), size: imageSize))
} }
} else { } else {
if let image = UIImage(bundleImageName: name)?.cgImage { if let image = UIImage(bundleImageName: name), let cgImage = image.cgImage {
context.draw(image, in: bounds) let imageSize = CGSize(width: image.size.width * scaleFactor, height: image.size.height * scaleFactor)
context.draw(cgImage, in: CGRect(origin: CGPoint(x: (bounds.width - imageSize.width) * 0.5, y: (bounds.height - imageSize.height) * 0.5), size: imageSize))
} }
} }
}) })
@ -70,10 +72,11 @@ public struct PresentationResourcesSettings {
public static let language = renderIcon(name: "Settings/Menu/Language") public static let language = renderIcon(name: "Settings/Menu/Language")
public static let deleteAccount = renderIcon(name: "Chat/Info/GroupRemovedIcon") public static let deleteAccount = renderIcon(name: "Chat/Info/GroupRemovedIcon")
public static let powerSaving = renderIcon(name: "Settings/Menu/PowerSaving") public static let powerSaving = renderIcon(name: "Settings/Menu/PowerSaving")
public static let stories = renderIcon(name: "Settings/Menu/Stories") public static let stories = renderIcon(name: "Premium/Perk/Stories", scaleFactor: 0.97, backgroundColors: [UIColor(rgb: 0x5856D6)])
public static let premiumGift = renderIcon(name: "Settings/Menu/Gift") public static let premiumGift = renderIcon(name: "Settings/Menu/Gift")
public static let business = renderIcon(name: "Settings/Menu/Business", backgroundColors: [UIColor(rgb: 0xA95CE3), UIColor(rgb: 0xF16B80)]) public static let business = renderIcon(name: "Settings/Menu/Business", backgroundColors: [UIColor(rgb: 0xA95CE3), UIColor(rgb: 0xF16B80)])
public static let myProfile = renderIcon(name: "Settings/Menu/Profile") public static let myProfile = renderIcon(name: "Settings/Menu/Profile")
public static let reactions = renderIcon(name: "Settings/Menu/Reactions")
public static let premium = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in public static let premium = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size) let bounds = CGRect(origin: CGPoint(), size: size)

View File

@ -598,6 +598,7 @@ private final class PeerInfoInteraction {
let openBioContextMenu: (ASDisplayNode, ContextGesture?) -> Void let openBioContextMenu: (ASDisplayNode, ContextGesture?) -> Void
let openWorkingHoursContextMenu: (ASDisplayNode, ContextGesture?) -> Void let openWorkingHoursContextMenu: (ASDisplayNode, ContextGesture?) -> Void
let openBusinessLocationContextMenu: (ASDisplayNode, ContextGesture?) -> Void let openBusinessLocationContextMenu: (ASDisplayNode, ContextGesture?) -> Void
let openBirthdayContextMenu: (ASDisplayNode, ContextGesture?) -> Void
let getController: () -> ViewController? let getController: () -> ViewController?
init( init(
@ -664,6 +665,7 @@ private final class PeerInfoInteraction {
openBioContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void, openBioContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void,
openWorkingHoursContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void, openWorkingHoursContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void,
openBusinessLocationContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void, openBusinessLocationContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void,
openBirthdayContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void,
getController: @escaping () -> ViewController? getController: @escaping () -> ViewController?
) { ) {
self.openUsername = openUsername self.openUsername = openUsername
@ -729,6 +731,7 @@ private final class PeerInfoInteraction {
self.openBioContextMenu = openBioContextMenu self.openBioContextMenu = openBioContextMenu
self.openWorkingHoursContextMenu = openWorkingHoursContextMenu self.openWorkingHoursContextMenu = openWorkingHoursContextMenu
self.openBusinessLocationContextMenu = openBusinessLocationContextMenu self.openBusinessLocationContextMenu = openBusinessLocationContextMenu
self.openBirthdayContextMenu = openBirthdayContextMenu
self.getController = getController self.getController = getController
} }
} }
@ -1188,7 +1191,7 @@ private enum InfoSection: Int, CaseIterable {
case peerMembers case peerMembers
} }
private func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], chatLocation: ChatLocation, isOpenedFromChat: Bool) -> [(AnyHashable, [PeerInfoScreenItem])] { private func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], chatLocation: ChatLocation, isOpenedFromChat: Bool, isMyProfile: Bool) -> [(AnyHashable, [PeerInfoScreenItem])] {
guard let data = data else { guard let data = data else {
return [] return []
} }
@ -1210,6 +1213,9 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
let businessLocationContextAction: (ASDisplayNode, ContextGesture?, CGPoint?) -> Void = { node, gesture, _ in let businessLocationContextAction: (ASDisplayNode, ContextGesture?, CGPoint?) -> Void = { node, gesture, _ in
interaction.openBusinessLocationContextMenu(node, gesture) interaction.openBusinessLocationContextMenu(node, gesture)
} }
let birthdayContextAction: (ASDisplayNode, ContextGesture?, CGPoint?) -> Void = { node, gesture, _ in
interaction.openBirthdayContextMenu(node, gesture)
}
if let user = data.peer as? TelegramUser { if let user = data.peer as? TelegramUser {
if !callMessages.isEmpty { if !callMessages.isEmpty {
@ -1293,13 +1299,21 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
if today.day == Int(birthday.day) && today.month == Int(birthday.month) { if today.day == Int(birthday.day) && today.month == Int(birthday.month) {
hasBirthdayToday = true hasBirthdayToday = true
} }
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 400, context: context, label: hasBirthdayToday ? presentationData.strings.UserInfo_BirthdayToday : presentationData.strings.UserInfo_Birthday, text: stringForCompactBirthday(birthday, strings: presentationData.strings, showAge: true), textColor: .primary, leftIcon: hasBirthdayToday ? .birthday : nil, icon: hasBirthdayToday ? .premiumGift : nil, action: hasBirthdayToday ? { _, _ in
var birthdayAction: ((ASDisplayNode, Promise<Bool>?) -> Void)?
if isMyProfile {
birthdayAction = { node, _ in
birthdayContextAction(node, nil, nil)
}
} else if hasBirthdayToday {
birthdayAction = { _, _ in
interaction.openPremiumGift()
}
}
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 400, context: context, label: hasBirthdayToday ? presentationData.strings.UserInfo_BirthdayToday : presentationData.strings.UserInfo_Birthday, text: stringForCompactBirthday(birthday, strings: presentationData.strings, showAge: true), textColor: .primary, leftIcon: hasBirthdayToday ? .birthday : nil, icon: hasBirthdayToday ? .premiumGift : nil, action: birthdayAction, longTapAction: nil, iconAction: {
interaction.openPremiumGift() interaction.openPremiumGift()
} : nil, longTapAction: { sourceNode in }, contextAction: birthdayContextAction, requestLayout: {
interaction.openPeerInfoContextMenu(.birthday, sourceNode, nil)
}, iconAction: {
interaction.openPremiumGift()
}, contextAction: nil, requestLayout: {
})) }))
} }
@ -1312,7 +1326,9 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
interaction.requestLayout(false) interaction.requestLayout(false)
})) }))
} else if let about = cachedData.about, !about.isEmpty { } else if let about = cachedData.about, !about.isEmpty {
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.isPremium ? enabledPublicBioEntities : enabledPrivateBioEntities), action: nil, linkItemAction: bioLinkAction, contextAction: bioContextAction, requestLayout: { items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.isPremium ? enabledPublicBioEntities : enabledPrivateBioEntities), action: isMyProfile ? { node, _ in
bioContextAction(node, nil, nil)
} : nil, linkItemAction: bioLinkAction, contextAction: bioContextAction, requestLayout: {
interaction.requestLayout(false) interaction.requestLayout(false)
})) }))
} }
@ -1546,7 +1562,9 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
if case .group = channel.info { if case .group = channel.info {
enabledEntities = enabledPrivateBioEntities enabledEntities = enabledPrivateBioEntities
} }
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledEntities), action: nil, linkItemAction: bioLinkAction, contextAction: bioContextAction, requestLayout: { items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledEntities), action: isMyProfile ? { node, _ in
bioContextAction(node, nil, nil)
} : nil, linkItemAction: bioLinkAction, contextAction: bioContextAction, requestLayout: {
interaction.requestLayout(true) interaction.requestLayout(true)
})) }))
} }
@ -1596,7 +1614,9 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
} }
if let aboutText = aboutText { if let aboutText = aboutText {
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPrivateBioEntities), action: nil, linkItemAction: bioLinkAction, contextAction: bioContextAction, requestLayout: { items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPrivateBioEntities), action: isMyProfile ? { node, _ in
bioContextAction(node, nil, nil)
} : nil, linkItemAction: bioLinkAction, contextAction: bioContextAction, requestLayout: {
interaction.requestLayout(true) interaction.requestLayout(true)
})) }))
} }
@ -2780,6 +2800,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
return return
} }
self.openBusinessLocationContextMenu(node: node, gesture: gesture) self.openBusinessLocationContextMenu(node: node, gesture: gesture)
}, openBirthdayContextMenu: { [weak self] node, gesture in
guard let self else {
return
}
self.openBirthdayContextMenu(node: node, gesture: gesture)
}, },
getController: { [weak self] in getController: { [weak self] in
return self?.controller return self?.controller
@ -7160,6 +7185,65 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
self.controller?.present(contextController, in: .window(.root)) self.controller?.present(contextController, in: .window(.root))
} }
private func openBirthdayContextMenu(node: ASDisplayNode, gesture: ContextGesture?) {
guard let sourceNode = node as? ContextExtractedContentContainingNode else {
return
}
guard let cachedData = self.data?.cachedData else {
return
}
var birthday: TelegramBirthday?
if let cachedData = cachedData as? CachedUserData {
birthday = cachedData.birthday
}
guard let birthday else {
return
}
let copyAction = { [weak self] in
guard let self else {
return
}
let presentationData = self.presentationData
let text = stringForCompactBirthday(birthday, strings: presentationData.strings)
UIPasteboard.general.string = text
//TODO:localize
self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: "Birthday copied."), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
var items: [ContextMenuItem] = []
if self.isMyProfile {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Edit Birthday", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
c.dismiss {
guard let self else {
return
}
self.state = self.state.withIsEditingBirthDate(true)
self.headerNode.navigationButtonContainer.performAction?(.edit, nil, nil)
}
})))
}
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Copy", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c.dismiss {
copyAction()
}
})))
let actions = ContextController.Items(content: .list(items))
let contextController = ContextController(presentationData: self.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture)
self.controller?.present(contextController, in: .window(.root))
}
private func openPhone(value: String, node: ASDisplayNode, gesture: ContextGesture?, progress: Promise<Bool>?) { private func openPhone(value: String, node: ASDisplayNode, gesture: ContextGesture?, progress: Promise<Bool>?) {
guard let sourceNode = node as? ContextExtractedContentContainingNode else { guard let sourceNode = node as? ContextExtractedContentContainingNode else {
return return
@ -10826,7 +10910,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
insets.left += sectionInset insets.left += sectionInset
insets.right += sectionInset insets.right += sectionInset
let items = self.isSettings ? settingsItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isExpanded: self.headerNode.isAvatarExpanded) : infoItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, chatLocation: self.chatLocation, isOpenedFromChat: self.isOpenedFromChat) let items = self.isSettings ? settingsItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isExpanded: self.headerNode.isAvatarExpanded) : infoItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, chatLocation: self.chatLocation, isOpenedFromChat: self.isOpenedFromChat, isMyProfile: self.isMyProfile)
contentHeight += headerHeight contentHeight += headerHeight
if !((self.isSettings || self.isMyProfile) && self.state.isEditing) { if !((self.isSettings || self.isMyProfile) && self.state.isEditing) {