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

View File

@ -14,10 +14,16 @@ public enum ItemListSwitchItemNodeType {
}
public class ItemListSwitchItem: ListViewItem, ItemListItem {
public enum TextColor {
case primary
case accent
}
let presentationData: ItemListPresentationData
let icon: UIImage?
let title: String
let text: String?
let textColor: TextColor
let titleBadgeComponent: AnyComponent<Empty>?
let value: Bool
let type: ItemListSwitchItemNodeType
@ -31,13 +37,15 @@ public class ItemListSwitchItem: ListViewItem, ItemListItem {
let style: ItemListStyle
let updated: (Bool) -> Void
let activatedWhileDisabled: () -> Void
let action: (() -> Void)?
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.icon = icon
self.title = title
self.text = text
self.textColor = textColor
self.titleBadgeComponent = titleBadgeComponent
self.value = value
self.type = type
@ -51,6 +59,7 @@ public class ItemListSwitchItem: ListViewItem, ItemListItem {
self.style = style
self.updated = updated
self.activatedWhileDisabled = activatedWhileDisabled
self.action = action
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 {
@ -132,7 +152,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
private let iconNode: ASImageNode
private let titleNode: TextNode
private let textNode: TextNode
private var textNode: TextNode?
private var switchNode: ASDisplayNode & ItemListSwitchNodeImpl
private let switchGestureNode: ASDisplayNode
private var disabledOverlayNode: ASDisplayNode?
@ -168,11 +188,9 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
self.iconNode.displaysAsynchronously = false
self.titleNode = TextNode()
self.titleNode.anchorPoint = CGPoint()
self.titleNode.isUserInteractionEnabled = false
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
switch type {
case .regular:
self.switchNode = SwitchNode()
@ -190,7 +208,6 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.switchNode)
self.addSubnode(self.switchGestureNode)
self.addSubnode(self.activateArea)
@ -230,7 +247,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
let itemSeparatorColor: UIColor
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?
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 (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)
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
textLayoutAndApply = (textLayout, textApply)
}
if !item.enabled {
@ -294,6 +321,13 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] animated in
if let strongSelf = self {
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
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))
@ -309,7 +343,9 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.activateArea.accessibilityTraits = accessibilityTraits
if let icon = item.icon {
var iconTransition = transition
if strongSelf.iconNode.supernode == nil {
iconTransition = .immediate
strongSelf.addSubnode(strongSelf.iconNode)
}
if updateIcon {
@ -321,19 +357,12 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
} else {
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 {
strongSelf.iconNode.image = nil
strongSelf.iconNode.removeFromSupernode()
}
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
if let currentDisabledOverlayNode = currentDisabledOverlayNode {
if currentDisabledOverlayNode != strongSelf.disabledOverlayNode {
strongSelf.disabledOverlayNode = currentDisabledOverlayNode
@ -367,7 +396,6 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
}
let _ = titleApply()
let _ = textApply()
switch item.style {
case .plain:
@ -383,7 +411,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
if strongSelf.maskNode.supernode != nil {
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:
if strongSelf.backgroundNode.supernode == nil {
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.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)
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.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.maskNode, frame: strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0))
transition.updateFrame(node: 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.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 strongSelf.switchNode.bounds.size.width.isZero {
@ -437,7 +489,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
}
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
if switchView.isOn != item.value {
switchView.setOn(item.value, animated: animated)
@ -447,6 +499,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.switchGestureNode.isHidden = item.enableInteractiveChanges && item.enabled
if item.displayLocked {
var lockedIconTransition = transition
var updateLockedIconImage = false
if let _ = updatedTheme {
updateLockedIconImage = true
@ -456,6 +509,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
if let current = strongSelf.lockedIconNode {
lockedIconNode = current
} else {
lockedIconTransition = .immediate
updateLockedIconImage = true
lockedIconNode = ASImageNode()
strongSelf.lockedIconNode = lockedIconNode
@ -469,7 +523,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
let switchFrame = strongSelf.switchNode.frame
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 {
strongSelf.lockedIconNode = nil
@ -492,17 +546,19 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
containerSize: contentSize
)
if let view = componentView.view {
var titleBadgeTransition = transition
if view.superview == nil {
titleBadgeTransition = .immediate
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 {
strongSelf.titleBadgeComponentView = nil
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 openPeerCategory: (NotificationsPeerCategory) -> Void
let openReactions: () -> Void
let updateInAppSounds: (Bool) -> Void
let updateInAppVibration: (Bool) -> Void
@ -80,7 +81,7 @@ private final class NotificationsAndSoundsArguments {
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.presentController = presentController
self.pushController = pushController
@ -88,6 +89,7 @@ private final class NotificationsAndSoundsArguments {
self.authorizeNotifications = authorizeNotifications
self.suppressWarning = suppressWarning
self.openPeerCategory = openPeerCategory
self.openReactions = openReactions
self.updateInAppSounds = updateInAppSounds
self.updateInAppVibration = updateInAppVibration
self.updateInAppPreviews = updateInAppPreviews
@ -145,6 +147,7 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
case groupChats(PresentationTheme, String, String, String)
case channels(PresentationTheme, String, String, String)
case stories(PresentationTheme, String, String, String)
case reactions(PresentationTheme, String, String, String)
case inAppHeader(PresentationTheme, String)
case inAppSounds(PresentationTheme, String, Bool)
@ -171,7 +174,7 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
return NotificationsAndSoundsSection.accounts.rawValue
case .permissionInfo, .permissionEnable:
return NotificationsAndSoundsSection.permission.rawValue
case .categoriesHeader, .privateChats, .groupChats, .channels, .stories:
case .categoriesHeader, .privateChats, .groupChats, .channels, .stories, .reactions:
return NotificationsAndSoundsSection.categories.rawValue
case .inAppHeader, .inAppSounds, .inAppVibrate, .inAppPreviews:
return NotificationsAndSoundsSection.inApp.rawValue
@ -208,6 +211,8 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
return 8
case .stories:
return 9
case .reactions:
return 10
case .inAppHeader:
return 14
case .inAppSounds:
@ -326,6 +331,12 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
} else {
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):
if case let .inAppHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
@ -451,9 +462,13 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
arguments.openPeerCategory(.channel)
})
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)
})
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):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
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))
//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(.inAppSounds(presentationData.theme, presentationData.strings.Notifications_InAppNotificationsSounds, inAppSettings.playSounds))
entries.append(.inAppVibrate(presentationData.theme, presentationData.strings.Notifications_InAppNotificationsVibrate, inAppSettings.vibrate))
@ -655,6 +695,10 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions
})
}, focusOnItemTag: nil))
})
}, openReactions: {
pushControllerImpl?(reactionNotificationSettingsController(
context: context
))
}, updateInAppSounds: { value in
let _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
var settings = settings

View File

@ -92,6 +92,7 @@ public enum NotificationsPeerCategoryEntryTag: ItemListItemTag {
private enum NotificationsPeerCategoryEntry: ItemListNodeEntry {
enum StableId: Hashable {
case enableHeader
case enable
case enableImportant
case importantInfo
@ -104,6 +105,7 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry {
case removeAllExceptions
}
case enableHeader(String)
case enable(PresentationTheme, String, Bool)
case enableImportant(PresentationTheme, String, Bool)
case importantInfo(PresentationTheme, String)
@ -118,7 +120,7 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry {
var section: ItemListSectionId {
switch self {
case .enable, .enableImportant, .importantInfo:
case .enableHeader, .enable, .enableImportant, .importantInfo:
return NotificationsPeerCategorySection.enable.rawValue
case .optionsHeader, .previews, .sound:
return NotificationsPeerCategorySection.options.rawValue
@ -129,6 +131,8 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry {
var stableId: StableId {
switch self {
case .enableHeader:
return .enableHeader
case .enable:
return .enable
case .enableImportant:
@ -154,6 +158,8 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry {
var sortIndex: Int32 {
switch self {
case .enableHeader:
return -1
case .enable:
return 0
case .enableImportant:
@ -192,6 +198,12 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry {
static func ==(lhs: NotificationsPeerCategoryEntry, rhs: NotificationsPeerCategoryEntry) -> Bool {
switch lhs {
case let .enableHeader(text):
if case .enableHeader(text) = rhs {
return true
} else {
return false
}
case let .enable(lhsTheme, lhsText, lhsValue):
if case let .enable(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
@ -263,6 +275,8 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! NotificationsPeerCategoryControllerArguments
switch self {
case let .enableHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .enable(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateEnabled(updatedValue)
@ -330,6 +344,9 @@ private func notificationsPeerCategoryEntries(category: NotificationsPeerCategor
}
if case .stories = category {
//TODO:localize
entries.append(.enableHeader("NOTIFY ME ABOUT..."))
var allEnabled = false
var importantEnabled = false
@ -345,16 +362,16 @@ private func notificationsPeerCategoryEntries(category: NotificationsPeerCategor
importantEnabled = true
}
entries.append(.enable(presentationData.theme, presentationData.strings.NotificationSettings_Stories_ShowAll, allEnabled))
entries.append(.enable(presentationData.theme, "New Stories", 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))
}
if notificationSettings.enabled || !notificationExceptions.isEmpty {
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)))
}
} 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 groupChats: MessageNotificationSettings
public var channels: MessageNotificationSettings
public var reactionSettings: PeerReactionNotificationSettings
public var contactsJoined: Bool
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.groupChats = groupChats
self.channels = channels
self.reactionSettings = reactionSettings
self.contactsJoined = contactsJoined
}
@ -61,6 +63,7 @@ public struct GlobalNotificationSettingsSet: Codable, Equatable {
self.privateChats = try container.decode(MessageNotificationSettings.self, forKey: "p")
self.groupChats = try container.decode(MessageNotificationSettings.self, forKey: "g")
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
}
@ -71,6 +74,7 @@ public struct GlobalNotificationSettingsSet: Codable, Equatable {
try container.encode(self.privateChats, forKey: "p")
try container.encode(self.groupChats, forKey: "g")
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")
}
}

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 let muteState: PeerMuteState
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 groupChats: CategorySettings
public var channels: CategorySettings
public var reactionSettings: ReactionSettings
public var contactsJoined: Bool
public init(
privateChats: CategorySettings,
groupChats: CategorySettings,
channels: CategorySettings,
reactionSettings: ReactionSettings,
contactsJoined: Bool
) {
self.privateChats = privateChats
self.groupChats = groupChats
self.channels = channels
self.reactionSettings = reactionSettings
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 {
init(_ globalNotificationSettings: GlobalNotificationSettingsSet) {
self.init(
privateChats: CategorySettings(globalNotificationSettings.privateChats),
groupChats: CategorySettings(globalNotificationSettings.groupChats),
channels: CategorySettings(globalNotificationSettings.channels),
reactionSettings: ReactionSettings(globalNotificationSettings.reactionSettings),
contactsJoined: globalNotificationSettings.contactsJoined
)
}
@ -647,6 +685,7 @@ public extension EngineGlobalNotificationSettings {
privateChats: self.privateChats._asMessageNotificationSettings(),
groupChats: self.groupChats._asMessageNotificationSettings(),
channels: self.channels._asMessageNotificationSettings(),
reactionSettings: self.reactionSettings._asReactionSettings(),
contactsJoined: self.contactsJoined
)
}

View File

@ -26,7 +26,7 @@ private func addRoundedRectPath(context: CGContext, rect: CGRect, radius: CGFloa
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
let bounds = CGRect(origin: CGPoint(), size: size)
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.resetClip()
if let image = generateTintedImage(image: UIImage(bundleImageName: name), color: .white)?.cgImage {
context.draw(image, in: bounds)
if let image = generateTintedImage(image: UIImage(bundleImageName: name), color: .white), let cgImage = image.cgImage {
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 {
if let image = UIImage(bundleImageName: name)?.cgImage {
context.draw(image, in: bounds)
if let image = UIImage(bundleImageName: name), let cgImage = image.cgImage {
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 deleteAccount = renderIcon(name: "Chat/Info/GroupRemovedIcon")
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 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 reactions = renderIcon(name: "Settings/Menu/Reactions")
public static let premium = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)

View File

@ -598,6 +598,7 @@ private final class PeerInfoInteraction {
let openBioContextMenu: (ASDisplayNode, ContextGesture?) -> Void
let openWorkingHoursContextMenu: (ASDisplayNode, ContextGesture?) -> Void
let openBusinessLocationContextMenu: (ASDisplayNode, ContextGesture?) -> Void
let openBirthdayContextMenu: (ASDisplayNode, ContextGesture?) -> Void
let getController: () -> ViewController?
init(
@ -664,6 +665,7 @@ private final class PeerInfoInteraction {
openBioContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void,
openWorkingHoursContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void,
openBusinessLocationContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void,
openBirthdayContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void,
getController: @escaping () -> ViewController?
) {
self.openUsername = openUsername
@ -729,6 +731,7 @@ private final class PeerInfoInteraction {
self.openBioContextMenu = openBioContextMenu
self.openWorkingHoursContextMenu = openWorkingHoursContextMenu
self.openBusinessLocationContextMenu = openBusinessLocationContextMenu
self.openBirthdayContextMenu = openBirthdayContextMenu
self.getController = getController
}
}
@ -1188,7 +1191,7 @@ private enum InfoSection: Int, CaseIterable {
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 {
return []
}
@ -1210,6 +1213,9 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
let businessLocationContextAction: (ASDisplayNode, ContextGesture?, CGPoint?) -> Void = { node, gesture, _ in
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 !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) {
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()
} : nil, longTapAction: { sourceNode in
interaction.openPeerInfoContextMenu(.birthday, sourceNode, nil)
}, iconAction: {
interaction.openPremiumGift()
}, contextAction: nil, requestLayout: {
}, contextAction: birthdayContextAction, requestLayout: {
}))
}
@ -1312,7 +1326,9 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
interaction.requestLayout(false)
}))
} 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)
}))
}
@ -1546,7 +1562,9 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
if case .group = channel.info {
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)
}))
}
@ -1596,7 +1614,9 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
}
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)
}))
}
@ -2780,6 +2800,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
return
}
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
return self?.controller
@ -7160,6 +7185,65 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
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>?) {
guard let sourceNode = node as? ContextExtractedContentContainingNode else {
return
@ -10826,7 +10910,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
insets.left += 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
if !((self.isSettings || self.isMyProfile) && self.state.isEditing) {