From 1ac4c70dece19cf8e66d0b2e91f8317105b01f62 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 17 Dec 2021 00:20:30 +0400 Subject: [PATCH 1/4] Reaction list setup --- .../ReactionListContextMenuContent.swift | 14 +- ...tControllerExtractedPresentationNode.swift | 4 +- .../Sources/ItemListReactionItem.swift | 915 ++++++++++++++++++ .../PeerAllowedReactionListController.swift | 309 ++++++ submodules/Postbox/Sources/Coding.swift | 8 + .../Sources/State/MessageReactions.swift | 37 + .../SyncCore/SyncCore_CachedChannelData.swift | 104 +- .../SyncCore/SyncCore_CachedGroupData.swift | 68 +- .../Peers/TelegramEnginePeers.swift | 4 + .../Peers/UpdateCachedPeerData.swift | 6 +- .../TelegramUI/Sources/ChatController.swift | 17 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 46 +- 12 files changed, 1473 insertions(+), 59 deletions(-) create mode 100644 submodules/PeerInfoUI/Sources/ItemListReactionItem.swift create mode 100644 submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index d5df070e9d..a668f5c1c3 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -212,12 +212,12 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let contentSize = CGSize(width: sideInset * 2.0 + titleSize.width + iconSize.width + iconSpacing, height: titleSize.height) - self.titleLabelNode.frame = CGRect(origin: CGPoint(x: sideInset + iconSize.width + iconSpacing, y: floor((constrainedSize.height - titleSize.height) / 2.0)), size: titleSize) + self.titleLabelNode.frame = CGRect(origin: CGPoint(x: sideInset + iconSize.width + iconSpacing, y: floorToScreenPixels((constrainedSize.height - titleSize.height) / 2.0)), size: titleSize) if let reactionIconNode = self.reactionIconNode { - reactionIconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floor((constrainedSize.height - iconSize.height) / 2.0)), size: iconSize) + reactionIconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((constrainedSize.height - iconSize.height) / 2.0)), size: iconSize) } else if let iconNode = self.iconNode { - iconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floor((constrainedSize.height - iconSize.height) / 2.0)), size: iconSize) + iconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((constrainedSize.height - iconSize.height) / 2.0)), size: iconSize) } return CGSize(width: contentSize.width, height: constrainedSize.height) @@ -264,9 +264,9 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } func update(size: CGSize, presentationData: PresentationData, selectedReaction: String?, transition: ContainedViewLayoutTransition) { - let sideInset: CGFloat = 16.0 + let sideInset: CGFloat = 11.0 let spacing: CGFloat = 0.0 - let verticalInset: CGFloat = 6.0 + let verticalInset: CGFloat = 7.0 self.selectionHighlightNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor let highlightHeight: CGFloat = size.height - verticalInset * 2.0 @@ -354,7 +354,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } func update(size: CGSize, presentationData: PresentationData, item: EngineMessageReactionListContext.Item, isLast: Bool, syncronousLoad: Bool) { - let avatarInset: CGFloat = 10.0 + let avatarInset: CGFloat = 12.0 let avatarSpacing: CGFloat = 8.0 let avatarSize: CGFloat = 28.0 @@ -723,7 +723,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent var topContentHeight: CGFloat = 0.0 if let backButtonNode = self.backButtonNode { - let backButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: 44.0)) + let backButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: 45.0)) backButtonNode.update(size: backButtonFrame.size, presentationData: self.presentationData, isLast: self.tabListNode == nil) transition.updateFrame(node: backButtonNode, frame: backButtonFrame) topContentHeight += backButtonFrame.height diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 65dd811954..6f1489864f 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -299,9 +299,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo var actionsFrame = CGRect(origin: CGPoint(x: 0.0, y: contentRect.maxY + contentActionsSpacing), size: actionsSize) if contentRect.midX < layout.size.width / 2.0 { - actionsFrame.origin.x = contentRect.minX + actionsSideInset - 3.0 + actionsFrame.origin.x = contentRect.minX + actionsSideInset - 4.0 } else { - actionsFrame.origin.x = contentRect.maxX - actionsSideInset - actionsSize.width + actionsFrame.origin.x = contentRect.maxX - actionsSideInset - actionsSize.width - 1.0 } transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame) diff --git a/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift b/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift new file mode 100644 index 0000000000..20893de63a --- /dev/null +++ b/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift @@ -0,0 +1,915 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import ItemListUI +import PresentationDataUtils +import StickerResources +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import ShimmerEffect + +public struct ItemListReactionItemEditing: Equatable { + public var editable: Bool + public var editing: Bool + public var revealed: Bool + public var reorderable: Bool + public var selectable: Bool + + public init(editable: Bool, editing: Bool, revealed: Bool, reorderable: Bool, selectable: Bool) { + self.editable = editable + self.editing = editing + self.revealed = revealed + self.reorderable = reorderable + self.selectable = selectable + } +} + +public enum ItemListReactionItemControl: Equatable { + case none + case installation(installed: Bool) + case selection + case check(checked: Bool) +} + +public final class ItemListReactionItem: ListViewItem, ItemListItem { + let presentationData: ItemListPresentationData + let account: Account + let packInfo: StickerPackCollectionInfo + let itemCount: String + let topItem: StickerPackItem? + let unread: Bool + let control: ItemListReactionItemControl + let editing: ItemListReactionItemEditing + let enabled: Bool + let playAnimatedStickers: Bool + public let sectionId: ItemListSectionId + let action: (() -> Void)? + let setPackIdWithRevealedOptions: (ItemCollectionId?, ItemCollectionId?) -> Void + let addPack: () -> Void + let removePack: () -> Void + let toggleSelected: () -> Void + + public init(presentationData: ItemListPresentationData, account: Account, packInfo: StickerPackCollectionInfo, itemCount: String, topItem: StickerPackItem?, unread: Bool, control: ItemListReactionItemControl, editing: ItemListReactionItemEditing, enabled: Bool, playAnimatedStickers: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, addPack: @escaping () -> Void, removePack: @escaping () -> Void, toggleSelected: @escaping () -> Void) { + self.presentationData = presentationData + self.account = account + self.packInfo = packInfo + self.itemCount = itemCount + self.topItem = topItem + self.unread = unread + self.control = control + self.editing = editing + self.enabled = enabled + self.playAnimatedStickers = playAnimatedStickers + self.sectionId = sectionId + self.action = action + self.setPackIdWithRevealedOptions = setPackIdWithRevealedOptions + self.addPack = addPack + self.removePack = removePack + self.toggleSelected = toggleSelected + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = ItemListReactionItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply(false) }) + }) + } + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? ItemListReactionItemNode { + let makeLayout = nodeValue.asyncLayout() + + var animated = true + if case .None = animation { + animated = false + } + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply(animated) + }) + } + } + } + } + } + + public var selectable: Bool = true + + public func selected(listView: ListView){ + listView.clearHighlightAnimated(true) + self.action?() + } +} + +public enum StickerPackThumbnailItem: Equatable { + case still(TelegramMediaImageRepresentation) + case animated(MediaResource, PixelDimensions) + + public static func ==(lhs: StickerPackThumbnailItem, rhs: StickerPackThumbnailItem) -> Bool { + switch lhs { + case let .still(representation): + if case .still(representation) = rhs { + return true + } else { + return false + } + case let .animated(lhsResource, lhsDimensions): + if case let .animated(rhsResource, rhsDimensions) = rhs, lhsResource.isEqual(to: rhsResource), lhsDimensions == rhsDimensions { + return true + } else { + return false + } + } + } +} + +class ItemListReactionItemNode: ItemListRevealOptionsItemNode { + private var currentThumbnailItem: StickerPackThumbnailItem? + + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private var disabledOverlayNode: ASDisplayNode? + private let maskNode: ASImageNode + + private let containerNode: ASDisplayNode + override var controlsContainer: ASDisplayNode { + return self.containerNode + } + + fileprivate let imageNode: TransformImageNode + private var animationNode: AnimatedStickerNode? + private var placeholderNode: StickerShimmerEffectNode? + private let unreadNode: ASImageNode + private let titleNode: TextNode + private let statusNode: TextNode + private let installTextNode: TextNode + private let installationActionBackgroundNode: ASImageNode + private let installationActionNode: HighlightableButtonNode + private let selectionIconNode: ASImageNode + + private var layoutParams: (ItemListReactionItem, ListViewItemLayoutParams, ItemListNeighbors)? + + private var selectableControlNode: ItemListSelectableControlNode? + private var editableControlNode: ItemListEditableControlNode? + private var reorderControlNode: ItemListEditableReorderControlNode? + + private let activateArea: AccessibilityAreaNode + + private let fetchDisposable = MetaDisposable() + + override var canBeSelected: Bool { + if self.selectableControlNode != nil || self.editableControlNode != nil || self.disabledOverlayNode != nil { + return false + } + if let item = self.layoutParams?.0, item.action != nil { + return super.canBeSelected + } else { + return false + } + } + + override var visibility: ListViewItemNodeVisibility { + didSet { + let wasVisible = oldValue != .none + let isVisible = self.visibility != .none + + if wasVisible != isVisible { + self.animationNode?.visibility = isVisible && (self.layoutParams?.0.playAnimatedStickers ?? true) + } + } + } + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.containerNode = ASDisplayNode() + + self.maskNode = ASImageNode() + self.maskNode.isUserInteractionEnabled = false + + self.imageNode = TransformImageNode() + self.imageNode.isLayerBacked = !smartInvertColorsEnabled() + + self.placeholderNode = StickerShimmerEffectNode() + self.placeholderNode?.isUserInteractionEnabled = false + + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.statusNode = TextNode() + self.statusNode.isUserInteractionEnabled = false + self.statusNode.contentMode = .left + self.statusNode.contentsScale = UIScreen.main.scale + + self.unreadNode = ASImageNode() + self.unreadNode.isLayerBacked = true + self.unreadNode.displaysAsynchronously = false + self.unreadNode.displayWithoutProcessing = true + + self.installationActionBackgroundNode = ASImageNode() + self.installationActionBackgroundNode.displaysAsynchronously = false + self.installationActionBackgroundNode.displayWithoutProcessing = true + self.installationActionBackgroundNode.isLayerBacked = true + self.installationActionNode = HighlightableButtonNode() + + self.installTextNode = TextNode() + self.installTextNode.isUserInteractionEnabled = false + self.installTextNode.contentMode = .left + self.installTextNode.contentsScale = UIScreen.main.scale + + self.selectionIconNode = ASImageNode() + self.selectionIconNode.displaysAsynchronously = false + self.selectionIconNode.displayWithoutProcessing = true + self.selectionIconNode.isLayerBacked = true + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + self.activateArea = AccessibilityAreaNode() + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.containerNode) + + if let placeholderNode = self.placeholderNode { + self.containerNode.addSubnode(placeholderNode) + } + + self.containerNode.addSubnode(self.imageNode) + self.containerNode.addSubnode(self.titleNode) + self.containerNode.addSubnode(self.statusNode) + self.containerNode.addSubnode(self.unreadNode) + self.containerNode.addSubnode(self.installationActionBackgroundNode) + self.containerNode.addSubnode(self.installTextNode) + self.containerNode.addSubnode(self.installationActionNode) + self.containerNode.addSubnode(self.selectionIconNode) + self.addSubnode(self.activateArea) + + self.installationActionNode.addTarget(self, action: #selector(self.installationActionPressed), forControlEvents: .touchUpInside) + self.installationActionNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.installationActionBackgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.installationActionBackgroundNode.alpha = 0.4 + } else { + strongSelf.installationActionBackgroundNode.alpha = 1.0 + strongSelf.installationActionBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + var firstTime = true + self.imageNode.imageUpdated = { [weak self] image in + guard let strongSelf = self else { + return + } + if image != nil { + strongSelf.removePlaceholder(animated: !firstTime) + if firstTime { + strongSelf.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + firstTime = false + } + } + + deinit { + self.fetchDisposable.dispose() + } + + private func removePlaceholder(animated: Bool) { + if let placeholderNode = self.placeholderNode { + self.placeholderNode = nil + if !animated { + placeholderNode.removeFromSupernode() + } else { + placeholderNode.allowsGroupOpacity = true + placeholderNode.alpha = 0.0 + placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in + placeholderNode?.removeFromSupernode() + placeholderNode?.allowsGroupOpacity = false + }) + } + } + } + + private var absoluteLocation: (CGRect, CGSize)? + override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.absoluteLocation = (rect, containerSize) + if let placeholderNode = placeholderNode { + placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + placeholderNode.frame.minX, y: rect.minY + placeholderNode.frame.minY), size: placeholderNode.frame.size), within: containerSize) + } + } + + override func tapped() { + guard let item = self.layoutParams?.0, item.editing.editing && item.editing.selectable else { + return + } + item.toggleSelected() + } + + func asyncLayout() -> (_ item: ItemListReactionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let makeImageLayout = self.imageNode.asyncLayout() + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeStatusLayout = TextNode.asyncLayout(self.statusNode) + let makeInstallLayout = TextNode.asyncLayout(self.installTextNode) + let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) + let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode) + let selectableControlLayout = ItemListSelectableControlNode.asyncLayout(self.selectableControlNode) + + let previousThumbnailItem = self.currentThumbnailItem + var currentDisabledOverlayNode = self.disabledOverlayNode + + let currentItem = self.layoutParams?.0 + + return { item, params, neighbors in + let titleFont = Font.bold(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + let statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) + + var titleAttributedString: NSAttributedString? + var statusAttributedString: NSAttributedString? + + var updatedTheme: PresentationTheme? + + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + let packRevealOptions: [ItemListRevealOption] + if item.editing.editable && item.enabled { + packRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)] + } else { + packRevealOptions = [] + } + + var rightInset: CGFloat = params.rightInset + + var installationBackgroundImage: UIImage? + var installationText: String? + var checkImage: UIImage? + switch item.control { + case .none: + break + case let .installation(installed): + if installed { + installationBackgroundImage = PresentationResourcesChat.chatInputMediaPanelAddedPackButtonImage(item.presentationData.theme) + installationText = item.presentationData.strings.Stickers_Installed + } else { + installationBackgroundImage = PresentationResourcesChat.chatInputMediaPanelAddPackButtonImage(item.presentationData.theme) + installationText = item.presentationData.strings.Stickers_Install + } + case .selection: + rightInset += 16.0 + checkImage = PresentationResourcesItemList.checkIconImage(item.presentationData.theme) + default: + break + } + + var unreadImage: UIImage? + if item.unread { + unreadImage = PresentationResourcesItemList.stickerUnreadDotImage(item.presentationData.theme) + } + + titleAttributedString = NSAttributedString(string: item.packInfo.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + statusAttributedString = NSAttributedString(string: item.itemCount, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + + let leftInset: CGFloat = 65.0 + params.leftInset + + let verticalInset: CGFloat = 11.0 + let titleSpacing: CGFloat = 2.0 + + let insets = itemListNeighborsGroupedInsets(neighbors, params) + let separatorHeight = UIScreenPixel + + var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)? + var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)? + var selectableControlSizeAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)? + + var editingOffset: CGFloat = 0.0 + var reorderInset: CGFloat = 0.0 + + if item.editing.editing { + if item.editing.selectable { + var selected = false + if case let .check(checked) = item.control { + selected = checked + } + let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, selected, true) + selectableControlSizeAndApply = sizeAndApply + editingOffset = sizeAndApply.0 + } else { + let sizeAndApply = editableControlLayout(item.presentationData.theme, false) + editableControlSizeAndApply = sizeAndApply + editingOffset = sizeAndApply.0 + } + + if item.editing.reorderable { + let sizeAndApply = reorderControlLayout(item.presentationData.theme) + reorderControlSizeAndApply = sizeAndApply + reorderInset = sizeAndApply.0 + } + } + + var installed = false + if case .installation(true) = item.control { + installed = true + } + + let (installLayout, installApply) = makeInstallLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: installationText ?? "", font: Font.semibold(13.0), textColor: installed ? item.presentationData.theme.list.itemCheckColors.fillColor : item.presentationData.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let installWidth: CGFloat + if installLayout.size.width > 0.0 { + installWidth = installLayout.size.width + 32.0 + } else { + installWidth = 0.0 + } + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - 10.0 - reorderInset - installWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - reorderInset - installWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + statusLayout.size.height) + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + if !item.enabled { + if currentDisabledOverlayNode == nil { + currentDisabledOverlayNode = ASDisplayNode() + currentDisabledOverlayNode?.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.5) + } + } else { + currentDisabledOverlayNode = nil + } + + var thumbnailItem: StickerPackThumbnailItem? + var resourceReference: MediaResourceReference? + if let thumbnail = item.packInfo.thumbnail { + if item.packInfo.flags.contains(.isAnimated) { + thumbnailItem = .animated(thumbnail.resource, thumbnail.dimensions) + resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: item.packInfo.id.id, accessHash: item.packInfo.accessHash), resource: thumbnail.resource) + } else { + thumbnailItem = .still(thumbnail) + resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: item.packInfo.id.id, accessHash: item.packInfo.accessHash), resource: thumbnail.resource) + } + } else if let item = item.topItem { + if item.file.isAnimatedSticker { + thumbnailItem = .animated(item.file.resource, item.file.dimensions ?? PixelDimensions(width: 100, height: 100)) + resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: item.file.resource) + } else if let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource { + thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) + resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource) + } + } + + var updatedImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + var updatedFetchSignal: Signal? + + let imageBoundingSize = CGSize(width: 34.0, height: 34.0) + var imageApply: (() -> Void)? + let fileUpdated = thumbnailItem != previousThumbnailItem + + var imageSize: CGSize? + + if let thumbnailItem = thumbnailItem { + switch thumbnailItem { + case let .still(representation): + let stillImageSize = representation.dimensions.cgSize.aspectFitted(imageBoundingSize) + imageSize = stillImageSize + + if fileUpdated { + imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: stillImageSize, boundingSize: stillImageSize, intrinsicInsets: UIEdgeInsets())) + updatedImageSignal = chatMessageStickerPackThumbnail(postbox: item.account.postbox, resource: representation.resource, nilIfEmpty: true) + } + case let .animated(resource, _): + imageSize = imageBoundingSize + + if fileUpdated { + imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageBoundingSize, boundingSize: imageBoundingSize, intrinsicInsets: UIEdgeInsets())) + updatedImageSignal = chatMessageStickerPackThumbnail(postbox: item.account.postbox, resource: resource, animated: true, nilIfEmpty: true) + } + } + if fileUpdated, let resourceReference = resourceReference { + updatedFetchSignal = fetchedMediaResource(mediaBox: item.account.postbox.mediaBox, reference: resourceReference) + } + } else { + updatedImageSignal = .single({ _ in return nil }) + updatedFetchSignal = .complete() + } + + return (layout, { [weak self] animated in + if let strongSelf = self { + strongSelf.layoutParams = (item, params, neighbors) + + 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.accessibilityLabel = titleAttributedString?.string ?? "" + strongSelf.activateArea.accessibilityValue = statusAttributedString?.string ?? "" + if item.enabled { + strongSelf.activateArea.accessibilityTraits = [] + } else { + strongSelf.activateArea.accessibilityTraits = .notEnabled + } + + if fileUpdated { + strongSelf.currentThumbnailItem = thumbnailItem + } + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + } + + let revealOffset = strongSelf.revealOffset + + 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 + strongSelf.addSubnode(currentDisabledOverlayNode) + currentDisabledOverlayNode.alpha = 0.0 + transition.updateAlpha(node: currentDisabledOverlayNode, alpha: 1.0) + currentDisabledOverlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight)) + } else { + transition.updateFrame(node: currentDisabledOverlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight))) + } + } else if let disabledOverlayNode = strongSelf.disabledOverlayNode { + transition.updateAlpha(node: disabledOverlayNode, alpha: 0.0, completion: { [weak disabledOverlayNode] _ in + disabledOverlayNode?.removeFromSupernode() + }) + strongSelf.disabledOverlayNode = nil + } + + if let editableControlSizeAndApply = editableControlSizeAndApply { + let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.contentSize.height)) + if strongSelf.editableControlNode == nil { + let editableControlNode = editableControlSizeAndApply.1(layout.contentSize.height) + editableControlNode.tapped = { + if let strongSelf = self { + strongSelf.setRevealOptionsOpened(true, animated: true) + strongSelf.revealOptionsInteractivelyOpened() + } + } + strongSelf.editableControlNode = editableControlNode + strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.imageNode) + editableControlNode.frame = editableControlFrame + transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY)) + editableControlNode.alpha = 0.0 + transition.updateAlpha(node: editableControlNode, alpha: 1.0) + } else { + strongSelf.editableControlNode?.frame = editableControlFrame + } + strongSelf.editableControlNode?.isHidden = !item.editing.editable + } else if let editableControlNode = strongSelf.editableControlNode { + var editableControlFrame = editableControlNode.frame + editableControlFrame.origin.x = -editableControlFrame.size.width + strongSelf.editableControlNode = nil + transition.updateAlpha(node: editableControlNode, alpha: 0.0) + transition.updateFrame(node: editableControlNode, frame: editableControlFrame, completion: { [weak editableControlNode] _ in + editableControlNode?.removeFromSupernode() + }) + } + + if let selectableControlSizeAndApply = selectableControlSizeAndApply { + let selectableControlSize = CGSize(width: selectableControlSizeAndApply.0, height: layout.contentSize.height) + let selectableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: selectableControlSize) + if strongSelf.selectableControlNode == nil { + let selectableControlNode = selectableControlSizeAndApply.1(selectableControlSize, false) + strongSelf.selectableControlNode = selectableControlNode + strongSelf.addSubnode(selectableControlNode) + selectableControlNode.frame = selectableControlFrame + transition.animatePosition(node: selectableControlNode, from: CGPoint(x: -selectableControlFrame.size.width / 2.0, y: selectableControlFrame.midY)) + selectableControlNode.alpha = 0.0 + transition.updateAlpha(node: selectableControlNode, alpha: 1.0) + } else if let selectableControlNode = strongSelf.selectableControlNode { + transition.updateFrame(node: selectableControlNode, frame: selectableControlFrame) + let _ = selectableControlSizeAndApply.1(selectableControlSize, transition.isAnimated) + } + } else if let selectableControlNode = strongSelf.selectableControlNode { + var selectableControlFrame = selectableControlNode.frame + selectableControlFrame.origin.x = -selectableControlFrame.size.width + strongSelf.selectableControlNode = nil + transition.updateAlpha(node: selectableControlNode, alpha: 0.0) + transition.updateFrame(node: selectableControlNode, frame: selectableControlFrame, completion: { [weak selectableControlNode] _ in + selectableControlNode?.removeFromSupernode() + }) + } + + if let reorderControlSizeAndApply = reorderControlSizeAndApply { + if strongSelf.reorderControlNode == nil { + let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate) + strongSelf.reorderControlNode = reorderControlNode + strongSelf.addSubnode(reorderControlNode) + reorderControlNode.alpha = 0.0 + transition.updateAlpha(node: reorderControlNode, alpha: 1.0) + } + let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0, y: 0.0), size: CGSize(width: reorderControlSizeAndApply.0, height: layout.contentSize.height)) + strongSelf.reorderControlNode?.frame = reorderControlFrame + } else if let reorderControlNode = strongSelf.reorderControlNode { + strongSelf.reorderControlNode = nil + transition.updateAlpha(node: reorderControlNode, alpha: 0.0, completion: { [weak reorderControlNode] _ in + reorderControlNode?.removeFromSupernode() + }) + } + + imageApply?() + + let _ = titleApply() + let _ = statusApply() + let _ = installApply() + + switch item.control { + case .none: + strongSelf.installationActionNode.isHidden = true + strongSelf.installationActionBackgroundNode.isHidden = true + strongSelf.selectionIconNode.isHidden = true + case let .installation(installed): + strongSelf.installationActionBackgroundNode.isHidden = false + strongSelf.installationActionNode.isHidden = false + strongSelf.selectionIconNode.isHidden = true + strongSelf.installationActionNode.isUserInteractionEnabled = !installed + + if let backgroundImage = installationBackgroundImage { + strongSelf.installationActionBackgroundNode.image = backgroundImage + } + + let installationActionFrame = CGRect(origin: CGPoint(x: params.width - rightInset - installWidth - 16.0, y: 0.0), size: CGSize(width: 50.0, height: layout.contentSize.height)) + strongSelf.installationActionNode.frame = installationActionFrame + + let buttonFrame = CGRect(origin: CGPoint(x: params.width - rightInset - installWidth - 16.0, y: installationActionFrame.minY + floor((installationActionFrame.size.height - 28.0) / 2.0)), size: CGSize(width: installWidth, height: 28.0)) + strongSelf.installationActionBackgroundNode.frame = buttonFrame + strongSelf.installTextNode.frame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - installLayout.size.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - installLayout.size.height) / 2.0) + 1.0), size: installLayout.size) + case .selection: + strongSelf.installationActionNode.isHidden = true + strongSelf.installationActionBackgroundNode.isHidden = true + strongSelf.selectionIconNode.isHidden = false + if let image = checkImage { + strongSelf.selectionIconNode.image = image + strongSelf.selectionIconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - image.size.width - floor((44.0 - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) + } + case .check: + strongSelf.installationActionNode.isHidden = true + strongSelf.installationActionBackgroundNode.isHidden = true + strongSelf.selectionIconNode.isHidden = true + } + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.addSubnode(strongSelf.maskNode) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + editingOffset + bottomStripeOffset = -separatorHeight + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + 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.containerNode.frame = CGRect(origin: CGPoint(), size: strongSelf.backgroundNode.frame.size) + 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 + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) + + if let unreadImage = unreadImage { + strongSelf.unreadNode.image = unreadImage + strongSelf.unreadNode.isHidden = false + strongSelf.unreadNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 16.0), size: unreadImage.size) + } else { + strongSelf.unreadNode.isHidden = true + } + + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: (strongSelf.unreadNode.isHidden ? 0.0 : 10.0) + leftInset + revealOffset + editingOffset, y: verticalInset), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size)) + + let boundingSize = CGSize(width: 34.0, height: 34.0) + if let thumbnailItem = thumbnailItem, let imageSize = imageSize { + let imageFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0 + floor((boundingSize.width - imageSize.width) / 2.0), y: floor((layout.contentSize.height - imageSize.height) / 2.0)), size: imageSize) + var thumbnailDimensions = PixelDimensions(width: 512, height: 512) + switch thumbnailItem { + case let .still(representation): + transition.updateFrame(node: strongSelf.imageNode, frame: imageFrame) + thumbnailDimensions = representation.dimensions + case let .animated(resource, _): + transition.updateFrame(node: strongSelf.imageNode, frame: imageFrame) + + let animationNode: AnimatedStickerNode + if let current = strongSelf.animationNode { + animationNode = current + } else { + animationNode = AnimatedStickerNode() + strongSelf.animationNode = animationNode + strongSelf.addSubnode(animationNode) + + animationNode.setup(source: AnimatedStickerResourceSource(account: item.account, resource: resource), width: 80, height: 80, mode: .cached) + } + animationNode.visibility = strongSelf.visibility != .none && item.playAnimatedStickers + animationNode.isHidden = !item.playAnimatedStickers + strongSelf.imageNode.isHidden = item.playAnimatedStickers + if let animationNode = strongSelf.animationNode { + transition.updateFrame(node: animationNode, frame: imageFrame) + } + } + + if let placeholderNode = strongSelf.placeholderNode { + placeholderNode.frame = imageFrame + + placeholderNode.update(backgroundColor: nil, foregroundColor: item.presentationData.theme.list.disclosureArrowColor.blitOver(item.presentationData.theme.list.itemBlocksBackgroundColor, alpha: 0.55), shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), data: item.packInfo.immediateThumbnailData, size: imageFrame.size, imageSize: thumbnailDimensions.cgSize) + } + } + + if let updatedImageSignal = updatedImageSignal { + strongSelf.imageNode.setSignal(updatedImageSignal) + } + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel)) + + strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) + + strongSelf.setRevealOptions((left: [], right: packRevealOptions)) + strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) + + if let updatedFetchSignal = updatedFetchSignal { + strongSelf.fetchDisposable.set(updatedFetchSignal.start()) + } + } + }) + } + } + + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + + override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + super.updateRevealOffset(offset: offset, transition: transition) + + guard let params = self.layoutParams?.1 else { + return + } + + let leftInset: CGFloat = 65.0 + params.leftInset + + let editingOffset: CGFloat + if let editableControlNode = self.editableControlNode { + editingOffset = editableControlNode.bounds.size.width + var editableControlFrame = editableControlNode.frame + editableControlFrame.origin.x = params.leftInset + offset + transition.updateFrame(node: editableControlNode, frame: editableControlFrame) + } else { + editingOffset = 0.0 + } + + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + self.revealOffset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size)) + transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + self.revealOffset + editingOffset, y: self.statusNode.frame.minY), size: self.statusNode.bounds.size)) + + let boundingSize = CGSize(width: 34.0, height: 34.0) + + transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: params.leftInset + self.revealOffset + editingOffset + 15.0 + floor((boundingSize.width - self.imageNode.frame.size.width) / 2.0), y: self.imageNode.frame.minY), size: self.imageNode.frame.size)) + if let animationNode = self.animationNode { + transition.updateFrame(node: animationNode, frame: CGRect(origin: CGPoint(x: params.leftInset + self.revealOffset + editingOffset + 15.0 + floor((boundingSize.width - animationNode.frame.size.width) / 2.0), y: animationNode.frame.minY), size: animationNode.frame.size)) + } + } + + override func revealOptionsInteractivelyOpened() { + if let (item, _, _) = self.layoutParams { + item.setPackIdWithRevealedOptions(item.packInfo.id, nil) + } + } + + override func revealOptionsInteractivelyClosed() { + if let (item, _, _) = self.layoutParams { + item.setPackIdWithRevealedOptions(nil, item.packInfo.id) + } + } + + override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { + self.setRevealOptionsOpened(false, animated: true) + self.revealOptionsInteractivelyClosed() + + if let (item, _, _) = self.layoutParams { + item.removePack() + } + } + + @objc func installationActionPressed() { + if let (item, _, _) = self.layoutParams { + item.addPack() + } + } + + override func isReorderable(at point: CGPoint) -> Bool { + if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point), !self.isDisplayingRevealedOptions { + return true + } + return false + } + + override func snapshotForReordering() -> UIView? { + self.backgroundNode.alpha = 0.9 + let result = self.view.snapshotContentTree() + self.backgroundNode.alpha = 1.0 + return result + } +} diff --git a/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift b/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift new file mode 100644 index 0000000000..4a6050281f --- /dev/null +++ b/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift @@ -0,0 +1,309 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AccountContext +import PresentationDataUtils + +private final class PeerAllowedReactionListControllerArguments { + let context: AccountContext + let toggleAll: () -> Void + let toggleItem: (String) -> Void + + init( + context: AccountContext, + toggleAll: @escaping () -> Void, + toggleItem: @escaping (String) -> Void + ) { + self.context = context + self.toggleAll = toggleAll + self.toggleItem = toggleItem + } +} + +private enum PeerAllowedReactionListControllerSection: Int32 { + case all + case items +} + +private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry { + enum StableId: Hashable { + case allowAll + case allowAllInfo + case itemsHeader + case item(String) + } + + case allowAll(text: String, isEnabled: Bool) + case allowAllInfo(String) + + case itemsHeader(String) + case item(index: Int, value: String, file: TelegramMediaFile?, text: String, isEnabled: Bool) + + var section: ItemListSectionId { + switch self { + case .allowAll, .allowAllInfo: + return PeerAllowedReactionListControllerSection.all.rawValue + case .itemsHeader, .item: + return PeerAllowedReactionListControllerSection.items.rawValue + } + } + + var stableId: StableId { + switch self { + case .allowAll: + return .allowAll + case .allowAllInfo: + return .allowAllInfo + case .itemsHeader: + return .itemsHeader + case let .item(_, value, _, _, _): + return .item(value) + } + } + + var sortId: Int { + switch self { + case .allowAll: + return 0 + case .allowAllInfo: + return 1 + case .itemsHeader: + return 2 + case let .item(index, _, _, _, _): + return 100 + index + } + } + + static func ==(lhs: PeerAllowedReactionListControllerEntry, rhs: PeerAllowedReactionListControllerEntry) -> Bool { + switch lhs { + case let .allowAll(text, isEnabled): + if case .allowAll(text, isEnabled) = rhs { + return true + } else { + return false + } + case let .allowAllInfo(text): + if case .allowAllInfo(text) = rhs { + return true + } else { + return false + } + case let .itemsHeader(text): + if case .itemsHeader(text) = rhs { + return true + } else { + return false + } + case let .item(index, value, file, text, isEnabled): + if case .item(index, value, file, text, isEnabled) = rhs { + return true + } else { + return false + } + } + } + + static func <(lhs: PeerAllowedReactionListControllerEntry, rhs: PeerAllowedReactionListControllerEntry) -> Bool { + return lhs.sortId < rhs.sortId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! PeerAllowedReactionListControllerArguments + switch self { + case let .allowAll(text, isEnabled): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: isEnabled, sectionId: self.section, style: .blocks, updated: { _ in + arguments.toggleAll() + }) + case let .allowAllInfo(text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .itemsHeader(text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .item(_, value, file, text, isEnabled): + let _ = file + return ItemListSwitchItem(presentationData: presentationData, title: "\(value) \(text)", value: isEnabled, sectionId: self.section, style: .blocks, updated: { _ in + arguments.toggleItem(value) + }) + } + } +} + +private struct PeerAllowedReactionListControllerState: Equatable { + var updatedAllowedReactions: Set? = nil +} + +private func peerAllowedReactionListControllerEntries( + presentationData: PresentationData, + availableReactions: AvailableReactions?, + cachedData: CachedPeerData?, + state: PeerAllowedReactionListControllerState +) -> [PeerAllowedReactionListControllerEntry] { + var entries: [PeerAllowedReactionListControllerEntry] = [] + + if let availableReactions = availableReactions, let allowedReactions = state.updatedAllowedReactions { + entries.append(.allowAll(text: "Allow Reactions", isEnabled: !allowedReactions.isEmpty)) + entries.append(.allowAllInfo("Allow subscribers to reacts to channel posts.")) + + entries.append(.itemsHeader("AVAILABLE REACTIONS")) + var index = 0 + for availableReaction in availableReactions.reactions { + entries.append(.item(index: index, value: availableReaction.value, file: availableReaction.staticIcon, text: availableReaction.title, isEnabled: allowedReactions.contains(availableReaction.value))) + index += 1 + } + } + + return entries +} + +public func peerAllowedReactionListController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + peerId: PeerId +) -> ViewController { + let statePromise = ValuePromise(PeerAllowedReactionListControllerState(), ignoreRepeated: true) + let stateValue = Atomic(value: PeerAllowedReactionListControllerState()) + let updateState: ((PeerAllowedReactionListControllerState) -> PeerAllowedReactionListControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var dismissImpl: (() -> Void)? + let _ = dismissImpl + + let actionsDisposable = DisposableSet() + actionsDisposable.add((context.account.postbox.transaction { transaction -> Set? in + let cachedData = transaction.getPeerCachedData(peerId: peerId) + if let cachedData = cachedData as? CachedChannelData { + return cachedData.allowedReactions.flatMap(Set.init) + } else if let cachedData = cachedData as? CachedGroupData { + return cachedData.allowedReactions.flatMap(Set.init) + } else { + return nil + } + } + |> deliverOnMainQueue).start(next: { allowedReactions in + updateState { state in + var state = state + state.updatedAllowedReactions = allowedReactions + return state + } + })) + + let arguments = PeerAllowedReactionListControllerArguments( + context: context, + toggleAll: { + let _ = (context.engine.stickers.availableReactions() + |> take(1) + |> deliverOnMainQueue).start(next: { availableReactions in + guard let availableReactions = availableReactions else { + return + } + updateState { state in + var state = state + if var updatedAllowedReactions = state.updatedAllowedReactions { + if updatedAllowedReactions.isEmpty { + for availableReaction in availableReactions.reactions { + updatedAllowedReactions.insert(availableReaction.value) + } + } else { + updatedAllowedReactions.removeAll() + } + state.updatedAllowedReactions = updatedAllowedReactions + } + return state + } + }) + }, + toggleItem: { reaction in + updateState { state in + var state = state + if var updatedAllowedReactions = state.updatedAllowedReactions { + if updatedAllowedReactions.contains(reaction) { + updatedAllowedReactions.remove(reaction) + } else { + updatedAllowedReactions.insert(reaction) + } + state.updatedAllowedReactions = updatedAllowedReactions + } + return state + } + } + ) + + let peerView = context.account.viewTracker.peerView(peerId) + |> deliverOnMainQueue + + let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData + let signal = combineLatest(queue: .mainQueue(), + presentationData, + statePromise.get(), + context.engine.stickers.availableReactions(), + peerView + ) + |> deliverOnMainQueue + |> map { presentationData, state, availableReactions, peerView -> (ItemListControllerState, (ItemListNodeState, Any)) in + //TODO:localize + let title: String = "Reactions" + + let entries = peerAllowedReactionListControllerEntries( + presentationData: presentationData, + availableReactions: availableReactions, + cachedData: peerView.cachedData, + state: state + ) + + let controllerState = ItemListControllerState( + presentationData: ItemListPresentationData(presentationData), + title: .text(title), + leftNavigationButton: nil, + rightNavigationButton: nil, + backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), + animateChanges: false + ) + let listState = ItemListNodeState( + presentationData: ItemListPresentationData(presentationData), + entries: entries, + style: .blocks, + animateChanges: true + ) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(context: context, state: signal) + controller.willDisappear = { _ in + let _ = (context.account.postbox.transaction { transaction -> Set? in + let cachedData = transaction.getPeerCachedData(peerId: peerId) + if let cachedData = cachedData as? CachedChannelData { + return cachedData.allowedReactions.flatMap(Set.init) + } else if let cachedData = cachedData as? CachedGroupData { + return cachedData.allowedReactions.flatMap(Set.init) + } else { + return nil + } + } + |> deliverOnMainQueue).start(next: { initialAllowedReactions in + let updatedAllowedReactions = stateValue.with({ $0 }).updatedAllowedReactions + if let updatedAllowedReactions = updatedAllowedReactions, initialAllowedReactions != updatedAllowedReactions { + let _ = context.engine.peers.updatePeerAllowedReactions(peerId: peerId, allowedReactions: Array(updatedAllowedReactions)).start() + } + }) + } + dismissImpl = { [weak controller] in + guard let controller = controller else { + return + } + controller.dismiss() + } + + return controller +} diff --git a/submodules/Postbox/Sources/Coding.swift b/submodules/Postbox/Sources/Coding.swift index 9128c80edd..2a2252e00a 100644 --- a/submodules/Postbox/Sources/Coding.swift +++ b/submodules/Postbox/Sources/Coding.swift @@ -1311,6 +1311,14 @@ public final class PostboxDecoder { return [] } } + + public func decodeOptionalStringArrayForKey(_ key: String) -> [String]? { + if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .StringArray) { + return decodeStringArrayRaw() + } else { + return nil + } + } public func decodeStringArrayRaw() -> [String] { var length: Int32 = 0 diff --git a/submodules/TelegramCore/Sources/State/MessageReactions.swift b/submodules/TelegramCore/Sources/State/MessageReactions.swift index 37bf1907ee..7168e62eb0 100644 --- a/submodules/TelegramCore/Sources/State/MessageReactions.swift +++ b/submodules/TelegramCore/Sources/State/MessageReactions.swift @@ -460,3 +460,40 @@ public final class EngineMessageReactionListContext { } } } + +public enum UpdatePeerAllowedReactionsError { + case generic +} + +func _internal_updatePeerAllowedReactions(account: Account, peerId: PeerId, allowedReactions: [String]) -> Signal { + return account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> castError(UpdatePeerAllowedReactionsError.self) + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer = inputPeer else { + return .fail(.generic) + } + return account.network.request(Api.functions.messages.setChatAvailableReactions(peer: inputPeer, availableReactions: allowedReactions)) + |> mapError { _ -> UpdatePeerAllowedReactionsError in + return .generic + } + |> mapToSignal { result -> Signal in + account.stateManager.addUpdates(result) + + return account.postbox.transaction { transaction -> Void in + transaction.updatePeerCachedData(peerIds: [peerId], update: { _, current in + if let current = current as? CachedChannelData { + return current.withUpdatedAllowedReactions(allowedReactions) + } else if let current = current as? CachedGroupData { + return current.withUpdatedAllowedReactions(allowedReactions) + } else { + return current + } + }) + } + |> ignoreValues + |> castError(UpdatePeerAllowedReactionsError.self) + } + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift index afd4930def..3422a04b51 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift @@ -228,6 +228,7 @@ public final class CachedChannelData: CachedPeerData { public let themeEmoticon: String? public let inviteRequestsPending: Int32? public let sendAsPeerId: PeerId? + public let allowedReactions: [String]? public let peerIds: Set public let messageIds: Set @@ -265,9 +266,39 @@ public final class CachedChannelData: CachedPeerData { self.themeEmoticon = nil self.inviteRequestsPending = nil self.sendAsPeerId = nil + self.allowedReactions = nil } - public init(isNotAccessible: Bool, flags: CachedChannelFlags, about: String?, participantsSummary: CachedChannelParticipantsSummary, exportedInvitation: ExportedInvitation?, botInfos: [CachedPeerBotInfo], peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, stickerPack: StickerPackCollectionInfo?, minAvailableMessageId: MessageId?, migrationReference: ChannelMigrationReference?, linkedDiscussionPeerId: LinkedDiscussionPeerId, peerGeoLocation: PeerGeoLocation?, slowModeTimeout: Int32?, slowModeValidUntilTimestamp: Int32?, hasScheduledMessages: Bool, statsDatacenterId: Int32, invitedBy: PeerId?, invitedOn: Int32?, photo: TelegramMediaImage?, activeCall: ActiveCall?, callJoinPeerId: PeerId?, autoremoveTimeout: CachedPeerAutoremoveTimeout, pendingSuggestions: [String], themeEmoticon: String?, inviteRequestsPending: Int32?, sendAsPeerId: PeerId?) { + public init( + isNotAccessible: Bool, + flags: CachedChannelFlags, + about: String?, + participantsSummary: CachedChannelParticipantsSummary, + exportedInvitation: ExportedInvitation?, + botInfos: [CachedPeerBotInfo], + peerStatusSettings: PeerStatusSettings?, + pinnedMessageId: MessageId?, + stickerPack: StickerPackCollectionInfo?, + minAvailableMessageId: MessageId?, + migrationReference: ChannelMigrationReference?, + linkedDiscussionPeerId: LinkedDiscussionPeerId, + peerGeoLocation: PeerGeoLocation?, + slowModeTimeout: Int32?, + slowModeValidUntilTimestamp: Int32?, + hasScheduledMessages: Bool, + statsDatacenterId: Int32, + invitedBy: PeerId?, + invitedOn: Int32?, + photo: TelegramMediaImage?, + activeCall: ActiveCall?, + callJoinPeerId: PeerId?, + autoremoveTimeout: CachedPeerAutoremoveTimeout, + pendingSuggestions: [String], + themeEmoticon: String?, + inviteRequestsPending: Int32?, + sendAsPeerId: PeerId?, + allowedReactions: [String]? + ) { self.isNotAccessible = isNotAccessible self.flags = flags self.about = about @@ -295,6 +326,7 @@ public final class CachedChannelData: CachedPeerData { self.themeEmoticon = themeEmoticon self.inviteRequestsPending = inviteRequestsPending self.sendAsPeerId = sendAsPeerId + self.allowedReactions = allowedReactions var peerIds = Set() for botInfo in botInfos { @@ -322,111 +354,115 @@ public final class CachedChannelData: CachedPeerData { } public func withUpdatedIsNotAccessible(_ isNotAccessible: Bool) -> CachedChannelData { - return CachedChannelData(isNotAccessible: isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedFlags(_ flags: CachedChannelFlags) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedAbout(_ about: String?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedParticipantsSummary(_ participantsSummary: CachedChannelParticipantsSummary) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedExportedInvitation(_ exportedInvitation: ExportedInvitation?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedBotInfos(_ botInfos: [CachedPeerBotInfo]) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedPeerStatusSettings(_ peerStatusSettings: PeerStatusSettings?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedStickerPack(_ stickerPack: StickerPackCollectionInfo?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedMinAvailableMessageId(_ minAvailableMessageId: MessageId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedMigrationReference(_ migrationReference: ChannelMigrationReference?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedLinkedDiscussionPeerId(_ linkedDiscussionPeerId: LinkedDiscussionPeerId) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedPeerGeoLocation(_ peerGeoLocation: PeerGeoLocation?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedSlowModeTimeout(_ slowModeTimeout: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedSlowModeValidUntilTimestamp(_ slowModeValidUntilTimestamp: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedStatsDatacenterId(_ statsDatacenterId: Int32) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedInvitedBy(_ invitedBy: PeerId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedInvitedOn(_ invitedOn: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedPhoto(_ photo: TelegramMediaImage?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedActiveCall(_ activeCall: ActiveCall?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedCallJoinPeerId(_ callJoinPeerId: PeerId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedAutoremoveTimeout(_ autoremoveTimeout: CachedPeerAutoremoveTimeout) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedPendingSuggestions(_ pendingSuggestions: [String]) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedThemeEmoticon(_ themeEmoticon: String?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedInviteRequestsPending(_ inviteRequestsPending: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedSendAsPeerId(_ sendAsPeerId: PeerId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: sendAsPeerId, allowedReactions: self.allowedReactions) + } + + public func withUpdatedAllowedReactions(_ allowedReactions: [String]?) -> CachedChannelData { + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: allowedReactions) } public init(decoder: PostboxDecoder) { @@ -516,6 +552,8 @@ public final class CachedChannelData: CachedPeerData { self.sendAsPeerId = decoder.decodeOptionalInt64ForKey("sendAsPeerId").flatMap(PeerId.init) + self.allowedReactions = decoder.decodeOptionalStringArrayForKey("allowedReactions") + if case let .known(linkedDiscussionPeerIdValue) = self.linkedDiscussionPeerId { if let linkedDiscussionPeerIdValue = linkedDiscussionPeerIdValue { peerIds.insert(linkedDiscussionPeerIdValue) @@ -660,6 +698,12 @@ public final class CachedChannelData: CachedPeerData { } else { encoder.encodeNil(forKey: "sendAsPeerId") } + + if let allowedReactions = self.allowedReactions { + encoder.encodeStringArray(allowedReactions, forKey: "allowedReactions") + } else { + encoder.encodeNil(forKey: "allowedReactions") + } } public func isEqual(to: CachedPeerData) -> Bool { @@ -775,6 +819,10 @@ public final class CachedChannelData: CachedPeerData { return false } + if other.allowedReactions != self.allowedReactions { + return false + } + return true } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift index 96b1cfbee2..5f556539a6 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift @@ -55,6 +55,7 @@ public final class CachedGroupData: CachedPeerData { public let callJoinPeerId: PeerId? public let themeEmoticon: String? public let inviteRequestsPending: Int32? + public let allowedReactions: [String]? public let peerIds: Set public let messageIds: Set @@ -78,9 +79,27 @@ public final class CachedGroupData: CachedPeerData { self.callJoinPeerId = nil self.themeEmoticon = nil self.inviteRequestsPending = nil + self.allowedReactions = nil } - public init(participants: CachedGroupParticipants?, exportedInvitation: ExportedInvitation?, botInfos: [CachedPeerBotInfo], peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, about: String?, flags: CachedGroupFlags, hasScheduledMessages: Bool, invitedBy: PeerId?, photo: TelegramMediaImage?, activeCall: CachedChannelData.ActiveCall?, autoremoveTimeout: CachedPeerAutoremoveTimeout, callJoinPeerId: PeerId?, themeEmoticon: String?, inviteRequestsPending: Int32?) { + public init( + participants: CachedGroupParticipants?, + exportedInvitation: ExportedInvitation?, + botInfos: [CachedPeerBotInfo], + peerStatusSettings: PeerStatusSettings?, + pinnedMessageId: MessageId?, + about: String?, + flags: CachedGroupFlags, + hasScheduledMessages: Bool, + invitedBy: PeerId?, + photo: TelegramMediaImage?, + activeCall: CachedChannelData.ActiveCall?, + autoremoveTimeout: CachedPeerAutoremoveTimeout, + callJoinPeerId: PeerId?, + themeEmoticon: String?, + inviteRequestsPending: Int32?, + allowedReactions: [String]? + ) { self.participants = participants self.exportedInvitation = exportedInvitation self.botInfos = botInfos @@ -96,6 +115,7 @@ public final class CachedGroupData: CachedPeerData { self.callJoinPeerId = callJoinPeerId self.themeEmoticon = themeEmoticon self.inviteRequestsPending = inviteRequestsPending + self.allowedReactions = allowedReactions var messageIds = Set() if let pinnedMessageId = self.pinnedMessageId { @@ -160,6 +180,8 @@ public final class CachedGroupData: CachedPeerData { self.inviteRequestsPending = decoder.decodeOptionalInt32ForKey("irp") + self.allowedReactions = decoder.decodeOptionalStringArrayForKey("allowedReactions") + var messageIds = Set() if let pinnedMessageId = self.pinnedMessageId { messageIds.insert(pinnedMessageId) @@ -249,6 +271,12 @@ public final class CachedGroupData: CachedPeerData { } else { encoder.encodeNil(forKey: "irp") } + + if let allowedReactions = self.allowedReactions { + encoder.encodeStringArray(allowedReactions, forKey: "allowedReactions") + } else { + encoder.encodeNil(forKey: "allowedReactions") + } } public func isEqual(to: CachedPeerData) -> Bool { @@ -264,66 +292,74 @@ public final class CachedGroupData: CachedPeerData { return false } + if self.allowedReactions != other.allowedReactions { + return false + } + return self.participants == other.participants && self.exportedInvitation == other.exportedInvitation && self.botInfos == other.botInfos && self.peerStatusSettings == other.peerStatusSettings && self.pinnedMessageId == other.pinnedMessageId && self.about == other.about && self.flags == other.flags && self.hasScheduledMessages == other.hasScheduledMessages && self.autoremoveTimeout == other.autoremoveTimeout && self.invitedBy == other.invitedBy && self.themeEmoticon == other.themeEmoticon && self.inviteRequestsPending == other.inviteRequestsPending } public func withUpdatedParticipants(_ participants: CachedGroupParticipants?) -> CachedGroupData { - return CachedGroupData(participants: participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedExportedInvitation(_ exportedInvitation: ExportedInvitation?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedBotInfos(_ botInfos: [CachedPeerBotInfo]) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedPeerStatusSettings(_ peerStatusSettings: PeerStatusSettings?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedAbout(_ about: String?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedFlags(_ flags: CachedGroupFlags) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedInvitedBy(_ invitedBy: PeerId?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedPhoto(_ photo: TelegramMediaImage?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedActiveCall(_ activeCall: CachedChannelData.ActiveCall?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedAutoremoveTimeout(_ autoremoveTimeout: CachedPeerAutoremoveTimeout) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedCallJoinPeerId(_ callJoinPeerId: PeerId?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedThemeEmoticon(_ themeEmoticon: String?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedInviteRequestsPending(_ inviteRequestsPending: Int32?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: inviteRequestsPending, allowedReactions: self.allowedReactions) + } + + public func withUpdatedAllowedReactions(_ allowedReactions: [String]?) -> CachedGroupData { + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: allowedReactions) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index d62a62be31..80d83c7b0f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -668,6 +668,10 @@ public extension TelegramEngine { public func updatePeerSendAsPeer(peerId: PeerId, sendAs: PeerId) -> Signal { return _internal_updatePeerSendAsPeer(account: self.account, peerId: peerId, sendAs: sendAs) } + + public func updatePeerAllowedReactions(peerId: PeerId, allowedReactions: [String]) -> Signal { + return _internal_updatePeerAllowedReactions(account: account, peerId: peerId, allowedReactions: allowedReactions) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 2d63b65899..e90149c97e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -274,7 +274,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } switch fullChat { - case let .chatFull(chatFullFlags, _, chatFullAbout, chatFullParticipants, chatFullChatPhoto, _, chatFullExportedInvite, chatFullBotInfo, chatFullPinnedMsgId, _, chatFullCall, _, chatFullGroupcallDefaultJoinAs, chatFullThemeEmoticon, chatFullRequestsPending, _, _): + case let .chatFull(chatFullFlags, _, chatFullAbout, chatFullParticipants, chatFullChatPhoto, _, chatFullExportedInvite, chatFullBotInfo, chatFullPinnedMsgId, _, chatFullCall, _, chatFullGroupcallDefaultJoinAs, chatFullThemeEmoticon, chatFullRequestsPending, _, allowedReactions): var botInfos: [CachedPeerBotInfo] = [] for botInfo in chatFullBotInfo ?? [] { switch botInfo { @@ -366,6 +366,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee .withUpdatedCallJoinPeerId(groupCallDefaultJoinAs?.peerId) .withUpdatedThemeEmoticon(chatFullThemeEmoticon) .withUpdatedInviteRequestsPending(chatFullRequestsPending) + .withUpdatedAllowedReactions(allowedReactions) }) case .channelFull: break @@ -403,7 +404,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } switch fullChat { - case let .channelFull(flags, _, about, participantsCount, adminsCount, kickedCount, bannedCount, _, _, _, _, chatPhoto, _, apiExportedInvite, apiBotInfos, migratedFromChatId, migratedFromMaxId, pinnedMsgId, stickerSet, minAvailableMsgId, _, linkedChatId, location, slowmodeSeconds, slowmodeNextSendDate, statsDc, _, inputCall, ttl, pendingSuggestions, groupcallDefaultJoinAs, themeEmoticon, requestsPending, _, defaultSendAs, _): + case let .channelFull(flags, _, about, participantsCount, adminsCount, kickedCount, bannedCount, _, _, _, _, chatPhoto, _, apiExportedInvite, apiBotInfos, migratedFromChatId, migratedFromMaxId, pinnedMsgId, stickerSet, minAvailableMsgId, _, linkedChatId, location, slowmodeSeconds, slowmodeNextSendDate, statsDc, _, inputCall, ttl, pendingSuggestions, groupcallDefaultJoinAs, themeEmoticon, requestsPending, _, defaultSendAs, allowedReactions): var channelFlags = CachedChannelFlags() if (flags & (1 << 3)) != 0 { channelFlags.insert(.canDisplayParticipants) @@ -596,6 +597,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee .withUpdatedThemeEmoticon(themeEmoticon) .withUpdatedInviteRequestsPending(requestsPending) .withUpdatedSendAsPeerId(sendAsPeerId) + .withUpdatedAllowedReactions(allowedReactions) }) if let minAvailableMessageId = minAvailableMessageId, minAvailableMessageIdUpdated { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index f78c5927af..6fd992c8c7 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -952,8 +952,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = combineLatest(queue: .mainQueue(), contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), strongSelf.context.engine.stickers.availableReactions(), + strongSelf.context.account.postbox.transaction { transaction -> Set? in + let cachedData = transaction.getPeerCachedData(peerId: topMessage.id.peerId) + if let cachedData = cachedData as? CachedChannelData { + return cachedData.allowedReactions.flatMap(Set.init) + } else if let cachedData = cachedData as? CachedGroupData { + return cachedData.allowedReactions.flatMap(Set.init) + } else { + return nil + } + }, ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager) - ).start(next: { actions, availableReactions, chatTextSelectionTips in + ).start(next: { actions, availableReactions, allowedReactions, chatTextSelectionTips in var actions = actions guard let strongSelf = self else { @@ -1003,8 +1013,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G actions.context = strongSelf.context - if canAddMessageReactions(message: topMessage), let availableReactions = availableReactions { + if canAddMessageReactions(message: topMessage), let availableReactions = availableReactions, let allowedReactions = allowedReactions { for reaction in availableReactions.reactions { + if !allowedReactions.contains(reaction.value) { + continue + } actions.reactionItems.append(ReactionContextItem( reaction: ReactionContextItem.Reaction(rawValue: reaction.value), stillAnimation: reaction.selectAnimation, diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 5ccb99dd7d..f11948814c 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -464,6 +464,7 @@ private final class PeerInfoInteraction { let openFaq: (String?) -> Void let openAddMember: () -> Void let openQrCode: () -> Void + let editingOpenReactionsSetup: () -> Void init( openUsername: @escaping (String) -> Void, @@ -504,7 +505,8 @@ private final class PeerInfoInteraction { openDeletePeer: @escaping () -> Void, openFaq: @escaping (String?) -> Void, openAddMember: @escaping () -> Void, - openQrCode: @escaping () -> Void + openQrCode: @escaping () -> Void, + editingOpenReactionsSetup: @escaping () -> Void ) { self.openUsername = openUsername self.openPhone = openPhone @@ -545,6 +547,7 @@ private final class PeerInfoInteraction { self.openFaq = openFaq self.openAddMember = openAddMember self.openQrCode = openQrCode + self.editingOpenReactionsSetup = editingOpenReactionsSetup } } @@ -1127,7 +1130,8 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr let ItemDiscussionGroup = 3 let ItemSignMessages = 4 let ItemSignMessagesHelp = 5 - let ItemDeleteChannel = 5 + let ItemDeleteChannel = 6 + let ItemReactions = 7 let isCreator = channel.flags.contains(.isCreator) @@ -1176,6 +1180,13 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr })) } + if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { + //TODO:localize + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .none, text: "Reactions", icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: { + interaction.editingOpenReactionsSetup() + })) + } + if isCreator || (channel.adminRights != nil && channel.hasPermission(.sendMessages)) { let messagesShouldHaveSignatures: Bool switch channel.info { @@ -1210,6 +1221,7 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr let ItemLocation = 112 let ItemLocationSetup = 113 let ItemDeleteGroup = 114 + let ItemReactions = 115 let isCreator = channel.flags.contains(.isCreator) let isPublic = channel.username != nil @@ -1283,11 +1295,25 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr })) } + if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { + //TODO:localize + items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .none, text: "Reactions", icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: { + interaction.editingOpenReactionsSetup() + })) + } + if !isPublic, case .known(nil) = cachedData.linkedDiscussionPeerId { items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPreHistory, label: .text(cachedData.flags.contains(.preHistoryEnabled) ? presentationData.strings.GroupInfo_GroupHistoryVisible : presentationData.strings.GroupInfo_GroupHistoryHidden), text: presentationData.strings.GroupInfo_GroupHistoryShort, icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: { interaction.editingOpenPreHistorySetup() })) } + } else { + if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { + //TODO:localize + items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .none, text: "Reactions", icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: { + interaction.editingOpenReactionsSetup() + })) + } } if cachedData.flags.contains(.canSetStickerSet) && canEditPeerInfo(context: context, peer: channel) { @@ -1353,6 +1379,7 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr let ItemPermissions = 104 let ItemAdmins = 105 let ItemMemberRequests = 106 + let ItemReactions = 107 var canViewAdminsAndBanned = false @@ -1382,6 +1409,11 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr interaction.editingOpenPreHistorySetup() })) + //TODO:localize + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .none, text: "Reactions", icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: { + interaction.editingOpenReactionsSetup() + })) + canViewAdminsAndBanned = true } else if case let .admin(rights, _) = group.role { if rights.rights.contains(.canInviteUsers) { @@ -1677,6 +1709,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate }, openQrCode: { [weak self] in self?.openQrCode() + }, + editingOpenReactionsSetup: { [weak self] in + self?.editingOpenReactionsSetup() } ) @@ -4698,6 +4733,13 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate self.controller?.push(channelDiscussionGroupSetupController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id)) } + private func editingOpenReactionsSetup() { + guard let data = self.data, let peer = data.peer else { + return + } + self.controller?.push(peerAllowedReactionListController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id)) + } + private func editingToggleMessageSignatures(value: Bool) { self.toggleShouldChannelMessagesSignaturesDisposable.set(self.context.engine.peers.toggleShouldChannelMessagesSignatures(peerId: self.peerId, enabled: value).start()) } From a10b815fa8df895ce552ee0400bd2169cc7c8f98 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 17 Dec 2021 00:52:08 +0400 Subject: [PATCH 2/4] Implement reaction switch [skip ci] --- .../Components/ReactionImageComponent/BUILD | 24 + .../Sources/ReactionImageComponent.swift | 96 ++ .../ReactionListContextMenuContent/BUILD | 1 + .../ReactionListContextMenuContent.swift | 43 +- submodules/PeerInfoUI/BUILD | 1 + .../Sources/ItemListReactionItem.swift | 948 +++++------------- .../PeerAllowedReactionListController.swift | 16 +- 7 files changed, 385 insertions(+), 744 deletions(-) create mode 100644 submodules/Components/ReactionImageComponent/BUILD create mode 100644 submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift diff --git a/submodules/Components/ReactionImageComponent/BUILD b/submodules/Components/ReactionImageComponent/BUILD new file mode 100644 index 0000000000..603bc1d792 --- /dev/null +++ b/submodules/Components/ReactionImageComponent/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ReactionImageComponent", + module_name = "ReactionImageComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/AccountContext:AccountContext", + "//submodules/WebPBinding:WebPBinding", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift b/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift new file mode 100644 index 0000000000..8de5bcd575 --- /dev/null +++ b/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift @@ -0,0 +1,96 @@ +import Foundation +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import Postbox +import TelegramCore +import AccountContext +import TelegramPresentationData +import UIKit +import WebPBinding + +public final class ReactionImageNode: ASImageNode { + private var disposable: Disposable? + public let size: CGSize + + public init(context: AccountContext, availableReactions: AvailableReactions?, reaction: String) { + var file: TelegramMediaFile? + if let availableReactions = availableReactions { + for availableReaction in availableReactions.reactions { + if availableReaction.value == reaction { + file = availableReaction.staticIcon + break + } + } + } + if let file = file { + self.size = file.dimensions?.cgSize ?? CGSize(width: 18.0, height: 18.0) + + super.init() + + self.disposable = (context.account.postbox.mediaBox.resourceData(file.resource) + |> deliverOnMainQueue).start(next: { [weak self] data in + guard let strongSelf = self else { + return + } + + if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + if let image = WebP.convert(fromWebP: dataValue) { + strongSelf.image = image + } + } + }) + } else { + self.size = CGSize(width: 18.0, height: 18.0) + super.init() + } + } + + deinit { + self.disposable?.dispose() + } +} + +public final class ReactionFileImageNode: ASImageNode { + private let disposable = MetaDisposable() + + private var currentFile: TelegramMediaFile? + + override public init() { + } + + deinit { + self.disposable.dispose() + } + + public func asyncLayout() -> (_ context: AccountContext, _ file: TelegramMediaFile?) -> (size: CGSize, apply: () -> Void) { + return { [weak self] context, file in + let size = file?.dimensions?.cgSize ?? CGSize(width: 18.0, height: 18.0) + + return (size, { + guard let strongSelf = self else { + return + } + if strongSelf.currentFile != file { + strongSelf.currentFile = file + + if let file = file { + strongSelf.disposable.set((context.account.postbox.mediaBox.resourceData(file.resource) + |> deliverOnMainQueue).start(next: { data in + guard let strongSelf = self else { + return + } + + if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + if let image = WebP.convert(fromWebP: dataValue) { + strongSelf.image = image + } + } + })) + } + } + }) + } + } +} diff --git a/submodules/Components/ReactionListContextMenuContent/BUILD b/submodules/Components/ReactionListContextMenuContent/BUILD index 70072765c9..feaf8c08f7 100644 --- a/submodules/Components/ReactionListContextMenuContent/BUILD +++ b/submodules/Components/ReactionListContextMenuContent/BUILD @@ -21,6 +21,7 @@ swift_library( "//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode", "//submodules/ContextUI:ContextUI", "//submodules/AvatarNode:AvatarNode", + "//submodules/Components/ReactionImageComponent:ReactionImageComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index a668f5c1c3..5f7ea8334e 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -12,48 +12,7 @@ import WebPBinding import AnimatedAvatarSetNode import ContextUI import AvatarNode - -private final class ReactionImageNode: ASImageNode { - private var disposable: Disposable? - let size: CGSize - - init(context: AccountContext, availableReactions: AvailableReactions?, reaction: String) { - var file: TelegramMediaFile? - if let availableReactions = availableReactions { - for availableReaction in availableReactions.reactions { - if availableReaction.value == reaction { - file = availableReaction.staticIcon - break - } - } - } - if let file = file { - self.size = file.dimensions?.cgSize ?? CGSize(width: 18.0, height: 18.0) - - super.init() - - self.disposable = (context.account.postbox.mediaBox.resourceData(file.resource) - |> deliverOnMainQueue).start(next: { [weak self] data in - guard let strongSelf = self else { - return - } - - if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { - if let image = WebP.convert(fromWebP: dataValue) { - strongSelf.image = image - } - } - }) - } else { - self.size = CGSize(width: 18.0, height: 18.0) - super.init() - } - } - - deinit { - self.disposable?.dispose() - } -} +import ReactionImageComponent private let avatarFont = avatarPlaceholderFont(size: 16.0) diff --git a/submodules/PeerInfoUI/BUILD b/submodules/PeerInfoUI/BUILD index 25accba0be..cfd857c73d 100644 --- a/submodules/PeerInfoUI/BUILD +++ b/submodules/PeerInfoUI/BUILD @@ -73,6 +73,7 @@ swift_library( "//submodules/UIKitRuntimeUtils:UIKitRuntimeUtils", "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", + "//submodules/Components/ReactionImageComponent:ReactionImageComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift b/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift index 20893de63a..af0568660f 100644 --- a/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift +++ b/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift @@ -3,74 +3,47 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox -import TelegramCore import TelegramPresentationData +import SwitchNode +import TelegramCore import ItemListUI -import PresentationDataUtils -import StickerResources -import AnimatedStickerNode -import TelegramAnimatedStickerNode -import ShimmerEffect +import ReactionImageComponent +import AccountContext -public struct ItemListReactionItemEditing: Equatable { - public var editable: Bool - public var editing: Bool - public var revealed: Bool - public var reorderable: Bool - public var selectable: Bool - - public init(editable: Bool, editing: Bool, revealed: Bool, reorderable: Bool, selectable: Bool) { - self.editable = editable - self.editing = editing - self.revealed = revealed - self.reorderable = reorderable - self.selectable = selectable - } -} - -public enum ItemListReactionItemControl: Equatable { - case none - case installation(installed: Bool) - case selection - case check(checked: Bool) -} - -public final class ItemListReactionItem: ListViewItem, ItemListItem { +public class ItemListReactionItem: ListViewItem, ItemListItem { + let context: AccountContext let presentationData: ItemListPresentationData - let account: Account - let packInfo: StickerPackCollectionInfo - let itemCount: String - let topItem: StickerPackItem? - let unread: Bool - let control: ItemListReactionItemControl - let editing: ItemListReactionItemEditing + let file: TelegramMediaFile? + let title: String + let value: Bool let enabled: Bool - let playAnimatedStickers: Bool public let sectionId: ItemListSectionId - let action: (() -> Void)? - let setPackIdWithRevealedOptions: (ItemCollectionId?, ItemCollectionId?) -> Void - let addPack: () -> Void - let removePack: () -> Void - let toggleSelected: () -> Void + let style: ItemListStyle + let updated: (Bool) -> Void + public let tag: ItemListItemTag? - public init(presentationData: ItemListPresentationData, account: Account, packInfo: StickerPackCollectionInfo, itemCount: String, topItem: StickerPackItem?, unread: Bool, control: ItemListReactionItemControl, editing: ItemListReactionItemEditing, enabled: Bool, playAnimatedStickers: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, addPack: @escaping () -> Void, removePack: @escaping () -> Void, toggleSelected: @escaping () -> Void) { + public init( + context: AccountContext, + presentationData: ItemListPresentationData, + file: TelegramMediaFile?, + title: String, + value: Bool, + enabled: Bool = true, + sectionId: ItemListSectionId, + style: ItemListStyle, + updated: @escaping (Bool) -> Void, + tag: ItemListItemTag? = nil + ) { + self.context = context self.presentationData = presentationData - self.account = account - self.packInfo = packInfo - self.itemCount = itemCount - self.topItem = topItem - self.unread = unread - self.control = control - self.editing = editing + self.file = file + self.title = title + self.value = value self.enabled = enabled - self.playAnimatedStickers = playAnimatedStickers self.sectionId = sectionId - self.action = action - self.setPackIdWithRevealedOptions = setPackIdWithRevealedOptions - self.addPack = addPack - self.removePack = removePack - self.toggleSelected = toggleSelected + self.style = style + self.updated = updated + self.tag = tag } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -94,15 +67,14 @@ public final class ItemListReactionItem: ListViewItem, ItemListItem { if let nodeValue = node() as? ItemListReactionItemNode { let makeLayout = nodeValue.asyncLayout() - var animated = true - if case .None = animation { - animated = false - } - async { let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { _ in + var animated = true + if case .None = animation { + animated = false + } apply(animated) }) } @@ -110,98 +82,67 @@ public final class ItemListReactionItem: ListViewItem, ItemListItem { } } } - - public var selectable: Bool = true - - public func selected(listView: ListView){ - listView.clearHighlightAnimated(true) - self.action?() - } } -public enum StickerPackThumbnailItem: Equatable { - case still(TelegramMediaImageRepresentation) - case animated(MediaResource, PixelDimensions) +private protocol ItemListSwitchNodeImpl { + var frameColor: UIColor { get set } + var contentColor: UIColor { get set } + var handleColor: UIColor { get set } + var positiveContentColor: UIColor { get set } + var negativeContentColor: UIColor { get set } - public static func ==(lhs: StickerPackThumbnailItem, rhs: StickerPackThumbnailItem) -> Bool { - switch lhs { - case let .still(representation): - if case .still(representation) = rhs { - return true - } else { - return false - } - case let .animated(lhsResource, lhsDimensions): - if case let .animated(rhsResource, rhsDimensions) = rhs, lhsResource.isEqual(to: rhsResource), lhsDimensions == rhsDimensions { - return true - } else { - return false - } + var isOn: Bool { get } + func setOn(_ value: Bool, animated: Bool) +} + +extension SwitchNode: ItemListSwitchNodeImpl { + var positiveContentColor: UIColor { + get { + return .white + } set(value) { + + } + } + var negativeContentColor: UIColor { + get { + return .white + } set(value) { + } } } -class ItemListReactionItemNode: ItemListRevealOptionsItemNode { - private var currentThumbnailItem: StickerPackThumbnailItem? - +extension IconSwitchNode: ItemListSwitchNodeImpl { +} + +public class ItemListReactionItemNode: ListViewItemNode, ItemListItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode - private var disabledOverlayNode: ASDisplayNode? private let maskNode: ASImageNode - private let containerNode: ASDisplayNode - override var controlsContainer: ASDisplayNode { - return self.containerNode - } - - fileprivate let imageNode: TransformImageNode - private var animationNode: AnimatedStickerNode? - private var placeholderNode: StickerShimmerEffectNode? - private let unreadNode: ASImageNode + private let imageNode: ReactionFileImageNode private let titleNode: TextNode - private let statusNode: TextNode - private let installTextNode: TextNode - private let installationActionBackgroundNode: ASImageNode - private let installationActionNode: HighlightableButtonNode - private let selectionIconNode: ASImageNode - - private var layoutParams: (ItemListReactionItem, ListViewItemLayoutParams, ItemListNeighbors)? - - private var selectableControlNode: ItemListSelectableControlNode? - private var editableControlNode: ItemListEditableControlNode? - private var reorderControlNode: ItemListEditableReorderControlNode? + private var switchNode: ASDisplayNode & ItemListSwitchNodeImpl + private let switchGestureNode: ASDisplayNode + private var disabledOverlayNode: ASDisplayNode? private let activateArea: AccessibilityAreaNode - private let fetchDisposable = MetaDisposable() + private var item: ItemListReactionItem? - override var canBeSelected: Bool { - if self.selectableControlNode != nil || self.editableControlNode != nil || self.disabledOverlayNode != nil { - return false - } - if let item = self.layoutParams?.0, item.action != nil { - return super.canBeSelected - } else { - return false - } + public var tag: ItemListItemTag? { + return self.item?.tag } - override var visibility: ListViewItemNodeVisibility { - didSet { - let wasVisible = oldValue != .none - let isVisible = self.visibility != .none - - if wasVisible != isVisible { - self.animationNode?.visibility = isVisible && (self.layoutParams?.0.playAnimatedStickers ?? true) - } - } - } - - init() { + public init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.maskNode = ASImageNode() + self.maskNode.isUserInteractionEnabled = false self.topStripeNode = ASDisplayNode() self.topStripeNode.isLayerBacked = true @@ -209,154 +150,63 @@ class ItemListReactionItemNode: ItemListRevealOptionsItemNode { self.bottomStripeNode = ASDisplayNode() self.bottomStripeNode.isLayerBacked = true - self.containerNode = ASDisplayNode() - - self.maskNode = ASImageNode() - self.maskNode.isUserInteractionEnabled = false - - self.imageNode = TransformImageNode() - self.imageNode.isLayerBacked = !smartInvertColorsEnabled() - - self.placeholderNode = StickerShimmerEffectNode() - self.placeholderNode?.isUserInteractionEnabled = false + self.imageNode = ReactionFileImageNode() self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false - self.titleNode.contentMode = .left - self.titleNode.contentsScale = UIScreen.main.scale - self.statusNode = TextNode() - self.statusNode.isUserInteractionEnabled = false - self.statusNode.contentMode = .left - self.statusNode.contentsScale = UIScreen.main.scale - - self.unreadNode = ASImageNode() - self.unreadNode.isLayerBacked = true - self.unreadNode.displaysAsynchronously = false - self.unreadNode.displayWithoutProcessing = true - - self.installationActionBackgroundNode = ASImageNode() - self.installationActionBackgroundNode.displaysAsynchronously = false - self.installationActionBackgroundNode.displayWithoutProcessing = true - self.installationActionBackgroundNode.isLayerBacked = true - self.installationActionNode = HighlightableButtonNode() - - self.installTextNode = TextNode() - self.installTextNode.isUserInteractionEnabled = false - self.installTextNode.contentMode = .left - self.installTextNode.contentsScale = UIScreen.main.scale - - self.selectionIconNode = ASImageNode() - self.selectionIconNode.displaysAsynchronously = false - self.selectionIconNode.displayWithoutProcessing = true - self.selectionIconNode.isLayerBacked = true + self.switchNode = SwitchNode() self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isLayerBacked = true + self.switchGestureNode = ASDisplayNode() + self.activateArea = AccessibilityAreaNode() - super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.containerNode) - - if let placeholderNode = self.placeholderNode { - self.containerNode.addSubnode(placeholderNode) - } - - self.containerNode.addSubnode(self.imageNode) - self.containerNode.addSubnode(self.titleNode) - self.containerNode.addSubnode(self.statusNode) - self.containerNode.addSubnode(self.unreadNode) - self.containerNode.addSubnode(self.installationActionBackgroundNode) - self.containerNode.addSubnode(self.installTextNode) - self.containerNode.addSubnode(self.installationActionNode) - self.containerNode.addSubnode(self.selectionIconNode) + self.addSubnode(self.imageNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.switchNode) + self.addSubnode(self.switchGestureNode) self.addSubnode(self.activateArea) - self.installationActionNode.addTarget(self, action: #selector(self.installationActionPressed), forControlEvents: .touchUpInside) - self.installationActionNode.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.installationActionBackgroundNode.layer.removeAnimation(forKey: "opacity") - strongSelf.installationActionBackgroundNode.alpha = 0.4 - } else { - strongSelf.installationActionBackgroundNode.alpha = 1.0 - strongSelf.installationActionBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } + self.activateArea.activate = { [weak self] in + guard let strongSelf = self, let item = strongSelf.item, item.enabled else { + return false } + let value = !strongSelf.switchNode.isOn + strongSelf.switchNode.setOn(value, animated: true) + item.updated(value) + return true } + } + + override public func didLoad() { + super.didLoad() - var firstTime = true - self.imageNode.imageUpdated = { [weak self] image in - guard let strongSelf = self else { - return - } - if image != nil { - strongSelf.removePlaceholder(animated: !firstTime) - if firstTime { - strongSelf.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - } - firstTime = false - } + (self.switchNode.view as? UISwitch)?.addTarget(self, action: #selector(self.switchValueChanged(_:)), for: .valueChanged) + self.switchGestureNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } - deinit { - self.fetchDisposable.dispose() - } - - private func removePlaceholder(animated: Bool) { - if let placeholderNode = self.placeholderNode { - self.placeholderNode = nil - if !animated { - placeholderNode.removeFromSupernode() - } else { - placeholderNode.allowsGroupOpacity = true - placeholderNode.alpha = 0.0 - placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in - placeholderNode?.removeFromSupernode() - placeholderNode?.allowsGroupOpacity = false - }) - } - } - } - - private var absoluteLocation: (CGRect, CGSize)? - override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { - self.absoluteLocation = (rect, containerSize) - if let placeholderNode = placeholderNode { - placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + placeholderNode.frame.minX, y: rect.minY + placeholderNode.frame.minY), size: placeholderNode.frame.size), within: containerSize) - } - } - - override func tapped() { - guard let item = self.layoutParams?.0, item.editing.editing && item.editing.selectable else { - return - } - item.toggleSelected() - } - - func asyncLayout() -> (_ item: ItemListReactionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { + func asyncLayout() -> (_ item: ItemListReactionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeImageLayout = self.imageNode.asyncLayout() let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - let makeStatusLayout = TextNode.asyncLayout(self.statusNode) - let makeInstallLayout = TextNode.asyncLayout(self.installTextNode) - let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) - let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode) - let selectableControlLayout = ItemListSelectableControlNode.asyncLayout(self.selectableControlNode) - let previousThumbnailItem = self.currentThumbnailItem + let currentItem = self.item var currentDisabledOverlayNode = self.disabledOverlayNode - let currentItem = self.layoutParams?.0 - return { item, params, neighbors in - let titleFont = Font.bold(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) - let statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) + var contentSize: CGSize + var insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor - var titleAttributedString: NSAttributedString? - var statusAttributedString: NSAttributedString? + let sideImageInset: CGFloat = 44.0 + + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) var updatedTheme: PresentationTheme? @@ -364,190 +214,51 @@ class ItemListReactionItemNode: ItemListRevealOptionsItemNode { updatedTheme = item.presentationData.theme } - let packRevealOptions: [ItemListRevealOption] - if item.editing.editable && item.enabled { - packRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)] - } else { - packRevealOptions = [] + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: 44.0) + insets = itemListNeighborsPlainInsets(neighbors) + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + contentSize = CGSize(width: params.width, height: 44.0) + insets = itemListNeighborsGroupedInsets(neighbors, params) } - var rightInset: CGFloat = params.rightInset + let (imageSize, imageApply) = makeImageLayout(item.context, item.file) - var installationBackgroundImage: UIImage? - var installationText: String? - var checkImage: UIImage? - switch item.control { - case .none: - break - case let .installation(installed): - if installed { - installationBackgroundImage = PresentationResourcesChat.chatInputMediaPanelAddedPackButtonImage(item.presentationData.theme) - installationText = item.presentationData.strings.Stickers_Installed - } else { - installationBackgroundImage = PresentationResourcesChat.chatInputMediaPanelAddPackButtonImage(item.presentationData.theme) - installationText = item.presentationData.strings.Stickers_Install - } - case .selection: - rightInset += 16.0 - checkImage = PresentationResourcesItemList.checkIconImage(item.presentationData.theme) - default: - break - } + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 80.0 - sideImageInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - var unreadImage: UIImage? - if item.unread { - unreadImage = PresentationResourcesItemList.stickerUnreadDotImage(item.presentationData.theme) - } - - titleAttributedString = NSAttributedString(string: item.packInfo.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) - statusAttributedString = NSAttributedString(string: item.itemCount, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) - - let leftInset: CGFloat = 65.0 + params.leftInset - - let verticalInset: CGFloat = 11.0 - let titleSpacing: CGFloat = 2.0 - - let insets = itemListNeighborsGroupedInsets(neighbors, params) - let separatorHeight = UIScreenPixel - - var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)? - var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)? - var selectableControlSizeAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)? - - var editingOffset: CGFloat = 0.0 - var reorderInset: CGFloat = 0.0 - - if item.editing.editing { - if item.editing.selectable { - var selected = false - if case let .check(checked) = item.control { - selected = checked - } - let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, selected, true) - selectableControlSizeAndApply = sizeAndApply - editingOffset = sizeAndApply.0 - } else { - let sizeAndApply = editableControlLayout(item.presentationData.theme, false) - editableControlSizeAndApply = sizeAndApply - editingOffset = sizeAndApply.0 - } - - if item.editing.reorderable { - let sizeAndApply = reorderControlLayout(item.presentationData.theme) - reorderControlSizeAndApply = sizeAndApply - reorderInset = sizeAndApply.0 - } - } - - var installed = false - if case .installation(true) = item.control { - installed = true - } - - let (installLayout, installApply) = makeInstallLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: installationText ?? "", font: Font.semibold(13.0), textColor: installed ? item.presentationData.theme.list.itemCheckColors.fillColor : item.presentationData.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let installWidth: CGFloat - if installLayout.size.width > 0.0 { - installWidth = installLayout.size.width + 32.0 - } else { - installWidth = 0.0 - } - - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - 10.0 - reorderInset - installWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - reorderInset - installWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - - let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + statusLayout.size.height) - let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - let layoutSize = layout.size + contentSize.height = max(contentSize.height, titleLayout.size.height + 22.0) if !item.enabled { if currentDisabledOverlayNode == nil { currentDisabledOverlayNode = ASDisplayNode() - currentDisabledOverlayNode?.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.5) } } else { currentDisabledOverlayNode = nil } - var thumbnailItem: StickerPackThumbnailItem? - var resourceReference: MediaResourceReference? - if let thumbnail = item.packInfo.thumbnail { - if item.packInfo.flags.contains(.isAnimated) { - thumbnailItem = .animated(thumbnail.resource, thumbnail.dimensions) - resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: item.packInfo.id.id, accessHash: item.packInfo.accessHash), resource: thumbnail.resource) - } else { - thumbnailItem = .still(thumbnail) - resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: item.packInfo.id.id, accessHash: item.packInfo.accessHash), resource: thumbnail.resource) - } - } else if let item = item.topItem { - if item.file.isAnimatedSticker { - thumbnailItem = .animated(item.file.resource, item.file.dimensions ?? PixelDimensions(width: 100, height: 100)) - resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: item.file.resource) - } else if let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource { - thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) - resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource) - } - } + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size - var updatedImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? - var updatedFetchSignal: Signal? - - let imageBoundingSize = CGSize(width: 34.0, height: 34.0) - var imageApply: (() -> Void)? - let fileUpdated = thumbnailItem != previousThumbnailItem - - var imageSize: CGSize? - - if let thumbnailItem = thumbnailItem { - switch thumbnailItem { - case let .still(representation): - let stillImageSize = representation.dimensions.cgSize.aspectFitted(imageBoundingSize) - imageSize = stillImageSize - - if fileUpdated { - imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: stillImageSize, boundingSize: stillImageSize, intrinsicInsets: UIEdgeInsets())) - updatedImageSignal = chatMessageStickerPackThumbnail(postbox: item.account.postbox, resource: representation.resource, nilIfEmpty: true) - } - case let .animated(resource, _): - imageSize = imageBoundingSize - - if fileUpdated { - imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageBoundingSize, boundingSize: imageBoundingSize, intrinsicInsets: UIEdgeInsets())) - updatedImageSignal = chatMessageStickerPackThumbnail(postbox: item.account.postbox, resource: resource, animated: true, nilIfEmpty: true) - } - } - if fileUpdated, let resourceReference = resourceReference { - updatedFetchSignal = fetchedMediaResource(mediaBox: item.account.postbox.mediaBox, reference: resourceReference) - } - } else { - updatedImageSignal = .single({ _ in return nil }) - updatedFetchSignal = .complete() - } - - return (layout, { [weak self] animated in + return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] animated in if let strongSelf = self { - strongSelf.layoutParams = (item, params, neighbors) + 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.accessibilityLabel = titleAttributedString?.string ?? "" - strongSelf.activateArea.accessibilityValue = statusAttributedString?.string ?? "" + + strongSelf.activateArea.accessibilityLabel = item.title + strongSelf.activateArea.accessibilityValue = item.value ? item.presentationData.strings.VoiceOver_Common_On : item.presentationData.strings.VoiceOver_Common_Off + strongSelf.activateArea.accessibilityHint = item.presentationData.strings.VoiceOver_Common_SwitchHint + var accessibilityTraits = UIAccessibilityTraits() if item.enabled { - strongSelf.activateArea.accessibilityTraits = [] } else { - strongSelf.activateArea.accessibilityTraits = .notEnabled + accessibilityTraits.insert(.notEnabled) } - - if fileUpdated { - strongSelf.currentThumbnailItem = thumbnailItem - } - - if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor - } - - let revealOffset = strongSelf.revealOffset + strongSelf.activateArea.accessibilityTraits = accessibilityTraits let transition: ContainedViewLayoutTransition if animated { @@ -559,13 +270,14 @@ class ItemListReactionItemNode: ItemListRevealOptionsItemNode { if let currentDisabledOverlayNode = currentDisabledOverlayNode { if currentDisabledOverlayNode != strongSelf.disabledOverlayNode { strongSelf.disabledOverlayNode = currentDisabledOverlayNode - strongSelf.addSubnode(currentDisabledOverlayNode) + strongSelf.insertSubnode(currentDisabledOverlayNode, belowSubnode: strongSelf.switchGestureNode) currentDisabledOverlayNode.alpha = 0.0 transition.updateAlpha(node: currentDisabledOverlayNode, alpha: 1.0) currentDisabledOverlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight)) } else { transition.updateFrame(node: currentDisabledOverlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight))) } + currentDisabledOverlayNode.backgroundColor = itemBackgroundColor.withAlphaComponent(0.6) } else if let disabledOverlayNode = strongSelf.disabledOverlayNode { transition.updateAlpha(node: disabledOverlayNode, alpha: 0.0, completion: { [weak disabledOverlayNode] _ in disabledOverlayNode?.removeFromSupernode() @@ -573,230 +285,125 @@ class ItemListReactionItemNode: ItemListRevealOptionsItemNode { strongSelf.disabledOverlayNode = nil } - if let editableControlSizeAndApply = editableControlSizeAndApply { - let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.contentSize.height)) - if strongSelf.editableControlNode == nil { - let editableControlNode = editableControlSizeAndApply.1(layout.contentSize.height) - editableControlNode.tapped = { - if let strongSelf = self { - strongSelf.setRevealOptionsOpened(true, animated: true) - strongSelf.revealOptionsInteractivelyOpened() - } - } - strongSelf.editableControlNode = editableControlNode - strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.imageNode) - editableControlNode.frame = editableControlFrame - transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY)) - editableControlNode.alpha = 0.0 - transition.updateAlpha(node: editableControlNode, alpha: 1.0) - } else { - strongSelf.editableControlNode?.frame = editableControlFrame - } - strongSelf.editableControlNode?.isHidden = !item.editing.editable - } else if let editableControlNode = strongSelf.editableControlNode { - var editableControlFrame = editableControlNode.frame - editableControlFrame.origin.x = -editableControlFrame.size.width - strongSelf.editableControlNode = nil - transition.updateAlpha(node: editableControlNode, alpha: 0.0) - transition.updateFrame(node: editableControlNode, frame: editableControlFrame, completion: { [weak editableControlNode] _ in - editableControlNode?.removeFromSupernode() - }) + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + + strongSelf.switchNode.frameColor = item.presentationData.theme.list.itemSwitchColors.frameColor + strongSelf.switchNode.contentColor = item.presentationData.theme.list.itemSwitchColors.contentColor + strongSelf.switchNode.handleColor = item.presentationData.theme.list.itemSwitchColors.handleColor + strongSelf.switchNode.positiveContentColor = item.presentationData.theme.list.itemSwitchColors.positiveColor + strongSelf.switchNode.negativeContentColor = item.presentationData.theme.list.itemSwitchColors.negativeColor + + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } - if let selectableControlSizeAndApply = selectableControlSizeAndApply { - let selectableControlSize = CGSize(width: selectableControlSizeAndApply.0, height: layout.contentSize.height) - let selectableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: selectableControlSize) - if strongSelf.selectableControlNode == nil { - let selectableControlNode = selectableControlSizeAndApply.1(selectableControlSize, false) - strongSelf.selectableControlNode = selectableControlNode - strongSelf.addSubnode(selectableControlNode) - selectableControlNode.frame = selectableControlFrame - transition.animatePosition(node: selectableControlNode, from: CGPoint(x: -selectableControlFrame.size.width / 2.0, y: selectableControlFrame.midY)) - selectableControlNode.alpha = 0.0 - transition.updateAlpha(node: selectableControlNode, alpha: 1.0) - } else if let selectableControlNode = strongSelf.selectableControlNode { - transition.updateFrame(node: selectableControlNode, frame: selectableControlFrame) - let _ = selectableControlSizeAndApply.1(selectableControlSize, transition.isAnimated) - } - } else if let selectableControlNode = strongSelf.selectableControlNode { - var selectableControlFrame = selectableControlNode.frame - selectableControlFrame.origin.x = -selectableControlFrame.size.width - strongSelf.selectableControlNode = nil - transition.updateAlpha(node: selectableControlNode, alpha: 0.0) - transition.updateFrame(node: selectableControlNode, frame: selectableControlFrame, completion: { [weak selectableControlNode] _ in - selectableControlNode?.removeFromSupernode() - }) - } - - if let reorderControlSizeAndApply = reorderControlSizeAndApply { - if strongSelf.reorderControlNode == nil { - let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate) - strongSelf.reorderControlNode = reorderControlNode - strongSelf.addSubnode(reorderControlNode) - reorderControlNode.alpha = 0.0 - transition.updateAlpha(node: reorderControlNode, alpha: 1.0) - } - let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0, y: 0.0), size: CGSize(width: reorderControlSizeAndApply.0, height: layout.contentSize.height)) - strongSelf.reorderControlNode?.frame = reorderControlFrame - } else if let reorderControlNode = strongSelf.reorderControlNode { - strongSelf.reorderControlNode = nil - transition.updateAlpha(node: reorderControlNode, alpha: 0.0, completion: { [weak reorderControlNode] _ in - reorderControlNode?.removeFromSupernode() - }) - } - - imageApply?() - let _ = titleApply() - let _ = statusApply() - let _ = installApply() - - switch item.control { - case .none: - strongSelf.installationActionNode.isHidden = true - strongSelf.installationActionBackgroundNode.isHidden = true - strongSelf.selectionIconNode.isHidden = true - case let .installation(installed): - strongSelf.installationActionBackgroundNode.isHidden = false - strongSelf.installationActionNode.isHidden = false - strongSelf.selectionIconNode.isHidden = true - strongSelf.installationActionNode.isUserInteractionEnabled = !installed - - if let backgroundImage = installationBackgroundImage { - strongSelf.installationActionBackgroundNode.image = backgroundImage + + let leftInset = 16.0 + params.leftInset + sideImageInset + + switch item.style { + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() } - - let installationActionFrame = CGRect(origin: CGPoint(x: params.width - rightInset - installWidth - 16.0, y: 0.0), size: CGSize(width: 50.0, height: layout.contentSize.height)) - strongSelf.installationActionNode.frame = installationActionFrame - - let buttonFrame = CGRect(origin: CGPoint(x: params.width - rightInset - installWidth - 16.0, y: installationActionFrame.minY + floor((installationActionFrame.size.height - 28.0) / 2.0)), size: CGSize(width: installWidth, height: 28.0)) - strongSelf.installationActionBackgroundNode.frame = buttonFrame - strongSelf.installTextNode.frame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - installLayout.size.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - installLayout.size.height) / 2.0) + 1.0), size: installLayout.size) - case .selection: - strongSelf.installationActionNode.isHidden = true - strongSelf.installationActionBackgroundNode.isHidden = true - strongSelf.selectionIconNode.isHidden = false - if let image = checkImage { - strongSelf.selectionIconNode.image = image - strongSelf.selectionIconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - image.size.width - floor((44.0 - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + 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)) + case .blocks: + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, aboveSubnode: strongSelf.switchGestureNode) } - case .check: - strongSelf.installationActionNode.isHidden = true - strongSelf.installationActionBackgroundNode.isHidden = true - strongSelf.selectionIconNode.isHidden = true - } - - if strongSelf.backgroundNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) - } - if strongSelf.topStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) - } - if strongSelf.bottomStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) - } - if strongSelf.maskNode.supernode == nil { - strongSelf.addSubnode(strongSelf.maskNode) - } - - let hasCorners = itemListHasRoundedBlockLayout(params) - var hasTopCorners = false - var hasBottomCorners = false - switch neighbors.top { - case .sameSection(false): - strongSelf.topStripeNode.isHidden = true - default: - hasTopCorners = true - strongSelf.topStripeNode.isHidden = hasCorners - } - let bottomStripeInset: CGFloat - let bottomStripeOffset: CGFloat - switch neighbors.bottom { - case .sameSection(false): - bottomStripeInset = leftInset + editingOffset - bottomStripeOffset = -separatorHeight - default: - bottomStripeInset = 0.0 - bottomStripeOffset = 0.0 - hasBottomCorners = true - strongSelf.bottomStripeNode.isHidden = hasCorners - } - - 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.containerNode.frame = CGRect(origin: CGPoint(), size: strongSelf.backgroundNode.frame.size) - 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 + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) - - if let unreadImage = unreadImage { - strongSelf.unreadNode.image = unreadImage - strongSelf.unreadNode.isHidden = false - strongSelf.unreadNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 16.0), size: unreadImage.size) - } else { - strongSelf.unreadNode.isHidden = true - } - - transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: (strongSelf.unreadNode.isHidden ? 0.0 : 10.0) + leftInset + revealOffset + editingOffset, y: verticalInset), size: titleLayout.size)) - transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size)) - - let boundingSize = CGSize(width: 34.0, height: 34.0) - if let thumbnailItem = thumbnailItem, let imageSize = imageSize { - let imageFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0 + floor((boundingSize.width - imageSize.width) / 2.0), y: floor((layout.contentSize.height - imageSize.height) / 2.0)), size: imageSize) - var thumbnailDimensions = PixelDimensions(width: 512, height: 512) - switch thumbnailItem { - case let .still(representation): - transition.updateFrame(node: strongSelf.imageNode, frame: imageFrame) - thumbnailDimensions = representation.dimensions - case let .animated(resource, _): - transition.updateFrame(node: strongSelf.imageNode, frame: imageFrame) - - let animationNode: AnimatedStickerNode - if let current = strongSelf.animationNode { - animationNode = current - } else { - animationNode = AnimatedStickerNode() - strongSelf.animationNode = animationNode - strongSelf.addSubnode(animationNode) - - animationNode.setup(source: AnimatedStickerResourceSource(account: item.account, resource: resource), width: 80, height: 80, mode: .cached) - } - animationNode.visibility = strongSelf.visibility != .none && item.playAnimatedStickers - animationNode.isHidden = !item.playAnimatedStickers - strongSelf.imageNode.isHidden = item.playAnimatedStickers - if let animationNode = strongSelf.animationNode { - transition.updateFrame(node: animationNode, frame: imageFrame) - } - } - - if let placeholderNode = strongSelf.placeholderNode { - placeholderNode.frame = imageFrame - placeholderNode.update(backgroundColor: nil, foregroundColor: item.presentationData.theme.list.disclosureArrowColor.blitOver(item.presentationData.theme.list.itemBlocksBackgroundColor, alpha: 0.55), shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), data: item.packInfo.immediateThumbnailData, size: imageFrame.size, imageSize: thumbnailDimensions.cgSize) + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = 16.0 + params.leftInset + sideImageInset + default: + bottomStripeInset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + 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)) + } + + let imageFitSize = imageSize.aspectFitted(CGSize(width: 30.0, height: 30.0)) + strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor(sideImageInset - imageFitSize.width), y: floor((contentSize.height - imageFitSize.height) / 2.0)), size: imageFitSize) + imageApply() + + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) + if let switchView = strongSelf.switchNode.view as? UISwitch { + if strongSelf.switchNode.bounds.size.width.isZero { + switchView.sizeToFit() } + 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) + strongSelf.switchGestureNode.frame = strongSelf.switchNode.frame + if switchView.isOn != item.value { + switchView.setOn(item.value, animated: animated) + } + switchView.isUserInteractionEnabled = true } + strongSelf.switchGestureNode.isHidden = item.enabled - if let updatedImageSignal = updatedImageSignal { - strongSelf.imageNode.setSignal(updatedImageSignal) - } - - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel)) - - strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - - strongSelf.setRevealOptions((left: [], right: packRevealOptions)) - strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) - - if let updatedFetchSignal = updatedFetchSignal { - strongSelf.fetchDisposable.set(updatedFetchSignal.start()) - } + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel)) } }) } } - override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + override public func accessibilityActivate() -> Bool { + guard let item = self.item else { + return false + } + if !item.enabled { + return false + } + if let switchNode = self.switchNode as? IconSwitchNode { + switchNode.isOn = !switchNode.isOn + item.updated(switchNode.isOn) + } else if let switchNode = self.switchNode as? SwitchNode { + switchNode.isOn = !switchNode.isOn + item.updated(switchNode.isOn) + } + return true + } + + override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) if highlighted { @@ -834,82 +441,27 @@ class ItemListReactionItemNode: ItemListRevealOptionsItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } - override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { - super.updateRevealOffset(offset: offset, transition: transition) - - guard let params = self.layoutParams?.1 else { - return - } - - let leftInset: CGFloat = 65.0 + params.leftInset - - let editingOffset: CGFloat - if let editableControlNode = self.editableControlNode { - editingOffset = editableControlNode.bounds.size.width - var editableControlFrame = editableControlNode.frame - editableControlFrame.origin.x = params.leftInset + offset - transition.updateFrame(node: editableControlNode, frame: editableControlFrame) - } else { - editingOffset = 0.0 - } - - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + self.revealOffset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size)) - transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + self.revealOffset + editingOffset, y: self.statusNode.frame.minY), size: self.statusNode.bounds.size)) - - let boundingSize = CGSize(width: 34.0, height: 34.0) - - transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: params.leftInset + self.revealOffset + editingOffset + 15.0 + floor((boundingSize.width - self.imageNode.frame.size.width) / 2.0), y: self.imageNode.frame.minY), size: self.imageNode.frame.size)) - if let animationNode = self.animationNode { - transition.updateFrame(node: animationNode, frame: CGRect(origin: CGPoint(x: params.leftInset + self.revealOffset + editingOffset + 15.0 + floor((boundingSize.width - animationNode.frame.size.width) / 2.0), y: animationNode.frame.minY), size: animationNode.frame.size)) + @objc private func switchValueChanged(_ switchView: UISwitch) { + if let item = self.item { + let value = switchView.isOn + item.updated(value) } } - override func revealOptionsInteractivelyOpened() { - if let (item, _, _) = self.layoutParams { - item.setPackIdWithRevealedOptions(item.packInfo.id, nil) + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if let item = self.item, let switchView = self.switchNode.view as? UISwitch, case .ended = recognizer.state { + if item.enabled { + let value = switchView.isOn + item.updated(!value) + } } } - - override func revealOptionsInteractivelyClosed() { - if let (item, _, _) = self.layoutParams { - item.setPackIdWithRevealedOptions(nil, item.packInfo.id) - } - } - - override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { - self.setRevealOptionsOpened(false, animated: true) - self.revealOptionsInteractivelyClosed() - - if let (item, _, _) = self.layoutParams { - item.removePack() - } - } - - @objc func installationActionPressed() { - if let (item, _, _) = self.layoutParams { - item.addPack() - } - } - - override func isReorderable(at point: CGPoint) -> Bool { - if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point), !self.isDisplayingRevealedOptions { - return true - } - return false - } - - override func snapshotForReordering() -> UIView? { - self.backgroundNode.alpha = 0.9 - let result = self.view.snapshotContentTree() - self.backgroundNode.alpha = 1.0 - return result - } } diff --git a/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift b/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift index 4a6050281f..93807265cb 100644 --- a/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift +++ b/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift @@ -127,10 +127,18 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry { case let .itemsHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .item(_, value, file, text, isEnabled): - let _ = file - return ItemListSwitchItem(presentationData: presentationData, title: "\(value) \(text)", value: isEnabled, sectionId: self.section, style: .blocks, updated: { _ in - arguments.toggleItem(value) - }) + return ItemListReactionItem( + context: arguments.context, + presentationData: presentationData, + file: file, + title: text, + value: isEnabled, + sectionId: self.section, + style: .blocks, + updated: { _ in + arguments.toggleItem(value) + } + ) } } } From b0d3aa257803e96a061e456f172a076974eede13 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 17 Dec 2021 23:04:46 +0400 Subject: [PATCH 3/4] Limit contact search to main tab --- submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 5b4b00a598..245220e46c 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -866,7 +866,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let foundRemotePeers: Signal<([FoundPeer], [FoundPeer], Bool), NoError> let currentRemotePeersValue: ([FoundPeer], [FoundPeer]) = currentRemotePeers.with { $0 } ?? ([], []) - if let query = query { + if let query = query, tagMask == nil { foundRemotePeers = ( .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) |> then( From 95a492a25f3d1533d815c7d83b29a8623ace7875 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sat, 18 Dec 2021 00:47:09 +0400 Subject: [PATCH 4/4] Reaction improvements --- .../Sources/CalendarMessageScreen.swift | 16 - .../Sources/CallListController.swift | 1 + .../Sources/ChatListController.swift | 1 + .../Sources/ReactionButtonListComponent.swift | 299 ++++++++---------- .../ReactionListContextMenuContent.swift | 177 ++++++++--- .../ContextUI/Sources/ContextController.swift | 7 +- .../ContextControllerActionsStackNode.swift | 190 +++++++++-- ...tControllerExtractedPresentationNode.swift | 132 +++++--- .../Source/ContextControllerSourceNode.swift | 17 +- submodules/Display/Source/SimpleLayer.swift | 26 ++ .../Sources/HashtagSearchController.swift | 3 +- .../Peers/UpdateCachedPeerData.swift | 4 +- .../DefaultDarkPresentationTheme.swift | 115 ++++++- .../DefaultDarkTintedPresentationTheme.swift | 87 ++++- .../Sources/DefaultDayPresentationTheme.swift | 150 ++++++++- .../Sources/PresentationTheme.swift | 40 ++- .../Sources/PresentationThemeCodable.swift | 14 +- .../Menu/Reactions.imageset/Contents.json | 12 + .../Menu/Reactions.imageset/reactions_30.pdf | 180 +++++++++++ .../TelegramUI/Sources/ChatController.swift | 291 +++++++++++------ .../Sources/ChatControllerInteraction.swift | 6 +- .../ChatInterfaceStateContextMenus.swift | 4 +- .../ChatMessageAnimatedStickerItemNode.swift | 23 +- .../ChatMessageAttachedContentNode.swift | 3 +- .../Sources/ChatMessageBubbleItemNode.swift | 29 +- .../ChatMessageContactBubbleContentNode.swift | 12 +- ...essageContextControllerContentSource.swift | 79 +++++ .../ChatMessageDateAndStatusNode.swift | 82 +++-- .../ChatMessageFileBubbleContentNode.swift | 9 + .../ChatMessageInstantVideoItemNode.swift | 21 ++ .../ChatMessageInteractiveFileNode.swift | 3 +- ...atMessageInteractiveInstantVideoNode.swift | 3 +- .../ChatMessageInteractiveMediaNode.swift | 3 +- .../Sources/ChatMessageItemView.swift | 3 + .../ChatMessageMapBubbleContentNode.swift | 3 +- .../ChatMessagePollBubbleContentNode.swift | 3 +- ...hatMessageReactionsFooterContentNode.swift | 129 +++++++- ...atMessageRestrictedBubbleContentNode.swift | 3 +- .../Sources/ChatMessageStickerItemNode.swift | 24 +- .../ChatMessageTextBubbleContentNode.swift | 12 +- .../ChatRecentActionsControllerNode.swift | 1 + .../Sources/DrawingStickersScreen.swift | 3 +- .../OverlayAudioPlayerControllerNode.swift | 1 + .../Sources/PeerInfo/PeerInfoScreen.swift | 57 +++- .../Sources/SharedAccountContext.swift | 3 +- 45 files changed, 1802 insertions(+), 479 deletions(-) create mode 100644 submodules/Display/Source/SimpleLayer.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/reactions_30.pdf diff --git a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift index 9680abb33b..dee5e42818 100644 --- a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift +++ b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift @@ -13,22 +13,6 @@ import DirectMediaImageCache import TelegramStringFormatting import TooltipUI -private final class NullActionClass: NSObject, CAAction { - @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { - } -} - -private let nullAction = NullActionClass() - -private class SimpleLayer: CALayer { - override func action(forKey event: String) -> CAAction? { - return nullAction - } - - func update(size: CGSize) { - } -} - private enum SelectionTransition { case begin case change diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index f8750eeecb..0941143254 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -491,6 +491,7 @@ private final class CallListTabBarContextExtractedContentSource: ContextExtracte let keepInPlace: Bool = true let ignoreContentTouches: Bool = true let blurBackground: Bool = true + let centerActionsHorizontally: Bool = true private let controller: ViewController private let sourceNode: ContextExtractedContentContainingNode diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 00fe2f8e1a..ffdbc52d92 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2900,6 +2900,7 @@ private final class ChatListTabBarContextExtractedContentSource: ContextExtracte let keepInPlace: Bool = true let ignoreContentTouches: Bool = true let blurBackground: Bool = true + let centerActionsHorizontally: Bool = true private let controller: ChatListController private let sourceNode: ContextExtractedContentContainingNode diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index 846ee21db2..437824c587 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -11,32 +11,7 @@ import UIKit import WebPBinding import AnimatedAvatarSetNode -private final class NullActionClass: NSObject, CAAction { - @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { - } -} - -private let nullAction = NullActionClass() - -private final class SimpleLayer: CALayer { - override func action(forKey event: String) -> CAAction? { - return nullAction - } - - override init() { - super.init() - } - - override init(layer: Any) { - super.init(layer: layer) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -fileprivate final class CounterLayer: CALayer { +fileprivate final class CounterLayer: SimpleLayer { fileprivate final class Layout { struct Spec: Equatable { let clippingHeight: CGFloat @@ -106,10 +81,6 @@ fileprivate final class CounterLayer: CALayer { fatalError("init(coder:) has not been implemented") } - override func action(forKey event: String) -> CAAction? { - return nullAction - } - func apply(layout: Layout, animation: ListViewItemUpdateAnimation) { /*if animation.isAnimated, let previousContents = self.contents { self.animate(from: previousContents as! CGImage, to: layout.image.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2) @@ -121,7 +92,7 @@ fileprivate final class CounterLayer: CALayer { } } -public final class ReactionButtonAsyncView: UIButton { +public final class ReactionButtonAsyncNode: ContextControllerSourceNode { fileprivate final class Layout { struct Spec: Equatable { var component: ReactionButtonComponent @@ -139,6 +110,7 @@ public final class ReactionButtonAsyncView: UIButton { let counterFrame: CGRect? let backgroundImage: UIImage + let extractedBackgroundImage: UIImage let size: CGSize @@ -151,6 +123,7 @@ public final class ReactionButtonAsyncView: UIButton { counter: CounterLayer.Layout?, counterFrame: CGRect?, backgroundImage: UIImage, + extractedBackgroundImage: UIImage, size: CGSize ) { self.spec = spec @@ -161,6 +134,7 @@ public final class ReactionButtonAsyncView: UIButton { self.counter = counter self.counterFrame = counterFrame self.backgroundImage = backgroundImage + self.extractedBackgroundImage = extractedBackgroundImage self.size = size } @@ -199,8 +173,10 @@ public final class ReactionButtonAsyncView: UIButton { } let backgroundImage: UIImage + let extractedBackgroundImage: UIImage if let currentLayout = currentLayout, currentLayout.spec.component.isSelected == spec.component.isSelected, currentLayout.spec.component.colors == spec.component.colors, previousDisplayCounter == currentDisplayCounter { backgroundImage = currentLayout.backgroundImage + extractedBackgroundImage = currentLayout.extractedBackgroundImage } else { backgroundImage = generateImage(CGSize(width: height + 18.0, height: height), rotatedContext: { size, context in UIGraphicsPushContext(context) @@ -225,6 +201,31 @@ public final class ReactionButtonAsyncView: UIButton { string.draw(at: CGPoint(x: size.width - sideInsets - boundingRect.width, y: (size.height - boundingRect.height) / 2.0)) } + UIGraphicsPopContext() + })!.stretchableImage(withLeftCapWidth: Int(height / 2.0), topCapHeight: Int(height / 2.0)) + extractedBackgroundImage = generateImage(CGSize(width: height + 18.0, height: height), rotatedContext: { size, context in + UIGraphicsPushContext(context) + + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.copy) + + context.setFillColor(UIColor(argb: spec.component.colors.extractedBackground).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: height, height: height))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - height, y: 0.0), size: CGSize(width: height, height: size.height))) + context.fill(CGRect(origin: CGPoint(x: height / 2.0, y: 0.0), size: CGSize(width: size.width - height, height: size.height))) + + context.setBlendMode(.normal) + + if let currentDisplayCounter = currentDisplayCounter { + let textColor = UIColor(argb: spec.component.colors.extractedForeground) + let string = NSAttributedString(string: currentDisplayCounter, font: Font.medium(11.0), textColor: textColor) + let boundingRect = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) + if textColor.alpha < 1.0 { + context.setBlendMode(.copy) + } + string.draw(at: CGPoint(x: size.width - sideInsets - boundingRect.width, y: (size.height - boundingRect.height) / 2.0)) + } + UIGraphicsPopContext() })!.stretchableImage(withLeftCapWidth: Int(height / 2.0), topCapHeight: Int(height / 2.0)) } @@ -270,6 +271,7 @@ public final class ReactionButtonAsyncView: UIButton { counter: counter, counterFrame: counterFrame, backgroundImage: backgroundImage, + extractedBackgroundImage: extractedBackgroundImage, size: size ) } @@ -277,21 +279,64 @@ public final class ReactionButtonAsyncView: UIButton { private var layout: Layout? + public let containerNode: ContextExtractedContentContainingNode + private let buttonNode: HighlightTrackingButtonNode public let iconView: UIImageView private var counterLayer: CounterLayer? private var avatarsView: AnimatedAvatarSetView? private let iconImageDisposable = MetaDisposable() - override init(frame: CGRect) { + override init() { + self.containerNode = ContextExtractedContentContainingNode() + self.buttonNode = HighlightTrackingButtonNode() + self.iconView = UIImageView() self.iconView.isUserInteractionEnabled = false - super.init(frame: CGRect()) + super.init() - self.addSubview(self.iconView) + self.targetNodeForActivationProgress = self.containerNode.contentNode - self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + self.addSubnode(self.containerNode) + self.containerNode.contentNode.addSubnode(self.buttonNode) + self.buttonNode.view.addSubview(self.iconView) + + self.buttonNode.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + + self.buttonNode.highligthedChanged = { [weak self] highlighted in + guard let strongSelf = self else { + return + } + let _ = strongSelf + if highlighted { + } else { + } + } + + self.isGestureEnabled = true + + self.containerNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, _ in + guard let strongSelf = self, let layout = strongSelf.layout else { + return + } + + let backgroundImage = isExtracted ? layout.extractedBackgroundImage : layout.backgroundImage + + let previousContents = strongSelf.buttonNode.layer.contents + + let backgroundCapInsets = backgroundImage.capInsets + if backgroundCapInsets.left.isZero && backgroundCapInsets.top.isZero { + strongSelf.buttonNode.layer.contentsScale = backgroundImage.scale + strongSelf.buttonNode.layer.contents = backgroundImage.cgImage + } else { + ASDisplayNodeSetResizableContents(strongSelf.buttonNode.layer, backgroundImage) + } + + if let previousContents = previousContents { + strongSelf.buttonNode.layer.animate(from: previousContents as! CGImage, to: backgroundImage.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2) + } + } } required init?(coder aDecoder: NSCoder) { @@ -310,12 +355,17 @@ public final class ReactionButtonAsyncView: UIButton { } fileprivate func apply(layout: Layout, animation: ListViewItemUpdateAnimation) { + self.containerNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.containerNode.contentNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.containerNode.contentRect = CGRect(origin: CGPoint(), size: layout.size) + animation.animator.updateFrame(layer: self.buttonNode.layer, frame: CGRect(origin: CGPoint(), size: layout.size), completion: nil) + let backgroundCapInsets = layout.backgroundImage.capInsets if backgroundCapInsets.left.isZero && backgroundCapInsets.top.isZero { - self.layer.contentsScale = layout.backgroundImage.scale - self.layer.contents = layout.backgroundImage.cgImage + self.buttonNode.layer.contentsScale = layout.backgroundImage.scale + self.buttonNode.layer.contents = layout.backgroundImage.cgImage } else { - ASDisplayNodeSetResizableContents(self.layer, layout.backgroundImage) + ASDisplayNodeSetResizableContents(self.buttonNode.layer, layout.backgroundImage) } animation.animator.updateFrame(layer: self.iconView.layer, frame: layout.imageFrame, completion: nil) @@ -372,7 +422,7 @@ public final class ReactionButtonAsyncView: UIButton { avatarsView = AnimatedAvatarSetView() avatarsView.isUserInteractionEnabled = false self.avatarsView = avatarsView - self.addSubview(avatarsView) + self.buttonNode.view.addSubview(avatarsView) } let content = AnimatedAvatarSetContext().update(peers: layout.spec.component.avatarPeers, animated: false) let avatarsSize = avatarsView.update( @@ -399,7 +449,7 @@ public final class ReactionButtonAsyncView: UIButton { self.layout = layout } - public static func asyncLayout(_ view: ReactionButtonAsyncView?) -> (ReactionButtonComponent) -> (size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ReactionButtonAsyncView) { + public static func asyncLayout(_ view: ReactionButtonAsyncNode?) -> (ReactionButtonComponent) -> (size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ReactionButtonAsyncNode) { let currentLayout = view?.layout return { component in @@ -414,11 +464,11 @@ public final class ReactionButtonAsyncView: UIButton { return (size: layout.size, apply: { animation in var animation = animation - let updatedView: ReactionButtonAsyncView + let updatedView: ReactionButtonAsyncNode if let view = view { updatedView = view } else { - updatedView = ReactionButtonAsyncView() + updatedView = ReactionButtonAsyncNode() animation = .None } @@ -464,17 +514,23 @@ public final class ReactionButtonComponent: Component { public var selectedBackground: UInt32 public var deselectedForeground: UInt32 public var selectedForeground: UInt32 + public var extractedBackground: UInt32 + public var extractedForeground: UInt32 public init( deselectedBackground: UInt32, selectedBackground: UInt32, deselectedForeground: UInt32, - selectedForeground: UInt32 + selectedForeground: UInt32, + extractedBackground: UInt32, + extractedForeground: UInt32 ) { self.deselectedBackground = deselectedBackground self.selectedBackground = selectedBackground self.deselectedForeground = deselectedForeground self.selectedForeground = selectedForeground + self.extractedBackground = extractedBackground + self.extractedForeground = extractedForeground } } @@ -675,6 +731,25 @@ public final class ReactionButtonComponent: Component { } public final class ReactionButtonsAsyncLayoutContainer { + public struct Reaction { + public var reaction: ReactionButtonComponent.Reaction + public var count: Int + public var peers: [EnginePeer] + public var isSelected: Bool + + public init( + reaction: ReactionButtonComponent.Reaction, + count: Int, + peers: [EnginePeer], + isSelected: Bool + ) { + self.reaction = reaction + self.count = count + self.peers = peers + self.isSelected = isSelected + } + } + public struct Result { public struct Item { public var size: CGSize @@ -687,15 +762,15 @@ public final class ReactionButtonsAsyncLayoutContainer { public struct ApplyResult { public struct Item { public var value: String - public var view: ReactionButtonAsyncView + public var node: ReactionButtonAsyncNode public var size: CGSize } public var items: [Item] - public var removedViews: [ReactionButtonAsyncView] + public var removedNodes: [ReactionButtonAsyncNode] } - public private(set) var buttons: [String: ReactionButtonAsyncView] = [:] + public private(set) var buttons: [String: ReactionButtonAsyncNode] = [:] public init() { } @@ -703,12 +778,12 @@ public final class ReactionButtonsAsyncLayoutContainer { public func update( context: AccountContext, action: @escaping (String) -> Void, - reactions: [ReactionButtonsLayoutContainer.Reaction], + reactions: [ReactionButtonsAsyncLayoutContainer.Reaction], colors: ReactionButtonComponent.Colors, constrainedWidth: CGFloat ) -> Result { var items: [Result.Item] = [] - var applyItems: [(key: String, size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ReactionButtonAsyncView)] = [] + var applyItems: [(key: String, size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ReactionButtonAsyncNode)] = [] var validIds = Set() for reaction in reactions.sorted(by: { lhs, rhs in @@ -737,7 +812,7 @@ public final class ReactionButtonsAsyncLayoutContainer { } } - let viewLayout = ReactionButtonAsyncView.asyncLayout(self.buttons[reaction.reaction.value]) + let viewLayout = ReactionButtonAsyncNode.asyncLayout(self.buttons[reaction.reaction.value]) let (size, apply) = viewLayout(ReactionButtonComponent( context: context, colors: colors, @@ -760,10 +835,10 @@ public final class ReactionButtonsAsyncLayoutContainer { removeIds.append(id) } } - var removedViews: [ReactionButtonAsyncView] = [] + var removedNodes: [ReactionButtonAsyncNode] = [] for id in removeIds { - if let view = self.buttons.removeValue(forKey: id) { - removedViews.append(view) + if let node = self.buttons.removeValue(forKey: id) { + removedNodes.append(node) } } @@ -772,128 +847,18 @@ public final class ReactionButtonsAsyncLayoutContainer { apply: { animation in var items: [ApplyResult.Item] = [] for (key, size, apply) in applyItems { - let view = apply(animation) - items.append(ApplyResult.Item(value: key, view: view, size: size)) + let node = apply(animation) + items.append(ApplyResult.Item(value: key, node: node, size: size)) if let current = self.buttons[key] { - assert(current === view) + assert(current === node) } else { - self.buttons[key] = view + self.buttons[key] = node } } - return ApplyResult(items: items, removedViews: removedViews) + return ApplyResult(items: items, removedNodes: removedNodes) } ) } } - -public final class ReactionButtonsLayoutContainer { - public struct Reaction { - public var reaction: ReactionButtonComponent.Reaction - public var count: Int - public var peers: [EnginePeer] - public var isSelected: Bool - - public init( - reaction: ReactionButtonComponent.Reaction, - count: Int, - peers: [EnginePeer], - isSelected: Bool - ) { - self.reaction = reaction - self.count = count - self.peers = peers - self.isSelected = isSelected - } - } - - public struct Result { - public struct Item { - public var view: ComponentHostView - public var size: CGSize - } - - public var items: [Item] - public var removedViews: [ComponentHostView] - } - - public private(set) var buttons: [String: ComponentHostView] = [:] - - public init() { - } - - public func update( - context: AccountContext, - action: @escaping (String) -> Void, - reactions: [Reaction], - colors: ReactionButtonComponent.Colors, - constrainedWidth: CGFloat, - transition: Transition - ) -> Result { - var items: [Result.Item] = [] - var removedViews: [ComponentHostView] = [] - - var validIds = Set() - for reaction in reactions.sorted(by: { lhs, rhs in - var lhsCount = lhs.count - if lhs.isSelected { - lhsCount -= 1 - } - var rhsCount = rhs.count - if rhs.isSelected { - rhsCount -= 1 - } - if lhsCount != rhsCount { - return lhsCount > rhsCount - } - return lhs.reaction.value < rhs.reaction.value - }) { - validIds.insert(reaction.reaction.value) - - let view: ComponentHostView - var itemTransition = transition - if let current = self.buttons[reaction.reaction.value] { - itemTransition = .immediate - view = current - } else { - view = ComponentHostView() - self.buttons[reaction.reaction.value] = view - } - let itemSize = view.update( - transition: itemTransition, - component: AnyComponent(ReactionButtonComponent( - context: context, - colors: colors, - reaction: reaction.reaction, - avatarPeers: reaction.peers, - count: reaction.count, - isSelected: reaction.isSelected, - action: action - )), - environment: {}, - containerSize: CGSize(width: constrainedWidth, height: 1000.0) - ) - items.append(Result.Item( - view: view, - size: itemSize - )) - } - - var removeIds: [String] = [] - for (id, view) in self.buttons { - if !validIds.contains(id) { - removeIds.append(id) - removedViews.append(view) - } - } - for id in removeIds { - self.buttons.removeValue(forKey: id) - } - - return Result( - items: items, - removedViews: removedViews - ) - } -} diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index 5f7ea8334e..c4bf2af919 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -316,6 +316,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let avatarInset: CGFloat = 12.0 let avatarSpacing: CGFloat = 8.0 let avatarSize: CGFloat = 28.0 + let sideInset: CGFloat = 16.0 let reaction: String? = item.reaction if let reaction = reaction { @@ -336,7 +337,6 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.avatarNode.frame = CGRect(origin: CGPoint(x: avatarInset, y: floor((size.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) self.avatarNode.setPeer(context: self.context, theme: presentationData.theme, peer: item.peer, synchronousLoad: true) - let sideInset: CGFloat = 16.0 self.titleLabelNode.attributedText = NSAttributedString(string: item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.regular(17.0), textColor: presentationData.theme.contextMenu.primaryColor) var maxTextWidth: CGFloat = size.width - avatarInset - avatarSize - avatarSpacing - sideInset if reactionIconNode != nil { @@ -377,6 +377,9 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent private var itemNodes: [Int: ItemNode] = [:] + private var placeholderItemImage: UIImage? + private var placeholderLayers: [Int: SimpleLayer] = [:] + init( context: AccountContext, availableReactions: AvailableReactions?, @@ -393,7 +396,6 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.requestUpdateApparentHeight = requestUpdateApparentHeight self.openPeer = openPeer - self.presentationData = context.sharedContext.currentPresentationData.with({ $0 }) self.listContext = context.engine.messages.messageReactionList(message: message, reaction: reaction) self.state = EngineMessageReactionListContext.State(message: message, reaction: reaction) @@ -423,9 +425,11 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent animateIn = true } strongSelf.state = state - strongSelf.requestUpdate(strongSelf, .immediate) + strongSelf.requestUpdate(strongSelf, animateIn ? .animated(duration: 0.2, curve: .easeInOut) : .immediate) if animateIn { - strongSelf.scrollNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + for (_, itemNode) in strongSelf.itemNodes { + itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } } }) } @@ -438,7 +442,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent if self.ignoreScrolling { return } - self.updateVisibleItems(syncronousLoad: false) + self.updateVisibleItems(animated: false, syncronousLoad: false) if let size = self.currentSize { var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height @@ -452,7 +456,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } } - private func updateVisibleItems(syncronousLoad: Bool) { + private func updateVisibleItems(animated: Bool, syncronousLoad: Bool) { guard let size = self.currentSize else { return } @@ -463,34 +467,50 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -180.0) var validIds = Set() + var validPlaceholderIds = Set() let minVisibleIndex = max(0, Int(floor(visibleBounds.minY / itemHeight))) let maxVisibleIndex = Int(ceil(visibleBounds.maxY / itemHeight)) if minVisibleIndex <= maxVisibleIndex { for index in minVisibleIndex ... maxVisibleIndex { - if index >= self.state.items.count { - break - } - - validIds.insert(index) - - let itemNode: ItemNode - if let current = self.itemNodes[index] { - itemNode = current - } else { - let openPeer = self.openPeer - let peerId = self.state.items[index].peer.id - itemNode = ItemNode(context: self.context, availableReactions: self.availableReactions, action: { - openPeer(peerId) - }) - self.itemNodes[index] = itemNode - self.scrollNode.addSubnode(itemNode) - } - let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: CGFloat(index) * itemHeight), size: CGSize(width: size.width, height: itemHeight)) - itemNode.update(size: itemFrame.size, presentationData: presentationData, item: self.state.items[index], isLast: index == self.state.items.count - 1, syncronousLoad: syncronousLoad) - itemNode.frame = itemFrame + + if index < self.state.items.count { + validIds.insert(index) + + let itemNode: ItemNode + if let current = self.itemNodes[index] { + itemNode = current + } else { + let openPeer = self.openPeer + let peerId = self.state.items[index].peer.id + itemNode = ItemNode(context: self.context, availableReactions: self.availableReactions, action: { + openPeer(peerId) + }) + self.itemNodes[index] = itemNode + self.scrollNode.addSubnode(itemNode) + } + + itemNode.update(size: itemFrame.size, presentationData: presentationData, item: self.state.items[index], isLast: index == self.state.items.count - 1, syncronousLoad: syncronousLoad) + itemNode.frame = itemFrame + } else { + validPlaceholderIds.insert(index) + + let placeholderLayer: SimpleLayer + if let current = self.placeholderLayers[index] { + placeholderLayer = current + } else { + placeholderLayer = SimpleLayer() + if let placeholderItemImage = self.placeholderItemImage { + ASDisplayNodeSetResizableContents(placeholderLayer, placeholderItemImage) + } + self.placeholderLayers[index] = placeholderLayer + self.scrollNode.layer.addSublayer(placeholderLayer) + } + + placeholderLayer.frame = itemFrame + } } } @@ -501,18 +521,71 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent itemNode.removeFromSupernode() } } - for id in removeIds { self.itemNodes.removeValue(forKey: id) } + var removePlaceholderIds: [Int] = [] + for (id, placeholderLayer) in self.placeholderLayers { + if !validPlaceholderIds.contains(id) { + removePlaceholderIds.append(id) + if animated { + placeholderLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak placeholderLayer] _ in + placeholderLayer?.removeFromSuperlayer() + }) + } else { + placeholderLayer.removeFromSuperlayer() + } + } + } + for id in removePlaceholderIds { + self.placeholderLayers.removeValue(forKey: id) + } + if self.state.canLoadMore && maxVisibleIndex >= self.state.items.count - 16 { self.listContext.loadMore() } } - func update(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> (size: CGSize, apparentHeight: CGFloat) { + func update(presentationData: PresentationData, constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> (size: CGSize, apparentHeight: CGFloat) { let itemHeight: CGFloat = 44.0 + + if self.presentationData?.theme !== presentationData.theme { + let sideInset: CGFloat = 40.0 + let avatarInset: CGFloat = 12.0 + let avatarSpacing: CGFloat = 8.0 + let avatarSize: CGFloat = 28.0 + let lineHeight: CGFloat = 8.0 + + let shimmeringForegroundColor: UIColor + let shimmeringColor: UIColor + if presentationData.theme.overallDarkAppearance { + let backgroundColor = presentationData.theme.contextMenu.backgroundColor.blitOver(presentationData.theme.list.plainBackgroundColor, alpha: 1.0) + + shimmeringForegroundColor = presentationData.theme.contextMenu.primaryColor.blitOver(backgroundColor, alpha: 0.1) + shimmeringColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3) + } else { + shimmeringForegroundColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.07) + shimmeringColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3) + } + let _ = shimmeringColor + + self.placeholderItemImage = generateImage(CGSize(width: avatarInset + avatarSize + avatarSpacing + lineHeight + 2.0 + sideInset, height: itemHeight), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(shimmeringForegroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: avatarInset, y: floor((size.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))) + + context.fillEllipse(in: CGRect(origin: CGPoint(x: avatarInset + avatarSize + avatarSpacing, y: floor((size.height - lineHeight) / 2.0)), size: CGSize(width: lineHeight + 2.0, height: lineHeight))) + })?.stretchableImage(withLeftCapWidth: Int(avatarInset + avatarSize + avatarSpacing + lineHeight / 2.0 + 1.0), topCapHeight: 0) + + if let placeholderItemImage = self.placeholderItemImage { + for (_, placeholderLayer) in self.placeholderLayers { + ASDisplayNodeSetResizableContents(placeholderLayer, placeholderItemImage) + } + } + } + self.presentationData = presentationData + let size = CGSize(width: constrainedSize.width, height: CGFloat(self.state.totalCount) * itemHeight) let containerSize = CGSize(width: size.width, height: min(constrainedSize.height, size.height)) @@ -528,7 +601,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } self.ignoreScrolling = false - self.updateVisibleItems(syncronousLoad: !transition.isAnimated) + self.updateVisibleItems(animated: transition.isAnimated, syncronousLoad: !transition.isAnimated) var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height apparentHeight = max(apparentHeight, 44.0) @@ -563,9 +636,10 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent context: AccountContext, availableReactions: AvailableReactions?, message: EngineMessage, + reaction: String?, requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void, - back: @escaping () -> Void, + back: (() -> Void)?, openPeer: @escaping (PeerId) -> Void ) { self.context = context @@ -579,20 +653,26 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent var requestUpdateTab: ((ReactionsTabNode, ContainedViewLayoutTransition) -> Void)? var requestUpdateTabApparentHeight: ((ReactionsTabNode, ContainedViewLayoutTransition) -> Void)? - self.backButtonNode = BackButtonNode() - self.backButtonNode?.action = { - back() + if let back = back { + self.backButtonNode = BackButtonNode() + self.backButtonNode?.action = { + back() + } } var reactions: [(String?, Int)] = [] var totalCount: Int = 0 if let reactionsAttribute = message._asMessage().reactionsAttribute { - for reaction in reactionsAttribute.reactions { - totalCount += Int(reaction.count) - reactions.append((reaction.value, Int(reaction.count))) + for listReaction in reactionsAttribute.reactions { + if reaction == nil || listReaction.value == reaction { + totalCount += Int(listReaction.count) + reactions.append((listReaction.value, Int(listReaction.count))) + } } } - reactions.insert((nil, totalCount), at: 0) + if reaction == nil { + reactions.insert((nil, totalCount), at: 0) + } if reactions.count > 2 { self.tabListNode = ReactionTabListNode(context: context, availableReactions: availableReactions, reactions: reactions, message: message) @@ -600,13 +680,11 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.reactions = reactions - self.separatorNode = ASDisplayNode() - self.currentTabNode = ReactionsTabNode( context: context, availableReactions: availableReactions, message: message, - reaction: nil, + reaction: reaction, requestUpdate: { tab, transition in requestUpdateTab?(tab, transition) }, @@ -620,6 +698,10 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent super.init() + if self.backButtonNode != nil || self.tabListNode != nil { + self.separatorNode = ASDisplayNode() + } + if let backButtonNode = self.backButtonNode { self.addSubnode(backButtonNode) } @@ -677,12 +759,12 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } } - func update(constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat) { + func update(presentationData: PresentationData, constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat) { let constrainedSize = CGSize(width: min(260.0, constrainedWidth), height: maxHeight) var topContentHeight: CGFloat = 0.0 if let backButtonNode = self.backButtonNode { - let backButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: 45.0)) + let backButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: 44.0)) backButtonNode.update(size: backButtonFrame.size, presentationData: self.presentationData, isLast: self.tabListNode == nil) transition.updateFrame(node: backButtonNode, frame: backButtonFrame) topContentHeight += backButtonFrame.height @@ -704,7 +786,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent if self.currentTabNode.bounds.isEmpty { currentTabTransition = .immediate } - let currentTabLayout = self.currentTabNode.update(constrainedSize: CGSize(width: constrainedSize.width, height: constrainedSize.height - topContentHeight), transition: currentTabTransition) + let currentTabLayout = self.currentTabNode.update(presentationData: presentationData, constrainedSize: CGSize(width: constrainedSize.width, height: constrainedSize.height - topContentHeight), transition: currentTabTransition) currentTabTransition.updateFrame(node: self.currentTabNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: currentTabLayout.size.width, height: currentTabLayout.size.height + 100.0))) if let dismissedTabNode = self.dismissedTabNode { @@ -734,13 +816,15 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let context: AccountContext let availableReactions: AvailableReactions? let message: EngineMessage - let back: () -> Void + let reaction: String? + let back: (() -> Void)? let openPeer: (PeerId) -> Void - public init(context: AccountContext, availableReactions: AvailableReactions?, message: EngineMessage, back: @escaping () -> Void, openPeer: @escaping (PeerId) -> Void) { + public init(context: AccountContext, availableReactions: AvailableReactions?, message: EngineMessage, reaction: String?, back: (() -> Void)?, openPeer: @escaping (PeerId) -> Void) { self.context = context self.availableReactions = availableReactions self.message = message + self.reaction = reaction self.back = back self.openPeer = openPeer } @@ -753,6 +837,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent context: self.context, availableReactions: self.availableReactions, message: self.message, + reaction: self.reaction, requestUpdate: requestUpdate, requestUpdateApparentHeight: requestUpdateApparentHeight, back: self.back, diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 8a9a1ab98a..481ae15f1e 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -2033,6 +2033,7 @@ public protocol ContextExtractedContentSource: AnyObject { var keepInPlace: Bool { get } var ignoreContentTouches: Bool { get } var blurBackground: Bool { get } + var centerActionsHorizontally: Bool { get } var shouldBeDismissed: Signal { get } func takeView() -> ContextControllerTakeViewInfo? @@ -2043,6 +2044,10 @@ public extension ContextExtractedContentSource { var centerVertically: Bool { return false } + + var centerActionsHorizontally: Bool { + return false + } var shouldBeDismissed: Signal { return .single(false) @@ -2076,7 +2081,7 @@ public enum ContextContentSource { } public protocol ContextControllerItemsNode: ASDisplayNode { - func update(constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat) + func update(presentationData: PresentationData, constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat) var apparentHeight: CGFloat { get } } diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 2cc4332b79..13d67fbeba 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -503,6 +503,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta transition: ContainedViewLayoutTransition ) -> (size: CGSize, apparentHeight: CGFloat) { let contentLayout = self.contentNode.update( + presentationData: presentationData, constrainedWidth: constrainedSize.width, maxHeight: constrainedSize.height, bottomInset: 0.0, @@ -555,18 +556,65 @@ func makeContextControllerActionsStackItem(items: ContextController.Items) -> Co final class ContextControllerActionsStackNode: ASDisplayNode { final class NavigationContainer: ASDisplayNode { + var requestUpdate: ((ContainedViewLayoutTransition) -> Void)? + var requestPop: (() -> Void)? + var transitionFraction: CGFloat = 0.0 + + private var panRecognizer: UIPanGestureRecognizer? + + var isNavigationEnabled: Bool = false { + didSet { + self.panRecognizer?.isEnabled = self.isNavigationEnabled + } + } + override init() { super.init() self.clipsToBounds = true self.cornerRadius = 14.0 + + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + self.panRecognizer = panRecognizer + self.view.addGestureRecognizer(panRecognizer) + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + self.transitionFraction = 0.0 + case .changed: + let distanceFactor: CGFloat = recognizer.translation(in: self.view).x / self.bounds.width + let transitionFraction = max(0.0, min(1.0, distanceFactor)) + if self.transitionFraction != transitionFraction { + self.transitionFraction = transitionFraction + self.requestUpdate?(.immediate) + } + case .ended, .cancelled: + let distanceFactor: CGFloat = recognizer.translation(in: self.view).x / self.bounds.width + let transitionFraction = max(0.0, min(1.0, distanceFactor)) + if transitionFraction > 0.2 { + self.transitionFraction = 0.0 + self.requestPop?() + } else { + self.transitionFraction = 0.0 + self.requestUpdate?(.animated(duration: 0.45, curve: .spring)) + } + default: + break + } + } + + func update(presentationData: PresentationData, size: CGSize, transition: ContainedViewLayoutTransition) { } } final class ItemContainer: ASDisplayNode { let requestUpdate: (ContainedViewLayoutTransition) -> Void let node: ContextControllerActionsStackItemNode + let dimNode: ASDisplayNode let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])? + var storedScrollingState: CGFloat? let positionLock: CGFloat? init( @@ -586,18 +634,24 @@ final class ContextControllerActionsStackNode: ASDisplayNode { requestUpdateApparentHeight: requestUpdateApparentHeight ) + self.dimNode = ASDisplayNode() + self.dimNode.isUserInteractionEnabled = false + self.dimNode.alpha = 0.0 + self.reactionItems = reactionItems self.positionLock = positionLock super.init() self.addSubnode(self.node) + self.addSubnode(self.dimNode) } func update( presentationData: PresentationData, constrainedSize: CGSize, standardWidth: CGFloat, + transitionFraction: CGFloat, transition: ContainedViewLayoutTransition ) -> (size: CGSize, apparentHeight: CGFloat) { let (size, apparentHeight) = self.node.update( @@ -606,10 +660,24 @@ final class ContextControllerActionsStackNode: ASDisplayNode { standardWidth: standardWidth, transition: transition ) - transition.updateFrame(node: self.node, frame: CGRect(origin: CGPoint(), size: size)) + + let maxScaleOffset: CGFloat = 10.0 + let scaleOffset: CGFloat = 0.0 * transitionFraction + maxScaleOffset * (1.0 - transitionFraction) + let scale: CGFloat = (size.width - scaleOffset) / size.width + let yOffset: CGFloat = size.height * (1.0 - scale) + transition.updatePosition(node: self.node, position: CGPoint(x: size.width / 2.0 + scaleOffset / 2.0, y: size.height / 2.0 - yOffset / 2.0)) + transition.updateBounds(node: self.node, bounds: CGRect(origin: CGPoint(), size: size)) + transition.updateTransformScale(node: self.node, scale: scale) return (size, apparentHeight) } + + func updateDimNode(presentationData: PresentationData, size: CGSize, transitionFraction: CGFloat, transition: ContainedViewLayoutTransition) { + self.dimNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor + + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateAlpha(node: self.dimNode, alpha: 1.0 - transitionFraction) + } } private let getController: () -> ContextControllerProtocol? @@ -628,6 +696,10 @@ final class ContextControllerActionsStackNode: ASDisplayNode { return self.itemContainers.last?.positionLock } + var storedScrollingState: CGFloat? { + return self.itemContainers.last?.storedScrollingState + } + init( getController: @escaping () -> ContextControllerProtocol?, requestDismiss: @escaping (ContextMenuActionResult) -> Void, @@ -642,6 +714,20 @@ final class ContextControllerActionsStackNode: ASDisplayNode { super.init() self.addSubnode(self.navigationContainer) + + self.navigationContainer.requestUpdate = { [weak self] transition in + guard let strongSelf = self else { + return + } + strongSelf.requestUpdate(transition) + } + + self.navigationContainer.requestPop = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.pop() + } } func replace(item: ContextControllerActionsStackItem, animated: Bool) { @@ -653,11 +739,15 @@ final class ContextControllerActionsStackNode: ASDisplayNode { } } self.itemContainers.removeAll() + self.navigationContainer.isNavigationEnabled = self.itemContainers.count > 1 - self.push(item: item, positionLock: nil, animated: animated) + self.push(item: item, currentScrollingState: nil, positionLock: nil, animated: animated) } - func push(item: ContextControllerActionsStackItem, positionLock: CGFloat?, animated: Bool) { + func push(item: ContextControllerActionsStackItem, currentScrollingState: CGFloat?, positionLock: CGFloat?, animated: Bool) { + if let itemContainer = self.itemContainers.last { + itemContainer.storedScrollingState = currentScrollingState + } let itemContainer = ItemContainer( getController: self.getController, requestDismiss: self.requestDismiss, @@ -674,6 +764,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { ) self.itemContainers.append(itemContainer) self.navigationContainer.addSubnode(itemContainer) + self.navigationContainer.isNavigationEnabled = self.itemContainers.count > 1 let transition: ContainedViewLayoutTransition if animated { @@ -684,6 +775,10 @@ final class ContextControllerActionsStackNode: ASDisplayNode { self.requestUpdate(transition) } + func clearStoredScrollingState() { + self.itemContainers.last?.storedScrollingState = nil + } + func pop() { if self.itemContainers.count == 1 { //dismiss @@ -693,6 +788,8 @@ final class ContextControllerActionsStackNode: ASDisplayNode { self.dismissingItemContainers.append((itemContainer, true)) } + self.navigationContainer.isNavigationEnabled = self.itemContainers.count > 1 + let transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring) self.requestUpdate(transition) } @@ -706,8 +803,17 @@ final class ContextControllerActionsStackNode: ASDisplayNode { let animateAppearingContainers = transition.isAnimated && !self.dismissingItemContainers.isEmpty + struct ItemLayout { + var size: CGSize + var apparentHeight: CGFloat + var transitionFraction: CGFloat + var alphaTransitionFraction: CGFloat + var itemTransition: ContainedViewLayoutTransition + var animateAppearingContainer: Bool + } + var topItemSize = CGSize() - var topItemApparentHeight: CGFloat = 0.0 + var itemLayouts: [ItemLayout] = [] for i in 0 ..< self.itemContainers.count { let itemContainer = self.itemContainers[i] @@ -720,31 +826,77 @@ final class ContextControllerActionsStackNode: ASDisplayNode { let itemConstrainedHeight: CGFloat = constrainedSize.height + let transitionFraction: CGFloat + let alphaTransitionFraction: CGFloat + if i == self.itemContainers.count - 1 { + transitionFraction = self.navigationContainer.transitionFraction + alphaTransitionFraction = 1.0 + } else if i == self.itemContainers.count - 2 { + transitionFraction = self.navigationContainer.transitionFraction - 1.0 + alphaTransitionFraction = self.navigationContainer.transitionFraction + } else { + transitionFraction = 0.0 + alphaTransitionFraction = 0.0 + } + let itemSize = itemContainer.update( presentationData: presentationData, constrainedSize: CGSize(width: constrainedSize.width, height: itemConstrainedHeight), - standardWidth: 260.0, + standardWidth: 250.0, + transitionFraction: alphaTransitionFraction, transition: itemContainerTransition ) if i == self.itemContainers.count - 1 { topItemSize = itemSize.size - topItemApparentHeight = itemSize.apparentHeight } - let itemFrame: CGRect - if i == self.itemContainers.count - 1 { - itemFrame = CGRect(origin: CGPoint(), size: itemSize.size) - } else { - itemFrame = CGRect(origin: CGPoint(x: -itemSize.size.width, y: 0.0), size: itemSize.size) - } - - itemContainerTransition.updateFrame(node: itemContainer, frame: itemFrame) - if animateAppearingContainer { - transition.animatePositionAdditive(node: itemContainer, offset: CGPoint(x: itemContainer.bounds.width, y: 0.0)) - } + itemLayouts.append(ItemLayout( + size: itemSize.size, + apparentHeight: itemSize.apparentHeight, + transitionFraction: transitionFraction, + alphaTransitionFraction: alphaTransitionFraction, + itemTransition: itemContainerTransition, + animateAppearingContainer: animateAppearingContainer + )) } - transition.updateFrame(node: self.navigationContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: topItemSize.width, height: max(44.0, topItemApparentHeight)))) + let topItemApparentHeight: CGFloat + let topItemWidth: CGFloat + if itemLayouts.isEmpty { + topItemApparentHeight = 0.0 + topItemWidth = 0.0 + } else if itemLayouts.count == 1 { + topItemApparentHeight = itemLayouts[0].apparentHeight + topItemWidth = itemLayouts[0].size.width + } else { + let lastItemLayout = itemLayouts[itemLayouts.count - 1] + let previousItemLayout = itemLayouts[itemLayouts.count - 2] + let transitionFraction = self.navigationContainer.transitionFraction + + topItemApparentHeight = lastItemLayout.apparentHeight * (1.0 - transitionFraction) + previousItemLayout.apparentHeight * transitionFraction + topItemWidth = lastItemLayout.size.width * (1.0 - transitionFraction) + previousItemLayout.size.width * transitionFraction + } + + let navigationContainerFrame = CGRect(origin: CGPoint(), size: CGSize(width: topItemWidth, height: max(14 * 2.0, topItemApparentHeight))) + transition.updateFrame(node: self.navigationContainer, frame: navigationContainerFrame) + self.navigationContainer.update(presentationData: presentationData, size: navigationContainerFrame.size, transition: transition) + + for i in 0 ..< self.itemContainers.count { + let xOffset: CGFloat + if itemLayouts[i].transitionFraction < 0.0 { + xOffset = itemLayouts[i].transitionFraction * itemLayouts[i].size.width + } else { + xOffset = itemLayouts[i].transitionFraction * topItemWidth + } + let itemFrame = CGRect(origin: CGPoint(x: xOffset, y: 0.0), size: itemLayouts[i].size) + + itemLayouts[i].itemTransition.updateFrame(node: self.itemContainers[i], frame: itemFrame) + if itemLayouts[i].animateAppearingContainer { + transition.animatePositionAdditive(node: self.itemContainers[i], offset: CGPoint(x: itemFrame.width, y: 0.0)) + } + + self.itemContainers[i].updateDimNode(presentationData: presentationData, size: CGSize(width: itemLayouts[i].size.width, height: navigationContainerFrame.size.height), transitionFraction: itemLayouts[i].alphaTransitionFraction, transition: transition) + } for (itemContainer, isPopped) in self.dismissingItemContainers { transition.updatePosition(node: itemContainer, position: CGPoint(x: isPopped ? itemContainer.bounds.width * 3.0 / 2.0 : -itemContainer.bounds.width / 2.0, y: itemContainer.position.y), completion: { [weak itemContainer] _ in @@ -753,6 +905,6 @@ final class ContextControllerActionsStackNode: ASDisplayNode { } self.dismissingItemContainers.removeAll() - return CGSize(width: topItemSize.width, height: topItemSize.height) + return CGSize(width: topItemWidth, height: topItemSize.height) } } diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 6f1489864f..efa227ca95 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -137,6 +137,20 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo return result } } + + if !self.source.ignoreContentTouches, let contentNode = self.contentNode { + let contentPoint = self.view.convert(point, to: contentNode.containingNode.contentNode.view) + if let result = contentNode.containingNode.contentNode.customHitTest?(contentPoint) { + return result + } else if let result = contentNode.containingNode.contentNode.hitTest(contentPoint, with: event) { + if result is TextSelectionNodeView { + return result + } else if contentNode.containingNode.contentRect.contains(contentPoint) { + return contentNode.containingNode.contentNode.view + } + } + } + return self.scrollNode.hitTest(self.view.convert(point, to: self.scrollNode.view), with: event) } else { return nil @@ -148,16 +162,21 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } func pushItems(items: ContextController.Items) { + let currentScrollingState = self.getCurrentScrollingState() let positionLock = self.getActionsStackPositionLock() - self.actionsStackNode.push(item: makeContextControllerActionsStackItem(items: items), positionLock: positionLock, animated: true) + self.actionsStackNode.push(item: makeContextControllerActionsStackItem(items: items), currentScrollingState: currentScrollingState, positionLock: positionLock, animated: true) } func popItems() { self.actionsStackNode.pop() } + private func getCurrentScrollingState() -> CGFloat { + return self.scrollNode.view.contentOffset.y + } + private func getActionsStackPositionLock() -> CGFloat? { - return self.actionsStackNode.frame.minY + return self.actionsStackNode.view.convert(CGPoint(), to: self.view).y } func update( @@ -166,7 +185,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo transition: ContainedViewLayoutTransition, stateTransition: ContextControllerPresentationNodeStateTransition? ) { - let contentActionsSpacing: CGFloat = 8.0 + let contentActionsSpacing: CGFloat = 7.0 + let actionsEdgeInset: CGFloat = 12.0 let actionsSideInset: CGFloat = 6.0 let topInset: CGFloat = layout.insets(options: .statusBar).top + 8.0 let bottomInset: CGFloat = 10.0 @@ -236,7 +256,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo contentNode.storedGlobalFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view) } } - //let contentRectGlobalFrame = contentNode.storedGlobalFrame ?? convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view) + + let contentParentGlobalFrame = convertFrame(contentNode.containingNode.bounds, from: contentNode.containingNode.view, to: self.view) + let contentRectGlobalFrame = CGRect(origin: CGPoint(x: contentNode.containingNode.contentRect.minX, y: (contentNode.storedGlobalFrame?.maxY ?? 0.0) - contentNode.containingNode.contentRect.height), size: contentNode.containingNode.contentRect.size) var contentRect = CGRect(origin: CGPoint(x: contentRectGlobalFrame.minX, y: contentRectGlobalFrame.maxY - contentNode.containingNode.contentRect.size.height), size: contentNode.containingNode.contentRect.size) if case .animateOut = stateTransition { @@ -255,7 +277,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if let actionsPositionLock = self.actionsStackNode.topPositionLock { actionsConstrainedHeight = layout.size.height - bottomInset - layout.intrinsicInsets.bottom - actionsPositionLock } else { - actionsConstrainedHeight = layout.size.height + actionsConstrainedHeight = layout.size.height - contentTopInset - contentRect.height - contentActionsSpacing - bottomInset - layout.intrinsicInsets.bottom } let actionsSize = self.actionsStackNode.update( @@ -266,18 +288,23 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if case .animateOut = stateTransition { } else { - if contentRect.minY < contentTopInset { - contentRect.origin.y = contentTopInset + if let topPositionLock = self.actionsStackNode.topPositionLock { + contentRect.origin.y = topPositionLock - contentActionsSpacing - contentRect.height + } else if self.source.keepInPlace { + } else { + if contentRect.minY < contentTopInset { + contentRect.origin.y = contentTopInset + } + var combinedBounds = CGRect(origin: CGPoint(x: 0.0, y: contentRect.minY), size: CGSize(width: layout.size.width, height: contentRect.height + contentActionsSpacing + actionsSize.height)) + if combinedBounds.maxY > layout.size.height - bottomInset - layout.intrinsicInsets.bottom { + combinedBounds.origin.y = layout.size.height - bottomInset - layout.intrinsicInsets.bottom - combinedBounds.height + } + if combinedBounds.minY < contentTopInset { + combinedBounds.origin.y = contentTopInset + } + + contentRect.origin.y = combinedBounds.minY } - var combinedBounds = CGRect(origin: CGPoint(x: 0.0, y: contentRect.minY), size: CGSize(width: layout.size.width, height: contentRect.height + contentActionsSpacing + actionsSize.height)) - if combinedBounds.maxY > layout.size.height - bottomInset - layout.intrinsicInsets.bottom { - combinedBounds.origin.y = layout.size.height - bottomInset - layout.intrinsicInsets.bottom - combinedBounds.height - } - if combinedBounds.minY < contentTopInset { - combinedBounds.origin.y = contentTopInset - } - - contentRect.origin.y = combinedBounds.minY } if let reactionContextNode = self.reactionContextNode { @@ -297,22 +324,51 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo transition.updateFrame(node: self.contentRectDebugNode, frame: contentRect) - var actionsFrame = CGRect(origin: CGPoint(x: 0.0, y: contentRect.maxY + contentActionsSpacing), size: actionsSize) - if contentRect.midX < layout.size.width / 2.0 { - actionsFrame.origin.x = contentRect.minX + actionsSideInset - 4.0 + var actionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: contentRect.maxY + contentActionsSpacing), size: actionsSize) + if self.source.keepInPlace { + actionsFrame.origin.y = contentRect.minY - contentActionsSpacing - actionsFrame.height + } + if self.source.centerActionsHorizontally { + actionsFrame.origin.x = floor(contentParentGlobalFrame.minX + contentRect.midX - actionsFrame.width / 2.0) + if actionsFrame.maxX > layout.size.width - actionsEdgeInset { + actionsFrame.origin.x = layout.size.width - actionsEdgeInset - actionsFrame.width + } + if actionsFrame.minX < actionsEdgeInset { + actionsFrame.origin.x = actionsEdgeInset + } } else { - actionsFrame.origin.x = contentRect.maxX - actionsSideInset - actionsSize.width - 1.0 + if contentRect.midX < layout.size.width / 2.0 { + actionsFrame.origin.x = contentParentGlobalFrame.minX + contentRect.minX + actionsSideInset - 4.0 + } else { + actionsFrame.origin.x = contentParentGlobalFrame.minX + contentRect.maxX - actionsSideInset - actionsSize.width - 1.0 + } + if actionsFrame.maxX > layout.size.width - actionsEdgeInset { + actionsFrame.origin.x = layout.size.width - actionsEdgeInset - actionsFrame.width + } + if actionsFrame.minX < actionsEdgeInset { + actionsFrame.origin.x = actionsEdgeInset + } } transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame) - contentTransition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: contentRect.minX - contentNode.containingNode.contentRect.minX, y: contentRect.minY - contentNode.containingNode.contentRect.minY), size: contentNode.containingNode.bounds.size)) + contentTransition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingNode.contentRect.minX, y: contentRect.minY - contentNode.containingNode.contentRect.minY), size: contentNode.containingNode.bounds.size)) - let contentHeight = actionsFrame.maxY + bottomInset + layout.intrinsicInsets.bottom + let contentHeight: CGFloat + if self.actionsStackNode.topPositionLock != nil { + contentHeight = layout.size.height + } else { + contentHeight = actionsFrame.maxY + bottomInset + layout.intrinsicInsets.bottom + } let contentSize = CGSize(width: layout.size.width, height: contentHeight) if self.scrollNode.view.contentSize != contentSize { let previousContentOffset = self.scrollNode.view.contentOffset self.scrollNode.view.contentSize = contentSize + if let storedScrollingState = self.actionsStackNode.storedScrollingState { + self.actionsStackNode.clearStoredScrollingState() + + self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: storedScrollingState) + } if case .none = stateTransition, transition.isAnimated { let contentOffset = self.scrollNode.view.contentOffset transition.animateOffsetAdditive(layer: self.scrollNode.layer, offset: previousContentOffset.y - contentOffset.y) @@ -370,7 +426,13 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo let actionsSize = self.actionsStackNode.bounds.size let actionsPositionDeltaXDistance: CGFloat = 0.0 - let actionsPositionDeltaYDistance = -animationInContentDistance - actionsSize.height / 2.0 - contentActionsSpacing + let actionsVerticalTransitionDirection: CGFloat + if contentNode.frame.minY < self.actionsStackNode.frame.minY { + actionsVerticalTransitionDirection = -1.0 + } else { + actionsVerticalTransitionDirection = 1.0 + } + let actionsPositionDeltaYDistance = -animationInContentDistance + actionsVerticalTransitionDirection * actionsSize.height / 2.0 - contentActionsSpacing self.actionsStackNode.layer.animateSpring( from: NSValue(cgPoint: CGPoint(x: actionsPositionDeltaXDistance, y: actionsPositionDeltaYDistance)), to: NSValue(cgPoint: CGPoint()), @@ -436,7 +498,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo public var updateDistractionFreeMode: ((Bool) -> Void)? public var requestDismiss: (() -> Void)*/ case let .animateOut(result, completion): - let duration: Double = 0.25 + let duration: Double = self.reactionContextNodeIsAnimatingOut ? 0.25 : 0.2 let putBackInfo = self.source.putBack() @@ -463,11 +525,17 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) } - print("animationInContentDistance: \(animationInContentDistance)") + let actionsVerticalTransitionDirection: CGFloat + if contentNode.frame.minY < self.actionsStackNode.frame.minY { + actionsVerticalTransitionDirection = -1.0 + } else { + actionsVerticalTransitionDirection = 1.0 + } contentNode.containingNode.willUpdateIsExtractedToContextPreview?(false, transition) contentNode.offsetContainerNode.position = contentNode.offsetContainerNode.position.offsetBy(dx: 0.0, dy: -animationInContentDistance) + let reactionContextNodeIsAnimatingOut = self.reactionContextNodeIsAnimatingOut contentNode.offsetContainerNode.layer.animate( from: animationInContentDistance as NSNumber, to: 0.0 as NSNumber, @@ -477,7 +545,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo delay: 0.0, additive: true, completion: { [weak self] _ in - Queue.mainQueue().after(0.2 * UIView.animationDurationFactor(), { + Queue.mainQueue().after(reactionContextNodeIsAnimatingOut ? 0.2 * UIView.animationDurationFactor() : 0.0, { contentNode.containingNode.isExtractedToContextPreview = false contentNode.containingNode.isExtractedToContextPreviewUpdated?(false) @@ -489,16 +557,6 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo }) } ) - /*Queue.mainQueue().after((duration + 0.2) * UIView.animationDurationFactor(), { [weak self] in - contentNode.containingNode.isExtractedToContextPreview = false - contentNode.containingNode.isExtractedToContextPreviewUpdated?(false) - - if let strongSelf = self, let contentNode = strongSelf.contentNode { - contentNode.containingNode.addSubnode(contentNode.containingNode.contentNode) - } - - completion() - })*/ self.actionsStackNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) self.actionsStackNode.layer.animate( @@ -514,7 +572,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo let actionsSize = self.actionsStackNode.bounds.size let actionsPositionDeltaXDistance: CGFloat = 0.0 - let actionsPositionDeltaYDistance = -animationInContentDistance - actionsSize.height / 2.0 - contentActionsSpacing + let actionsPositionDeltaYDistance = -animationInContentDistance + actionsVerticalTransitionDirection * actionsSize.height / 2.0 - contentActionsSpacing self.actionsStackNode.layer.animate( from: NSValue(cgPoint: CGPoint()), to: NSValue(cgPoint: CGPoint(x: actionsPositionDeltaXDistance, y: actionsPositionDeltaYDistance)), diff --git a/submodules/Display/Source/ContextControllerSourceNode.swift b/submodules/Display/Source/ContextControllerSourceNode.swift index 08735ca8a9..1cadfe7ba7 100644 --- a/submodules/Display/Source/ContextControllerSourceNode.swift +++ b/submodules/Display/Source/ContextControllerSourceNode.swift @@ -1,7 +1,7 @@ import Foundation import AsyncDisplayKit -public final class ContextControllerSourceNode: ASDisplayNode { +open class ContextControllerSourceNode: ASDisplayNode { private var contextGesture: ContextGesture? public var isGestureEnabled: Bool = true { @@ -14,6 +14,7 @@ public final class ContextControllerSourceNode: ASDisplayNode { public var activated: ((ContextGesture, CGPoint) -> Void)? public var shouldBegin: ((CGPoint) -> Bool)? public var customActivationProgress: ((CGFloat, ContextGestureTransition) -> Void)? + public weak var additionalActivationProgressLayer: CALayer? public var targetNodeForActivationProgress: ASDisplayNode? public var targetNodeForActivationProgressContentRect: CGRect? @@ -23,7 +24,7 @@ public final class ContextControllerSourceNode: ASDisplayNode { self.contextGesture?.isEnabled = self.isGestureEnabled } - override public func didLoad() { + override open func didLoad() { super.didLoad() let contextGesture = ContextGesture(target: self, action: nil) @@ -75,15 +76,27 @@ public final class ContextControllerSourceNode: ASDisplayNode { case .update: let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0) targetNode.layer.sublayerTransform = sublayerTransform + if let additionalActivationProgressLayer = strongSelf.additionalActivationProgressLayer { + additionalActivationProgressLayer.transform = sublayerTransform + } case .begin: let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0) targetNode.layer.sublayerTransform = sublayerTransform + if let additionalActivationProgressLayer = strongSelf.additionalActivationProgressLayer { + additionalActivationProgressLayer.transform = sublayerTransform + } case .ended: let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0) let previousTransform = targetNode.layer.sublayerTransform targetNode.layer.sublayerTransform = sublayerTransform targetNode.layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: sublayerTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2) + + if let additionalActivationProgressLayer = strongSelf.additionalActivationProgressLayer { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: { + additionalActivationProgressLayer.transform = sublayerTransform + }) + } } } } diff --git a/submodules/Display/Source/SimpleLayer.swift b/submodules/Display/Source/SimpleLayer.swift new file mode 100644 index 0000000000..43f6e3520c --- /dev/null +++ b/submodules/Display/Source/SimpleLayer.swift @@ -0,0 +1,26 @@ +import UIKit + +private final class NullActionClass: NSObject, CAAction { + @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { + } +} + +private let nullAction = NullActionClass() + +open class SimpleLayer: CALayer { + override open func action(forKey event: String) -> CAAction? { + return nullAction + } + + override public init() { + super.init() + } + + override public init(layer: Any) { + super.init(layer: layer) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 31b9dc9796..a569b6ce2a 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -84,8 +84,7 @@ public final class HashtagSearchController: TelegramBaseController { let listInteraction = ListMessageItemInteraction(openMessage: { message, mode -> Bool in return true - }, openMessageContextMenu: { message, bool, node, rect, gesture in - + }, openMessageContextMenu: { message, bool, node, rect, gesture in }, toggleMessagesSelection: { messageId, selected in }, openUrl: { url, _, _, message in }, openInstantPage: { message, data in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index e90149c97e..508e71d359 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -366,7 +366,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee .withUpdatedCallJoinPeerId(groupCallDefaultJoinAs?.peerId) .withUpdatedThemeEmoticon(chatFullThemeEmoticon) .withUpdatedInviteRequestsPending(chatFullRequestsPending) - .withUpdatedAllowedReactions(allowedReactions) + .withUpdatedAllowedReactions(allowedReactions ?? []) }) case .channelFull: break @@ -597,7 +597,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee .withUpdatedThemeEmoticon(themeEmoticon) .withUpdatedInviteRequestsPending(requestsPending) .withUpdatedSendAsPeerId(sendAsPeerId) - .withUpdatedAllowedReactions(allowedReactions) + .withUpdatedAllowedReactions(allowedReactions ?? []) }) if let minAvailableMessageId = minAvailableMessageId, minAvailableMessageIdUpdated { diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift index 981e8f9f82..781c2a4f4d 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift @@ -173,6 +173,20 @@ public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, edit animateMessageColors: animateBubbleColors, message: chat.message.withUpdated( incoming: chat.message.incoming.withUpdated( + bubble: chat.message.outgoing.bubble.withUpdated( + withWallpaper: chat.message.incoming.bubble.withWallpaper.withUpdated( + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.07), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) + ), + withoutWallpaper: chat.message.incoming.bubble.withoutWallpaper.withUpdated( + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.07), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) + ) + ), linkTextColor: accentColor, linkHighlightColor: accentColor?.withAlphaComponent(0.5), accentTextColor: accentColor, @@ -200,12 +214,20 @@ public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, edit withWallpaper: chat.message.outgoing.bubble.withWallpaper.withUpdated( fill: outgoingBubbleFillColors, highlightedFill: outgoingBubbleFillColors?.first?.withMultipliedBrightnessBy(1.421), - stroke: .clear + stroke: .clear, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.12), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff), + reactionActiveForeground: UIColor(rgb: 0x000000, alpha: 0.0) ), withoutWallpaper: chat.message.outgoing.bubble.withoutWallpaper.withUpdated( fill: outgoingBubbleFillColors, highlightedFill: outgoingBubbleFillColors?.first?.withMultipliedBrightnessBy(1.421), - stroke: .clear + stroke: .clear, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.12), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff), + reactionActiveForeground: UIColor(rgb: 0x000000, alpha: 0.0) ) ), primaryTextColor: outgoingPrimaryTextColor, @@ -223,6 +245,20 @@ public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, edit fileDurationColor: outgoingSecondaryTextColor, polls: chat.message.outgoing.polls.withUpdated(radioButton: outgoingPrimaryTextColor, radioProgress: outgoingPrimaryTextColor, highlight: outgoingPrimaryTextColor?.withAlphaComponent(0.12), separator: outgoingSecondaryTextColor, bar: outgoingPrimaryTextColor) ), + freeform: chat.message.freeform.withUpdated( + withWallpaper: chat.message.freeform.withWallpaper.withUpdated( + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.12), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) + ), + withoutWallpaper: chat.message.freeform.withoutWallpaper.withUpdated( + reactionInactiveBackground: chat.message.incoming.bubble.withoutWallpaper.fill.last, + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) + ) + ), infoLinkTextColor: accentColor, outgoingCheckColor: outgoingCheckColor, selectionControlColors: chat.message.selectionControlColors.withUpdated(fillColor: accentColor, foregroundColor: badgeTextColor) @@ -441,9 +477,78 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati let incomingBubbleAlpha: CGFloat = 0.9 let message = PresentationThemeChatMessage( - incoming: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x1D1D1D, alpha: incomingBubbleAlpha)], highlightedFill: UIColor(rgb: 0x353539), stroke: UIColor(rgb: 0x262628), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x1D1D1D, alpha: incomingBubbleAlpha)], highlightedFill: UIColor(rgb: 0x353539), stroke: UIColor(rgb: 0x262628), shadow: nil)), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.4), mediaControlInnerBackgroundColor: UIColor(rgb: 0x262628), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x1f1f1f).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0x737373), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff, alpha: 0.12), separator: UIColor(rgb: 0x000000), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff)), - outgoing: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x61BCF9), UIColor(rgb: 0x007AFF)], highlightedFill: UIColor(rgb: 0x61BCF9), stroke: .clear, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x61BCF9), UIColor(rgb: 0x007AFF)], highlightedFill: UIColor(rgb: 0x61BCF9), stroke: .clear, shadow: nil)), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaControlInnerBackgroundColor: UIColor(rgb: 0x313131), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x313131).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xffffff), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff).withAlphaComponent(0.12), separator: UIColor(rgb: 0xffffff, alpha: 0.5), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0xffffff), barNegative: UIColor(rgb: 0xffffff)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff)), - freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x1f1f1f)], highlightedFill: UIColor(rgb: 0x2a2a2a), stroke: UIColor(rgb: 0x1f1f1f), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x1f1f1f)], highlightedFill: UIColor(rgb: 0x2a2a2a), stroke: UIColor(rgb: 0x1f1f1f), shadow: nil)), + incoming: PresentationThemePartedColors( + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x1D1D1D, alpha: incomingBubbleAlpha)], + highlightedFill: UIColor(rgb: 0x353539), + stroke: UIColor(rgb: 0x262628), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.1), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x1D1D1D, alpha: incomingBubbleAlpha)], + highlightedFill: UIColor(rgb: 0x353539), + stroke: UIColor(rgb: 0x262628), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.1), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ) + ), + primaryTextColor: UIColor(rgb: 0xffffff), + secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.4), mediaControlInnerBackgroundColor: UIColor(rgb: 0x262628), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x1f1f1f).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0x737373), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff, alpha: 0.12), separator: UIColor(rgb: 0x000000), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff) + ), + outgoing: PresentationThemePartedColors( + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x61BCF9), UIColor(rgb: 0x007AFF)], + highlightedFill: UIColor(rgb: 0x61BCF9), + stroke: .clear, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.1), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x61BCF9), UIColor(rgb: 0x007AFF)], + highlightedFill: UIColor(rgb: 0x61BCF9), + stroke: .clear, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.1), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ) + ), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaControlInnerBackgroundColor: UIColor(rgb: 0x313131), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x313131).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xffffff), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff).withAlphaComponent(0.12), separator: UIColor(rgb: 0xffffff, alpha: 0.5), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0xffffff), barNegative: UIColor(rgb: 0xffffff)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff) + ), + freeform: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x1f1f1f)], + highlightedFill: UIColor(rgb: 0x2a2a2a), + stroke: UIColor(rgb: 0x1f1f1f), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0x1f1f1f), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x1f1f1f)], + highlightedFill: UIColor(rgb: 0x2a2a2a), + stroke: UIColor(rgb: 0x1f1f1f), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0x1f1f1f), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ) + ), infoPrimaryTextColor: UIColor(rgb: 0xffffff), infoLinkTextColor: UIColor(rgb: 0xffffff), outgoingCheckColor: UIColor(rgb: 0xffffff), diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift index c8ea3de526..fe7bcaead4 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift @@ -300,7 +300,7 @@ public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme } let incomingFillColor = mainBackgroundColor?.withMultipliedAlpha(0.9) - + chat = chat.withUpdated( defaultWallpaper: defaultWallpaper, animateMessageColors: animateBubbleColors, @@ -310,12 +310,20 @@ public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme withWallpaper: chat.message.outgoing.bubble.withWallpaper.withUpdated( fill: incomingFillColor.flatMap({ [$0] }), highlightedFill: highlightedIncomingBubbleColor, - stroke: mainBackgroundColor + stroke: mainBackgroundColor, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.07), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) ), withoutWallpaper: chat.message.outgoing.bubble.withoutWallpaper.withUpdated( fill: incomingFillColor.flatMap({ [$0] }), highlightedFill: highlightedIncomingBubbleColor, - stroke: mainBackgroundColor + stroke: mainBackgroundColor, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.07), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) ) ), secondaryTextColor: mainSecondaryTextColor?.withAlphaComponent(0.5), @@ -699,9 +707,76 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres let incomingBubbleAlpha: CGFloat = 0.9 let message = PresentationThemeChatMessage( - incoming: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [incomingFillColor.withAlphaComponent(incomingBubbleAlpha)], highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [incomingFillColor.withAlphaComponent(incomingBubbleAlpha)], highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor, shadow: nil)), primaryTextColor: .white, secondaryTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), linkTextColor: accentColor, linkHighlightColor: accentColor.withAlphaComponent(0.5), scamColor: UIColor(rgb: 0xff6767), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: accentColor, accentControlColor: accentColor, accentControlDisabledColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaActiveControlColor: accentColor, mediaInactiveControlColor: accentColor.withAlphaComponent(0.5), mediaControlInnerBackgroundColor: mainBackgroundColor, pendingActivityColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileTitleColor: accentColor, fileDescriptionColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileDurationColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.585, brightness: 0.23), polls: PresentationThemeChatBubblePolls(radioButton: accentColor.withMultiplied(hue: 0.995, saturation: 0.317, brightness: 0.51), radioProgress: accentColor, highlight: accentColor.withAlphaComponent(0.12), separator: mainSeparatorColor, bar: accentColor, barIconForeground: .white, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: accentColor.withAlphaComponent(0.2), textSelectionKnobColor: accentColor), - outgoing: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColors, highlightedFill: highlightedOutgoingBubbleColor, stroke: outgoingBubbleFillColors[0], shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColors, highlightedFill: highlightedOutgoingBubbleColor, stroke: outgoingBubbleFillColors[0], shadow: nil)), primaryTextColor: outgoingPrimaryTextColor, secondaryTextColor: outgoingSecondaryTextColor, linkTextColor: outgoingLinkTextColor, linkHighlightColor: UIColor.white.withAlphaComponent(0.5), scamColor: outgoingScamColor, textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: outgoingPrimaryTextColor, accentControlColor: outgoingPrimaryTextColor, accentControlDisabledColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaActiveControlColor: outgoingPrimaryTextColor, mediaInactiveControlColor: outgoingSecondaryTextColor, mediaControlInnerBackgroundColor: outgoingBubbleFillColors[0], pendingActivityColor: outgoingSecondaryTextColor, fileTitleColor: outgoingPrimaryTextColor, fileDescriptionColor: outgoingSecondaryTextColor, fileDurationColor: outgoingSecondaryTextColor, mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.804, brightness: 0.51), polls: PresentationThemeChatBubblePolls(radioButton: outgoingPrimaryTextColor, radioProgress: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0), highlight: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0).withAlphaComponent(0.12), separator: mainSeparatorColor, bar: outgoingPrimaryTextColor, barIconForeground: .clear, barPositive: outgoingPrimaryTextColor, barNegative: outgoingPrimaryTextColor), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: UIColor.white.withAlphaComponent(0.2), textSelectionKnobColor: UIColor.white), - freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [mainBackgroundColor], highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [mainBackgroundColor], highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor, shadow: nil)), + incoming: PresentationThemePartedColors( + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [incomingFillColor.withAlphaComponent(incomingBubbleAlpha)], + highlightedFill: highlightedIncomingBubbleColor, + stroke: mainBackgroundColor, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.07), + reactionInactiveForeground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff, alpha: 1.0) + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [incomingFillColor.withAlphaComponent(incomingBubbleAlpha)], + highlightedFill: highlightedIncomingBubbleColor, + stroke: mainBackgroundColor, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.07), + reactionInactiveForeground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff, alpha: 1.0) + ) + ), primaryTextColor: .white, secondaryTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), linkTextColor: accentColor, linkHighlightColor: accentColor.withAlphaComponent(0.5), scamColor: UIColor(rgb: 0xff6767), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: accentColor, accentControlColor: accentColor, accentControlDisabledColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaActiveControlColor: accentColor, mediaInactiveControlColor: accentColor.withAlphaComponent(0.5), mediaControlInnerBackgroundColor: mainBackgroundColor, pendingActivityColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileTitleColor: accentColor, fileDescriptionColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileDurationColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.585, brightness: 0.23), polls: PresentationThemeChatBubblePolls(radioButton: accentColor.withMultiplied(hue: 0.995, saturation: 0.317, brightness: 0.51), radioProgress: accentColor, highlight: accentColor.withAlphaComponent(0.12), separator: mainSeparatorColor, bar: accentColor, barIconForeground: .white, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: accentColor.withAlphaComponent(0.2), textSelectionKnobColor: accentColor + ), + outgoing: PresentationThemePartedColors( + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: outgoingBubbleFillColors, + highlightedFill: highlightedOutgoingBubbleColor, + stroke: outgoingBubbleFillColors[0], + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.1), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: outgoingBubbleFillColors, + highlightedFill: highlightedOutgoingBubbleColor, + stroke: outgoingBubbleFillColors[0], + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.1), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ) + ), primaryTextColor: outgoingPrimaryTextColor, secondaryTextColor: outgoingSecondaryTextColor, linkTextColor: outgoingLinkTextColor, linkHighlightColor: UIColor.white.withAlphaComponent(0.5), scamColor: outgoingScamColor, textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: outgoingPrimaryTextColor, accentControlColor: outgoingPrimaryTextColor, accentControlDisabledColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaActiveControlColor: outgoingPrimaryTextColor, mediaInactiveControlColor: outgoingSecondaryTextColor, mediaControlInnerBackgroundColor: outgoingBubbleFillColors[0], pendingActivityColor: outgoingSecondaryTextColor, fileTitleColor: outgoingPrimaryTextColor, fileDescriptionColor: outgoingSecondaryTextColor, fileDurationColor: outgoingSecondaryTextColor, mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.804, brightness: 0.51), polls: PresentationThemeChatBubblePolls(radioButton: outgoingPrimaryTextColor, radioProgress: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0), highlight: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0).withAlphaComponent(0.12), separator: mainSeparatorColor, bar: outgoingPrimaryTextColor, barIconForeground: .clear, barPositive: outgoingPrimaryTextColor, barNegative: outgoingPrimaryTextColor), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: UIColor.white.withAlphaComponent(0.2), textSelectionKnobColor: UIColor.white + ), + freeform: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [mainBackgroundColor], + highlightedFill: highlightedIncomingBubbleColor, + stroke: mainBackgroundColor, + shadow: nil, + reactionInactiveBackground: incomingFillColor.withAlphaComponent(incomingBubbleAlpha), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [mainBackgroundColor], + highlightedFill: highlightedIncomingBubbleColor, + stroke: mainBackgroundColor, + shadow: nil, + reactionInactiveBackground: incomingFillColor.withAlphaComponent(incomingBubbleAlpha), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) + ) + ), infoPrimaryTextColor: UIColor(rgb: 0xffffff), infoLinkTextColor: accentColor, outgoingCheckColor: outgoingCheckColor, diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index 02a483e189..67edfdc8a3 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -15,6 +15,18 @@ public func selectDateFillStaticColor(theme: PresentationTheme, wallpaper: Teleg } } +public func selectReactionFillStaticColor(theme: PresentationTheme, wallpaper: TelegramWallpaper) -> UIColor { + if case .color = wallpaper { + return theme.chat.message.freeform.withoutWallpaper.reactionInactiveBackground + } else if theme.overallDarkAppearance { + return theme.chat.message.freeform.withoutWallpaper.reactionInactiveBackground + } else if case .builtin = wallpaper { + return UIColor(rgb: 0x748391, alpha: 0.45) + } else { + return theme.chat.serviceMessage.components.withCustomWallpaper.dateFillStatic + } +} + public func dateFillNeedsBlur(theme: PresentationTheme, wallpaper: TelegramWallpaper) -> Bool { if case .builtin = wallpaper { return false @@ -526,7 +538,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio let message = PresentationThemeChatMessage( incoming: PresentationThemePartedColors( - bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xffffff)], highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: bubbleStrokeColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xffffff)], highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: bubbleStrokeColor, shadow: nil)), + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xffffff)], + highlightedFill: UIColor(rgb: 0xd9f4ff), + stroke: bubbleStrokeColor, + shadow: nil, + reactionInactiveBackground: defaultDayAccentColor.withMultipliedAlpha(0.1), + reactionInactiveForeground: defaultDayAccentColor, + reactionActiveBackground: defaultDayAccentColor, + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xffffff)], + highlightedFill: UIColor(rgb: 0xd9f4ff), + stroke: bubbleStrokeColor, + shadow: nil, + reactionInactiveBackground: defaultDayAccentColor.withMultipliedAlpha(0.1), + reactionInactiveForeground: defaultDayAccentColor, + reactionActiveBackground: defaultDayAccentColor, + reactionActiveForeground: .clear + ) + ), primaryTextColor: UIColor(rgb: 0x000000), secondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6), linkTextColor: UIColor(rgb: 0x004bad), @@ -548,7 +581,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x596e89, alpha: 0.35)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: .clear), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: defaultDayAccentColor.withAlphaComponent(0.2), textSelectionKnobColor: defaultDayAccentColor), outgoing: PresentationThemePartedColors( - bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe1ffc7)], highlightedFill: UIColor(rgb: 0xc8ffa6), stroke: bubbleStrokeColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe1ffc7)], highlightedFill: UIColor(rgb: 0xc8ffa6), stroke: bubbleStrokeColor, shadow: nil)), + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xe1ffc7)], + highlightedFill: UIColor(rgb: 0xc8ffa6), + stroke: bubbleStrokeColor, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0x2DA32F).withMultipliedAlpha(0.12), + reactionInactiveForeground: UIColor(rgb: 0x2DA32F), + reactionActiveBackground: UIColor(rgb: 0x2DA32F), + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xe1ffc7)], + highlightedFill: UIColor(rgb: 0xc8ffa6), + stroke: bubbleStrokeColor, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0x2DA32F).withMultipliedAlpha(0.12), + reactionInactiveForeground: UIColor(rgb: 0x2DA32F), + reactionActiveBackground: UIColor(rgb: 0x2DA32F), + reactionActiveForeground: .clear + ) + ), primaryTextColor: UIColor(rgb: 0x000000), secondaryTextColor: UIColor(rgb: 0x008c09, alpha: 0.8), linkTextColor: UIColor(rgb: 0x004bad), @@ -572,7 +626,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xbbde9f), textSelectionKnobColor: UIColor(rgb: 0x3fc33b)), - freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xffffff)], highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: UIColor(rgb: 0x86a9c9, alpha: 0.5), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xffffff)], highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: UIColor(rgb: 0x86a9c9, alpha: 0.5), shadow: nil)), + freeform: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xffffff)], + highlightedFill: UIColor(rgb: 0xd9f4ff), + stroke: UIColor(rgb: 0x86a9c9, alpha: 0.5), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 0.8), + reactionActiveForeground: UIColor(white: 0.0, alpha: 0.1) + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xffffff)], + highlightedFill: UIColor(rgb: 0xd9f4ff), + stroke: UIColor(rgb: 0x86a9c9, alpha: 0.5), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 0.8), + reactionActiveForeground: UIColor(white: 0.0, alpha: 0.1) + ) + ), infoPrimaryTextColor: UIColor(rgb: 0x000000), infoLinkTextColor: UIColor(rgb: 0x004bad), outgoingCheckColor: UIColor(rgb: 0x19c700), @@ -591,7 +666,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio let messageDay = PresentationThemeChatMessage( incoming: PresentationThemePartedColors( - bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xffffff)], highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xffffff), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xf1f1f4)], highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xf1f1f4), shadow: nil)), + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xffffff)], + highlightedFill: UIColor(rgb: 0xdadade), + stroke: UIColor(rgb: 0xffffff), + shadow: nil, + reactionInactiveBackground: defaultDayAccentColor.withMultipliedAlpha(0.1), + reactionInactiveForeground: defaultDayAccentColor, + reactionActiveBackground: defaultDayAccentColor, + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xf1f1f4)], + highlightedFill: UIColor(rgb: 0xdadade), + stroke: UIColor(rgb: 0xf1f1f4), + shadow: nil, + reactionInactiveBackground: .clear, + reactionInactiveForeground: defaultDayAccentColor, + reactionActiveBackground: defaultDayAccentColor, + reactionActiveForeground: .clear + ) + ), primaryTextColor: UIColor(rgb: 0x000000), secondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6), linkTextColor: UIColor(rgb: 0x004bad), @@ -616,7 +712,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio textSelectionColor: defaultDayAccentColor.withAlphaComponent(0.3), textSelectionKnobColor: defaultDayAccentColor), outgoing: PresentationThemePartedColors( - bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x57b2e0), defaultDayAccentColor], highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), stroke: .clear, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x57b2e0), defaultDayAccentColor], highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), stroke: .clear, shadow: nil)), + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x57b2e0), defaultDayAccentColor], + highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), + stroke: .clear, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.12), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff), + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x57b2e0), defaultDayAccentColor], + highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), + stroke: .clear, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.12), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff), + reactionActiveForeground: .clear + ) + ), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.65), linkTextColor: UIColor(rgb: 0xffffff), @@ -640,7 +757,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: defaultDayAccentColor), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff)), - freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe5e5ea)], highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xe5e5ea), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe5e5ea)], highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xe5e5ea), shadow: nil)), + freeform: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xe5e5ea)], + highlightedFill: UIColor(rgb: 0xdadade), + stroke: UIColor(rgb: 0xe5e5ea), + shadow: nil, + reactionInactiveBackground: defaultDayAccentColor.withMultipliedAlpha(0.1), + reactionInactiveForeground: defaultDayAccentColor, + reactionActiveBackground: defaultDayAccentColor, + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xe5e5ea)], + highlightedFill: UIColor(rgb: 0xdadade), + stroke: UIColor(rgb: 0xe5e5ea), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xF1F0F5), + reactionInactiveForeground: defaultDayAccentColor, + reactionActiveBackground: defaultDayAccentColor, + reactionActiveForeground: .clear + ) + ), infoPrimaryTextColor: UIColor(rgb: 0x000000), infoLinkTextColor: UIColor(rgb: 0x004bad), outgoingCheckColor: UIColor(rgb: 0xffffff), diff --git a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift index c0d0a57389..d0ea881704 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift @@ -639,16 +639,50 @@ public final class PresentationThemeBubbleColorComponents { public let highlightedFill: UIColor public let stroke: UIColor public let shadow: PresentationThemeBubbleShadow? + public let reactionInactiveBackground: UIColor + public let reactionInactiveForeground: UIColor + public let reactionActiveBackground: UIColor + public let reactionActiveForeground: UIColor - public init(fill: [UIColor], highlightedFill: UIColor, stroke: UIColor, shadow: PresentationThemeBubbleShadow?) { + public init( + fill: [UIColor], + highlightedFill: UIColor, + stroke: UIColor, + shadow: PresentationThemeBubbleShadow?, + reactionInactiveBackground: UIColor, + reactionInactiveForeground: UIColor, + reactionActiveBackground: UIColor, + reactionActiveForeground: UIColor + ) { self.fill = fill self.highlightedFill = highlightedFill self.stroke = stroke self.shadow = shadow + self.reactionInactiveBackground = reactionInactiveBackground + self.reactionInactiveForeground = reactionInactiveForeground + self.reactionActiveBackground = reactionActiveBackground + self.reactionActiveForeground = reactionActiveForeground } - public func withUpdated(fill: [UIColor]? = nil, highlightedFill: UIColor? = nil, stroke: UIColor? = nil) -> PresentationThemeBubbleColorComponents { - return PresentationThemeBubbleColorComponents(fill: fill ?? self.fill, highlightedFill: highlightedFill ?? self.highlightedFill, stroke: stroke ?? self.stroke, shadow: self.shadow) + public func withUpdated( + fill: [UIColor]? = nil, + highlightedFill: UIColor? = nil, + stroke: UIColor? = nil, + reactionInactiveBackground: UIColor? = nil, + reactionInactiveForeground: UIColor? = nil, + reactionActiveBackground: UIColor? = nil, + reactionActiveForeground: UIColor? = nil + ) -> PresentationThemeBubbleColorComponents { + return PresentationThemeBubbleColorComponents( + fill: fill ?? self.fill, + highlightedFill: highlightedFill ?? self.highlightedFill, + stroke: stroke ?? self.stroke, + shadow: self.shadow, + reactionInactiveBackground: reactionInactiveBackground ?? self.reactionInactiveBackground, + reactionInactiveForeground: reactionInactiveForeground ?? self.reactionInactiveForeground, + reactionActiveBackground: reactionActiveBackground ?? self.reactionActiveBackground, + reactionActiveForeground: reactionActiveForeground ?? self.reactionActiveForeground + ) } } diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift index e5f27ae7d9..3105038c81 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift @@ -1097,6 +1097,10 @@ extension PresentationThemeBubbleColorComponents: Codable { case stroke case shadow case bgList + case reactionInactiveBg + case reactionInactiveFg + case reactionActiveBg + case reactionActiveFg } public convenience init(from decoder: Decoder) throws { @@ -1122,7 +1126,11 @@ extension PresentationThemeBubbleColorComponents: Codable { fill: fill, highlightedFill: try decodeColor(values, .highlightedBg), stroke: try decodeColor(values, .stroke), - shadow: try? values.decode(PresentationThemeBubbleShadow.self, forKey: .shadow) + shadow: try? values.decode(PresentationThemeBubbleShadow.self, forKey: .shadow), + reactionInactiveBackground: try decodeColor(values, .reactionInactiveBg), + reactionInactiveForeground: try decodeColor(values, .reactionInactiveFg), + reactionActiveBackground: try decodeColor(values, .reactionActiveBg), + reactionActiveForeground: try decodeColor(values, .reactionActiveFg) ) } @@ -1141,6 +1149,10 @@ extension PresentationThemeBubbleColorComponents: Codable { } try encodeColor(&values, self.highlightedFill, .highlightedBg) try encodeColor(&values, self.stroke, .stroke) + try encodeColor(&values, self.reactionInactiveBackground, .reactionInactiveBg) + try encodeColor(&values, self.reactionInactiveForeground, .reactionInactiveFg) + try encodeColor(&values, self.reactionActiveBackground, .reactionActiveBg) + try encodeColor(&values, self.reactionActiveForeground, .reactionActiveFg) } } diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/Contents.json new file mode 100644 index 0000000000..d2272e6102 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "reactions_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/reactions_30.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/reactions_30.pdf new file mode 100644 index 0000000000..1994256b47 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/reactions_30.pdf @@ -0,0 +1,180 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +1.000000 0.176471 0.333333 scn +0.000000 18.799999 m +0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c +1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c +5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c +18.799999 30.000000 l +22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c +27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c +30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c +30.000000 11.200001 l +30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c +28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c +24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c +11.200000 0.000000 l +7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c +2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c +0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c +0.000000 18.799999 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 5.000000 5.523438 cm +1.000000 1.000000 1.000000 scn +7.132812 18.054688 m +7.234375 18.054688 7.312500 18.117188 7.335938 18.226562 c +7.523438 19.359375 7.523438 19.359375 8.664062 19.570312 c +8.781250 19.593750 8.851562 19.664062 8.851562 19.765625 c +8.851562 19.875000 8.781250 19.937500 8.671875 19.968750 c +7.515625 20.195312 7.539062 20.195312 7.335938 21.304688 c +7.312500 21.414062 7.242188 21.476562 7.132812 21.476562 c +7.031250 21.476562 6.953125 21.406250 6.937500 21.304688 c +6.726562 20.179688 6.750000 20.171875 5.601562 19.968750 c +5.484375 19.945312 5.414062 19.867188 5.414062 19.765625 c +5.414062 19.671875 5.484375 19.593750 5.593750 19.570312 c +6.750000 19.343750 6.742188 19.351562 6.937500 18.226562 c +6.953125 18.125000 7.031250 18.054688 7.132812 18.054688 c +h +12.695312 15.976562 m +12.843750 15.976562 12.960938 16.078125 12.968750 16.242188 c +13.187500 18.023438 13.265625 18.078125 15.085938 18.367188 c +15.265625 18.382812 15.359375 18.484375 15.359375 18.640625 c +15.359375 18.781250 15.265625 18.882812 15.125000 18.906250 c +13.273438 19.273438 13.187500 19.250000 12.968750 21.031250 c +12.960938 21.187500 12.843750 21.289062 12.695312 21.289062 c +12.554688 21.289062 12.445312 21.187500 12.429688 21.039062 c +12.195312 19.218750 12.148438 19.164062 10.281250 18.906250 c +10.140625 18.890625 10.039062 18.781250 10.039062 18.640625 c +10.039062 18.492188 10.140625 18.390625 10.281250 18.367188 c +12.148438 17.992188 12.187500 18.007812 12.429688 16.218750 c +12.445312 16.078125 12.554688 15.976562 12.695312 15.976562 c +h +6.531250 2.656250 m +9.039062 0.148438 12.257812 0.359375 14.531250 2.640625 c +16.179688 4.281250 16.617188 6.031250 16.078125 8.109375 c +15.789062 9.585938 14.906250 11.281250 14.250000 12.523438 c +13.867188 13.257812 13.398438 14.210938 13.117188 14.546875 c +12.804688 14.937500 12.320312 14.984375 11.945312 14.671875 c +11.507812 14.320312 11.476562 13.835938 11.726562 13.125000 c +12.640625 10.656250 l +12.726562 10.437500 12.710938 10.304688 12.625000 10.226562 c +12.531250 10.132812 12.414062 10.117188 12.234375 10.289062 c +6.367188 16.164062 l +5.992188 16.539062 5.406250 16.539062 5.031250 16.164062 c +4.664062 15.789062 4.664062 15.203125 5.039062 14.828125 c +9.351562 10.515625 l +9.171875 10.421875 8.976562 10.320312 8.789062 10.195312 c +3.820312 15.164062 l +3.445312 15.539062 2.859375 15.539062 2.484375 15.164062 c +2.109375 14.789062 2.109375 14.210938 2.484375 13.835938 c +7.398438 8.921875 l +7.257812 8.757812 7.125000 8.578125 7.000000 8.398438 c +2.484375 12.914062 l +2.109375 13.289062 1.523438 13.289062 1.148438 12.921875 c +0.773438 12.546875 0.781250 11.960938 1.148438 11.585938 c +6.046875 6.695312 l +5.960938 6.460938 5.898438 6.226562 5.843750 6.007812 c +2.429688 9.414062 l +2.054688 9.789062 1.476562 9.789062 1.101562 9.421875 c +0.726562 9.046875 0.726562 8.460938 1.101562 8.085938 c +6.531250 2.656250 l +h +10.773438 14.898438 m +9.507812 16.164062 l +9.125000 16.539062 8.539062 16.531250 8.171875 16.164062 c +8.148438 16.140625 8.132812 16.125000 8.117188 16.101562 c +10.585938 13.632812 l +10.570312 14.093750 10.625000 14.515625 10.773438 14.898438 c +h +17.664062 2.640625 m +19.312500 4.289062 19.750000 6.031250 19.218750 8.109375 c +18.921875 9.585938 18.046875 11.281250 17.382812 12.523438 c +17.007812 13.257812 16.531250 14.210938 16.250000 14.546875 c +15.937500 14.929688 15.460938 14.976562 15.078125 14.671875 c +14.906250 14.539062 14.789062 14.375000 14.734375 14.195312 c +14.953125 13.781250 15.171875 13.359375 15.390625 12.945312 c +16.023438 11.742188 16.953125 9.937500 17.242188 8.328125 c +17.867188 5.804688 17.257812 3.671875 15.375000 1.796875 c +15.031250 1.453125 14.671875 1.156250 14.304688 0.898438 c +15.507812 1.046875 16.687500 1.648438 17.664062 2.640625 c +h +2.304688 0.000000 m +2.437500 0.000000 2.523438 0.085938 2.539062 0.218750 c +2.804688 1.750000 2.796875 1.773438 4.390625 2.070312 c +4.531250 2.101562 4.617188 2.171875 4.617188 2.312500 c +4.617188 2.445312 4.531250 2.523438 4.398438 2.546875 c +2.796875 2.875000 2.820312 2.890625 2.539062 4.398438 c +2.523438 4.531250 2.437500 4.617188 2.304688 4.617188 c +2.171875 4.617188 2.093750 4.531250 2.062500 4.398438 c +1.781250 2.867188 1.820312 2.843750 0.218750 2.546875 c +0.085938 2.523438 0.000000 2.445312 0.000000 2.312500 c +0.000000 2.171875 0.078125 2.101562 0.210938 2.070312 c +1.820312 1.750000 1.796875 1.742188 2.062500 0.218750 c +2.093750 0.085938 2.171875 0.000000 2.304688 0.000000 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 5442 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000005532 00000 n +0000005555 00000 n +0000005728 00000 n +0000005802 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +5861 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index b8802be302..57631dd4d9 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -66,6 +66,7 @@ import ChatListUI import CalendarMessageScreen import ReactionSelectionNode import LottieMeshSwift +import ReactionListContextMenuContent #if DEBUG import os.signpost @@ -952,16 +953,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = combineLatest(queue: .mainQueue(), contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), strongSelf.context.engine.stickers.availableReactions(), - strongSelf.context.account.postbox.transaction { transaction -> Set? in - let cachedData = transaction.getPeerCachedData(peerId: topMessage.id.peerId) - if let cachedData = cachedData as? CachedChannelData { - return cachedData.allowedReactions.flatMap(Set.init) - } else if let cachedData = cachedData as? CachedGroupData { - return cachedData.allowedReactions.flatMap(Set.init) - } else { - return nil - } - }, + peerAllowedReactions(context: strongSelf.context, peerId: topMessage.id.peerId), ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager) ).start(next: { actions, availableReactions, allowedReactions, chatTextSelectionTips in var actions = actions @@ -1014,9 +1006,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G actions.context = strongSelf.context if canAddMessageReactions(message: topMessage), let availableReactions = availableReactions, let allowedReactions = allowedReactions { - for reaction in availableReactions.reactions { - if !allowedReactions.contains(reaction.value) { - continue + filterReactions: for reaction in availableReactions.reactions { + switch allowedReactions { + case let .set(set): + if !set.contains(reaction.value) { + continue filterReactions + } + case .all: + break } actions.reactionItems.append(ReactionContextItem( reaction: ReactionContextItem.Reaction(rawValue: reaction.value), @@ -1095,6 +1092,45 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.window?.presentInGlobalOverlay(controller) }) } + }, openMessageReactionContextMenu: { [weak self] message, sourceNode, gesture, value in + guard let strongSelf = self else { + return + } + + let _ = (strongSelf.context.engine.stickers.availableReactions() + |> deliverOnMainQueue).start(next: { availableReactions in + guard let strongSelf = self else { + return + } + + var dismissController: ((@escaping () -> Void) -> Void)? + + let items = ContextController.Items(content: .custom(ReactionListContextMenuContent(context: strongSelf.context, availableReactions: availableReactions, message: EngineMessage(message), reaction: value, back: nil, openPeer: { id in + dismissController?({ + guard let strongSelf = self else { + return + } + + strongSelf.openPeer(peerId: id, navigation: .default, fromMessage: message) + }) + }))) + + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageReactionContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, contentNode: sourceNode)), items: .single(items), recognizer: nil, gesture: gesture) + + dismissController = { [weak controller] completion in + controller?.dismiss(completion: { + completion() + }) + } + + strongSelf.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss() + } + return true + }) + strongSelf.window?.presentInGlobalOverlay(controller) + }) }, updateMessageReaction: { [weak self] initialMessage, reaction in guard let strongSelf = self else { return @@ -1105,115 +1141,140 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let message = messages.first else { return } - if !canAddMessageReactions(message: message) { - return - } - strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in - guard let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item else { - return - } - guard item.message.id == message.id else { + let _ = (peerAllowedReactions(context: strongSelf.context, peerId: message.id.peerId) + |> deliverOnMainQueue).start(next: { allowedReactions in + guard let strongSelf = self else { return } - var updatedReaction: String? - switch reaction { - case .default: - updatedReaction = item.associatedData.defaultReaction - case let .reaction(value): - updatedReaction = value - } + let _ = allowedReactions - var removedReaction: String? - - for attribute in message.attributes { - if let attribute = attribute as? ReactionsMessageAttribute { - for listReaction in attribute.reactions { - switch reaction { - case .default: - if listReaction.isSelected { - updatedReaction = nil - removedReaction = listReaction.value - } - case let .reaction(value): - if listReaction.value == value && listReaction.isSelected { - updatedReaction = nil - removedReaction = value - } - } - } - } else if let attribute = attribute as? PendingReactionsMessageAttribute { - if attribute.value != nil { - switch reaction { - case .default: - updatedReaction = nil - removedReaction = attribute.value - case let .reaction(value): - if attribute.value == value { - updatedReaction = nil - removedReaction = value - } - } - } + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in + guard let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item else { + return } - } - - if let updatedReaction = updatedReaction { - if strongSelf.selectPollOptionFeedback == nil { - strongSelf.selectPollOptionFeedback = HapticFeedback() + guard item.message.id == message.id else { + return } - strongSelf.selectPollOptionFeedback?.tap() - itemNode.awaitingAppliedReaction = (updatedReaction, { [weak itemNode] in - guard let strongSelf = self else { + if !canAddMessageReactions(message: message) { + itemNode.openMessageContextMenu() + return + } + + var updatedReaction: String? + switch reaction { + case .default: + updatedReaction = item.associatedData.defaultReaction + case let .reaction(value): + updatedReaction = value + } + + var removedReaction: String? + + for attribute in message.attributes { + if let attribute = attribute as? ReactionsMessageAttribute { + for listReaction in attribute.reactions { + switch reaction { + case .default: + if listReaction.isSelected { + updatedReaction = nil + removedReaction = listReaction.value + } + case let .reaction(value): + if listReaction.value == value && listReaction.isSelected { + updatedReaction = nil + removedReaction = value + } + } + } + } else if let attribute = attribute as? PendingReactionsMessageAttribute { + if attribute.value != nil { + switch reaction { + case .default: + updatedReaction = nil + removedReaction = attribute.value + case let .reaction(value): + if attribute.value == value { + updatedReaction = nil + removedReaction = value + } + } + } + } + } + + if let updatedReaction = updatedReaction { + guard let allowedReactions = allowedReactions else { + itemNode.openMessageContextMenu() return } - if let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) { - for reaction in availableReactions.reactions { - if reaction.value == updatedReaction { - let standaloneReactionAnimation = StandaloneReactionAnimation(context: strongSelf.context, theme: strongSelf.presentationData.theme, reaction: ReactionContextItem( - reaction: ReactionContextItem.Reaction(rawValue: reaction.value), - stillAnimation: reaction.selectAnimation, - listAnimation: reaction.activateAnimation, - applicationAnimation: reaction.effectAnimation - )) - - strongSelf.currentStandaloneReactionAnimation = standaloneReactionAnimation - strongSelf.currentStandaloneReactionItemNode = itemNode - - strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) - standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds - standaloneReactionAnimation.animateReactionSelection(targetView: targetView, hideNode: true, completion: { [weak standaloneReactionAnimation] in - standaloneReactionAnimation?.removeFromSupernode() - }) - + switch allowedReactions { + case let .set(set): + if !set.contains(updatedReaction) { + itemNode.openMessageContextMenu() + return + } + case .all: + break + } + + if strongSelf.selectPollOptionFeedback == nil { + strongSelf.selectPollOptionFeedback = HapticFeedback() + } + strongSelf.selectPollOptionFeedback?.tap() + + itemNode.awaitingAppliedReaction = (updatedReaction, { [weak itemNode] in + guard let strongSelf = self else { + return + } + if let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) { + for reaction in availableReactions.reactions { + if reaction.value == updatedReaction { + let standaloneReactionAnimation = StandaloneReactionAnimation(context: strongSelf.context, theme: strongSelf.presentationData.theme, reaction: ReactionContextItem( + reaction: ReactionContextItem.Reaction(rawValue: reaction.value), + stillAnimation: reaction.selectAnimation, + listAnimation: reaction.activateAnimation, + applicationAnimation: reaction.effectAnimation + )) + + strongSelf.currentStandaloneReactionAnimation = standaloneReactionAnimation + strongSelf.currentStandaloneReactionItemNode = itemNode + + strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) + standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds + standaloneReactionAnimation.animateReactionSelection(targetView: targetView, hideNode: true, completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + }) + + break + } + } + } + }) + } else if let removedReaction = removedReaction, let targetView = itemNode.targetReactionView(value: removedReaction), shouldDisplayInlineDateReactions(message: message) { + var hideRemovedReaction: Bool = false + if let reactions = mergedMessageReactions(attributes: message.attributes) { + for reaction in reactions.reactions { + if reaction.value == removedReaction { + hideRemovedReaction = reaction.count == 1 break } } } - }) - } else if let removedReaction = removedReaction, let targetView = itemNode.targetReactionView(value: removedReaction), shouldDisplayInlineDateReactions(message: message) { - var hideRemovedReaction: Bool = false - if let reactions = mergedMessageReactions(attributes: message.attributes) { - for reaction in reactions.reactions { - if reaction.value == removedReaction { - hideRemovedReaction = reaction.count == 1 - break - } - } + + let standaloneDismissAnimation = StandaloneDismissReactionAnimation() + standaloneDismissAnimation.frame = strongSelf.chatDisplayNode.bounds + strongSelf.chatDisplayNode.addSubnode(standaloneDismissAnimation) + standaloneDismissAnimation.animateReactionDismiss(sourceView: targetView, hideNode: hideRemovedReaction, completion: { [weak standaloneDismissAnimation] in + standaloneDismissAnimation?.removeFromSupernode() + }) } - let standaloneDismissAnimation = StandaloneDismissReactionAnimation() - standaloneDismissAnimation.frame = strongSelf.chatDisplayNode.bounds - strongSelf.chatDisplayNode.addSubnode(standaloneDismissAnimation) - standaloneDismissAnimation.animateReactionDismiss(sourceView: targetView, hideNode: hideRemovedReaction, completion: { [weak standaloneDismissAnimation] in - standaloneDismissAnimation?.removeFromSupernode() - }) + let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageId: message.id, reaction: updatedReaction).start() } - - let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageId: message.id, reaction: updatedReaction).start() - } + }) }, activateMessagePinch: { [weak self] sourceNode in guard let strongSelf = self else { return @@ -14400,3 +14461,23 @@ func canAddMessageReactions(message: Message) -> Bool { } return true } + +enum AllowedReactions { + case set(Set) + case all +} + +func peerAllowedReactions(context: AccountContext, peerId: PeerId) -> Signal { + return context.account.postbox.transaction { transaction -> AllowedReactions? in + let cachedData = transaction.getPeerCachedData(peerId: peerId) + if let cachedData = cachedData as? CachedChannelData { + return cachedData.allowedReactions.flatMap { return AllowedReactions.set(Set($0)) } + } else if let cachedData = cachedData as? CachedGroupData { + return cachedData.allowedReactions.flatMap { return AllowedReactions.set(Set($0)) } + } else if peerId.namespace == Namespaces.Peer.CloudUser { + return .all + } else { + return nil + } + } +} diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index 087df11a59..bc03ba8ad5 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -57,6 +57,7 @@ public final class ChatControllerInteraction { let openPeerMention: (String) -> Void let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void let updateMessageReaction: (Message, ChatControllerInteractionReaction) -> Void + let openMessageReactionContextMenu: (Message, ContextExtractedContentContainingNode, ContextGesture?, String) -> Void let activateMessagePinch: (PinchSourceContainerNode) -> Void let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void let navigateToMessage: (MessageId, MessageId) -> Void @@ -153,6 +154,7 @@ public final class ChatControllerInteraction { openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, + openMessageReactionContextMenu: @escaping (Message, ContextExtractedContentContainingNode, ContextGesture?, String) -> Void, updateMessageReaction: @escaping (Message, ChatControllerInteractionReaction) -> Void, activateMessagePinch: @escaping (PinchSourceContainerNode) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, @@ -236,6 +238,7 @@ public final class ChatControllerInteraction { self.openPeer = openPeer self.openPeerMention = openPeerMention self.openMessageContextMenu = openMessageContextMenu + self.openMessageReactionContextMenu = openMessageReactionContextMenu self.updateMessageReaction = updateMessageReaction self.activateMessagePinch = activateMessagePinch self.openMessageContextActions = openMessageContextActions @@ -321,7 +324,8 @@ public final class ChatControllerInteraction { static var `default`: ChatControllerInteraction { return ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in + }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentControllerInCurrent: { _, _ in }, navigationController: { return nil }, chatControllerNode: { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 3a494304d4..3293a6c33a 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1192,6 +1192,8 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if group.participantCount <= 50 { hasReadReports = true } + } else { + reactionCount = 0 } var readStats = readStats @@ -1211,7 +1213,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }) } else if !stats.peers.isEmpty || reactionCount != 0 { if reactionCount != 0 { - c.pushItems(items: .single(ContextController.Items(content: .custom(ReactionListContextMenuContent(context: context, availableReactions: availableReactions, message: EngineMessage(message), back: { [weak c] in + c.pushItems(items: .single(ContextController.Items(content: .custom(ReactionListContextMenuContent(context: context, availableReactions: availableReactions, message: EngineMessage(message), reaction: nil, back: { [weak c] in c?.popItems() }, openPeer: { [weak c] id in c?.dismiss(completion: { diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index c443c5136c..850c080dd7 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -367,6 +367,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let shareButtonNode = strongSelf.shareButtonNode, shareButtonNode.frame.contains(point) { return .fail } + if let reactionButtonsNode = strongSelf.reactionButtonsNode { + if let _ = reactionButtonsNode.hitTest(strongSelf.view.convert(point, to: reactionButtonsNode.view), with: nil) { + return .fail + } + } if false, strongSelf.telegramFile == nil { if let animationNode = strongSelf.animationNode, animationNode.frame.contains(point) { @@ -972,7 +977,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) @@ -1414,6 +1420,14 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + reactionButtonsNode.openReactionPreview = { gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } reactionButtonsNode.frame = reactionButtonsFrame if let (rect, containerSize) = strongSelf.absoluteRect { var rect = rect @@ -2382,6 +2396,13 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } + override func openMessageContextMenu() { + guard let item = self.item else { + return + } + item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.imageNode.frame, nil) + } + override func targetReactionView(value: String) -> UIView? { if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { return result diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index 339ac262f2..27785eca94 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -642,7 +642,8 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: message.isSelfExpiring + hasAutoremove: message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: message) )) } let _ = statusSuggestedWidthAndContinue diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 26bbb5a99a..d8eae2024a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -249,11 +249,11 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ result.append((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default))) needReactions = false } else if result.last?.1 == ChatMessageCommentFooterContentNode.self { - if result[result.count - 2].1 == ChatMessageTextBubbleContentNode.self { + /*if result[result.count - 2].1 == ChatMessageTextBubbleContentNode.self { } else { result.insert((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)), at: result.count - 1) needReactions = false - } + }*/ } } } @@ -805,6 +805,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return .fail } + if let reactionButtonsNode = strongSelf.reactionButtonsNode { + if let _ = reactionButtonsNode.hitTest(strongSelf.view.convert(point, to: reactionButtonsNode.view), with: nil) { + return .fail + } + } + if let avatarNode = strongSelf.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(point) { return .waitForSingleTap } @@ -1596,7 +1602,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: message.isSelfExpiring + hasAutoremove: message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: message) )) mosaicStatusSizeAndApply = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) @@ -2829,6 +2836,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + reactionButtonsNode.openReactionPreview = { [weak strongSelf] gesture, sourceNode, value in + guard let strongSelf = strongSelf, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } reactionButtonsNode.frame = reactionButtonsFrame strongSelf.addSubnode(reactionButtonsNode) if animation.isAnimated { @@ -3848,6 +3863,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return self.mainContextSourceNode.isExtractedToContextPreview || hasWallpaper || isPreview } + override func openMessageContextMenu() { + guard let item = self.item else { + return + } + let subFrame = self.backgroundNode.frame + item.controllerInteraction.openMessageContextMenu(item.message, true, self, subFrame, nil) + } + override func targetReactionView(value: String) -> UIView? { if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { return result diff --git a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift index 8c6be2f6fe..1869f576fb 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift @@ -48,6 +48,15 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + + self.dateAndStatusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } } override func accessibilityActivate() -> Bool { @@ -217,7 +226,8 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) } diff --git a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift index 79f72e066f..8a37064a6c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift @@ -81,3 +81,82 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou return result } } + +final class ChatMessageReactionContextExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = true + let blurBackground: Bool = true + let centerActionsHorizontally: Bool = true + + private weak var chatNode: ChatControllerNode? + private let postbox: Postbox + private let message: Message + private let contentNode: ContextExtractedContentContainingNode + + var shouldBeDismissed: Signal { + if self.message.adAttribute != nil { + return .single(false) + } + let viewKey = PostboxViewKey.messages(Set([self.message.id])) + return self.postbox.combinedView(keys: [viewKey]) + |> map { views -> Bool in + guard let view = views.views[viewKey] as? MessagesView else { + return false + } + if view.messages.isEmpty { + return true + } else { + return false + } + } + |> distinctUntilChanged + } + + init(chatNode: ChatControllerNode, postbox: Postbox, message: Message, contentNode: ContextExtractedContentContainingNode) { + self.chatNode = chatNode + self.postbox = postbox + self.message = message + self.contentNode = contentNode + } + + func takeView() -> ContextControllerTakeViewInfo? { + guard let chatNode = self.chatNode else { + return nil + } + + var result: ContextControllerTakeViewInfo? + chatNode.historyNode.forEachItemNode { itemNode in + guard let itemNode = itemNode as? ChatMessageItemView else { + return + } + guard let item = itemNode.item else { + return + } + if item.content.contains(where: { $0.0.stableId == self.message.stableId }) { + result = ContextControllerTakeViewInfo(contentContainingNode: self.contentNode, contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil)) + } + } + return result + } + + func putBack() -> ContextControllerPutBackViewInfo? { + guard let chatNode = self.chatNode else { + return nil + } + + var result: ContextControllerPutBackViewInfo? + chatNode.historyNode.forEachItemNode { itemNode in + guard let itemNode = itemNode as? ChatMessageItemView else { + return + } + guard let item = itemNode.item else { + return + } + if item.content.contains(where: { $0.0.stableId == self.message.stableId }) { + result = ContextControllerPutBackViewInfo(contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil)) + } + } + return result + } +} + diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift index 68d66a57b3..016f17676e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift @@ -148,6 +148,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { var replyCount: Int var isPinned: Bool var hasAutoremove: Bool + var canViewReactionList: Bool init( context: AccountContext, @@ -163,7 +164,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { reactionPeers: [(String, EnginePeer)], replyCount: Int, isPinned: Bool, - hasAutoremove: Bool + hasAutoremove: Bool, + canViewReactionList: Bool ) { self.context = context self.presentationData = presentationData @@ -179,6 +181,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { self.replyCount = replyCount self.isPinned = isPinned self.hasAutoremove = hasAutoremove + self.canViewReactionList = canViewReactionList } } @@ -220,6 +223,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } var reactionSelected: ((String) -> Void)? + var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingNode, String) -> Void)? override init() { self.dateNode = TextNode() @@ -284,18 +288,26 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { let reactionColors: ReactionButtonComponent.Colors switch arguments.type { case .BubbleIncoming, .ImageIncoming, .FreeIncoming: + let themeColors = bubbleColorComponents(theme: arguments.presentationData.theme.theme, incoming: true, wallpaper: !arguments.presentationData.theme.wallpaper.isEmpty) + reactionColors = ReactionButtonComponent.Colors( - deselectedBackground: arguments.presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb, - selectedBackground: arguments.presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(1.0).argb, - deselectedForeground: arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor.argb, - selectedForeground: arguments.presentationData.theme.theme.chat.message.incoming.bubble.withWallpaper.fill.last!.argb + deselectedBackground: themeColors.reactionInactiveBackground.argb, + selectedBackground: themeColors.reactionActiveBackground.argb, + deselectedForeground: themeColors.reactionInactiveForeground.argb, + selectedForeground: themeColors.reactionActiveForeground.argb, + extractedBackground: arguments.presentationData.theme.theme.contextMenu.backgroundColor.argb, + extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb ) case .BubbleOutgoing, .ImageOutgoing, .FreeOutgoing: + let themeColors = bubbleColorComponents(theme: arguments.presentationData.theme.theme, incoming: false, wallpaper: !arguments.presentationData.theme.wallpaper.isEmpty) + reactionColors = ReactionButtonComponent.Colors( - deselectedBackground: arguments.presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(0.1).argb, - selectedBackground: arguments.presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(1.0).argb, - deselectedForeground: arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb, - selectedForeground: arguments.presentationData.theme.theme.chat.message.outgoing.bubble.withWallpaper.fill.last!.argb + deselectedBackground: themeColors.reactionInactiveBackground.argb, + selectedBackground: themeColors.reactionActiveBackground.argb, + deselectedForeground: themeColors.reactionInactiveForeground.argb, + selectedForeground: themeColors.reactionActiveForeground.argb, + extractedBackground: arguments.presentationData.theme.theme.contextMenu.backgroundColor.argb, + extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb ) } @@ -641,6 +653,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { ) case let .trailingContent(contentWidth, reactionSettings): if let reactionSettings = reactionSettings, !reactionSettings.displayInline { + var totalReactionCount: Int = 0 + for reaction in arguments.reactions { + totalReactionCount += Int(reaction.count) + } + reactionButtonsResult = reactionButtonsContainer.update( context: arguments.context, action: { value in @@ -667,11 +684,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { peers.append(peer) } } - if peers.count != Int(reaction.count) { + if peers.count != Int(reaction.count) || arguments.reactionPeers.count != totalReactionCount { peers.removeAll() } - return ReactionButtonsLayoutContainer.Reaction( + return ReactionButtonsAsyncLayoutContainer.Reaction( reaction: ReactionButtonComponent.Reaction( value: reaction.value, iconFile: iconFile @@ -778,28 +795,47 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { reactionButtonPosition.y += item.size.height + 6.0 } - if item.view.superview == nil { - strongSelf.view.addSubview(item.view) - item.view.frame = CGRect(origin: reactionButtonPosition, size: item.size) + if item.node.supernode == nil { + strongSelf.addSubnode(item.node) + item.node.frame = CGRect(origin: reactionButtonPosition, size: item.size) if animation.isAnimated { - item.view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) - item.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + item.node.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + item.node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + item.node.isGestureEnabled = true + let itemValue = item.value + let itemNode = item.node + item.node.isGestureEnabled = arguments.canViewReactionList + item.node.activated = { [weak itemNode] gesture, _ in + guard let strongSelf = self else { + return + } + guard let itemNode = itemNode else { + return + } + + if let openReactionPreview = strongSelf.openReactionPreview { + openReactionPreview(gesture, itemNode.containerNode, itemValue) + } else { + gesture.cancel() + } } } else { - animation.animator.updateFrame(layer: item.view.layer, frame: CGRect(origin: reactionButtonPosition, size: item.size), completion: nil) + animation.animator.updateFrame(layer: item.node.layer, frame: CGRect(origin: reactionButtonPosition, size: item.size), completion: nil) } reactionButtonPosition.x += item.size.width + 6.0 } - for view in reactionButtons.removedViews { + for node in reactionButtons.removedNodes { if animation.isAnimated { - view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in - view?.removeFromSuperview() + node.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in + node?.removeFromSupernode() }) } else { - view.removeFromSuperview() + node.removeFromSupernode() } } @@ -1160,7 +1196,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { for (_, button) in self.reactionButtonsContainer.buttons { if button.frame.contains(point) { - if let result = button.hitTest(self.view.convert(point, to: button), with: event) { + if let result = button.hitTest(self.view.convert(point, to: button.view), with: event) { return result } } diff --git a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift index 935f6aa1b1..827b30dc56 100644 --- a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift @@ -63,6 +63,15 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + + self.interactiveFileNode.dateAndStatusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } } override func accessibilityActivate() -> Bool { diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 4027a3fc32..d7360637c9 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -184,6 +184,12 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD return .fail } } + + if let reactionButtonsNode = strongSelf.reactionButtonsNode { + if let _ = reactionButtonsNode.hitTest(strongSelf.view.convert(point, to: reactionButtonsNode.view), with: nil) { + return .fail + } + } } return .waitForSingleTap } @@ -797,6 +803,14 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + reactionButtonsNode.openReactionPreview = { gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } reactionButtonsNode.frame = reactionButtonsFrame strongSelf.addSubnode(reactionButtonsNode) if animation.isAnimated { @@ -1312,6 +1326,13 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } + override func openMessageContextMenu() { + guard let item = self.item else { + return + } + item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.interactiveVideoNode.frame, nil) + } + override func targetReactionView(value: String) -> UIView? { if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { return result diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 87cbc47714..260c025ce0 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -462,7 +462,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: isPinned && !associatedData.isInPinnedListMode, - hasAutoremove: message.isSelfExpiring + hasAutoremove: message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: message) )) } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift index c033782e6a..e0ca23424b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -303,7 +303,8 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index ecac91cdca..08514b4d60 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -522,7 +522,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio reactionPeers: dateAndStatus.dateReactionPeers, replyCount: dateAndStatus.dateReplies, isPinned: dateAndStatus.isPinned, - hasAutoremove: message.isSelfExpiring + hasAutoremove: message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: message) )) let (size, apply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) diff --git a/submodules/TelegramUI/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Sources/ChatMessageItemView.swift index 43d64f554f..212c3f51ca 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItemView.swift @@ -869,6 +869,9 @@ public class ChatMessageItemView: ListViewItemNode { } } + func openMessageContextMenu() { + } + func targetReactionView(value: String) -> UIView? { return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift index 1e0f25f693..eb244603e7 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift @@ -259,7 +259,8 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) diff --git a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift index 8363389824..bcff6c1fc7 100644 --- a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift @@ -1079,7 +1079,8 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) } diff --git a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift index d3f853a2b8..41b56929a6 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift @@ -13,6 +13,24 @@ import ReactionButtonListComponent import AccountContext import WallpaperBackgroundNode +func canViewMessageReactionList(message: Message) -> Bool { + if let peer = message.peers[message.id.peerId] { + if let channel = peer as? TelegramChannel { + if case .broadcast = channel.info { + return false + } else { + return true + } + } else if let _ = peer as? TelegramGroup { + return true + } else { + return false + } + } else { + return false + } +} + final class MessageReactionButtonsNode: ASDisplayNode { enum DisplayType { case incoming @@ -29,7 +47,9 @@ final class MessageReactionButtonsNode: ASDisplayNode { private let container: ReactionButtonsAsyncLayoutContainer private let backgroundMaskView: UIView private var backgroundMaskButtons: [String: UIView] = [:] + var reactionSelected: ((String) -> Void)? + var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingNode, String) -> Void)? override init() { self.container = ReactionButtonsAsyncLayoutContainer() @@ -53,7 +73,46 @@ final class MessageReactionButtonsNode: ASDisplayNode { type: DisplayType ) -> (proposedWidth: CGFloat, continueLayout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> Void)) { let reactionColors: ReactionButtonComponent.Colors + let themeColors: PresentationThemeBubbleColorComponents switch type { + case .incoming: + themeColors = bubbleColorComponents(theme: presentationData.theme.theme, incoming: true, wallpaper: !presentationData.theme.wallpaper.isEmpty) + reactionColors = ReactionButtonComponent.Colors( + deselectedBackground: themeColors.reactionInactiveBackground.argb, + selectedBackground: themeColors.reactionActiveBackground.argb, + deselectedForeground: themeColors.reactionInactiveForeground.argb, + selectedForeground: themeColors.reactionActiveForeground.argb, + extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb, + extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb + ) + case .outgoing: + themeColors = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty) + reactionColors = ReactionButtonComponent.Colors( + deselectedBackground: themeColors.reactionInactiveBackground.argb, + selectedBackground: themeColors.reactionActiveBackground.argb, + deselectedForeground: themeColors.reactionInactiveForeground.argb, + selectedForeground: themeColors.reactionActiveForeground.argb, + extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb, + extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb + ) + case .freeform: + if presentationData.theme.wallpaper.isEmpty { + themeColors = presentationData.theme.theme.chat.message.freeform.withoutWallpaper + } else { + themeColors = presentationData.theme.theme.chat.message.freeform.withWallpaper + } + + reactionColors = ReactionButtonComponent.Colors( + deselectedBackground: selectReactionFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper).argb, + selectedBackground: themeColors.reactionActiveBackground.argb, + deselectedForeground: themeColors.reactionInactiveForeground.argb, + selectedForeground: themeColors.reactionActiveForeground.argb, + extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb, + extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb + ) + } + + /*switch type { case .incoming: reactionColors = ReactionButtonComponent.Colors( deselectedBackground: presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb, @@ -75,6 +134,11 @@ final class MessageReactionButtonsNode: ASDisplayNode { deselectedForeground: UIColor(white: 1.0, alpha: 1.0).argb, selectedForeground: UIColor(white: 0.0, alpha: 0.1).argb ) + }*/ + + var totalReactionCount: Int = 0 + for reaction in reactions.reactions { + totalReactionCount += Int(reaction.count) } let reactionButtonsResult = self.container.update( @@ -109,11 +173,11 @@ final class MessageReactionButtonsNode: ASDisplayNode { } } - if peers.count != Int(reaction.count) { + if peers.count != Int(reaction.count) || totalReactionCount != reactions.recentPeers.count { peers.removeAll() } - return ReactionButtonsLayoutContainer.Reaction( + return ReactionButtonsAsyncLayoutContainer.Reaction( reaction: ReactionButtonComponent.Reaction( value: reaction.value, iconFile: iconFile @@ -244,15 +308,27 @@ final class MessageReactionButtonsNode: ASDisplayNode { strongSelf.backgroundMaskButtons[item.value] = itemMaskView } - if item.view.superview == nil { - strongSelf.view.addSubview(item.view) + if item.node.supernode == nil { + strongSelf.addSubnode(item.node) if animation.isAnimated { - item.view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) - item.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + item.node.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + item.node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - item.view.frame = itemFrame + item.node.frame = itemFrame + + let itemValue = item.value + let itemNode = item.node + item.node.isGestureEnabled = canViewMessageReactionList(message: message) + item.node.activated = { [weak itemNode] gesture, _ in + guard let strongSelf = self, let itemNode = itemNode else { + gesture.cancel() + return + } + strongSelf.openReactionPreview?(gesture, itemNode.containerNode, itemValue) + } + item.node.additionalActivationProgressLayer = itemMaskView.layer } else { - animation.animator.updateFrame(layer: item.view.layer, frame: itemFrame, completion: nil) + animation.animator.updateFrame(layer: item.node.layer, frame: itemFrame, completion: nil) } if itemMaskView.superview == nil { @@ -285,14 +361,14 @@ final class MessageReactionButtonsNode: ASDisplayNode { strongSelf.backgroundMaskButtons.removeValue(forKey: id) } - for view in reactionButtons.removedViews { + for node in reactionButtons.removedNodes { if animation.isAnimated { - view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in - view?.removeFromSuperview() + node.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in + node?.removeFromSupernode() }) } else { - view.removeFromSuperview() + node.removeFromSupernode() } } }) @@ -349,6 +425,18 @@ final class MessageReactionButtonsNode: ASDisplayNode { animation.animator.updateScale(layer: button.layer, scale: 0.01, completion: nil) } } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for (_, button) in self.container.buttons { + if button.frame.contains(point) { + if let result = button.hitTest(self.view.convert(point, to: button.view), with: event) { + return result + } + } + } + + return nil + } } final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode { @@ -367,6 +455,15 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + + self.buttonsNode.openReactionPreview = { [weak self] gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } } required init?(coder aDecoder: NSCoder) { @@ -502,6 +599,7 @@ final class ChatMessageReactionButtonsNode: ASDisplayNode { private let buttonsNode: MessageReactionButtonsNode var reactionSelected: ((String) -> Void)? + var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingNode, String) -> Void)? override init() { self.buttonsNode = MessageReactionButtonsNode() @@ -509,9 +607,14 @@ final class ChatMessageReactionButtonsNode: ASDisplayNode { super.init() self.addSubnode(self.buttonsNode) + self.buttonsNode.reactionSelected = { [weak self] value in self?.reactionSelected?(value) } + + self.buttonsNode.openReactionPreview = { [weak self] gesture, sourceNode, value in + self?.openReactionPreview?(gesture, sourceNode, value) + } } class func asyncLayout(_ maybeNode: ChatMessageReactionButtonsNode?) -> (_ arguments: ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)) { diff --git a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift index 13df9e7b0c..c7e3cb838a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -127,7 +127,8 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) } diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index 856bf74daa..916bf82ab3 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -173,6 +173,12 @@ class ChatMessageStickerItemNode: ChatMessageItemView { return .fail } + if let reactionButtonsNode = strongSelf.reactionButtonsNode { + if let _ = reactionButtonsNode.hitTest(strongSelf.view.convert(point, to: reactionButtonsNode.view), with: nil) { + return .fail + } + } + if let item = strongSelf.item, item.presentationData.largeEmoji && messageIsElligibleForLargeEmoji(item.message) { if strongSelf.imageNode.frame.contains(point) { return .waitForDoubleTap @@ -511,7 +517,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) @@ -988,6 +995,14 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + reactionButtonsNode.openReactionPreview = { gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } reactionButtonsNode.frame = reactionButtonsFrame strongSelf.addSubnode(reactionButtonsNode) if animation.isAnimated { @@ -1599,6 +1614,13 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } + override func openMessageContextMenu() { + guard let item = self.item else { + return + } + item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.imageNode.frame, nil) + } + override func targetReactionView(value: String) -> UIView? { if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { return result diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 167cdfa360..9a973932ec 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -76,6 +76,15 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + + self.statusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } } required init?(coder aDecoder: NSCoder) { @@ -309,7 +318,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) } diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index fc705ac0ad..c0f14cb7a1 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -255,6 +255,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self?.openPeerMention(name) }, openMessageContextMenu: { [weak self] message, selectAll, node, frame, _ in self?.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame) + }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift index 93cac14f9d..e498f2a6d4 100644 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift @@ -108,7 +108,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { var selectStickerImpl: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? self.controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in + }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, _, _, _, node, rect in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 80644ecb5a..ad27f89381 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -69,6 +69,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in + }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 8296a53bab..ae4372cd49 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -1182,7 +1182,17 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { //TODO:localize - items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .none, text: "Reactions", icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: { + let label: String + if let cachedData = data.cachedData as? CachedChannelData, let allowedReactions = cachedData.allowedReactions { + if allowedReactions.isEmpty { + label = "Disabled" + } else { + label = "\(allowedReactions.count)" + } + } else { + label = "" + } + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .text(label), text: "Reactions", icon: UIImage(bundleImageName: "Settings/Menu/Reactions"), action: { interaction.editingOpenReactionsSetup() })) } @@ -1297,7 +1307,17 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { //TODO:localize - items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .none, text: "Reactions", icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: { + let label: String + if let cachedData = data.cachedData as? CachedChannelData, let allowedReactions = cachedData.allowedReactions { + if allowedReactions.isEmpty { + label = "Disabled" + } else { + label = "\(allowedReactions.count)" + } + } else { + label = "" + } + items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .text(label), text: "Reactions", icon: UIImage(bundleImageName: "Settings/Menu/Reactions"), action: { interaction.editingOpenReactionsSetup() })) } @@ -1310,7 +1330,17 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr } else { if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { //TODO:localize - items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .none, text: "Reactions", icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: { + let label: String + if let cachedData = data.cachedData as? CachedChannelData, let allowedReactions = cachedData.allowedReactions { + if allowedReactions.isEmpty { + label = "Disabled" + } else { + label = "\(allowedReactions.count)" + } + } else { + label = "" + } + items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .text(label), text: "Reactions", icon: UIImage(bundleImageName: "Settings/Menu/Reactions"), action: { interaction.editingOpenReactionsSetup() })) } @@ -1409,10 +1439,22 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr interaction.editingOpenPreHistorySetup() })) - //TODO:localize - items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .none, text: "Reactions", icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: { - interaction.editingOpenReactionsSetup() - })) + do { + //TODO:localize + let label: String + if let cachedData = data.cachedData as? CachedGroupData, let allowedReactions = cachedData.allowedReactions { + if allowedReactions.isEmpty { + label = "Disabled" + } else { + label = "\(allowedReactions.count)" + } + } else { + label = "" + } + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .text(label), text: "Reactions", icon: UIImage(bundleImageName: "Settings/Menu/Reactions"), action: { + interaction.editingOpenReactionsSetup() + })) + } canViewAdminsAndBanned = true } else if case let .admin(rights, _) = group.role { @@ -1881,6 +1923,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) strongSelf.controller?.window?.presentInGlobalOverlay(controller) }) + }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { [weak self] message, node, rect, gesture in diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index d36d7c4937..7fd0a6c3d5 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1239,7 +1239,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { let controllerInteraction: ChatControllerInteraction controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in + }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: { message in tapMessage?(message)