From b0d3aa257803e96a061e456f172a076974eede13 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 17 Dec 2021 23:04:46 +0400 Subject: [PATCH 01/35] 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 02/35] 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) From 835ce9adaea4437e6db83b110b5f57bd39d169e3 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 18 Dec 2021 02:35:50 +0400 Subject: [PATCH 03/35] Various Improvements --- .../Telegram-iOS/Resources/PlaneLogoPlain.tgs | Bin 0 -> 2101 bytes .../Telegram-iOS/en.lproj/Localizable.strings | 4 + submodules/ChatListUI/BUILD | 1 + .../Sources/Node/ChatListItem.swift | 41 +- .../Sources/DebugController.swift | 24 +- submodules/Display/Source/TextNode.swift | 6 +- submodules/GalleryUI/BUILD | 1 + .../Sources/Items/ChatImageGalleryItem.swift | 3 + .../Sources/RecognizedTextSelectionNode.swift | 7 + .../Sources/InvisibleInkDustNode.swift | 18 +- .../QrCodeUI/Sources/QrCodeScreen.swift | 4 +- .../StorageUsageController.swift | 2 + .../RecentSessionsHeaderItem.swift | 2 +- .../Sources/SolidRoundedButtonNode.swift | 2 +- .../Sources/Utils/MessageUtils.swift | 9 + .../Sources/MessageContentKind.swift | 7 +- submodules/TelegramUI/BUILD | 1 + .../Translate.imageset/Contents.json | 12 + .../Translate.imageset/translate_24.pdf | 127 ++ .../TelegramUI/Sources/ChatController.swift | 3 + .../ChatInterfaceStateContextMenus.swift | 9 + .../ChatMessageAnimatedStickerItemNode.swift | 2 +- .../Sources/ChatMessageBubbleItemNode.swift | 2 +- .../ChatMessageInstantVideoItemNode.swift | 2 +- .../Sources/ChatMessageReplyInfoNode.swift | 43 +- .../Sources/ChatMessageStickerItemNode.swift | 2 +- .../ChatMessageTextBubbleContentNode.swift | 12 +- .../TelegramUI/Sources/ChatQrCodeScreen.swift | 1322 +++++++++++++++++ .../TelegramUI/Sources/ChatThemeScreen.swift | 6 +- .../Sources/EditAccessoryPanelNode.swift | 2 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 34 +- .../Sources/ReplyAccessoryPanelNode.swift | 2 +- .../Sources/ExperimentalUISettings.swift | 10 +- .../Sources/ChatTextInputAttributes.swift | 2 +- .../Sources/TextSelectionNode.swift | 7 + submodules/Translate/BUILD | 19 + submodules/Translate/Sources/Translate.swift | 27 + .../Sources/WallpaperBackgroundNode.swift | 18 +- .../Sources/WallpaperResources.swift | 256 ++-- 39 files changed, 1886 insertions(+), 165 deletions(-) create mode 100644 Telegram/Telegram-iOS/Resources/PlaneLogoPlain.tgs create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/translate_24.pdf create mode 100644 submodules/TelegramUI/Sources/ChatQrCodeScreen.swift create mode 100644 submodules/Translate/BUILD create mode 100644 submodules/Translate/Sources/Translate.swift diff --git a/Telegram/Telegram-iOS/Resources/PlaneLogoPlain.tgs b/Telegram/Telegram-iOS/Resources/PlaneLogoPlain.tgs new file mode 100644 index 0000000000000000000000000000000000000000..1bfbf196d7a2291cfdae0c8ac2e4aa55012ac24c GIT binary patch literal 2101 zcmV-52+H>#iwFp3d%R%)15j*XZe>hwXKzq!VQFqIYIARH0PUOIZX3H1$6qDbYc9dz z@Y4@)dQ%hy+B-uRYU3Omaco0UP8$S%_x{P{u2zy)ksL}vaTY-=ExAi_KKSEsmipdo zeqT2?B$_{(o5o5jbt9TjtLDZj(LB5~HyWSM`1D3J|7mV4Y0=!5&!;bV$l!sGA3ruX zxN*5&-)+#|?Lst<%Wrq9^!@u#{qXcL_qF-f+;DmPZ(koDKcr6At6cXPk9=>Q)1PI2 z=rjJlisq%1P%c5E52@h4sFZ=x*thZxw>}RwW)m=Oc7q8;|-I>yzQ!;fHq`P2e;i|Hwd#@%NUspDxuPB@GSC!4u6=k#bs{)(mPD(y9BG75Vx_lRwFC*VTi2ES6dt4s#rhNl$+02Y+>6`k*b4uX^GHkA_c&NO z_XdIdwxbq8+pT;f>yPyFezS9Dc2ms$_s8Ya-N@>C&+4lw8{aDF;vx%qON){4s9-7; zNrY_S0^`=|9TlF{NDtZ8xC70kb{EH8GEKg05o7g^t(dxsU1ubgilJ($d^m##r5Ad$ z2Y40lu1;j7GO$0Ub?~*iDX=N)EkOi$GeSc{=dfl^0a?Av9R}PIT$;TwBr%25*d^EZ z%a^+@c1+&Z{Jw&ZJ#0FYJaz7vE7tc-?`m(G&AuV1eam3^q1)RwN*|z6Exic~aht`= zN`PR{v(b@vp)utai#*ud zZQnKsEn|$MUT}?w+<*z&2GOU-Q~};J@u(Y47LYW$F{CD2P%D`3(j#KU5jWD zEhpIsGHFV}{JXi?tiIm8?oEjwItqIBtlxANNTs(5@14bCV0zD42)a_dIPeyz)Q&v^ zK7mDrgWK~KEcwQEMs?h^rr-1zCy7S}Fa8px-8vD;kQ9S~#j&Gy-ohjAFYbcbn$6t1 z3*dubA{C(VuB9Zz)?ZYz8T1enGgF7;KoKW294We700FR(wD8#MQLTpI;A2?zAa1&) zwLY(tXc@ibdmLt=@*<@1XB%^n25yFUh|+MF!8a#ND1tGDXJc|x`J^HkKT;FBqNUPGP z$0k2AUhEWX;jk7OCa0XhT8aWq)&<8pdE*ej49}2C=;^|#9F3Kpe|spg_F>PPv{1h6 z^_&;y{l&MGuJ@K?r3C&jx_kf@9sL)et7z> z#)jQmjn=%`X)TqwVX%jGZndkpHS6`l@OVve*b)68gzQ643yOp0-cAINtqGXK{ch){ z!^f`Gp6OUWyz4;4Jw$uRV#9!y6zde2$ZlJinTG*6&?EpJs>4Z&B**}?cFdA@f&|=3 z|Ne})=qB|sT%@^^v(>S~1y-yHUf3hNFc;?q)#Qb#$_snI3xAXsHF41=Cb4)A^ZI05 z;Aw4Xro~K)Q)!Xta2hS#5nAx2Y2j2*+r_H50$SL>C6T#+1}{Cjz4% zBC`@ktC<)xF;0#zVn>Y76T?pt!%q{#Um6$`D*~fWzSjW;^FA=x9s>r|1V*nHHkexe zr0vL0zV@dWq=p8yvywQTLMJZaRwTj3{~|bnO4hPSsk0%CIKat7NjB!}W28Mewb04g69B7bZ*AQjEi${!6jc}xxw>U&bFLtHYaR%cY4hl;ZE(e?+VAT^DOizilnMO2Wg$_hGQ1z#(#+6PEyrB!=W z%xTq`6z7q`oJtCsCdI{*syb9e1yfB_7`=}QV~(PtCM&e-ldAA9b`4#v8-mzL`AO^#>6MhC*q<% zENj6SYi3%^v^Y7d={j1tKCL;uA31VbG+Z1OH4_!4>e(-Qkf^XnkCs>6k8lZOjA5a} z$)_uN$XJ8jh?n};9ItTFz};9$)(5=cD4AXQF&mFzQjS+h3`#_js{84q_fiEf2u&%9 fGcqoV3@+!(ZOLGy+2HdPx8PveF)i literal 0 HcmV?d00001 diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 21c11250d4..5074a8ff8e 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7145,3 +7145,7 @@ Sorry for the inconvenience."; "ChatList.Archive" = "Archive"; "TextFormat.Spoiler" = "Spoiler"; + +"Conversation.ContextMenuTranslate" = "Translate"; + +"ClearCache.ClearDescription" = "All media will stay in the Telegram cloud and can be re-downloaded if you need it again."; diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 5123ae2fd3..652f74211d 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -61,6 +61,7 @@ swift_library( "//submodules/TelegramStringFormatting:TelegramStringFormatting", "//submodules/TelegramCallsUI:TelegramCallsUI", "//submodules/StickerResources:StickerResources", + "//submodules/TextFormat:TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 17b5c9cf6e..6db0fac958 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -17,6 +17,8 @@ import PhotoResources import ChatListSearchItemNode import ContextUI import ChatInterfaceState +import TextFormat +import InvisibleInkDustNode public enum ChatListItemContent { public final class DraftState: Equatable { @@ -427,6 +429,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let measureNode: TextNode private var currentItemHeight: CGFloat? let textNode: TextNode + var dustNode: InvisibleInkDustNode? let inputActivitiesNode: ChatListInputActivitiesNode let dateNode: TextNode let separatorNode: ASDisplayNode @@ -1049,12 +1052,26 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { attributedText = NSAttributedString(string: foldLineBreaks(draftText.replacingOccurrences(of: "\n\n", with: " ")), font: textFont, textColor: theme.messageTextColor) } else if let message = messages.last { var composedString: NSMutableAttributedString + + let entities = (message._asMessage().textEntitiesAttribute?.entities ?? []).filter { entity in + if case .Spoiler = entity.type { + return true + } else { + return false + } + } + let messageString: NSAttributedString + if !message.text.isEmpty { + messageString = stringWithAppliedEntities(message.text, entities: entities, baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false) + } else { + messageString = NSAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor) + } if let inlineAuthorPrefix = inlineAuthorPrefix { composedString = NSMutableAttributedString() composedString.append(NSAttributedString(string: "\(inlineAuthorPrefix): ", font: textFont, textColor: theme.titleColor)) - composedString.append(NSAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor)) + composedString.append(messageString) } else { - composedString = NSMutableAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor) + composedString = NSMutableAttributedString(attributedString: messageString) } if let searchQuery = item.interaction.searchTextHighightState { @@ -1395,7 +1412,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { textCutout = TextNodeCutout(topLeft: CGSize(width: textLeftCutout, height: 10.0), topRight: nil, bottomRight: nil) } let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: textAttributedString, backgroundColor: nil, maximumNumberOfLines: authorAttributedString == nil ? 2 : 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth - badgeSize, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: textCutout, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) - + let titleRectWidth = rawContentWidth - dateLayout.size.width - 10.0 - statusWidth - titleIconsWidth let (titleLayout, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -1730,6 +1747,24 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let textNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.minY + titleLayout.size.height - 1.0 + UIScreenPixel + (authorLayout.size.height.isZero ? 0.0 : (authorLayout.size.height - 3.0))), size: textLayout.size) strongSelf.textNode.frame = textNodeFrame + if !textLayout.spoilers.isEmpty { + let dustNode: InvisibleInkDustNode + if let current = strongSelf.dustNode { + dustNode = current + } else { + dustNode = InvisibleInkDustNode(textNode: nil) + dustNode.isUserInteractionEnabled = false + strongSelf.dustNode = dustNode + strongSelf.contextContainer.insertSubnode(dustNode, aboveSubnode: strongSelf.textNode) + } + dustNode.update(size: textNodeFrame.size, color: theme.messageTextColor, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) + dustNode.frame = textNodeFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) + + } else if let dustNode = strongSelf.dustNode { + strongSelf.dustNode = nil + dustNode.removeFromSupernode() + } + var animateInputActivitiesFrame = false let inputActivities = inputActivities?.filter({ switch $0.1 { diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 3ada505314..cd01fdb089 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -81,6 +81,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case enableDebugDataDisplay(Bool) case acceleratedStickers(Bool) case experimentalBackground(Bool) + case snow(Bool) case playerEmbedding(Bool) case playlistPlayback(Bool) case voiceConference @@ -102,7 +103,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logging.rawValue case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries: return DebugControllerSection.experiments.rawValue - case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .experimentalBackground: + case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .experimentalBackground, .snow: return DebugControllerSection.experiments.rawValue case .preferredVideoCodec: return DebugControllerSection.videoExperiments.rawValue @@ -173,14 +174,16 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 29 case .experimentalBackground: return 30 - case .playerEmbedding: + case .snow: return 31 - case .playlistPlayback: + case .playerEmbedding: return 32 - case .voiceConference: + case .playlistPlayback: return 33 + case .voiceConference: + return 34 case let .preferredVideoCodec(index, _, _, _): - return 34 + index + return 35 + index case .disableVideoAspectScaling: return 100 case .enableVoipTcp: @@ -814,6 +817,16 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) + case let .snow(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Snow", value: value, sectionId: self.section, style: .blocks, updated: { value in + let _ = arguments.sharedContext.accountManager.transaction ({ transaction in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in + var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings + settings.snow = value + return PreferencesEntry(settings) + }) + }).start() + }) case let .playerEmbedding(value): return ItemListSwitchItem(presentationData: presentationData, title: "Player Embedding", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in @@ -927,6 +940,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.enableDebugDataDisplay(experimentalSettings.enableDebugDataDisplay)) entries.append(.acceleratedStickers(experimentalSettings.acceleratedStickers)) entries.append(.experimentalBackground(experimentalSettings.experimentalBackground)) + entries.append(.snow(experimentalSettings.snow)) entries.append(.playerEmbedding(experimentalSettings.playerEmbedding)) entries.append(.playlistPlayback(experimentalSettings.playlistPlayback)) } diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 33ca210f87..ee16bed593 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -1005,6 +1005,10 @@ public class TextNode: ASDisplayNode { } let lineRange = CFRange(location: lastLineCharacterIndex, length: stringLength - lastLineCharacterIndex) + var brokenLineRange = CFRange(location: lastLineCharacterIndex, length: lineCharacterCount) + if brokenLineRange.location + brokenLineRange.length > attributedString.length { + brokenLineRange.length = attributedString.length - brokenLineRange.location + } if lineRange.length == 0 { break } @@ -1033,7 +1037,7 @@ public class TextNode: ASDisplayNode { } var headIndent: CGFloat = 0.0 - attributedString.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in + attributedString.enumerateAttributes(in: NSMakeRange(brokenLineRange.location, brokenLineRange.length), options: []) { attributes, range, _ in if let _ = attributes[NSAttributedString.Key.init(rawValue: "TelegramSpoiler")] { var ascent: CGFloat = 0.0 var descent: CGFloat = 0.0 diff --git a/submodules/GalleryUI/BUILD b/submodules/GalleryUI/BUILD index 87f0008595..71887c2ba2 100644 --- a/submodules/GalleryUI/BUILD +++ b/submodules/GalleryUI/BUILD @@ -39,6 +39,7 @@ swift_library( "//submodules/Speak:Speak", "//submodules/UndoUI:UndoUI", "//submodules/InvisibleInkDustNode:InvisibleInkDustNode", + "//submodules/Translate:Translate", ], visibility = [ "//visibility:public", diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index 824f19c07b..1e32f64027 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -16,6 +16,7 @@ import PresentationDataUtils import ImageContentAnalysis import TextSelectionNode import Speak +import Translate import ShareController import UndoUI @@ -352,6 +353,8 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { } case .speak: speakText(string) + case .translate: + translateText(context: strongSelf.context, text: string) } }) recognizedContentNode.barcodeAction = { [weak self] payload, rect in diff --git a/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift b/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift index 64fcc28b6d..dc42d26acf 100644 --- a/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift +++ b/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift @@ -203,6 +203,7 @@ public enum RecognizedTextSelectionAction { case share case lookup case speak + case translate } public final class RecognizedTextSelectionNode: ASDisplayNode { @@ -509,6 +510,12 @@ public final class RecognizedTextSelectionNode: ASDisplayNode { self?.performAction(selectedText, .lookup) let _ = self?.dismissSelection() })) +// if #available(iOS 15.0, *) { +// actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuTranslate, accessibilityLabel: self.strings.Conversation_ContextMenuTranslate), action: { [weak self] in +// self?.performAction(selectedText, .translate) +// let _ = self?.dismissSelection() +// })) +// } if isSpeakSelectionEnabled() { actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuSpeak, accessibilityLabel: self.strings.Conversation_ContextMenuSpeak), action: { [weak self] in self?.performAction(selectedText, .speak) diff --git a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift index cdded32c83..6974a73a49 100644 --- a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift @@ -62,7 +62,7 @@ public class InvisibleInkDustNode: ASDisplayNode { public var isRevealed = false - public init(textNode: TextNode) { + public init(textNode: TextNode?) { self.textNode = textNode self.emitterNode = ASDisplayNode() @@ -144,7 +144,7 @@ public class InvisibleInkDustNode: ASDisplayNode { } @objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) { - guard let (size, _, _) = self.currentParams, !self.isRevealed else { + guard let (size, _, _) = self.currentParams, let textNode = self.textNode, !self.isRevealed else { return } @@ -155,15 +155,15 @@ public class InvisibleInkDustNode: ASDisplayNode { self.emitterLayer?.setValue(position, forKeyPath: "emitterBehaviors.fingerAttractor.position") Queue.mainQueue().after(0.1 * UIView.animationDurationFactor()) { - self.textNode?.view.mask = self.textMaskNode.view - self.textNode?.alpha = 1.0 + textNode.view.mask = self.textMaskNode.view + textNode.alpha = 1.0 let radius = max(size.width, size.height) self.textSpotNode.frame = CGRect(x: position.x - radius / 2.0, y: position.y - radius / 2.0, width: radius, height: radius) self.textSpotNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - self.textSpotNode.layer.animateScale(from: 0.1, to: 3.5, duration: 0.71, removeOnCompletion: false, completion: { [weak self] _ in - self?.textNode?.view.mask = nil + self.textSpotNode.layer.animateScale(from: 0.1, to: 3.5, duration: 0.71, removeOnCompletion: false, completion: { _ in + textNode.view.mask = nil }) self.emitterNode.view.mask = self.emitterMaskNode.view @@ -187,7 +187,7 @@ public class InvisibleInkDustNode: ASDisplayNode { } - let textLength = CGFloat((self.textNode?.cachedLayout?.attributedString?.string ?? "").count) + let textLength = CGFloat((textNode.cachedLayout?.attributedString?.string ?? "").count) let timeToRead = min(45.0, ceil(max(4.0, textLength * 0.04))) Queue.mainQueue().after(timeToRead * UIView.animationDurationFactor()) { self.isRevealed = false @@ -195,9 +195,7 @@ public class InvisibleInkDustNode: ASDisplayNode { let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .linear) transition.updateAlpha(node: self, alpha: 1.0) - if let textNode = self.textNode { - transition.updateAlpha(node: textNode, alpha: 0.0) - } + transition.updateAlpha(node: textNode, alpha: 0.0) } } diff --git a/submodules/QrCodeUI/Sources/QrCodeScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScreen.swift index 366b0a84ff..b6aadc72ae 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScreen.swift @@ -267,8 +267,7 @@ public final class QrCodeScreen: ViewController { self.qrImageNode.clipsToBounds = true self.qrImageNode.cornerRadius = 16.0 - self.qrIconNode = AnimatedStickerNode() - + self.qrIconNode = AnimatedStickerNode() self.qrIconNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "PlaneLogo"), width: 240, height: 240, mode: .direct(cachePathPrefix: nil)) self.qrIconNode.visibility = true @@ -434,7 +433,6 @@ public final class QrCodeScreen: ViewController { let imageSide: CGFloat = 240.0 let imageSize = CGSize(width: imageSide, height: imageSide) let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil)) - let _ = imageApply() let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) diff --git a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift index b6bd4802fd..4939d18b57 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift @@ -521,6 +521,8 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P var totalSize: Int64 = 0 + items.append(ActionSheetTextItem(title: presentationData.strings.ClearCache_ClearDescription)) + for categoryId in validCategories { if let (_, size) = sizeIndex[categoryId] { let categorySize: Int64 = size diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift index bfc12f83f1..ef9d32fa65 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift @@ -150,7 +150,7 @@ class RecentSessionsHeaderItemNode: ListViewItemNode { strongSelf.buttonNode.title = item.context.sharedContext.currentPresentationData.with { $0 }.strings.AuthSessions_LinkDesktopDevice if let _ = updatedTheme { - strongSelf.buttonNode.icon = generateTintedImage(image: UIImage(bundleImageName: "Settings/QrButtonIcon"), color: .white) + strongSelf.buttonNode.icon = UIImage(bundleImageName: "Settings/QrButtonIcon") strongSelf.buttonNode.updateTheme(SolidRoundedButtonTheme(theme: item.theme)) } diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index 6f0b95f0f4..eaca96a86b 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -69,7 +69,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode { public var icon: UIImage? { didSet { - self.iconNode.image = generateTintedImage(image: self.iconNode.image, color: self.theme.foregroundColor) + self.iconNode.image = generateTintedImage(image: self.icon, color: self.theme.foregroundColor) } } diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index 5d804ee1a5..ff7393ebaf 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -322,6 +322,15 @@ public extension Message { } return nil } + + var textEntitiesAttribute: TextEntitiesMessageAttribute? { + for attribute in self.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + return attribute + } + } + return nil + } } public func _internal_parseMediaAttachment(data: Data) -> Media? { diff --git a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift index e51050345e..bbd4720999 100644 --- a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift +++ b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift @@ -229,12 +229,13 @@ public func stringForMediaKind(_ kind: MessageContentKind, strings: Presentation } } -public func descriptionStringForMessage(contentSettings: ContentSettings, message: EngineMessage, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: EnginePeer.Id) -> (String, Bool) { +public func descriptionStringForMessage(contentSettings: ContentSettings, message: EngineMessage, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: EnginePeer.Id) -> (String, Bool, Bool) { let contentKind = messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId) if !message.text.isEmpty && ![.expiredImage, .expiredVideo].contains(contentKind.key) { - return (foldLineBreaks(message.text), false) + return (foldLineBreaks(message.text), false, true) } - return stringForMediaKind(contentKind, strings: strings) + let result = stringForMediaKind(contentKind, strings: strings) + return (result.0, result.1, false) } public func foldLineBreaks(_ text: String) -> String { diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index d0f2a6d0de..3a3ff87bed 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -251,6 +251,7 @@ swift_library( "//submodules/InvisibleInkDustNode:InvisibleInkDustNode", "//submodules/QrCodeUI:QrCodeUI", "//submodules/Components/ReactionListContextMenuContent:ReactionListContextMenuContent", + "//submodules/Translate:Translate", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/Contents.json new file mode 100644 index 0000000000..1ce4a1d175 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "translate_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/translate_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/translate_24.pdf new file mode 100644 index 0000000000..7b61f5ea2e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/translate_24.pdf @@ -0,0 +1,127 @@ +%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 8.335022 7.270081 cm +0.000000 0.000000 0.000000 scn +5.135226 14.200185 m +4.875527 14.459883 4.454473 14.459883 4.194774 14.200185 c +3.935075 13.940486 3.935075 13.519431 4.194774 13.259732 c +6.194774 11.259732 l +6.454473 11.000034 6.875527 11.000034 7.135226 11.259732 c +7.394925 11.519431 7.394925 11.940486 7.135226 12.200185 c +5.135226 14.200185 l +h +11.665000 9.064959 m +10.317697 9.064959 l +10.212673 6.276149 9.432839 4.057345 7.885226 2.509732 c +7.853345 2.477852 7.821179 2.446297 7.788730 2.415067 c +9.029876 1.748239 10.640882 1.394958 12.665000 1.394958 c +13.032269 1.394958 13.330000 1.097227 13.330000 0.729958 c +13.330000 0.362688 13.032269 0.064959 12.665000 0.064959 c +10.248617 0.064959 8.228645 0.535731 6.665002 1.532337 c +5.101360 0.535731 3.081383 0.064959 0.665000 0.064959 c +0.297731 0.064959 0.000000 0.362688 0.000000 0.729958 c +0.000000 1.097227 0.297731 1.394958 0.665000 1.394958 c +2.689117 1.394958 4.300128 1.748238 5.541274 2.415066 c +5.508824 2.446296 5.476655 2.477852 5.444774 2.509732 c +3.897161 4.057345 3.117327 6.276149 3.012303 9.064959 c +1.665000 9.064959 l +1.297731 9.064959 1.000000 9.362689 1.000000 9.729959 c +1.000000 10.097228 1.297731 10.394958 1.665000 10.394958 c +3.665000 10.394958 l +9.665000 10.394958 l +11.665000 10.394958 l +12.032269 10.394958 12.330000 10.097228 12.330000 9.729959 c +12.330000 9.362689 12.032269 9.064959 11.665000 9.064959 c +h +4.343254 9.064959 m +8.986746 9.064959 l +8.882931 6.515748 8.171854 4.677263 6.944774 3.450184 c +6.854816 3.360226 6.761571 3.273041 6.665002 3.188664 c +6.568432 3.273041 6.475184 3.360226 6.385226 3.450184 c +5.158146 4.677263 4.447069 6.515748 4.343254 9.064959 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 2.334839 3.245422 cm +0.000000 0.000000 0.000000 scn +5.780663 12.006347 m +5.678422 12.256270 5.435204 12.419556 5.165175 12.419556 c +4.895147 12.419556 4.651928 12.256270 4.549686 12.006347 c +0.049686 1.006347 l +-0.089374 0.666422 0.073459 0.278128 0.413384 0.139067 c +0.753309 0.000007 1.141604 0.162840 1.280664 0.502765 c +2.747988 4.089556 l +7.582363 4.089556 l +9.049686 0.502765 l +9.188747 0.162840 9.577042 0.000007 9.916966 0.139067 c +10.256891 0.278128 10.419724 0.666422 10.280664 1.006347 c +5.780663 12.006347 l +h +7.038272 5.419556 m +5.165175 9.998237 l +3.292078 5.419556 l +7.038272 5.419556 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 2388 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.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 +0000002478 00000 n +0000002501 00000 n +0000002674 00000 n +0000002748 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2807 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 0ce59576db..d6da13ca51 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -60,6 +60,7 @@ import InviteLinksUI import Markdown import TelegramPermissionsUI import Speak +import Translate import UniversalMediaPlayer import WallpaperBackgroundNode import ChatListUI @@ -2759,6 +2760,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } case .speak: speakText(text.string) + case .translate: + translateText(context: context, text: text.string) } }, displayImportedMessageTooltip: { [weak self] _ in guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 3a494304d4..3da3ea5a96 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -760,6 +760,15 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState f(.default) }))) + if #available(iOS 15.0, *) { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuTranslate, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + controllerInteraction.performTextSelectionAction(0, NSAttributedString(string: message.text), .translate) + f(.default) + }))) + } + if isSpeakSelectionEnabled() && !message.text.isEmpty { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuSpeak, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Message"), color: theme.actionSheet.primaryTextColor) diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index c443c5136c..f497e2031c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -893,7 +893,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - if item.associatedData.isCopyProtectionEnabled { + if item.associatedData.isCopyProtectionEnabled || item.message.isCopyProtected() { needsShareButton = false } } diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 26bbb5a99a..6dcaa1dfe0 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -1161,7 +1161,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - if item.associatedData.isCopyProtectionEnabled { + if item.associatedData.isCopyProtectionEnabled || item.message.isCopyProtected() { needsShareButton = false } } diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 4027a3fc32..9c9a19dd9c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -351,7 +351,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } - if item.associatedData.isCopyProtectionEnabled { + if item.associatedData.isCopyProtectionEnabled || item.message.isCopyProtected() { needsShareButton = false } } diff --git a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift index 10c2a1ac0d..73eafa58ac 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift @@ -10,6 +10,8 @@ import AccountContext import LocalizedPeerData import PhotoResources import TelegramStringFormatting +import TextFormat +import InvisibleInkDustNode enum ChatMessageReplyInfoType { case bubble(incoming: Bool) @@ -21,6 +23,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { private let lineNode: ASImageNode private var titleNode: TextNode? private var textNode: TextNode? + private var dustNode: InvisibleInkDustNode? private var imageNode: TransformImageNode? private var previousMediaReference: AnyMediaReference? @@ -64,7 +67,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { } } - let (textString, isMedia) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: context.account.peerId) + let (textString, isMedia, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: context.account.peerId) let placeholderColor: UIColor = message.effectivelyIncoming(context.account.peerId) ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor let titleColor: UIColor @@ -89,6 +92,21 @@ class ChatMessageReplyInfoNode: ASDisplayNode { textColor = titleColor } + + let messageText: NSAttributedString + if isText { + let entities = (message.textEntitiesAttribute?.entities ?? []).filter { entity in + if case .Spoiler = entity.type { + return true + } else { + return false + } + } + messageText = stringWithAppliedEntities(message.text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false) + } else { + messageText = NSAttributedString(string: textString, font: textFont, textColor: textColor) + } + var leftInset: CGFloat = 11.0 let spacing: CGFloat = 2.0 @@ -131,7 +149,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { let textInsets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0) let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets)) - let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: textString, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets)) + let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets)) let imageSide = titleLayout.size.height + textLayout.size.height - 16.0 @@ -218,8 +236,27 @@ class ChatMessageReplyInfoNode: ASDisplayNode { node.imageNode?.captureProtected = message.isCopyProtected() titleNode.frame = CGRect(origin: CGPoint(x: leftInset - textInsets.left, y: spacing - textInsets.top), size: titleLayout.size) - textNode.frame = CGRect(origin: CGPoint(x: leftInset - textInsets.left, y: titleNode.frame.maxY - textInsets.bottom + spacing - textInsets.top), size: textLayout.size) + let textFrame = CGRect(origin: CGPoint(x: leftInset - textInsets.left, y: titleNode.frame.maxY - textInsets.bottom + spacing - textInsets.top), size: textLayout.size) + textNode.frame = textFrame + + if !textLayout.spoilers.isEmpty { + let dustNode: InvisibleInkDustNode + if let current = node.dustNode { + dustNode = current + } else { + dustNode = InvisibleInkDustNode(textNode: nil) + dustNode.isUserInteractionEnabled = false + node.dustNode = dustNode + node.contentNode.insertSubnode(dustNode, aboveSubnode: textNode) + } + dustNode.update(size: textFrame.size, color: titleColor, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) + } else if let dustNode = node.dustNode { + dustNode.removeFromSupernode() + node.dustNode = nil + } + node.lineNode.image = lineImage node.lineNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 3.0), size: CGSize(width: 2.0, height: max(0.0, size.height - 5.0))) diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index 856bf74daa..a35f84bc62 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -436,7 +436,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - if item.associatedData.isCopyProtectionEnabled { + if item.associatedData.isCopyProtectionEnabled || item.message.isCopyProtected() { needsShareButton = false } } diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 167cdfa360..2dd980e9bd 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -374,6 +374,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { if let (_, spoilerTextApply) = spoilerTextLayoutAndApply { let spoilerTextNode = spoilerTextApply() if strongSelf.spoilerTextNode == nil { + spoilerTextNode.alpha = 0.0 spoilerTextNode.isUserInteractionEnabled = false spoilerTextNode.contentMode = .topLeft spoilerTextNode.contentsScale = UIScreenScale @@ -384,8 +385,6 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } strongSelf.spoilerTextNode?.frame = textFrame - strongSelf.spoilerTextNode?.isHidden = false - strongSelf.spoilerTextNode?.alpha = 0.0 let dustNode: InvisibleInkDustNode if let current = strongSelf.dustNode { @@ -395,11 +394,16 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.dustNode = dustNode strongSelf.insertSubnode(dustNode, aboveSubnode: spoilerTextNode) } - dustNode.update(size: textFrame.size, color: messageTheme.primaryTextColor, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) + dustNode.update(size: textFrame.size, color: messageTheme.secondaryTextColor, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) } else if let spoilerTextNode = strongSelf.spoilerTextNode { strongSelf.spoilerTextNode = nil spoilerTextNode.removeFromSupernode() + + if let dustNode = strongSelf.dustNode { + strongSelf.dustNode = nil + dustNode.removeFromSupernode() + } } if let textSelectionNode = strongSelf.textSelectionNode { @@ -632,7 +636,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { override func updateIsExtractedToContextPreview(_ value: Bool) { if value { - if self.textSelectionNode == nil, let item = self.item, !item.associatedData.isCopyProtectionEnabled, let rootNode = item.controllerInteraction.chatControllerNode() { + if self.textSelectionNode == nil, let item = self.item, !item.associatedData.isCopyProtectionEnabled && !item.message.isCopyProtected(), let rootNode = item.controllerInteraction.chatControllerNode() { let selectionColor: UIColor let knobColor: UIColor if item.message.effectivelyIncoming(item.context.account.peerId) { diff --git a/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift b/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift new file mode 100644 index 0000000000..2f142ff0f0 --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift @@ -0,0 +1,1322 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import AccountContext +import SolidRoundedButtonNode +import TelegramPresentationData +import TelegramUIPreferences +import TelegramNotices +import PresentationDataUtils +import AnimationUI +import MergeLists +import MediaResources +import StickerResources +import WallpaperResources +import TooltipUI +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import ShimmerEffect +import WallpaperBackgroundNode +import QrCode +import AvatarNode + +private func closeButtonImage(theme: PresentationTheme) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(UIColor(rgb: 0x808084, alpha: 0.1).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(theme.actionSheet.inputClearButtonColor.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + }) +} + +private struct ThemeSettingsThemeEntry: Comparable, Identifiable { + let index: Int + let emoticon: String? + let emojiFile: TelegramMediaFile? + let themeReference: PresentationThemeReference? + let nightMode: Bool + var selected: Bool + let theme: PresentationTheme + let strings: PresentationStrings + let wallpaper: TelegramWallpaper? + + var stableId: Int { + return index + } + + static func ==(lhs: ThemeSettingsThemeEntry, rhs: ThemeSettingsThemeEntry) -> Bool { + if lhs.index != rhs.index { + return false + } + if lhs.emoticon != rhs.emoticon { + return false + } + + if lhs.themeReference?.index != rhs.themeReference?.index { + return false + } + if lhs.nightMode != rhs.nightMode { + return false + } + if lhs.selected != rhs.selected { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.wallpaper != rhs.wallpaper { + return false + } + return true + } + + static func <(lhs: ThemeSettingsThemeEntry, rhs: ThemeSettingsThemeEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(context: AccountContext, action: @escaping (String?) -> Void) -> ListViewItem { + return ThemeSettingsThemeIconItem(context: context, emoticon: self.emoticon, emojiFile: self.emojiFile, themeReference: self.themeReference, nightMode: self.nightMode, selected: self.selected, theme: self.theme, strings: self.strings, wallpaper: self.wallpaper, action: action) + } +} + + +private class ThemeSettingsThemeIconItem: ListViewItem { + let context: AccountContext + let emoticon: String? + let emojiFile: TelegramMediaFile? + let themeReference: PresentationThemeReference? + let nightMode: Bool + let selected: Bool + let theme: PresentationTheme + let strings: PresentationStrings + let wallpaper: TelegramWallpaper? + let action: (String?) -> Void + + public init(context: AccountContext, emoticon: String?, emojiFile: TelegramMediaFile?, themeReference: PresentationThemeReference?, nightMode: Bool, selected: Bool, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper?, action: @escaping (String?) -> Void) { + self.context = context + self.emoticon = emoticon + self.emojiFile = emojiFile + self.themeReference = themeReference + self.nightMode = nightMode + self.selected = selected + self.theme = theme + self.strings = strings + self.wallpaper = wallpaper + self.action = action + } + + 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 = ThemeSettingsThemeItemIconNode() + let (nodeLayout, apply) = node.asyncLayout()(self, params) + node.insets = nodeLayout.insets + node.contentSize = nodeLayout.contentSize + + 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 { + assert(node() is ThemeSettingsThemeItemIconNode) + if let nodeValue = node() as? ThemeSettingsThemeItemIconNode { + let layout = nodeValue.asyncLayout() + async { + let (nodeLayout, apply) = layout(self, params) + Queue.mainQueue().async { + completion(nodeLayout, { _ in + apply(animation.isAnimated) + }) + } + } + } + } + } + + public var selectable = true + public func selected(listView: ListView) { + self.action(self.emoticon) + } +} + +private struct ThemeSettingsThemeItemNodeTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let crossfade: Bool + let entries: [ThemeSettingsThemeEntry] +} + +private func ensureThemeVisible(listNode: ListView, emoticon: String?, animated: Bool) -> Bool { + var resultNode: ThemeSettingsThemeItemIconNode? + var previousNode: ThemeSettingsThemeItemIconNode? + var nextNode: ThemeSettingsThemeItemIconNode? + listNode.forEachItemNode { node in + guard let node = node as? ThemeSettingsThemeItemIconNode else { + return + } + if resultNode == nil { + if node.item?.emoticon == emoticon { + resultNode = node + } else { + previousNode = node + } + } else if nextNode == nil { + nextNode = node + } + } + if let resultNode = resultNode { + var nodeToEnsure = resultNode + if case let .visible(resultVisibility) = resultNode.visibility, resultVisibility == 1.0 { + if let previousNode = previousNode, case let .visible(previousVisibility) = previousNode.visibility, previousVisibility < 0.5 { + nodeToEnsure = previousNode + } else if let nextNode = nextNode, case let .visible(nextVisibility) = nextNode.visibility, nextVisibility < 0.5 { + nodeToEnsure = nextNode + } + } + listNode.ensureItemNodeVisible(nodeToEnsure, animated: animated, overflow: 57.0) + return true + } else { + return false + } +} + +private func preparedTransition(context: AccountContext, action: @escaping (String?) -> Void, from fromEntries: [ThemeSettingsThemeEntry], to toEntries: [ThemeSettingsThemeEntry], crossfade: Bool) -> ThemeSettingsThemeItemNodeTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, action: action), directionHint: .Down) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, action: action), directionHint: nil) } + + return ThemeSettingsThemeItemNodeTransition(deletions: deletions, insertions: insertions, updates: updates, crossfade: crossfade, entries: toEntries) +} + +private var cachedBorderImages: [String: UIImage] = [:] +private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selected: Bool) -> UIImage? { + let key = "\(theme.list.itemBlocksBackgroundColor.hexString)_\(selected ? "s" + theme.list.itemAccentColor.hexString : theme.list.disclosureArrowColor.hexString)" + if let image = cachedBorderImages[key] { + return image + } else { + let image = generateImage(CGSize(width: 18.0, height: 18.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let lineWidth: CGFloat + if selected { + lineWidth = 2.0 + context.setLineWidth(lineWidth) + context.setStrokeColor(theme.list.itemBlocksBackgroundColor.cgColor) + + context.strokeEllipse(in: bounds.insetBy(dx: 3.0 + lineWidth / 2.0, dy: 3.0 + lineWidth / 2.0)) + + var accentColor = theme.list.itemAccentColor + if accentColor.rgb == 0xffffff { + accentColor = UIColor(rgb: 0x999999) + } + context.setStrokeColor(accentColor.cgColor) + } else { + context.setStrokeColor(theme.list.disclosureArrowColor.withAlphaComponent(0.4).cgColor) + lineWidth = 1.0 + } + + if bordered || selected { + context.setLineWidth(lineWidth) + context.strokeEllipse(in: bounds.insetBy(dx: 1.0 + lineWidth / 2.0, dy: 1.0 + lineWidth / 2.0)) + } + })?.stretchableImage(withLeftCapWidth: 9, topCapHeight: 9) + cachedBorderImages[key] = image + return image + } +} + +private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { + private let containerNode: ASDisplayNode + private let emojiContainerNode: ASDisplayNode + private let imageNode: TransformImageNode + private let overlayNode: ASImageNode + private let textNode: TextNode + private let emojiNode: TextNode + private let emojiImageNode: TransformImageNode + private var animatedStickerNode: AnimatedStickerNode? + private var placeholderNode: StickerShimmerEffectNode + var snapshotView: UIView? + + var item: ThemeSettingsThemeIconItem? + + override var visibility: ListViewItemNodeVisibility { + didSet { + self.visibilityStatus = self.visibility != .none + } + } + + private var visibilityStatus: Bool = false { + didSet { + if self.visibilityStatus != oldValue { + self.animatedStickerNode?.visibility = self.visibilityStatus + } + } + } + + private let stickerFetchedDisposable = MetaDisposable() + + init() { + self.containerNode = ASDisplayNode() + self.emojiContainerNode = ASDisplayNode() + + self.imageNode = TransformImageNode() + self.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 82.0, height: 108.0)) + self.imageNode.isLayerBacked = true + self.imageNode.cornerRadius = 8.0 + self.imageNode.clipsToBounds = true + + self.overlayNode = ASImageNode() + self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 84.0, height: 110.0)) + self.overlayNode.isLayerBacked = true + + self.textNode = TextNode() + self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false + + self.emojiNode = TextNode() + self.emojiNode.isUserInteractionEnabled = false + self.emojiNode.displaysAsynchronously = false + + self.emojiImageNode = TransformImageNode() + + self.placeholderNode = StickerShimmerEffectNode() + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.imageNode) + self.containerNode.addSubnode(self.overlayNode) + self.containerNode.addSubnode(self.textNode) + + self.addSubnode(self.emojiContainerNode) + self.emojiContainerNode.addSubnode(self.emojiNode) + self.emojiContainerNode.addSubnode(self.emojiImageNode) + self.emojiContainerNode.addSubnode(self.placeholderNode) + + var firstTime = true + self.emojiImageNode.imageUpdated = { [weak self] image in + guard let strongSelf = self else { + return + } + if image != nil { + strongSelf.removePlaceholder(animated: !firstTime) + if firstTime { + strongSelf.emojiImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + firstTime = false + } + } + + deinit { + self.stickerFetchedDisposable.dispose() + } + + private func removePlaceholder(animated: Bool) { + if !animated { + self.placeholderNode.removeFromSupernode() + } else { + self.placeholderNode.alpha = 0.0 + self.placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in + self?.placeholderNode.removeFromSupernode() + }) + } + } + + override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + let emojiFrame = CGRect(origin: CGPoint(x: 28.0, y: 71.0), size: CGSize(width: 34.0, height: 34.0)) + self.placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + emojiFrame.minX, y: rect.minY + emojiFrame.minY), size: emojiFrame.size), within: containerSize) + } + + override func selected() { + let wasSelected = self.item?.selected ?? false + super.selected() + + if let animatedStickerNode = self.animatedStickerNode { + Queue.mainQueue().after(0.1) { + if !wasSelected { + animatedStickerNode.seekTo(.frameIndex(0)) + animatedStickerNode.play() + + let scale: CGFloat = 1.95 + animatedStickerNode.transform = CATransform3DMakeScale(scale, scale, 1.0) + animatedStickerNode.layer.animateSpring(from: 1.0 as NSNumber, to: scale as NSNumber, keyPath: "transform.scale", duration: 0.45) + + animatedStickerNode.completed = { [weak animatedStickerNode, weak self] _ in + guard let item = self?.item, item.selected else { + return + } + animatedStickerNode?.transform = CATransform3DIdentity + animatedStickerNode?.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) + } + } + } + } + + } + + func asyncLayout() -> (ThemeSettingsThemeIconItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let makeTextLayout = TextNode.asyncLayout(self.textNode) + let makeEmojiLayout = TextNode.asyncLayout(self.emojiNode) + let makeImageLayout = self.imageNode.asyncLayout() + + let currentItem = self.item + + return { [weak self] item, params in + var updatedEmoticon = false + var updatedThemeReference = false + var updatedTheme = false + var updatedWallpaper = false + var updatedSelected = false + var updatedNightMode = false + + if currentItem?.emoticon != item.emoticon { + updatedEmoticon = true + } + if currentItem?.themeReference != item.themeReference { + updatedThemeReference = true + } + if currentItem?.wallpaper != item.wallpaper { + updatedWallpaper = true + } + if currentItem?.theme !== item.theme { + updatedTheme = true + } + if currentItem?.selected != item.selected { + updatedSelected = true + } + if currentItem?.nightMode != item.nightMode { + updatedNightMode = true + } + + let text = NSAttributedString(string: item.strings.Conversation_Theme_NoTheme, font: Font.semibold(15.0), textColor: item.theme.actionSheet.controlAccentColor) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let emoticon = item.emoticon + let title = NSAttributedString(string: emoticon != nil ? "" : "❌", font: Font.regular(22.0), textColor: .black) + let (_, emojiApply) = makeEmojiLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 120.0, height: 90.0), insets: UIEdgeInsets()) + return (itemLayout, { animated in + if let strongSelf = self { + strongSelf.item = item + + if updatedThemeReference || updatedWallpaper || updatedNightMode { + if let themeReference = item.themeReference { + strongSelf.imageNode.setSignal(themeIconImage(account: item.context.account, accountManager: item.context.sharedContext.accountManager, theme: themeReference, color: nil, wallpaper: item.wallpaper, nightMode: item.nightMode, emoticon: true, qr: true)) + strongSelf.imageNode.backgroundColor = nil + } + } + if item.themeReference == nil { + strongSelf.imageNode.backgroundColor = item.theme.actionSheet.opaqueItemBackgroundColor + } + + if updatedTheme || updatedSelected { + strongSelf.overlayNode.image = generateBorderImage(theme: item.theme, bordered: false, selected: item.selected) + } + + if !item.selected && currentItem?.selected == true, let animatedStickerNode = strongSelf.animatedStickerNode { + animatedStickerNode.transform = CATransform3DIdentity + + let initialScale: CGFloat = CGFloat((animatedStickerNode.value(forKeyPath: "layer.presentationLayer.transform.scale.x") as? NSNumber)?.floatValue ?? 1.0) + animatedStickerNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) + } + + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((90.0 - textLayout.size.width) / 2.0), y: 24.0), size: textLayout.size) + strongSelf.textNode.isHidden = item.emoticon != nil + + strongSelf.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + strongSelf.containerNode.frame = CGRect(origin: CGPoint(x: 15.0, y: -15.0), size: CGSize(width: 90.0, height: 120.0)) + + strongSelf.emojiContainerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + strongSelf.emojiContainerNode.frame = CGRect(origin: CGPoint(x: 15.0, y: -15.0), size: CGSize(width: 90.0, height: 120.0)) + + let _ = textApply() + let _ = emojiApply() + + let imageSize = CGSize(width: 82.0, height: 108.0) + strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: 4.0, y: 6.0), size: imageSize) + let applyLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: .clear)) + applyLayout() + + strongSelf.overlayNode.frame = strongSelf.imageNode.frame.insetBy(dx: -1.0, dy: -1.0) + strongSelf.emojiNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 79.0), size: CGSize(width: 90.0, height: 30.0)) + + let emojiFrame = CGRect(origin: CGPoint(x: 28.0, y: 71.0), size: CGSize(width: 34.0, height: 34.0)) + if let file = item.emojiFile, updatedEmoticon { + let imageApply = strongSelf.emojiImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: emojiFrame.size, boundingSize: emojiFrame.size, intrinsicInsets: UIEdgeInsets())) + imageApply() + strongSelf.emojiImageNode.setSignal(chatMessageStickerPackThumbnail(postbox: item.context.account.postbox, resource: file.resource, animated: true, nilIfEmpty: true)) + strongSelf.emojiImageNode.frame = emojiFrame + + let animatedStickerNode: AnimatedStickerNode + if let current = strongSelf.animatedStickerNode { + animatedStickerNode = current + } else { + animatedStickerNode = AnimatedStickerNode() + animatedStickerNode.started = { [weak self] in + self?.emojiImageNode.isHidden = true + } + strongSelf.animatedStickerNode = animatedStickerNode + strongSelf.emojiContainerNode.insertSubnode(animatedStickerNode, belowSubnode: strongSelf.placeholderNode) + let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 128, height: 128, playbackMode: .still(.start), mode: .direct(cachePathPrefix: pathPrefix)) + + animatedStickerNode.anchorPoint = CGPoint(x: 0.5, y: 1.0) + } + animatedStickerNode.autoplay = true + animatedStickerNode.visibility = strongSelf.visibilityStatus + + strongSelf.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) + + let thumbnailDimensions = PixelDimensions(width: 512, height: 512) + strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, imageSize: thumbnailDimensions.cgSize) + strongSelf.placeholderNode.frame = emojiFrame + } + + if let animatedStickerNode = strongSelf.animatedStickerNode { + animatedStickerNode.frame = emojiFrame + animatedStickerNode.updateLayout(size: emojiFrame.size) + } + } + }) + } + } + + func crossfade() { + if let snapshotView = self.containerNode.view.snapshotView(afterScreenUpdates: false) { + snapshotView.transform = self.containerNode.view.transform + snapshotView.frame = self.containerNode.view.frame + self.view.insertSubview(snapshotView, aboveSubview: self.containerNode.view) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreen.themeCrossfadeDuration, delay: ChatQrCodeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + super.animateInsertion(currentTimestamp, duration: duration, short: short) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + super.animateRemoved(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + super.animateAdded(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } +} + +final class ChatQrCodeScreen: ViewController { + static let themeCrossfadeDuration: Double = 0.3 + static let themeCrossfadeDelay: Double = 0.05 + + private var controllerNode: ChatQrCodeScreenNode { + return self.displayNode as! ChatQrCodeScreenNode + } + + private var animatedIn = false + + private let context: AccountContext + private let animatedEmojiStickers: [String: [StickerPackItem]] + private let peer: Peer + + private var presentationData: PresentationData + private var presentationThemePromise = Promise() + private var presentationDataDisposable: Disposable? + + var dismissed: (() -> Void)? + + init(context: AccountContext, animatedEmojiStickers: [String: [StickerPackItem]], peer: Peer) { + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.animatedEmojiStickers = animatedEmojiStickers + self.peer = peer + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + self.blocksBackgroundWhenInOverlay = true + + self.presentationThemePromise.set(.single(nil)) + + self.presentationDataDisposable = (combineLatest(context.sharedContext.presentationData, self.presentationThemePromise.get()) + |> deliverOnMainQueue).start(next: { [weak self] presentationData, theme in + if let strongSelf = self { + var presentationData = presentationData + if let theme = theme { + presentationData = presentationData.withUpdated(theme: theme) + } + strongSelf.presentationData = presentationData + strongSelf.controllerNode.updatePresentationData(presentationData) + } + }) + + self.statusBar.statusBarStyle = .Ignore + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + override public func loadDisplayNode() { + self.displayNode = ChatQrCodeScreenNode(context: self.context, presentationData: self.presentationData, controller: self, animatedEmojiStickers: self.animatedEmojiStickers, peer: self.peer) + self.controllerNode.previewTheme = { [weak self] _, _, theme in + self?.presentationThemePromise.set(.single(theme)) + } + self.controllerNode.present = { [weak self] c in + self?.present(c, in: .current) + } + self.controllerNode.completion = { [weak self] emoticon in + guard let strongSelf = self else { + return + } + strongSelf.dismiss() + } + self.controllerNode.dismiss = { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + self.controllerNode.cancel = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.dismiss() + } + } + + override public func loadView() { + super.loadView() + + self.view.disablesInteractiveTransitionGestureRecognizer = true + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.animatedIn { + self.animatedIn = true + self.controllerNode.animateIn() + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss() + } + return true + }) + + self.controllerNode.animateOut(completion: completion) + + self.dismissed?() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) + } + + func dimTapped() { + self.controllerNode.dimTapped() + } +} + +private func iconColors(theme: PresentationTheme) -> [String: UIColor] { + let accentColor = theme.actionSheet.controlAccentColor + var colors: [String: UIColor] = [:] + colors["Sunny.Path 14.Path.Stroke 1"] = accentColor + colors["Sunny.Path 15.Path.Stroke 1"] = accentColor + colors["Path.Path.Stroke 1"] = accentColor + colors["Sunny.Path 39.Path.Stroke 1"] = accentColor + colors["Sunny.Path 24.Path.Stroke 1"] = accentColor + colors["Sunny.Path 25.Path.Stroke 1"] = accentColor + colors["Sunny.Path 18.Path.Stroke 1"] = accentColor + colors["Sunny.Path 41.Path.Stroke 1"] = accentColor + colors["Sunny.Path 43.Path.Stroke 1"] = accentColor + colors["Path 10.Path.Fill 1"] = accentColor + colors["Path 11.Path.Fill 1"] = accentColor + return colors +} + +private let defaultEmoticon = "🏠" + +private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDelegate { + private let context: AccountContext + private var presentationData: PresentationData + private weak var controller: ChatQrCodeScreen? + + private let dimNode: ASDisplayNode + private let containerNode: ASDisplayNode + private let wallpaperBackgroundNode: WallpaperBackgroundNode + private let codeBackgroundNode: ASDisplayNode + private let codeForegroundNode: ASDisplayNode + private var codeForegroundContentNode: ASDisplayNode? + private var codeForegroundDimNode: ASDisplayNode + private let codeMaskNode: ASDisplayNode + private let codeTextNode: ImmediateTextNode + private let codeImageNode: TransformImageNode + private let codeIconBackgroundNode: ASImageNode + private let codeIconNode: AnimatedStickerNode + private let avatarNode: ImageNode + private var qrCodeSize: Int? + + private let wrappingScrollNode: ASScrollNode + private let contentContainerNode: ASDisplayNode + private let topContentContainerNode: SparseNode + private let effectNode: ASDisplayNode + private let backgroundNode: ASDisplayNode + private let contentBackgroundNode: ASDisplayNode + private let titleNode: ASTextNode + private let cancelButton: HighlightableButtonNode + private let switchThemeButton: HighlightTrackingButtonNode + private let animationContainerNode: ASDisplayNode + private var animationNode: AnimationNode + private let doneButton: SolidRoundedButtonNode + + private let listNode: ListView + private var entries: [ThemeSettingsThemeEntry]? + private var enqueuedTransitions: [ThemeSettingsThemeItemNodeTransition] = [] + private var initialized = false + private var themes: [TelegramTheme] = [] + + private let peer: Peer + + private var selectedEmoticon: String? { + didSet { + self.selectedEmoticonPromise.set(self.selectedEmoticon) + } + } + private var selectedEmoticonPromise: ValuePromise + + private var isDarkAppearancePromise: ValuePromise + private var isDarkAppearance: Bool = false { + didSet { + self.isDarkAppearancePromise.set(self.isDarkAppearance) + } + } + + private var containerLayout: (ContainerViewLayout, CGFloat)? + + private let disposable = MetaDisposable() + + var present: ((ViewController) -> Void)? + var previewTheme: ((String?, Bool?, PresentationTheme) -> Void)? + var completion: ((String?) -> Void)? + var dismiss: (() -> Void)? + var cancel: (() -> Void)? + + init(context: AccountContext, presentationData: PresentationData, controller: ChatQrCodeScreen, animatedEmojiStickers: [String: [StickerPackItem]], peer: Peer) { + self.context = context + self.controller = controller + self.peer = peer + self.selectedEmoticon = defaultEmoticon + self.selectedEmoticonPromise = ValuePromise(self.selectedEmoticon) + self.presentationData = presentationData + + self.wrappingScrollNode = ASScrollNode() + self.wrappingScrollNode.view.alwaysBounceVertical = true + self.wrappingScrollNode.view.delaysContentTouches = false + self.wrappingScrollNode.view.canCancelContentTouches = true + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = .clear + + self.containerNode = ASDisplayNode() + + self.wallpaperBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: false, useExperimentalImplementation: self.context.sharedContext.immediateExperimentalUISettings.experimentalBackground) + + self.contentContainerNode = ASDisplayNode() + self.contentContainerNode.isOpaque = false + + self.topContentContainerNode = SparseNode() + self.topContentContainerNode.isOpaque = false + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.clipsToBounds = true + self.backgroundNode.cornerRadius = 16.0 + + self.isDarkAppearance = self.presentationData.theme.overallDarkAppearance + self.isDarkAppearancePromise = ValuePromise(self.presentationData.theme.overallDarkAppearance) + + let backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor + let textColor = self.presentationData.theme.actionSheet.primaryTextColor + let blurStyle: UIBlurEffect.Style = self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark + + self.effectNode = ASDisplayNode(viewBlock: { + return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle)) + }) + + self.contentBackgroundNode = ASDisplayNode() + self.contentBackgroundNode.backgroundColor = backgroundColor + + self.titleNode = ASTextNode() + self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.PeerInfo_QRCode_Title, font: Font.semibold(16.0), textColor: textColor) + + self.cancelButton = HighlightableButtonNode() + self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) + + self.switchThemeButton = HighlightTrackingButtonNode() + self.animationContainerNode = ASDisplayNode() + self.animationContainerNode.isUserInteractionEnabled = false + + self.animationNode = AnimationNode(animation: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme), scale: 1.0) + self.animationNode.isUserInteractionEnabled = false + + self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0, gloss: false) + self.doneButton.title = self.presentationData.strings.InviteLink_QRCode_Share + + self.listNode = ListView() + self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + + self.codeBackgroundNode = ASDisplayNode() + self.codeBackgroundNode.backgroundColor = .white + self.codeBackgroundNode.cornerRadius = 42.0 + if #available(iOS 13.0, *) { + self.codeBackgroundNode.layer.cornerCurve = .continuous + } + self.codeForegroundNode = ASDisplayNode() + self.codeForegroundNode.backgroundColor = .black + + self.codeForegroundDimNode = ASDisplayNode() + self.codeForegroundDimNode.alpha = 0.3 + self.codeForegroundDimNode.backgroundColor = .black + + self.codeMaskNode = ASDisplayNode() + + self.codeImageNode = TransformImageNode() + + self.codeIconBackgroundNode = ASImageNode() + + self.codeIconNode = AnimatedStickerNode() + self.codeIconNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "PlaneLogoPlain"), width: 240, height: 240, mode: .direct(cachePathPrefix: nil)) + self.codeIconNode.visibility = true + + self.codeTextNode = ImmediateTextNode() + self.codeTextNode.attributedText = NSAttributedString(string: "@\(peer.addressName ?? "")".uppercased(), font: Font.with(size: 24.0, design: .round, weight: .bold, traits: []), textColor: .black) + + self.avatarNode = ImageNode() + self.avatarNode.displaysAsynchronously = false + self.avatarNode.setSignal(peerAvatarCompleteImage(account: self.context.account, peer: EnginePeer(peer), size: CGSize(width: 180.0, height: 180.0), font: avatarPlaceholderFont(size: 78.0), fullSize: true)) + + super.init() + + self.backgroundColor = nil + self.isOpaque = false + + self.addSubnode(self.dimNode) + + self.wrappingScrollNode.view.delegate = self + self.addSubnode(self.wrappingScrollNode) + + self.wrappingScrollNode.addSubnode(self.containerNode) + + self.containerNode.addSubnode(self.wallpaperBackgroundNode) + + self.containerNode.addSubnode(self.codeBackgroundNode) + self.containerNode.addSubnode(self.codeForegroundNode) + + self.codeForegroundNode.addSubnode(self.codeForegroundDimNode) + + self.codeMaskNode.addSubnode(self.codeImageNode) + self.codeMaskNode.addSubnode(self.codeIconBackgroundNode) + self.codeMaskNode.addSubnode(self.codeTextNode) + + self.containerNode.addSubnode(self.avatarNode) + + self.wrappingScrollNode.addSubnode(self.codeIconNode) + + self.wrappingScrollNode.addSubnode(self.backgroundNode) + self.wrappingScrollNode.addSubnode(self.contentContainerNode) + self.wrappingScrollNode.addSubnode(self.topContentContainerNode) + + self.backgroundNode.addSubnode(self.effectNode) + self.backgroundNode.addSubnode(self.contentBackgroundNode) + self.contentContainerNode.addSubnode(self.titleNode) + self.contentContainerNode.addSubnode(self.doneButton) + + self.topContentContainerNode.addSubnode(self.animationContainerNode) + self.animationContainerNode.addSubnode(self.animationNode) + self.topContentContainerNode.addSubnode(self.switchThemeButton) + self.topContentContainerNode.addSubnode(self.listNode) + self.topContentContainerNode.addSubnode(self.cancelButton) + + self.switchThemeButton.addTarget(self, action: #selector(self.switchThemePressed), forControlEvents: .touchUpInside) + self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) + self.doneButton.pressed = { [weak self] in + if let strongSelf = self { + strongSelf.doneButton.isUserInteractionEnabled = false + strongSelf.completion?(strongSelf.selectedEmoticon) + } + } + + self.disposable.set(combineLatest(queue: Queue.mainQueue(), self.context.engine.themes.getChatThemes(accountManager: self.context.sharedContext.accountManager), self.selectedEmoticonPromise.get(), self.isDarkAppearancePromise.get()).start(next: { [weak self] themes, selectedEmoticon, isDarkAppearance in + guard let strongSelf = self else { + return + } + + let isFirstTime = strongSelf.entries == nil + let presentationData = strongSelf.presentationData + + var entries: [ThemeSettingsThemeEntry] = [] + entries.append(ThemeSettingsThemeEntry(index: 0, emoticon: defaultEmoticon, emojiFile: animatedEmojiStickers[defaultEmoticon]?.first?.file, themeReference: .builtin(.dayClassic), nightMode: isDarkAppearance, selected: selectedEmoticon == defaultEmoticon, theme: presentationData.theme, strings: presentationData.strings, wallpaper: nil)) + for theme in themes { + guard let emoticon = theme.emoticon else { + continue + } + entries.append(ThemeSettingsThemeEntry(index: entries.count, emoticon: emoticon, emojiFile: animatedEmojiStickers[emoticon]?.first?.file, themeReference: .cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: nil)), nightMode: isDarkAppearance, selected: selectedEmoticon == theme.emoticon, theme: presentationData.theme, strings: presentationData.strings, wallpaper: nil)) + } + + if selectedEmoticon == defaultEmoticon { + let presentationTheme = makeDefaultPresentationTheme(reference: isDarkAppearance ? .night : .dayClassic, serviceBackgroundColor: nil) + strongSelf.wallpaperBackgroundNode.update(wallpaper: presentationTheme.chat.defaultWallpaper) + } else if let theme = themes.first(where: { $0.emoticon == selectedEmoticon }) { + if let presentationTheme = makePresentationTheme(cloudTheme: theme, dark: isDarkAppearance) { + strongSelf.wallpaperBackgroundNode.update(wallpaper: presentationTheme.chat.defaultWallpaper) + } + } + + let action: (String?) -> Void = { [weak self] emoticon in + if let strongSelf = self, strongSelf.selectedEmoticon != emoticon { + strongSelf.animateCrossfade(animateIcon: true) + + var presentationTheme: PresentationTheme? + if emoticon == defaultEmoticon { + presentationTheme = makeDefaultPresentationTheme(reference: isDarkAppearance ? .night : .dayClassic, serviceBackgroundColor: nil) + } else if let theme = themes.first(where: { $0.emoticon == emoticon }) { + if let theme = makePresentationTheme(cloudTheme: theme, dark: isDarkAppearance) { + presentationTheme = theme + } + } + if let presentationTheme = presentationTheme { + strongSelf.previewTheme?(emoticon, strongSelf.isDarkAppearance, presentationTheme) + } + strongSelf.selectedEmoticon = emoticon + let _ = ensureThemeVisible(listNode: strongSelf.listNode, emoticon: emoticon, animated: true) + } + } + let previousEntries = strongSelf.entries ?? [] + let crossfade = previousEntries.count != entries.count + let transition = preparedTransition(context: strongSelf.context, action: action, from: previousEntries, to: entries, crossfade: crossfade) + strongSelf.enqueueTransition(transition) + + strongSelf.entries = entries + strongSelf.themes = themes + + if isDarkAppearance && selectedEmoticon == defaultEmoticon { + strongSelf.codeForegroundDimNode.alpha = 1.0 + } else { + strongSelf.codeForegroundDimNode.alpha = isDarkAppearance ? 0.4 : 0.3 + } + if strongSelf.codeForegroundContentNode == nil, let contentNode = strongSelf.wallpaperBackgroundNode.makeDimmedNode() { + contentNode.frame = CGRect(origin: CGPoint(x: -strongSelf.codeForegroundNode.frame.minX, y: -strongSelf.codeForegroundNode.frame.minY), size: strongSelf.wallpaperBackgroundNode.frame.size) + strongSelf.codeForegroundContentNode = contentNode + strongSelf.codeForegroundNode.insertSubnode(contentNode, at: 0) + } + + if isFirstTime { + for theme in themes { + if let wallpaper = theme.settings?.first?.wallpaper, case let .file(file) = wallpaper { + let account = strongSelf.context.account + let accountManager = strongSelf.context.sharedContext.accountManager + let path = accountManager.mediaBox.cachedRepresentationCompletePath(file.file.resource.id, representation: CachedPreparedPatternWallpaperRepresentation()) + if !FileManager.default.fileExists(atPath: path) { + let accountFullSizeData = Signal<(Data?, Bool), NoError> { subscriber in + let accountResource = account.postbox.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPreparedPatternWallpaperRepresentation(), complete: false, fetch: true) + + let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .media(media: .standalone(media: file.file), resource: file.file.resource)) + let fetchedFullSizeDisposable = fetchedFullSize.start() + let fullSizeDisposable = accountResource.start(next: { next in + subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) + + if next.complete, let data = try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedRead) { + accountManager.mediaBox.storeCachedResourceRepresentation(file.file.resource, representation: CachedPreparedPatternWallpaperRepresentation(), data: data) + } + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedFullSizeDisposable.dispose() + fullSizeDisposable.dispose() + } + } + let _ = accountFullSizeData.start() + } + } + } + } + })) + + self.switchThemeButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.animationNode.layer.removeAnimation(forKey: "opacity") + strongSelf.animationNode.alpha = 0.4 + } else { + strongSelf.animationNode.alpha = 1.0 + strongSelf.animationNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + self.codeImageNode.setSignal(qrCode(string: "https://t.me/\(peer.addressName ?? "")", color: .black, backgroundColor: nil, icon: .cutout, ecl: "Q") |> beforeNext { [weak self] size, _ in + guard let strongSelf = self else { + return + } + strongSelf.qrCodeSize = size + if let (layout, navigationHeight) = strongSelf.containerLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate) + } + } |> map { $0.1 }, attemptSynchronously: true) + } + + private func enqueueTransition(_ transition: ThemeSettingsThemeItemNodeTransition) { + self.enqueuedTransitions.append(transition) + + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + + private func dequeueTransition() { + guard let transition = self.enqueuedTransitions.first else { + return + } + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + if self.initialized && transition.crossfade { + options.insert(.AnimateCrossfade) + } + options.insert(.Synchronous) + + var scrollToItem: ListViewScrollToItem? + if !self.initialized { + scrollToItem = ListViewScrollToItem(index: 0, position: .bottom(-57.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down) + self.initialized = true + } + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in + }) + } + + func updatePresentationData(_ presentationData: PresentationData) { + guard !self.animatedOut else { + return + } + let previousTheme = self.presentationData.theme + self.presentationData = presentationData + + self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.semibold(16.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) + + if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + + self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) + self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme)) + + if self.animationNode.isPlaying { + if let animationNode = self.animationNode.makeCopy(colors: iconColors(theme: self.presentationData.theme), progress: 0.2) { + let previousAnimationNode = self.animationNode + self.animationNode = animationNode + + animationNode.completion = { [weak previousAnimationNode] in + previousAnimationNode?.removeFromSupernode() + } + animationNode.isUserInteractionEnabled = false + animationNode.frame = previousAnimationNode.frame + previousAnimationNode.supernode?.insertSubnode(animationNode, belowSubnode: previousAnimationNode) + previousAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreen.themeCrossfadeDuration, removeOnCompletion: false) + animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } else { + self.animationNode.setAnimation(name: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme)) + } + } + + override func didLoad() { + super.didLoad() + + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never + } + + self.codeForegroundNode.view.mask = self.codeMaskNode.view + + self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true + } + + @objc func cancelButtonPressed() { + self.cancel?() + } + + func dimTapped() { + self.cancelButtonPressed() + } + + @objc func switchThemePressed() { + self.switchThemeButton.isUserInteractionEnabled = false + Queue.mainQueue().after(0.5) { + self.switchThemeButton.isUserInteractionEnabled = true + } + + self.animateCrossfade(animateIcon: false) + self.animationNode.setAnimation(name: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme)) + self.animationNode.playOnce() + + let isDarkAppearance = !self.isDarkAppearance + + var presentationTheme: PresentationTheme? + if self.selectedEmoticon == defaultEmoticon { + presentationTheme = makeDefaultPresentationTheme(reference: isDarkAppearance ? .night : .dayClassic, serviceBackgroundColor: nil) + } else if let theme = self.themes.first(where: { $0.emoticon == self.selectedEmoticon }) { + if let theme = makePresentationTheme(cloudTheme: theme, dark: isDarkAppearance) { + presentationTheme = theme + } + } + if let presentationTheme = presentationTheme { + self.previewTheme?(self.selectedEmoticon, isDarkAppearance, presentationTheme) + } + + self.isDarkAppearance = isDarkAppearance + + if isDarkAppearance { + let _ = ApplicationSpecificNotice.incrementChatSpecificThemeDarkPreviewTip(accountManager: self.context.sharedContext.accountManager, count: 3, timestamp: Int32(Date().timeIntervalSince1970)).start() + } else { + let _ = ApplicationSpecificNotice.incrementChatSpecificThemeLightPreviewTip(accountManager: self.context.sharedContext.accountManager, count: 3, timestamp: Int32(Date().timeIntervalSince1970)).start() + } + } + + private func animateCrossfade(animateIcon: Bool) { + if let snapshotView = self.containerNode.view.snapshotView(afterScreenUpdates: false) { + self.wrappingScrollNode.view.insertSubview(snapshotView, aboveSubview: self.containerNode.view) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreen.themeCrossfadeDuration, delay: ChatQrCodeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + + if animateIcon, let snapshotView = self.animationNode.view.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = self.animationNode.frame + self.animationNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.animationNode.view) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreen.themeCrossfadeDuration, delay: ChatQrCodeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + + Queue.mainQueue().after(ChatQrCodeScreen.themeCrossfadeDelay) { + if let effectView = self.effectNode.view as? UIVisualEffectView { + UIView.animate(withDuration: ChatQrCodeScreen.themeCrossfadeDuration, delay: 0.0, options: .curveLinear) { + effectView.effect = UIBlurEffect(style: self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark) + } completion: { _ in + } + } + + let previousColor = self.contentBackgroundNode.backgroundColor ?? .clear + self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor + self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: ChatQrCodeScreen.themeCrossfadeDuration) + } + + if let snapshotView = self.contentContainerNode.view.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = self.contentContainerNode.frame + self.contentContainerNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.contentContainerNode.view) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreen.themeCrossfadeDuration, delay: ChatQrCodeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + + self.listNode.forEachVisibleItemNode { node in + if let node = node as? ThemeSettingsThemeItemIconNode { + node.crossfade() + } + } + } + + private var animatedOut = false + func animateIn() { + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + let dimPosition = self.dimNode.layer.position + + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + let targetBounds = self.bounds + self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) + self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) + transition.animateView({ + self.bounds = targetBounds + self.dimNode.position = dimPosition + }) + } + + func animateOut(completion: (() -> Void)? = nil) { + self.animatedOut = true + + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + self.wrappingScrollNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.dismiss?() + completion?() + } + }) + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + let contentOffset = scrollView.contentOffset + let additionalTopHeight = max(0.0, -contentOffset.y) + + if additionalTopHeight >= 30.0 { + self.cancelButtonPressed() + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.containerLayout = (layout, navigationBarHeight) + + var insets = layout.insets(options: [.statusBar, .input]) + let cleanInsets = layout.insets(options: [.statusBar]) + insets.top = max(10.0, insets.top) + + let bottomInset: CGFloat = 10.0 + cleanInsets.bottom + let titleHeight: CGFloat = 54.0 + let contentHeight = titleHeight + bottomInset + 188.0 + + let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) + + let sideInset = floor((layout.size.width - width) / 2.0) + let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight)) + let contentFrame = contentContainerFrame + + var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0)) + if backgroundFrame.minY < contentFrame.minY { + backgroundFrame.origin.y = contentFrame.minY + } + transition.updateFrame(node: self.wallpaperBackgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + self.wallpaperBackgroundNode.updateLayout(size: layout.size, transition: transition) + + transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + let titleSize = self.titleNode.measure(CGSize(width: width - 90.0, height: titleHeight)) + let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 19.0 + UIScreenPixel), size: titleSize) + transition.updateFrame(node: self.titleNode, frame: titleFrame) + + let switchThemeSize = CGSize(width: 44.0, height: 44.0) + let switchThemeFrame = CGRect(origin: CGPoint(x: 3.0, y: 6.0), size: switchThemeSize) + transition.updateFrame(node: self.switchThemeButton, frame: switchThemeFrame) + transition.updateFrame(node: self.animationContainerNode, frame: switchThemeFrame.insetBy(dx: 9.0, dy: 9.0)) + transition.updateFrame(node: self.animationNode, frame: CGRect(origin: CGPoint(), size: self.animationContainerNode.frame.size)) + + let cancelSize = CGSize(width: 44.0, height: 44.0) + let cancelFrame = CGRect(origin: CGPoint(x: contentFrame.width - cancelSize.width - 3.0, y: 6.0), size: cancelSize) + transition.updateFrame(node: self.cancelButton, frame: cancelFrame) + + let buttonInset: CGFloat = 16.0 + let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) + transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - insets.bottom - 6.0, width: contentFrame.width, height: doneButtonHeight)) + + transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) + transition.updateFrame(node: self.topContentContainerNode, frame: contentContainerFrame) + + var listInsets = UIEdgeInsets() + listInsets.top += layout.safeInsets.left + 12.0 + listInsets.bottom += layout.safeInsets.right + 12.0 + + let contentSize = CGSize(width: contentFrame.width, height: 120.0) + + self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: contentSize.height, height: contentSize.width) + self.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0 + titleHeight + 6.0) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: contentSize.height, height: contentSize.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + let codeInset: CGFloat = 45.0 + let codeBackgroundWidth = layout.size.width - codeInset * 2.0 + let codeBackgroundHeight = floor(codeBackgroundWidth * 1.1) + let codeBackgroundFrame = CGRect(x: codeInset, y: floor((layout.size.height - contentHeight - codeBackgroundHeight) / 2.0) + 44.0, width: codeBackgroundWidth, height: codeBackgroundHeight) + transition.updateFrame(node: self.codeBackgroundNode, frame: codeBackgroundFrame) + transition.updateFrame(node: self.codeForegroundNode, frame: codeBackgroundFrame) + transition.updateFrame(node: self.codeMaskNode, frame: CGRect(origin: CGPoint(), size: codeBackgroundFrame.size)) + transition.updateFrame(node: self.codeForegroundDimNode, frame: CGRect(origin: CGPoint(), size: codeBackgroundFrame.size)) + + if let codeForegroundContentNode = self.codeForegroundContentNode { + codeForegroundContentNode.frame = CGRect(origin: CGPoint(x: -self.codeForegroundNode.frame.minX, y: -self.codeForegroundNode.frame.minY), size: self.wallpaperBackgroundNode.frame.size) + } + + let makeImageLayout = self.codeImageNode.asyncLayout() + let imageSide: CGFloat = 220.0 + let imageSize = CGSize(width: imageSide, height: imageSide) + let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil)) + let _ = imageApply() + + let imageFrame = CGRect(origin: CGPoint(x: floor((codeBackgroundFrame.width - imageSize.width) / 2.0), y: floor((codeBackgroundFrame.width - imageSize.height) / 2.0)), size: imageSize) + transition.updateFrame(node: self.codeImageNode, frame: imageFrame) + + let codeTextSize = self.codeTextNode.updateLayout(codeBackgroundFrame.size) + transition.updateFrame(node: self.codeTextNode, frame: CGRect(origin: CGPoint(x: floor((codeBackgroundFrame.width - codeTextSize.width) / 2.0), y: imageFrame.maxY + floor((codeBackgroundHeight - imageFrame.maxY - codeTextSize.height) / 2.0) - 7.0), size: codeTextSize)) + + let avatarSize = CGSize(width: 100.0, height: 100.0) + transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - avatarSize.width) / 2.0), y: codeBackgroundFrame.minY - 70.0), size: avatarSize)) + + if let qrCodeSize = self.qrCodeSize { + let (_, cutoutFrame, _) = qrCodeCutout(size: qrCodeSize, dimensions: imageSize, scale: nil) + self.codeIconNode.updateLayout(size: cutoutFrame.size) + + let backgroundSize = CGSize(width: floorToScreenPixels(cutoutFrame.width - 8.0), height: floorToScreenPixels(cutoutFrame.height - 8.0)) + transition.updateFrame(node: self.codeIconBackgroundNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels(imageFrame.center.x - backgroundSize.width / 2.0), y: floorToScreenPixels(imageFrame.center.y - backgroundSize.height / 2.0)), size: backgroundSize)) + if self.codeIconBackgroundNode.image == nil { + self.codeIconBackgroundNode.image = generateFilledCircleImage(diameter: backgroundSize.width, color: .black) + } + + let imageCenter = imageFrame.center.offsetBy(dx: codeBackgroundFrame.minX, dy: codeBackgroundFrame.minY) + transition.updateBounds(node: self.codeIconNode, bounds: CGRect(origin: CGPoint(), size: cutoutFrame.size)) + transition.updatePosition(node: self.codeIconNode, position: imageCenter.offsetBy(dx: 0.0, dy: -1.0)) + } + } +} diff --git a/submodules/TelegramUI/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Sources/ChatThemeScreen.swift index 1e4084d40e..e24a60d6f0 100644 --- a/submodules/TelegramUI/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Sources/ChatThemeScreen.swift @@ -394,6 +394,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { var updatedTheme = false var updatedWallpaper = false var updatedSelected = false + var updatedNightMode = false if currentItem?.emoticon != item.emoticon { updatedEmoticon = true @@ -410,6 +411,9 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { if currentItem?.selected != item.selected { updatedSelected = true } + if currentItem?.nightMode != item.nightMode { + updatedNightMode = true + } let text = NSAttributedString(string: item.strings.Conversation_Theme_NoTheme, font: Font.semibold(15.0), textColor: item.theme.actionSheet.controlAccentColor) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) @@ -423,7 +427,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { if let strongSelf = self { strongSelf.item = item - if updatedThemeReference || updatedWallpaper { + if updatedThemeReference || updatedWallpaper || updatedNightMode { if let themeReference = item.themeReference { strongSelf.imageNode.setSignal(themeIconImage(account: item.context.account, accountManager: item.context.sharedContext.accountManager, theme: themeReference, color: nil, wallpaper: item.wallpaper, nightMode: item.nightMode, emoticon: true)) strongSelf.imageNode.backgroundColor = nil diff --git a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift index 15a1315d92..6100fa6f54 100644 --- a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift @@ -169,7 +169,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { if let currentEditMediaReference = self.currentEditMediaReference { effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media]) } - (text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(effectiveMessage), strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, dateTimeFormat: self.dateTimeFormat, accountPeerId: self.context.account.peerId) + (text, _, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(effectiveMessage), strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, dateTimeFormat: self.dateTimeFormat, accountPeerId: self.context.account.peerId) } var updatedMediaReference: AnyMediaReference? diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index cac743bd4c..f77ed82440 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -5394,7 +5394,35 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate guard let data = self.data, let peer = data.peer, let controller = self.controller else { return } - controller.present(QrCodeScreen(context: self.context, updatedPresentationData: controller.updatedPresentationData, subject: .peer(peer: EnginePeer(peer))), in: .window(.root)) + + let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) + |> map { animatedEmoji -> [String: [StickerPackItem]] in + var animatedEmojiStickers: [String: [StickerPackItem]] = [:] + switch animatedEmoji { + case let .result(_, items, _): + for item in items { + if let emoji = item.getStringRepresentationsOfIndexKeys().first { + animatedEmojiStickers[emoji.basicEmoji.0] = [item] + let strippedEmoji = emoji.basicEmoji.0.strippedEmoji + if animatedEmojiStickers[strippedEmoji] == nil { + animatedEmojiStickers[strippedEmoji] = [item] + } + } + } + default: + break + } + return animatedEmojiStickers + } + + let _ = (animatedEmojiStickers + |> deliverOnMainQueue).start(next: { [weak self, weak controller] animatedEmojiStickers in + if let strongSelf = self, let controller = controller { + controller.present(ChatQrCodeScreen(context: strongSelf.context, animatedEmojiStickers: animatedEmojiStickers, peer: peer), in: .window(.root)) + } + }) + +// controller.present(QrCodeScreen(context: self.context, updatedPresentationData: controller.updatedPresentationData, subject: .peer(peer: EnginePeer(peer))), in: .window(.root)) } fileprivate func openSettings(section: PeerInfoSettingsSection) { @@ -6639,7 +6667,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .done, isForExpandedView: false)) } else { if self.isSettings { - leftNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .qrCode, isForExpandedView: false)) + if let addressName = self.data?.peer?.addressName, !addressName.isEmpty { + leftNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .qrCode, isForExpandedView: false)) + } rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .edit, isForExpandedView: false)) rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .search, isForExpandedView: true)) diff --git a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift index 2cff35dd92..57550bec1e 100644 --- a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift @@ -106,7 +106,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { authorName = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder) } if let message = message { - (text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) + (text, _, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) } var updatedMediaReference: AnyMediaReference? diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 0fad7a6334..a97a6c14f6 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -19,6 +19,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var enableDebugDataDisplay: Bool public var acceleratedStickers: Bool public var experimentalBackground: Bool + public var snow: Bool public static var defaultSettings: ExperimentalUISettings { return ExperimentalUISettings( @@ -36,7 +37,8 @@ public struct ExperimentalUISettings: Codable, Equatable { experimentalCompatibility: false, enableDebugDataDisplay: false, acceleratedStickers: false, - experimentalBackground: false + experimentalBackground: false, + snow: false ) } @@ -55,7 +57,8 @@ public struct ExperimentalUISettings: Codable, Equatable { experimentalCompatibility: Bool, enableDebugDataDisplay: Bool, acceleratedStickers: Bool, - experimentalBackground: Bool + experimentalBackground: Bool, + snow: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory @@ -72,6 +75,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.enableDebugDataDisplay = enableDebugDataDisplay self.acceleratedStickers = acceleratedStickers self.experimentalBackground = experimentalBackground + self.snow = snow } public init(from decoder: Decoder) throws { @@ -92,6 +96,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.enableDebugDataDisplay = (try container.decodeIfPresent(Int32.self, forKey: "enableDebugDataDisplay") ?? 0) != 0 self.acceleratedStickers = (try container.decodeIfPresent(Int32.self, forKey: "acceleratedStickers") ?? 0) != 0 self.experimentalBackground = (try container.decodeIfPresent(Int32.self, forKey: "experimentalBackground") ?? 0) != 0 + self.snow = (try container.decodeIfPresent(Int32.self, forKey: "snow") ?? 0) != 0 } public func encode(to encoder: Encoder) throws { @@ -112,6 +117,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode((self.enableDebugDataDisplay ? 1 : 0) as Int32, forKey: "enableDebugDataDisplay") try container.encode((self.acceleratedStickers ? 1 : 0) as Int32, forKey: "acceleratedStickers") try container.encode((self.experimentalBackground ? 1 : 0) as Int32, forKey: "experimentalBackground") + try container.encode((self.snow ? 1 : 0) as Int32, forKey: "snow") } } diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 1a3d5022b4..14fca54731 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -698,7 +698,7 @@ public func breakChatInputText(_ text: NSAttributedString) -> [NSAttributedStrin } } -private let markdownRegexFormat = "(^|\\s|\\n)(````?)([\\s\\S]+?)(````?)([\\s\\n\\.,:?!;]|$)|(^|\\s)(`|\\*\\*|__|~~)([^\\n]+?)\\7([\\s\\.,:?!;]|$)|@(\\d+)\\s*\\((.+?)\\)" +private let markdownRegexFormat = "(^|\\s|\\n)(````?)([\\s\\S]+?)(````?)([\\s\\n\\.,:?!;]|$)|(^|\\s)(`|\\*\\*|__|~~|\\|\\|)([^\\n]+?)\\7([\\s\\.,:?!;]|$)|@(\\d+)\\s*\\((.+?)\\)" private let markdownRegex = try? NSRegularExpression(pattern: markdownRegexFormat, options: [.caseInsensitive, .anchorsMatchLines]) public func convertMarkdownToAttributes(_ text: NSAttributedString) -> NSAttributedString { diff --git a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift index 93f3111b3d..9ffa923f97 100644 --- a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift +++ b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift @@ -188,6 +188,7 @@ public enum TextSelectionAction { case share case lookup case speak + case translate } public final class TextSelectionNode: ASDisplayNode { @@ -501,6 +502,12 @@ public final class TextSelectionNode: ASDisplayNode { self?.performAction(attributedText, .lookup) self?.dismissSelection() })) +// if #available(iOS 15.0, *) { +// actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuTranslate, accessibilityLabel: self.strings.Conversation_ContextMenuTranslate), action: { [weak self] in +// self?.performAction(attributedText, .translate) +// self?.dismissSelection() +// })) +// } if isSpeakSelectionEnabled() { actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuSpeak, accessibilityLabel: self.strings.Conversation_ContextMenuSpeak), action: { [weak self] in self?.performAction(attributedText, .speak) diff --git a/submodules/Translate/BUILD b/submodules/Translate/BUILD new file mode 100644 index 0000000000..a6dcb04479 --- /dev/null +++ b/submodules/Translate/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "Translate", + module_name = "Translate", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/AccountContext:AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Translate/Sources/Translate.swift b/submodules/Translate/Sources/Translate.swift new file mode 100644 index 0000000000..539319d70b --- /dev/null +++ b/submodules/Translate/Sources/Translate.swift @@ -0,0 +1,27 @@ +import Foundation +import UIKit +import Display +import AccountContext + +// Incuding at least one Objective-C class in a swift file ensures that it doesn't get stripped by the linker +private final class LinkHelperClass: NSObject { +} + +public func translateText(context: AccountContext, text: String) { + guard !text.isEmpty else { + return + } + if #available(iOS 15.0, *) { + let textField = UITextField() + textField.text = text + if let navigationController = context.sharedContext.mainWindow?.viewController as? NavigationController, let topController = navigationController.topViewController as? ViewController { + topController.view.addSubview(textField) + textField.selectAll(nil) + textField.perform(NSSelectorFromString(["_", "trans", "late:"].joined(separator: "")), with: nil) + + DispatchQueue.main.async { + textField.removeFromSuperview() + } + } + } +} diff --git a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift index 1297e5144a..8105368973 100644 --- a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift +++ b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift @@ -56,6 +56,8 @@ public protocol WallpaperBackgroundNode: ASDisplayNode { func updateBubbleTheme(bubbleTheme: PresentationTheme, bubbleCorners: PresentationChatBubbleCorners) func hasBubbleBackground(for type: WallpaperBubbleType) -> Bool func makeBubbleBackground(for type: WallpaperBubbleType) -> WallpaperBubbleBackgroundNode? + + func makeDimmedNode() -> ASDisplayNode? } final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode { @@ -799,7 +801,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode if isFirstLayout && !self.frame.isEmpty { self.updateScale() - if false, self.newYearNode == nil { + if self.context.sharedContext.immediateExperimentalUISettings.snow, self.newYearNode == nil { let newYearNode = WallpaperNewYearNode() self.addSubnode(newYearNode) self.newYearNode = newYearNode @@ -897,6 +899,14 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode node.updateContents() return node } + + func makeDimmedNode() -> ASDisplayNode? { + if let gradientBackgroundNode = self.gradientBackgroundNode { + return GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode) + } else { + return nil + } + } } private protocol WallpaperComponentView: AnyObject { @@ -1695,6 +1705,10 @@ final class WallpaperBackgroundNodeMergedImpl: ASDisplayNode, WallpaperBackgroun node.updateContents() return node } + + func makeDimmedNode() -> ASDisplayNode? { + return nil + } } private let sharedStorage = WallpaperBackgroundNodeMergedImpl.SharedStorage() @@ -1738,7 +1752,7 @@ private class WallpaperNewYearNode: ASDisplayNode { cell1.scale = 0.04 cell1.scaleRange = 0.15 cell1.color = UIColor.white.withAlphaComponent(0.88).cgColor - cell1.alphaRange = -0.2 +// cell1.alphaRange = -0.2 particlesLayer.emitterCells = [cell1] } diff --git a/submodules/WallpaperResources/Sources/WallpaperResources.swift b/submodules/WallpaperResources/Sources/WallpaperResources.swift index b6e7549af1..0f1479172e 100644 --- a/submodules/WallpaperResources/Sources/WallpaperResources.swift +++ b/submodules/WallpaperResources/Sources/WallpaperResources.swift @@ -1300,7 +1300,23 @@ public func themeImage(account: Account, accountManager: AccountManager, theme: PresentationThemeReference, color: PresentationThemeAccentColor?, wallpaper: TelegramWallpaper? = nil, nightMode: Bool? = nil, emoticon: Bool = false, large: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +private let qrIconImage: UIImage = { + return generateImage(CGSize(width: 36.0, height: 36.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setFillColor(UIColor.white.cgColor) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 9.0).cgPath) + context.fillPath() + + if let image = UIImage(bundleImageName: "Settings/QrButtonIcon")?.cgImage { + context.clip(to: CGRect(x: 6.0, y: 6.0, width: 24.0, height: 24.0), mask: image) + context.clear(bounds) + } + })! +}() + +public func themeIconImage(account: Account, accountManager: AccountManager, theme: PresentationThemeReference, color: PresentationThemeAccentColor?, wallpaper: TelegramWallpaper? = nil, nightMode: Bool? = nil, emoticon: Bool = false, large: Bool = false, qr: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let colorsSignal: Signal<((UIColor, UIColor?, [UInt32]), [UIColor], [UIColor], UIImage?, Bool, Bool, CGFloat, Int32?), NoError> var reference: MediaResourceReference? @@ -1552,134 +1568,140 @@ public func themeIconImage(account: Account, accountManager: AccountManager 1 { - c.clip() + c.translateBy(x: 7.0, y: 27.0) + c.translateBy(x: 114.0, y: 32.0) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -114.0, y: -32.0) + + let _ = try? drawSvgPath(c, path: "M12.8304,29.8712 C10.0551,31.8416 6.6628,33 2.99998,33 C1.98426,33 0.989361,32.9109 0.022644,32.7402 C2.97318,31.9699 5.24596,29.5785 5.84625,26.5607 C5.99996,25.7879 5.99996,24.8586 5.99996,23 V16.0 H6.00743 C6.27176,7.11861 13.5546,0 22.5,0 H61.5 C70.6127,0 78,7.3873 78,16.5 C78,25.6127 70.6127,33 61.5,33 H22.5 C18.8883,33 15.5476,31.8396 12.8304,29.8712 ") + if Set(incomingColors.map(\.rgb)).count > 1 { + c.clip() - var colors: [CGColor] = [] - var locations: [CGFloat] = [] - for i in 0 ..< incomingColors.count { - let t = CGFloat(i) / CGFloat(incomingColors.count - 1) - locations.append(t) - colors.append(incomingColors[i].cgColor) + var colors: [CGColor] = [] + var locations: [CGFloat] = [] + for i in 0 ..< incomingColors.count { + let t = CGFloat(i) / CGFloat(incomingColors.count - 1) + locations.append(t) + colors.append(incomingColors[i].cgColor) + } + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as NSArray, locations: &locations)! + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 34.0), options: CGGradientDrawingOptions()) + } else { + c.setFillColor(incomingColors[0].cgColor) + c.fillPath() } - - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as NSArray, locations: &locations)! - c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 34.0), options: CGGradientDrawingOptions()) + + c.restoreGState() } else { - c.setFillColor(incomingColors[0].cgColor) - c.fillPath() - } - - c.restoreGState() - } else { - let rect = CGRect(x: 8.0, y: 44.0, width: 48.0, height: 24.0) - c.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 12.0).cgPath) - c.clip() - - if incomingColors.count >= 2 { - let gradientColors = incomingColors.reversed().map { $0.cgColor } as CFArray + let rect = CGRect(x: 8.0, y: 44.0, width: 48.0, height: 24.0) + c.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 12.0).cgPath) + c.clip() + + if incomingColors.count >= 2 { + let gradientColors = incomingColors.reversed().map { $0.cgColor } as CFArray - var locations: [CGFloat] = [] - for i in 0 ..< incomingColors.count { - let t = CGFloat(i) / CGFloat(incomingColors.count - 1) - locations.append(t) + var locations: [CGFloat] = [] + for i in 0 ..< incomingColors.count { + let t = CGFloat(i) / CGFloat(incomingColors.count - 1) + locations.append(t) + } + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: rect.minY), end: CGPoint(x: 0.0, y: rect.maxY), options: CGGradientDrawingOptions()) + } else if !incomingColors.isEmpty { + c.setFillColor(incomingColors[0].cgColor) + c.fill(rect) } - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + c.resetClip() + } + } else { + let incoming = generateGradientTintedImage(image: UIImage(bundleImageName: "Settings/ThemeBubble"), colors: incomingColors) + c.draw(incoming!.cgImage!, in: CGRect(x: 9.0, y: 34.0, width: 57.0, height: 16.0)) + } + + if !(emoticon && large) { + c.translateBy(x: drawingRect.width / 2.0, y: drawingRect.height / 2.0) + c.scaleBy(x: -1.0, y: 1.0) + c.translateBy(x: -drawingRect.width / 2.0, y: -drawingRect.height / 2.0) + } + + let outgoingColors = colors.2 + if emoticon { + if large { + c.saveGState() + + c.translateBy(x: (drawingRect.width - 120) - 71, y: 66.0) + c.translateBy(x: 114.0, y: 32.0) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: 0.0, y: -32.0) + + let _ = try? drawSvgPath(c, path: "M57.1696,29.8712 C59.9449,31.8416 63.3372,33 67,33 C68.0157,33 69.0106,32.9109 69.9773,32.7402 C67.0268,31.9699 64.754,29.5786 64.1537,26.5607 C64,25.7879 64,24.8586 64,23 V16.5 V16 H63.9926 C63.7282,7.11861 56.4454,0 47.5,0 H16.5 C7.3873,0 0,7.3873 0,16.5 C0,25.6127 7.3873,33 16.5,33 H47.5 C51.1117,33 54.4524,31.8396 57.1696,29.8712 ") + if Set(outgoingColors.map(\.rgb)).count > 1 { + c.clip() - c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: rect.minY), end: CGPoint(x: 0.0, y: rect.maxY), options: CGGradientDrawingOptions()) - } else if !incomingColors.isEmpty { - c.setFillColor(incomingColors[0].cgColor) - c.fill(rect) + var colors: [CGColor] = [] + var locations: [CGFloat] = [] + for i in 0 ..< outgoingColors.count { + let t = CGFloat(i) / CGFloat(outgoingColors.count - 1) + locations.append(t) + colors.append(outgoingColors[i].cgColor) + } + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as NSArray, locations: &locations)! + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 34.0), options: CGGradientDrawingOptions()) + } else { + c.setFillColor(outgoingColors[0].cgColor) + c.fillPath() + } + + c.restoreGState() + } else { + let rect = CGRect(x: 8.0, y: 72.0, width: 48.0, height: 24.0) + c.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 12.0).cgPath) + c.clip() + + if outgoingColors.count >= 2 { + let gradientColors = outgoingColors.reversed().map { $0.cgColor } as CFArray + + var locations: [CGFloat] = [] + for i in 0 ..< outgoingColors.count { + let t = CGFloat(i) / CGFloat(outgoingColors.count - 1) + locations.append(t) + } + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: rect.minY), end: CGPoint(x: 0.0, y: rect.maxY), options: CGGradientDrawingOptions()) + } else if !outgoingColors.isEmpty { + c.setFillColor(outgoingColors[0].cgColor) + c.fill(rect) + } } c.resetClip() - } - } else { - let incoming = generateGradientTintedImage(image: UIImage(bundleImageName: "Settings/ThemeBubble"), colors: incomingColors) - c.draw(incoming!.cgImage!, in: CGRect(x: 9.0, y: 34.0, width: 57.0, height: 16.0)) - } - - if !(emoticon && large) { - c.translateBy(x: drawingRect.width / 2.0, y: drawingRect.height / 2.0) - c.scaleBy(x: -1.0, y: 1.0) - c.translateBy(x: -drawingRect.width / 2.0, y: -drawingRect.height / 2.0) - } - - let outgoingColors = colors.2 - if emoticon { - if large { - c.saveGState() - - c.translateBy(x: (drawingRect.width - 120) - 71, y: 66.0) - c.translateBy(x: 114.0, y: 32.0) - c.scaleBy(x: 1.0, y: -1.0) - c.translateBy(x: 0.0, y: -32.0) - - let _ = try? drawSvgPath(c, path: "M57.1696,29.8712 C59.9449,31.8416 63.3372,33 67,33 C68.0157,33 69.0106,32.9109 69.9773,32.7402 C67.0268,31.9699 64.754,29.5786 64.1537,26.5607 C64,25.7879 64,24.8586 64,23 V16.5 V16 H63.9926 C63.7282,7.11861 56.4454,0 47.5,0 H16.5 C7.3873,0 0,7.3873 0,16.5 C0,25.6127 7.3873,33 16.5,33 H47.5 C51.1117,33 54.4524,31.8396 57.1696,29.8712 ") - if Set(outgoingColors.map(\.rgb)).count > 1 { - c.clip() - - var colors: [CGColor] = [] - var locations: [CGFloat] = [] - for i in 0 ..< outgoingColors.count { - let t = CGFloat(i) / CGFloat(outgoingColors.count - 1) - locations.append(t) - colors.append(outgoingColors[i].cgColor) - } - - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as NSArray, locations: &locations)! - c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 34.0), options: CGGradientDrawingOptions()) - } else { - c.setFillColor(outgoingColors[0].cgColor) - c.fillPath() - } - - c.restoreGState() } else { - let rect = CGRect(x: 8.0, y: 72.0, width: 48.0, height: 24.0) - c.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 12.0).cgPath) - c.clip() - - if outgoingColors.count >= 2 { - let gradientColors = outgoingColors.reversed().map { $0.cgColor } as CFArray - - var locations: [CGFloat] = [] - for i in 0 ..< outgoingColors.count { - let t = CGFloat(i) / CGFloat(outgoingColors.count - 1) - locations.append(t) - } - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! - - c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: rect.minY), end: CGPoint(x: 0.0, y: rect.maxY), options: CGGradientDrawingOptions()) - } else if !outgoingColors.isEmpty { - c.setFillColor(outgoingColors[0].cgColor) - c.fill(rect) - } + let outgoing = generateGradientTintedImage(image: UIImage(bundleImageName: "Settings/ThemeBubble"), colors: outgoingColors) + c.draw(outgoing!.cgImage!, in: CGRect(x: 9.0, y: 12.0, width: 57.0, height: 16.0)) } - - c.resetClip() - } else { - let outgoing = generateGradientTintedImage(image: UIImage(bundleImageName: "Settings/ThemeBubble"), colors: outgoingColors) - c.draw(outgoing!.cgImage!, in: CGRect(x: 9.0, y: 12.0, width: 57.0, height: 16.0)) } } addCorners(context, arguments: arguments) From e674cfbfa6adab385a05a1fb4ecc6292446da0d2 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 18 Dec 2021 18:02:50 +0400 Subject: [PATCH 04/35] Fix iOS 15.2 hanging on attachment menu opening --- .../Sources/TGMediaAssetsModernLibrary.m | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsModernLibrary.m b/submodules/LegacyComponents/Sources/TGMediaAssetsModernLibrary.m index 09ce790c9e..6cabd8e428 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsModernLibrary.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsModernLibrary.m @@ -9,6 +9,8 @@ @interface TGMediaAssetsModernLibrary () { SPipe *_libraryChangePipe; + + bool _registeredChangeObserver; } @end @@ -20,14 +22,21 @@ if (self != nil) { _libraryChangePipe = [[SPipe alloc] init]; - [[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self]; + + PHAuthorizationStatus authorizationStatus = [PHPhotoLibrary authorizationStatus]; + if (authorizationStatus == PHAuthorizationStatusAuthorized) { + _registeredChangeObserver = true; + [[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self]; + } } return self; } - (void)dealloc { - [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self]; + if (_registeredChangeObserver) { + [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self]; + } } - (SSignal *)assetGroups From 3fad53fbeb02cd4ec70799854008ad778e42df9f Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 18 Dec 2021 18:04:54 +0400 Subject: [PATCH 05/35] Fix iOS 15.2 hanging on attachment menu opening --- .../Sources/TGMediaAssetsModernLibrary.m | 13 +++++++++++-- versions.json | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsModernLibrary.m b/submodules/LegacyComponents/Sources/TGMediaAssetsModernLibrary.m index 09ce790c9e..6cabd8e428 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsModernLibrary.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsModernLibrary.m @@ -9,6 +9,8 @@ @interface TGMediaAssetsModernLibrary () { SPipe *_libraryChangePipe; + + bool _registeredChangeObserver; } @end @@ -20,14 +22,21 @@ if (self != nil) { _libraryChangePipe = [[SPipe alloc] init]; - [[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self]; + + PHAuthorizationStatus authorizationStatus = [PHPhotoLibrary authorizationStatus]; + if (authorizationStatus == PHAuthorizationStatusAuthorized) { + _registeredChangeObserver = true; + [[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self]; + } } return self; } - (void)dealloc { - [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self]; + if (_registeredChangeObserver) { + [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self]; + } } - (SSignal *)assetGroups diff --git a/versions.json b/versions.json index f4351a4b04..6c068aae84 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "8.3.1", + "app": "8.3.2", "bazel": "4.0.0", "xcode": "13.1" } From 884a31987c597343f4a8eddf49fa11ee7ca68123 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sat, 18 Dec 2021 19:52:27 +0400 Subject: [PATCH 06/35] Bump version --- versions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions.json b/versions.json index f4351a4b04..9c82469a48 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "8.3.1", + "app": "8.3", "bazel": "4.0.0", "xcode": "13.1" } From 2a1d8af7448234d1649fd5a0c2289d0009ea3f45 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 18 Dec 2021 21:21:24 +0400 Subject: [PATCH 07/35] Various Improvements --- .../Sources/InvisibleInkDustNode.swift | 2 +- .../Sources/ChatMessageTextBubbleContentNode.swift | 2 +- submodules/Translate/Sources/Translate.swift | 13 +++++++------ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift index 6974a73a49..4f7d3316cb 100644 --- a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift @@ -217,7 +217,7 @@ public class InvisibleInkDustNode: ASDisplayNode { square += Float(rect.width * rect.height) } - self.emitter?.birthRate = square * 0.3 + self.emitter?.birthRate = square * 0.4 } public func update(size: CGSize, color: UIColor, rects: [CGRect]) { diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 5cb4c3be32..5c181f4b3d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -404,7 +404,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.dustNode = dustNode strongSelf.insertSubnode(dustNode, aboveSubnode: spoilerTextNode) } - dustNode.update(size: textFrame.size, color: messageTheme.secondaryTextColor, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) + dustNode.update(size: textFrame.size, color: messageTheme.secondaryTextColor, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) } else if let spoilerTextNode = strongSelf.spoilerTextNode { strongSelf.spoilerTextNode = nil diff --git a/submodules/Translate/Sources/Translate.swift b/submodules/Translate/Sources/Translate.swift index 539319d70b..42334de9a4 100644 --- a/submodules/Translate/Sources/Translate.swift +++ b/submodules/Translate/Sources/Translate.swift @@ -12,15 +12,16 @@ public func translateText(context: AccountContext, text: String) { return } if #available(iOS 15.0, *) { - let textField = UITextField() - textField.text = text + let textView = UITextView() + textView.text = text + textView.isEditable = false if let navigationController = context.sharedContext.mainWindow?.viewController as? NavigationController, let topController = navigationController.topViewController as? ViewController { - topController.view.addSubview(textField) - textField.selectAll(nil) - textField.perform(NSSelectorFromString(["_", "trans", "late:"].joined(separator: "")), with: nil) + topController.view.addSubview(textView) + textView.selectAll(nil) + textView.perform(NSSelectorFromString(["_", "trans", "late:"].joined(separator: "")), with: nil) DispatchQueue.main.async { - textField.removeFromSuperview() + textView.removeFromSuperview() } } } From 5ddfb0dd630a93d7da9a96596dd3079d070afc4a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 18 Dec 2021 23:44:55 +0400 Subject: [PATCH 08/35] Fix spoiler dust on truncated lines --- submodules/Display/Source/TextNode.swift | 7 ++++--- .../TelegramUI/Sources/ChatMessageReplyInfoNode.swift | 2 +- .../Sources/ChatMessageTextBubbleContentNode.swift | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index ee16bed593..1fb4070ea1 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -973,7 +973,7 @@ public class TextNode: ASDisplayNode { let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, lastLineCharacterIndex, Double(lineConstrainedWidth)) - func addSpoiler(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int) { + func addSpoiler(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { var secondaryLeftOffset: CGFloat = 0.0 let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) var leftOffset = floor(rawLeftOffset) @@ -988,7 +988,7 @@ public class TextNode: ASDisplayNode { rightOffset = ceil(secondaryRightOffset) } - spoilers.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset), height: ascent + descent))) + spoilers.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent))) } var isLastLine = false @@ -1033,6 +1033,7 @@ public class TextNode: ASDisplayNode { let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString) coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(lineConstrainedSize.width), truncationType, truncationToken) ?? truncationToken + brokenLineRange.length = CTLineGetGlyphCount(coreTextLine) - 1 truncated = true } @@ -1063,7 +1064,7 @@ public class TextNode: ASDisplayNode { if let currentStartIndex = startIndex, let currentIndex = currentIndex { startIndex = nil let endIndex = currentIndex - addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) + addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex, rightInset: truncated ? 12.0 : 0.0) } } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) diff --git a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift index 73eafa58ac..a300505b13 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift @@ -250,8 +250,8 @@ class ChatMessageReplyInfoNode: ASDisplayNode { node.dustNode = dustNode node.contentNode.insertSubnode(dustNode, aboveSubnode: textNode) } - dustNode.update(size: textFrame.size, color: titleColor, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) + dustNode.update(size: dustNode.frame.size, color: titleColor, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) } else if let dustNode = node.dustNode { dustNode.removeFromSupernode() node.dustNode = nil diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 5c181f4b3d..4d4231b572 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -404,8 +404,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.dustNode = dustNode strongSelf.insertSubnode(dustNode, aboveSubnode: spoilerTextNode) } - dustNode.update(size: textFrame.size, color: messageTheme.secondaryTextColor, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) + dustNode.update(size: dustNode.frame.size, color: messageTheme.secondaryTextColor, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) } else if let spoilerTextNode = strongSelf.spoilerTextNode { strongSelf.spoilerTextNode = nil spoilerTextNode.removeFromSupernode() From 6cdf72427c7d9da2621bb4cf8375c13060ec4310 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 19 Dec 2021 18:34:04 +0400 Subject: [PATCH 09/35] Check if broken line range is out of string length --- submodules/Display/Source/TextNode.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 1fb4070ea1..03be16238d 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -1034,6 +1034,9 @@ public class TextNode: ASDisplayNode { coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(lineConstrainedSize.width), truncationType, truncationToken) ?? truncationToken brokenLineRange.length = CTLineGetGlyphCount(coreTextLine) - 1 + if brokenLineRange.location + brokenLineRange.length > attributedString.length { + brokenLineRange.length = attributedString.length - brokenLineRange.location + } truncated = true } From 42773dbe01ac0630e9e7f6216a83f8ef68a0c8fa Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sun, 19 Dec 2021 19:12:19 +0400 Subject: [PATCH 10/35] Reaction improvements --- .../ReactionListContextMenuContent.swift | 18 ++++++++++++- .../Sources/ReactionContextNode.swift | 25 ++++++++++++++----- .../TelegramUI/Sources/ChatController.swift | 18 ++++++++++--- .../ChatMessageAnimatedStickerItemNode.swift | 4 +-- .../ChatMessageAttachedContentNode.swift | 11 ++++++-- .../Sources/ChatMessageBubbleItemNode.swift | 14 ++++++++--- .../ChatMessageContactBubbleContentNode.swift | 2 +- .../ChatMessageDateAndStatusNode.swift | 2 +- .../ChatMessageFileBubbleContentNode.swift | 5 +++- .../ChatMessageInteractiveFileNode.swift | 7 ++++++ ...hatMessageReactionsFooterContentNode.swift | 4 +-- .../Sources/ChatMessageStickerItemNode.swift | 4 +-- .../ChatMessageTextBubbleContentNode.swift | 4 +-- 13 files changed, 91 insertions(+), 27 deletions(-) diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index c4bf2af919..e6e5740c51 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -187,6 +187,11 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent private let selectionHighlightNode: ASDisplayNode private let itemNodes: [ItemNode] + private struct ScrollToTabReaction { + var value: String? + } + private var scrollToTabReaction: ScrollToTabReaction? + var action: ((String?) -> Void)? init(context: AccountContext, availableReactions: AvailableReactions?, reactions: [(String?, Int)], message: EngineMessage) { @@ -217,6 +222,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent guard let strongSelf = self else { return } + strongSelf.scrollToTabReaction = ScrollToTabReaction(value: reaction) strongSelf.action?(reaction) } } @@ -256,6 +262,16 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent if self.scrollNode.view.contentSize != contentSize { self.scrollNode.view.contentSize = contentSize } + + if let scrollToTabReaction = self.scrollToTabReaction { + self.scrollToTabReaction = nil + for itemNode in self.itemNodes { + if itemNode.reaction == scrollToTabReaction.value { + self.scrollNode.view.scrollRectToVisible(itemNode.frame.insetBy(dx: -sideInset, dy: 0.0), animated: transition.isAnimated) + break + } + } + } } } @@ -494,7 +510,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent 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 { + } else if index < self.state.totalCount { validPlaceholderIds.insert(index) let placeholderLayer: SimpleLayer diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 1bcd8f363c..7686ab1d2d 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -254,6 +254,12 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { rect.origin.x = max(sideInset, rect.origin.x) rect.origin.y = max(insets.top + sideInset, rect.origin.y) rect.origin.x = min(containerSize.width - contentSize.width - sideInset, rect.origin.x) + if rect.maxX > containerSize.width - sideInset { + rect.origin.x = containerSize.width - sideInset - rect.width + } + if rect.minX < sideInset { + rect.origin.x = sideInset + } let cloudSourcePoint: CGFloat if isLeftAligned { @@ -309,11 +315,18 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { let completeContentWidth = CGFloat(self.items.count) * itemSize + (CGFloat(self.items.count) - 1.0) * itemSpacing + sideInset * 2.0 let minVisibleItemCount: CGFloat = min(CGFloat(self.items.count), 6.5) - let visibleContentWidth = floor(minVisibleItemCount * itemSize + (minVisibleItemCount - 1.0) * itemSpacing + sideInset * 2.0) + var visibleContentWidth = floor(minVisibleItemCount * itemSize + (minVisibleItemCount - 1.0) * itemSpacing + sideInset * 2.0) + if visibleContentWidth > size.width - sideInset * 2.0 { + visibleContentWidth = size.width - sideInset * 2.0 + } let contentHeight = verticalInset * 2.0 + rowHeight - let (backgroundFrame, isLeftAligned, cloudSourcePoint) = self.calculateBackgroundFrame(containerSize: size, insets: insets, anchorRect: anchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight)) + var backgroundInsets = insets + backgroundInsets.left += sideInset + backgroundInsets.right += sideInset + + let (backgroundFrame, isLeftAligned, cloudSourcePoint) = self.calculateBackgroundFrame(containerSize: CGSize(width: size.width - sideInset * 2.0, height: size.height), insets: backgroundInsets, anchorRect: anchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight)) self.isLeftAligned = isLeftAligned transition.updateFrame(node: self.contentContainer, frame: backgroundFrame) @@ -369,7 +382,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { let springDamping: CGFloat = 104.0 let springDelay: Double = 0.22 - let sourceBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: insets, anchorRect: animateInFromAnchorRect, contentSize: CGSize(width: backgroundFrame.height, height: contentHeight)).0 + let sourceBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: backgroundInsets, anchorRect: animateInFromAnchorRect, contentSize: CGSize(width: backgroundFrame.height, height: contentHeight)).0 self.backgroundNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - backgroundFrame.midX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) self.backgroundNode.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size).insetBy(dx: -shadowBlur, dy: -shadowBlur)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: backgroundFrame.size).insetBy(dx: -shadowBlur, dy: -shadowBlur)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) @@ -380,7 +393,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { //self.contentContainerMask.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - backgroundFrame.midX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) //self.contentContainerMask.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: backgroundFrame.size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) } else if let animateOutToAnchorRect = animateOutToAnchorRect { - let targetBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: insets, anchorRect: animateOutToAnchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight)).0 + let targetBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: backgroundInsets, anchorRect: animateOutToAnchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight)).0 let offset = CGPoint(x: -(targetBackgroundFrame.minX - backgroundFrame.minX), y: -(targetBackgroundFrame.minY - backgroundFrame.minY)) self.position = CGPoint(x: self.position.x - offset.x, y: self.position.y - offset.y) @@ -472,8 +485,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.9, removeOnCompletion: false) targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.8) targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 1.0, duration: duration, removeOnCompletion: false, completion: { [weak self, weak targetSnapshotView] _ in - if let strongSelf = self { - strongSelf.hapticFeedback.tap() + if let _ = self { + //strongSelf.hapticFeedback.tap() } completedTarget = true intermediateCompletion() diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index bfa1038a69..6428918210 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1173,6 +1173,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } var removedReaction: String? + var messageAlreadyHasThisReaction = false for attribute in message.attributes { if let attribute = attribute as? ReactionsMessageAttribute { @@ -1182,11 +1183,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if listReaction.isSelected { updatedReaction = nil removedReaction = listReaction.value + } else if listReaction.value == updatedReaction { + messageAlreadyHasThisReaction = true } case let .reaction(value): - if listReaction.value == value && listReaction.isSelected { - updatedReaction = nil - removedReaction = value + if listReaction.value == value { + messageAlreadyHasThisReaction = true + + if listReaction.isSelected { + updatedReaction = nil + removedReaction = value + } } } } @@ -1213,7 +1220,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } switch allowedReactions { case let .set(set): - if !set.contains(updatedReaction) { + if !messageAlreadyHasThisReaction && !set.contains(updatedReaction) { itemNode.openMessageContextMenu() return } @@ -14450,6 +14457,9 @@ extension Peer { } func canAddMessageReactions(message: Message) -> Bool { + if message.id.namespace != Namespaces.Message.Cloud { + return false + } if let peer = message.peers[message.id.peerId] { if let _ = peer as? TelegramSecretChat { return false diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index d734e96750..f7beb9ea94 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -945,8 +945,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var dateReplies = 0 let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message) for attribute in item.message.attributes { - if let _ = attribute as? EditedMessageAttribute, isEmoji { - edited = true + if let attribute = attribute as? EditedMessageAttribute, isEmoji { + edited = !attribute.isHidden } else if let attribute = attribute as? ViewCountMessageAttribute { viewCount = attribute.count } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation { diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index 27785eca94..a2e3bf89ae 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -939,11 +939,18 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { strongSelf.textNode.frame = textFrame.offsetBy(dx: 0.0, dy: textVerticalOffset) if let statusSizeAndApply = statusSizeAndApply { + var statusFrame = CGRect(origin: CGPoint(x: strongSelf.textNode.frame.minX, y: strongSelf.textNode.frame.maxY), size: statusSizeAndApply.0) + if let imageFrame = imageFrame { + statusFrame.origin.y = max(statusFrame.minY, imageFrame.maxY + 2.0) + } if strongSelf.statusNode.supernode == nil { strongSelf.addSubnode(strongSelf.statusNode) + strongSelf.statusNode.frame = statusFrame + statusSizeAndApply.1(.None) + } else { + animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: statusFrame, completion: nil) + statusSizeAndApply.1(animation) } - strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: strongSelf.textNode.frame.minX, y: strongSelf.textNode.frame.maxY), size: statusSizeAndApply.0) - statusSizeAndApply.1(animation) } else if strongSelf.statusNode.supernode != nil { strongSelf.statusNode.removeFromSupernode() } diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index de9e6b6496..c6eb92d433 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -835,7 +835,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } for contentNode in strongSelf.contentNodes { - let tapAction = contentNode.tapActionAtPoint(CGPoint(x: point.x - contentNode.frame.minX, y: point.y - contentNode.frame.minY), gesture: .tap, isEstimating: true) + let contentNodePoint = strongSelf.view.convert(point, to: contentNode.view) + let tapAction = contentNode.tapActionAtPoint(contentNodePoint, gesture: .tap, isEstimating: true) switch tapAction { case .none: if let _ = strongSelf.item?.controllerInteraction.tapMessage { @@ -2696,14 +2697,18 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } if let mosaicStatusOrigin = mosaicStatusOrigin, let (size, apply) = mosaicStatusSizeAndApply { - let mosaicStatusNode = apply(animation) + var statusNodeAnimation = animation + if strongSelf.mosaicStatusNode == nil { + statusNodeAnimation = .None + } + let mosaicStatusNode = apply(statusNodeAnimation) if mosaicStatusNode !== strongSelf.mosaicStatusNode { strongSelf.mosaicStatusNode?.removeFromSupernode() strongSelf.mosaicStatusNode = mosaicStatusNode strongSelf.clippingNode.addSubnode(mosaicStatusNode) } let absoluteOrigin = mosaicStatusOrigin.offsetBy(dx: contentOrigin.x, dy: contentOrigin.y) - mosaicStatusNode.frame = CGRect(origin: CGPoint(x: absoluteOrigin.x - layoutConstants.image.statusInsets.right - size.width, y: absoluteOrigin.y - layoutConstants.image.statusInsets.bottom - size.height), size: size) + statusNodeAnimation.animator.updateFrame(layer: mosaicStatusNode.layer, frame: CGRect(origin: CGPoint(x: absoluteOrigin.x - layoutConstants.image.statusInsets.right - size.width, y: absoluteOrigin.y - layoutConstants.image.statusInsets.bottom - size.height), size: size), completion: nil) } else if let mosaicStatusNode = strongSelf.mosaicStatusNode { strongSelf.mosaicStatusNode = nil mosaicStatusNode.removeFromSupernode() @@ -3880,6 +3885,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return result } } + if let mosaicStatusNode = self.mosaicStatusNode, let result = mosaicStatusNode.reactionView(value: value) { + return result + } return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift index 1869f576fb..e89c1e0dca 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift @@ -55,7 +55,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { return } - item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value) } } diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift index 016f17676e..f1a4707ab8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift @@ -791,7 +791,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { var reactionButtonPosition = CGPoint(x: -1.0, y: verticalReactionsInset) for item in reactionButtons.items { if reactionButtonPosition.x + item.size.width > boundingWidth { - reactionButtonPosition.x = 0.0 + reactionButtonPosition.x = -1.0 reactionButtonPosition.y += item.size.height + 6.0 } diff --git a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift index 827b30dc56..f09a642e57 100644 --- a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift @@ -70,7 +70,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { return } - item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value) } } @@ -181,6 +181,9 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { if self.interactiveFileNode.dateAndStatusNode.supernode != nil, let _ = self.interactiveFileNode.dateAndStatusNode.hitTest(self.view.convert(point, to: self.interactiveFileNode.dateAndStatusNode.view), with: nil) { return .ignore } + if self.interactiveFileNode.hasTapAction(at: self.view.convert(point, to: self.interactiveFileNode.view)) { + return .ignore + } return super.tapActionAtPoint(point, gesture: gesture, isEstimating: isEstimating) } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 260c025ce0..393f87cc06 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -1146,6 +1146,13 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } return super.hitTest(point, with: event) } + + func hasTapAction(at point: CGPoint) -> Bool { + if let _ = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: nil) { + return true + } + return false + } } diff --git a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift index 41b56929a6..db8d90b2d0 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift @@ -275,7 +275,7 @@ final class MessageReactionButtonsNode: ASDisplayNode { switch alignment { case .left: if reactionButtonPosition.x + item.size.width > boundingWidth { - reactionButtonPosition.x = 0.0 + reactionButtonPosition.x = -1.0 reactionButtonPosition.y += item.size.height + 6.0 } case .right: @@ -462,7 +462,7 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode return } - item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value) } } diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index 19a03cd5ed..619222d1f8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -485,8 +485,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { var dateReplies = 0 let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message) for attribute in item.message.attributes { - if let _ = attribute as? EditedMessageAttribute, isEmoji { - edited = true + if let attribute = attribute as? EditedMessageAttribute, isEmoji { + edited = !attribute.isHidden } else if let attribute = attribute as? ViewCountMessageAttribute { viewCount = attribute.count } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation { diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 5cb4c3be32..b9f7a5caef 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -83,7 +83,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { return } - item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value) } } @@ -128,7 +128,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 - let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message) + let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.topMessage) for attribute in item.message.attributes { if let attribute = attribute as? EditedMessageAttribute { From e281c50ebf940843dce02125a04d8a36ebf15bf9 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sun, 19 Dec 2021 19:40:18 +0400 Subject: [PATCH 11/35] Add missing reaction bar to messages with comments --- .../TelegramUI/Sources/ChatMessageBubbleItemNode.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index c6eb92d433..435e43d03a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -249,6 +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 == ChatMessageWebpageBubbleContentNode.self || + result[result.count - 2].1 == ChatMessagePollBubbleContentNode.self || + result[result.count - 2].1 == ChatMessageContactBubbleContentNode.self { + result.insert((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)), at: result.count - 1) + } /*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) From 468f740d547d803dadce37645b0f293cc36476ec Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sun, 19 Dec 2021 21:34:16 +0400 Subject: [PATCH 12/35] Update version number --- versions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions.json b/versions.json index 9c82469a48..320ec2a314 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "8.3", + "app": "8.4", "bazel": "4.0.0", "xcode": "13.1" } From b4382b6fc06ef80dbc764b683055489a808f957b Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 20 Dec 2021 02:09:50 +0400 Subject: [PATCH 13/35] Support custom themes --- .../Sources/DefaultDayPresentationTheme.swift | 24 ++++++++-- .../Sources/PresentationThemeCodable.swift | 46 +++++++++++++++++-- 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index 67edfdc8a3..1e68394109 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -250,10 +250,18 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti incoming: chat.message.incoming.withUpdated( bubble: chat.message.incoming.bubble.withUpdated( withWallpaper: chat.message.incoming.bubble.withWallpaper.withUpdated( - stroke: incomingBubbleStrokeColor + stroke: incomingBubbleStrokeColor, + reactionInactiveBackground: accentColor?.withMultipliedAlpha(0.1), + reactionInactiveForeground: accentColor, + reactionActiveBackground: accentColor, + reactionActiveForeground: .clear ), withoutWallpaper: chat.message.incoming.bubble.withoutWallpaper.withUpdated( - stroke: incomingBubbleStrokeColor + stroke: incomingBubbleStrokeColor, + reactionInactiveBackground: accentColor?.withMultipliedAlpha(0.1), + reactionInactiveForeground: accentColor, + reactionActiveBackground: accentColor, + reactionActiveForeground: .clear ) ), linkHighlightColor: accentColor?.withAlphaComponent(0.3), @@ -278,12 +286,20 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti withWallpaper: chat.message.outgoing.bubble.withWallpaper.withUpdated( fill: outgoingBubbleFillColors, highlightedFill: outgoingBubbleHighlightedFill, - stroke: outgoingBubbleStrokeColor + stroke: outgoingBubbleStrokeColor, + reactionInactiveBackground: outgoingControlColor?.withMultipliedAlpha(0.1), + reactionInactiveForeground: outgoingControlColor, + reactionActiveBackground: outgoingControlColor, + reactionActiveForeground: .clear ), withoutWallpaper: chat.message.outgoing.bubble.withoutWallpaper.withUpdated( fill: outgoingBubbleFillColors, highlightedFill: outgoingBubbleHighlightedFill, - stroke: outgoingBubbleStrokeColor + stroke: outgoingBubbleStrokeColor, + reactionInactiveBackground: outgoingControlColor?.withMultipliedAlpha(0.1), + reactionInactiveForeground: outgoingControlColor, + reactionActiveBackground: outgoingControlColor, + reactionActiveForeground: .clear ) ), primaryTextColor: outgoingPrimaryTextColor, diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift index 3105038c81..fd5d7f30bc 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift @@ -1101,6 +1101,7 @@ extension PresentationThemeBubbleColorComponents: Codable { case reactionInactiveFg case reactionActiveBg case reactionActiveFg + case __workaroundNonexistingKey } public convenience init(from decoder: Decoder) throws { @@ -1121,16 +1122,51 @@ extension PresentationThemeBubbleColorComponents: Codable { fill = [fillColor, gradientColor] } - + + let fallbackKeyPrefix: String + if codingPath.hasPrefix("chat.message.incoming.") { + fallbackKeyPrefix = "chat.message.incoming." + } else { + fallbackKeyPrefix = "chat.message.outgoing." + } + + let reactionInactiveBackground: UIColor + if let color = try? decodeColor(values, .reactionInactiveBg) { + reactionInactiveBackground = color + } else { + reactionInactiveBackground = (try decodeColor(values, .__workaroundNonexistingKey, fallbackKey: "\(fallbackKeyPrefix).accentControl")).withMultipliedAlpha(0.1) + } + + let reactionInactiveForeground: UIColor + if let color = try? decodeColor(values, .reactionInactiveFg) { + reactionInactiveForeground = color + } else { + reactionInactiveForeground = try decodeColor(values, .__workaroundNonexistingKey, fallbackKey: "\(fallbackKeyPrefix).accentControl") + } + + let reactionActiveBackground: UIColor + if let color = try? decodeColor(values, .reactionActiveBg) { + reactionActiveBackground = color + } else { + reactionActiveBackground = try decodeColor(values, .__workaroundNonexistingKey, fallbackKey: "\(fallbackKeyPrefix).accentControl") + } + + let reactionActiveForeground: UIColor + if let color = try? decodeColor(values, .reactionActiveFg) { + reactionActiveForeground = color + } else { + reactionActiveForeground = .clear + } + self.init( fill: fill, highlightedFill: try decodeColor(values, .highlightedBg), stroke: try decodeColor(values, .stroke), 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) + reactionInactiveBackground: reactionInactiveBackground, + reactionInactiveForeground: reactionInactiveForeground, + reactionActiveBackground: reactionActiveBackground, + reactionActiveForeground: reactionActiveForeground ) } From 2d3aee57f4957ec56fbc34a11d6fb26886144d89 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 20 Dec 2021 18:39:36 +0400 Subject: [PATCH 14/35] Various Improvements --- .../Sources/Node/ChatListItem.swift | 2 +- .../Sources/CreatePollTextInputItem.swift | 2 +- submodules/Display/Source/TextNode.swift | 6 +-- .../ChatItemGalleryFooterContentNode.swift | 2 +- .../Sources/RecognizedTextSelectionNode.swift | 18 +++---- .../ChatInterfaceStateContextMenus.swift | 2 +- .../Sources/ChatMessageNotificationItem.swift | 53 ++++++++++++++++--- .../Sources/ChatMessageReplyInfoNode.swift | 5 +- .../ChatMessageTextBubbleContentNode.swift | 34 ++++++++---- .../Sources/ChatTextInputPanelNode.swift | 22 ++++++-- .../PeerSelectionTextInputPanelNode.swift | 6 +-- .../Sources/ChatTextInputAttributes.swift | 14 ++--- .../Sources/TextSelectionNode.swift | 21 ++++---- 13 files changed, 133 insertions(+), 54 deletions(-) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 6db0fac958..cef20e0fe2 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1757,7 +1757,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.dustNode = dustNode strongSelf.contextContainer.insertSubnode(dustNode, aboveSubnode: strongSelf.textNode) } - dustNode.update(size: textNodeFrame.size, color: theme.messageTextColor, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) + dustNode.update(size: textNodeFrame.size, color: theme.messageTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) dustNode.frame = textNodeFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) } else if let dustNode = strongSelf.dustNode { diff --git a/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift b/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift index 430c2a1e9e..3e4812eba5 100644 --- a/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift +++ b/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift @@ -316,7 +316,7 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe rightInset += inlineAction.icon.size.width + 8.0 } - let itemText = textAttributedStringForStateText(item.text, fontSize: 17.0, textColor: item.presentationData.theme.chat.inputPanel.primaryTextColor, accentTextColor: item.presentationData.theme.chat.inputPanel.panelControlAccentColor, writingDirection: nil) + let itemText = textAttributedStringForStateText(item.text, fontSize: 17.0, textColor: item.presentationData.theme.chat.inputPanel.primaryTextColor, accentTextColor: item.presentationData.theme.chat.inputPanel.panelControlAccentColor, writingDirection: nil, spoilersRevealed: false) let measureText = NSMutableAttributedString(attributedString: itemText) let measureRawString = measureText.string if measureRawString.hasSuffix("\n") || measureRawString.isEmpty { diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 03be16238d..8871464fdf 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -173,7 +173,7 @@ public final class TextNodeLayout: NSObject { fileprivate let textStroke: (UIColor, CGFloat)? fileprivate let displaySpoilers: Bool public let hasRTL: Bool - public let spoilers: [CGRect] + public let spoilers: [(NSRange, CGRect)] fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, explicitAlignment: NSTextAlignment, resolvedAlignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, rawTextSize: CGSize, truncated: Bool, firstLineOffset: CGFloat, lines: [TextNodeLine], blockQuotes: [TextNodeBlockQuote], backgroundColor: UIColor?, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool) { self.attributedString = attributedString @@ -198,12 +198,12 @@ public final class TextNodeLayout: NSObject { self.textStroke = textStroke self.displaySpoilers = displaySpoilers var hasRTL = false - var spoilers: [CGRect] = [] + var spoilers: [(NSRange, CGRect)] = [] for line in lines { if line.isRTL { hasRTL = true } - spoilers.append(contentsOf: line.spoilers.map { $0.frame.offsetBy(dx: line.frame.minX, dy: line.frame.minY) }) + spoilers.append(contentsOf: line.spoilers.map { ( $0.range, $0.frame.offsetBy(dx: line.frame.minX, dy: line.frame.minY)) }) } self.hasRTL = hasRTL self.spoilers = spoilers diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index c3cb3842e9..070be199d5 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -737,7 +737,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } if let dustNode = self.dustNode { - dustNode.update(size: textFrame.size, color: .white, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) + dustNode.update(size: textFrame.size, color: .white, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) } } else { diff --git a/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift b/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift index dc42d26acf..8f5713c25c 100644 --- a/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift +++ b/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift @@ -510,18 +510,18 @@ public final class RecognizedTextSelectionNode: ASDisplayNode { self?.performAction(selectedText, .lookup) let _ = self?.dismissSelection() })) -// if #available(iOS 15.0, *) { -// actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuTranslate, accessibilityLabel: self.strings.Conversation_ContextMenuTranslate), action: { [weak self] in -// self?.performAction(selectedText, .translate) -// let _ = self?.dismissSelection() -// })) -// } - if isSpeakSelectionEnabled() { - actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuSpeak, accessibilityLabel: self.strings.Conversation_ContextMenuSpeak), action: { [weak self] in - self?.performAction(selectedText, .speak) + if #available(iOS 15.0, *) { + actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuTranslate, accessibilityLabel: self.strings.Conversation_ContextMenuTranslate), action: { [weak self] in + self?.performAction(selectedText, .translate) let _ = self?.dismissSelection() })) } +// if isSpeakSelectionEnabled() { +// actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuSpeak, accessibilityLabel: self.strings.Conversation_ContextMenuSpeak), action: { [weak self] in +// self?.performAction(selectedText, .speak) +// let _ = self?.dismissSelection() +// })) +// } actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in self?.performAction(selectedText, .share) let _ = self?.dismissSelection() diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index d9dc7d04ec..54fcc55573 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -760,7 +760,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState f(.default) }))) - if #available(iOS 15.0, *) { + if #available(iOS 15.0, *), !message.text.isEmpty { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuTranslate, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in diff --git a/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift b/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift index 3333e967ff..6ea076b896 100644 --- a/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift +++ b/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift @@ -13,6 +13,8 @@ import LocalizedPeerData import StickerResources import PhotoResources import TelegramStringFormatting +import TextFormat +import InvisibleInkDustNode public final class ChatMessageNotificationItem: NotificationItem { let context: AccountContext @@ -68,6 +70,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { private let titleIconNode: ASImageNode private let titleNode: TextNode private let textNode: TextNode + private var dustNode: InvisibleInkDustNode? private let imageNode: TransformImageNode private var titleAttributedText: NSAttributedString? @@ -157,6 +160,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { var imageDimensions: CGSize? var isRound = false var messageText: String + var messageEntities: [MessageTextEntity]? if item.messages.first?.id.peerId.namespace == Namespaces.Peer.SecretChat { messageText = item.strings.PUSH_ENCRYPTED_MESSAGE("").string } else if item.messages.count == 1 { @@ -180,7 +184,19 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { if message.containsSecretMedia { imageDimensions = nil } - messageText = descriptionStringForMessage(contentSettings: item.context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: item.strings, nameDisplayOrder: item.nameDisplayOrder, dateTimeFormat: item.dateTimeFormat, accountPeerId: item.context.account.peerId).0 + let (textString, _, isText) = descriptionStringForMessage(contentSettings: item.context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: item.strings, nameDisplayOrder: item.nameDisplayOrder, dateTimeFormat: item.dateTimeFormat, accountPeerId: item.context.account.peerId) + if isText { + messageText = message.text + messageEntities = message.textEntitiesAttribute?.entities.filter { entity in + if case .Spoiler = entity.type { + return true + } else { + return false + } + } + } else { + messageText = textString + } } else if item.messages.count > 1, let peer = item.messages[0].peers[item.messages[0].id.peerId] { var displayAuthor = true if let channel = peer as? TelegramChannel { @@ -286,8 +302,15 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { title = "📅 \(currentTitle)" } - messageText = messageText.replacingOccurrences(of: "\n\n", with: " ") - + let textFont = compact ? Font.regular(15.0) : Font.regular(16.0) + let textColor = presentationData.theme.inAppNotification.primaryTextColor + var attributedMessageText: NSAttributedString + if let messageEntities = messageEntities { + attributedMessageText = stringWithAppliedEntities(messageText, entities: messageEntities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false) + } else { + attributedMessageText = NSAttributedString(string: messageText.replacingOccurrences(of: "\n\n", with: " "), font: textFont, textColor: textColor) + } + self.titleAttributedText = NSAttributedString(string: title ?? "", font: compact ? Font.semibold(15.0) : Font.semibold(16.0), textColor: presentationData.theme.inAppNotification.primaryTextColor) let imageNodeLayout = self.imageNode.asyncLayout() @@ -325,7 +348,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { self.imageNode.setSignal(updateImageSignal) } - self.textAttributedText = NSAttributedString(string: messageText, font: compact ? Font.regular(15.0) : Font.regular(16.0), textColor: presentationData.theme.inAppNotification.primaryTextColor) + self.textAttributedText = attributedMessageText if let width = self.validLayout { let _ = self.updateLayout(width: width, transition: .immediate) @@ -361,7 +384,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: self.textAttributedText, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) let _ = titleApply() let _ = textApply() - + let textSpacing: CGFloat = 1.0 let titleFrame = CGRect(origin: CGPoint(x: leftInset + titleInset, y: 1.0 + floor((panelHeight - textLayout.size.height - titleLayout.size.height - textSpacing) / 2.0)), size: titleLayout.size) @@ -371,10 +394,28 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { transition.updateFrame(node: self.titleIconNode, frame: CGRect(origin: CGPoint(x: leftInset + 1.0, y: titleFrame.minY + 3.0), size: image.size)) } - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + textSpacing), size: textLayout.size)) + let textFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + textSpacing), size: textLayout.size) + transition.updateFrame(node: self.textNode, frame: textFrame) transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: width - 10.0 - imageSize.width, y: (panelHeight - imageSize.height) / 2.0), size: imageSize)) + if !textLayout.spoilers.isEmpty, let presentationData = self.item?.context.sharedContext.currentPresentationData.with({ $0 }) { + let dustNode: InvisibleInkDustNode + if let current = self.dustNode { + dustNode = current + } else { + dustNode = InvisibleInkDustNode(textNode: nil) + dustNode.isUserInteractionEnabled = false + self.dustNode = dustNode + self.insertSubnode(dustNode, aboveSubnode: self.textNode) + } + dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) + dustNode.update(size: dustNode.frame.size, color: presentationData.theme.inAppNotification.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + } else if let dustNode = self.dustNode { + dustNode.removeFromSupernode() + self.dustNode = nil + } + return panelHeight } } diff --git a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift index a300505b13..e4b5d79f78 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift @@ -73,6 +73,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { let titleColor: UIColor let lineImage: UIImage? let textColor: UIColor + let dustColor: UIColor switch type { case let .bubble(incoming): @@ -83,6 +84,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { } else { textColor = incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor } + dustColor = incoming ? presentationData.theme.theme.chat.message.incoming.secondaryTextColor : presentationData.theme.theme.chat.message.outgoing.secondaryTextColor case .standalone: let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) titleColor = serviceColor.primaryText @@ -90,6 +92,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { let graphics = PresentationResourcesChat.additionalGraphics(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) lineImage = graphics.chatServiceVerticalLineImage textColor = titleColor + dustColor = titleColor } @@ -251,7 +254,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { node.contentNode.insertSubnode(dustNode, aboveSubnode: textNode) } dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) - dustNode.update(size: dustNode.frame.size, color: titleColor, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + dustNode.update(size: dustNode.frame.size, color: dustColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) } else if let dustNode = node.dustNode { dustNode.removeFromSupernode() node.dustNode = nil diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 4d4231b572..2c7f91b79d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -405,7 +405,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.insertSubnode(dustNode, aboveSubnode: spoilerTextNode) } dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) - dustNode.update(size: dustNode.frame.size, color: messageTheme.secondaryTextColor, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + dustNode.update(size: dustNode.frame.size, color: messageTheme.secondaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) } else if let spoilerTextNode = strongSelf.spoilerTextNode { strongSelf.spoilerTextNode = nil spoilerTextNode.removeFromSupernode() @@ -667,20 +667,36 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } item.controllerInteraction.performTextSelectionAction(item.message.stableId, text, action) }) + textSelectionNode.updateRange = { [weak self] selectionRange in + if let strongSelf = self, let dustNode = strongSelf.dustNode, !dustNode.isRevealed, let textLayout = strongSelf.textNode.cachedLayout, !textLayout.spoilers.isEmpty, let selectionRange = selectionRange { + for (spoilerRange, _) in textLayout.spoilers { + if let intersection = selectionRange.intersection(spoilerRange), intersection.length > 0 { + dustNode.update(revealed: true) + return + } + } + } + } self.textSelectionNode = textSelectionNode self.addSubnode(textSelectionNode) self.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textNode) textSelectionNode.frame = self.textNode.frame textSelectionNode.highlightAreaNode.frame = self.textNode.frame } - } else if let textSelectionNode = self.textSelectionNode { - self.textSelectionNode = nil - self.updateIsTextSelectionActive?(false) - textSelectionNode.highlightAreaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in - textSelectionNode?.highlightAreaNode.removeFromSupernode() - textSelectionNode?.removeFromSupernode() - }) + } else { + if let textSelectionNode = self.textSelectionNode { + self.textSelectionNode = nil + self.updateIsTextSelectionActive?(false) + textSelectionNode.highlightAreaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in + textSelectionNode?.highlightAreaNode.removeFromSupernode() + textSelectionNode?.removeFromSupernode() + }) + } + + if let dustNode = self.dustNode, dustNode.isRevealed { + dustNode.update(revealed: false) + } } } diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index ba2f818023..1ef0d2ffde 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -17,6 +17,7 @@ import Speak import ObjCRuntimeUtils import AvatarNode import ContextUI +import InvisibleInkDustNode private let accessoryButtonFont = Font.medium(14.0) private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers]) @@ -240,6 +241,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let textInputContainerBackgroundNode: ASImageNode let textInputContainer: ASDisplayNode var textInputNode: EditableTextNode? + var dustNode: InvisibleInkDustNode? let textInputBackgroundNode: ASImageNode private var transparentTextInputBackgroundImage: UIImage? @@ -402,12 +404,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } - textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil) + textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed) textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count) self.updatingInputState = false self.keepSendButtonEnabled = keepSendButtonEnabled self.extendedSearchLayout = extendedSearchLayout self.updateTextNodeText(animated: animated) + self.updateSpoiler() } } @@ -440,6 +443,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private let accessoryButtonSpacing: CGFloat = 0.0 private let accessoryButtonInset: CGFloat = 2.0 + private var spoilersRevealed = false + init(presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) { self.presentationInterfaceState = presentationInterfaceState @@ -896,6 +901,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor.rawValue: textColor] textInputNode.tintColor = tintColor + + self.updateSpoiler() } } @@ -1770,9 +1777,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) - refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed) refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + self.updateSpoiler() + let inputTextState = self.inputTextState self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) }) @@ -1783,6 +1792,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + private func updateSpoiler() { + guard let textInputNode = self.textInputNode else { + return + } + print(textInputNode.attributedText?.description ?? "") + } + private func updateCounterTextNode(transition: ContainedViewLayoutTransition) { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let editMessage = presentationInterfaceState.interfaceState.editMessage, let inputTextMaxLength = editMessage.inputTextMaxLength { let textCount = Int32(textInputNode.textView.text.count) @@ -2203,7 +2219,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } - let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil) + let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed) string.replaceCharacters(in: range, with: cleanReplacementString) self.textInputNode?.attributedText = string self.textInputNode?.selectedRange = NSMakeRange(range.lowerBound + cleanReplacementString.length, 0) diff --git a/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift b/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift index d1f2131210..f1a3b97b39 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift @@ -207,7 +207,7 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } - textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil) + textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: false) textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count) self.updatingInputState = false self.updateTextNodeText(animated: animated) @@ -697,7 +697,7 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) - refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: false) refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) let inputTextState = self.inputTextState @@ -999,7 +999,7 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } - let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil) + let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: false) string.replaceCharacters(in: range, with: cleanReplacementString) self.textInputNode?.attributedText = string self.textInputNode?.selectedRange = NSMakeRange(range.lowerBound + cleanReplacementString.length, 0) diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 14fca54731..597539aaf2 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -47,7 +47,7 @@ public struct ChatTextFontAttributes: OptionSet { public static let blockQuote = ChatTextFontAttributes(rawValue: 1 << 3) } -public func textAttributedStringForStateText(_ stateText: NSAttributedString, fontSize: CGFloat, textColor: UIColor, accentTextColor: UIColor, writingDirection: NSWritingDirection?) -> NSAttributedString { +public func textAttributedStringForStateText(_ stateText: NSAttributedString, fontSize: CGFloat, textColor: UIColor, accentTextColor: UIColor, writingDirection: NSWritingDirection?, spoilersRevealed: Bool) -> NSAttributedString { let result = NSMutableAttributedString(string: stateText.string) let fullRange = NSRange(location: 0, length: result.length) @@ -408,7 +408,7 @@ private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributed } } -public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat) { +public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool) { guard let initialAttributedText = textNode.attributedText, initialAttributedText.length != 0 else { return } @@ -423,14 +423,14 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme var attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(initialAttributedText)) refreshTextMentions(text: text, initialAttributedText: initialAttributedText, attributedText: attributedText, fullRange: fullRange) - var resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection) + var resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed) text = resultAttributedText.string as NSString fullRange = NSRange(location: 0, length: initialAttributedText.length) attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(resultAttributedText)) refreshTextUrls(text: text, initialAttributedText: resultAttributedText, attributedText: attributedText, fullRange: fullRange) - resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection) + resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed) if !resultAttributedText.isEqual(to: initialAttributedText) { textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.font, range: fullRange) @@ -502,7 +502,7 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme } } -public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat) { +public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool = false) { guard let initialAttributedText = textNode.attributedText, initialAttributedText.length != 0 else { return } @@ -515,14 +515,14 @@ public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, th var text: NSString = initialAttributedText.string as NSString var fullRange = NSRange(location: 0, length: initialAttributedText.length) var attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(initialAttributedText)) - var resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection) + var resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed) text = resultAttributedText.string as NSString fullRange = NSRange(location: 0, length: initialAttributedText.length) attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(resultAttributedText)) refreshTextUrls(text: text, initialAttributedText: resultAttributedText, attributedText: attributedText, fullRange: fullRange) - resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection) + resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed) if !resultAttributedText.isEqual(to: initialAttributedText) { textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.font, range: fullRange) diff --git a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift index 9ffa923f97..930eaaf914 100644 --- a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift +++ b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift @@ -196,6 +196,7 @@ public final class TextSelectionNode: ASDisplayNode { private let strings: PresentationStrings private let textNode: TextNode private let updateIsActive: (Bool) -> Void + public var updateRange: ((NSRange?) -> Void)? private let present: (ViewController, Any?) -> Void private weak var rootNode: ASDisplayNode? private let performAction: (NSAttributedString, TextSelectionAction) -> Void @@ -396,6 +397,8 @@ public final class TextSelectionNode: ASDisplayNode { } private func updateSelection(range: NSRange?, animateIn: Bool) { + self.updateRange?(range) + var rects: (rects: [CGRect], start: TextRangeRectEdge, end: TextRangeRectEdge)? if let range = range { @@ -502,18 +505,18 @@ public final class TextSelectionNode: ASDisplayNode { self?.performAction(attributedText, .lookup) self?.dismissSelection() })) -// if #available(iOS 15.0, *) { -// actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuTranslate, accessibilityLabel: self.strings.Conversation_ContextMenuTranslate), action: { [weak self] in -// self?.performAction(attributedText, .translate) -// self?.dismissSelection() -// })) -// } - if isSpeakSelectionEnabled() { - actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuSpeak, accessibilityLabel: self.strings.Conversation_ContextMenuSpeak), action: { [weak self] in - self?.performAction(attributedText, .speak) + if #available(iOS 15.0, *) { + actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuTranslate, accessibilityLabel: self.strings.Conversation_ContextMenuTranslate), action: { [weak self] in + self?.performAction(attributedText, .translate) self?.dismissSelection() })) } +// if isSpeakSelectionEnabled() { +// actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuSpeak, accessibilityLabel: self.strings.Conversation_ContextMenuSpeak), action: { [weak self] in +// self?.performAction(attributedText, .speak) +// self?.dismissSelection() +// })) +// } actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in self?.performAction(attributedText, .share) self?.dismissSelection() From 7021252dc605c95d589af9fc79715f32776c6ad4 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 21 Dec 2021 03:46:43 +0400 Subject: [PATCH 15/35] Reaction improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../Sources/AccountContext.swift | 2 +- .../Sources/ChatController.swift | 11 +- .../ReactionListContextMenuContent.swift | 82 ++++- .../ContextUI/Sources/ContextController.swift | 140 +++++--- .../ContextControllerActionsStackNode.swift | 174 ++++++++- ...tControllerExtractedPresentationNode.swift | 18 +- .../ContextControllerPresentationNode.swift | 3 + .../ContainedViewLayoutTransition.swift | 8 +- .../Sources/Items/ItemListCheckboxItem.swift | 24 +- .../Items/ItemListDisclosureItem.swift | 15 +- .../Sources/ReactionContextNode.swift | 69 +++- .../Sources/ReactionSelectionNode.swift | 14 +- .../Source/Signal_Combine.swift | 6 + submodules/SettingsUI/BUILD | 1 + .../BubbleSettingsController.swift | 8 +- .../ForwardPrivacyChatPreviewItem.swift | 2 +- .../QuickReactionSetupController.swift | 338 ++++++++++++++++++ .../Reactions/ReactionChatPreviewItem.swift | 274 ++++++++++++++ .../InstalledStickerPacksController.swift | 121 ++++++- .../TextSizeSelectionController.swift | 8 +- .../ThemeAccentColorControllerNode.swift | 2 +- .../Themes/ThemePreviewControllerNode.swift | 2 +- .../Themes/ThemeSettingsChatPreviewItem.swift | 2 +- .../Sources/Themes/WallpaperGalleryItem.swift | 4 +- .../Sources/ShareController.swift | 2 +- .../Sources/VoiceChatInfoContextItem.swift | 10 + .../VoiceChatRecordingContextItem.swift | 8 + .../VoiceChatShareScreenContextItem.swift | 8 + .../Sources/VoiceChatVolumeContextItem.swift | 10 + .../Sources/Settings/ReactionSettings.swift | 23 ++ .../Sources/State/AccountViewTracker.swift | 4 +- .../Sources/State/MessageReactions.swift | 12 +- .../SyncCore/SyncCore_Namespaces.swift | 7 + .../Avatar/SampleAvatar1.imageset/Avatar8.pdf | Bin 0 -> 7034 bytes .../SampleAvatar1.imageset/Contents.json | 12 + .../TelegramUI/Sources/ChatController.swift | 4 +- .../Sources/ChatHistoryListNode.swift | 44 ++- .../ChatInterfaceStateContextMenus.swift | 84 ++--- .../ChatMessageActionButtonsNode.swift | 2 +- .../ChatMessageAttachedContentNode.swift | 21 +- .../ChatMessageDateAndStatusNode.swift | 4 +- .../ChatMessageFileBubbleContentNode.swift | 21 +- .../ChatMessageInstantVideoItemNode.swift | 15 +- .../ChatMessageInteractiveFileNode.swift | 204 +++++++---- ...hatMessageThrottledProcessingManager.swift | 20 +- .../ChatRecentActionsHistoryTransition.swift | 84 ++--- .../ChatSendAsPeerListContextItem.swift | 8 + .../ChatSendAsPeerTitleContextItem.swift | 10 + .../Sources/PeerInfo/PeerInfoScreen.swift | 2 +- .../Sources/SharedAccountContext.swift | 4 +- 51 files changed, 1602 insertions(+), 351 deletions(-) create mode 100644 submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift create mode 100644 submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift create mode 100644 submodules/TelegramCore/Sources/Settings/ReactionSettings.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Avatar8.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Contents.json diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 5074a8ff8e..8f90e53fc5 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7149,3 +7149,5 @@ Sorry for the inconvenience."; "Conversation.ContextMenuTranslate" = "Translate"; "ClearCache.ClearDescription" = "All media will stay in the Telegram cloud and can be re-downloaded if you need it again."; + +"ChatSettings.StickersAndReactions" = "Stickers and Emoji"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 3771943896..7065a4f7e4 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -602,7 +602,7 @@ public protocol SharedAccountContext: AnyObject { func makeComposeController(context: AccountContext) -> ViewController func makeChatListController(context: AccountContext, groupId: PeerGroupId, controlsHistoryPreload: Bool, hideNetworkActivityStatus: Bool, previewing: Bool, enableDebugActions: Bool) -> ChatListController func makeChatController(context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, botStart: ChatControllerInitialBotStart?, mode: ChatControllerPresentationMode) -> ChatController - func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)?, backgroundNode: ASDisplayNode?) -> ListViewItem + func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)?, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?) -> ListViewItem func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader func makePeerSharedMediaController(context: AccountContext, peerId: PeerId) -> ViewController? func makeContactSelectionController(_ params: ContactSelectionControllerParams) -> ContactSelectionController diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index ea22dc9a76..c7e32e93fb 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -29,7 +29,7 @@ public final class ChatMessageItemAssociatedData: Equatable { public let availableReactions: AvailableReactions? public let defaultReaction: String? - public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil, isCopyProtectionEnabled: Bool = false, availableReactions: AvailableReactions?) { + public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil, isCopyProtectionEnabled: Bool = false, availableReactions: AvailableReactions?, defaultReaction: String?) { self.automaticDownloadPeerType = automaticDownloadPeerType self.automaticDownloadNetworkType = automaticDownloadNetworkType self.isRecentActions = isRecentActions @@ -42,15 +42,6 @@ public final class ChatMessageItemAssociatedData: Equatable { self.currentlyPlayingMessageId = currentlyPlayingMessageId self.isCopyProtectionEnabled = isCopyProtectionEnabled self.availableReactions = availableReactions - - var defaultReaction: String? - if let availableReactions = availableReactions { - for reaction in availableReactions.reactions { - if reaction.title.lowercased().contains("thumbs up") { - defaultReaction = reaction.value - } - } - } self.defaultReaction = defaultReaction } diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index e6e5740c51..ce1b2c0593 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -371,6 +371,54 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } } + private struct ItemsState { + let listState: EngineMessageReactionListContext.State + let readStats: MessageReadStats? + + let mergedItems: [EngineMessageReactionListContext.Item] + + init(listState: EngineMessageReactionListContext.State, readStats: MessageReadStats?) { + self.listState = listState + self.readStats = readStats + + var mergedItems: [EngineMessageReactionListContext.Item] = listState.items + if !listState.canLoadMore, let readStats = readStats { + var existingPeers = Set(mergedItems.map(\.peer.id)) + for peer in readStats.peers { + if !existingPeers.contains(peer.id) { + existingPeers.insert(peer.id) + mergedItems.append(EngineMessageReactionListContext.Item(peer: peer, reaction: nil)) + } + } + } + self.mergedItems = mergedItems + } + + var totalCount: Int { + if !self.listState.canLoadMore { + return self.mergedItems.count + } else { + var value = self.listState.totalCount + if let readStats = self.readStats { + value = max(value, readStats.peers.count) + } + return value + } + } + + var canLoadMore: Bool { + return self.listState.canLoadMore + } + + func item(at index: Int) -> EngineMessageReactionListContext.Item? { + if index < self.mergedItems.count { + return self.mergedItems[index] + } else { + return nil + } + } + } + private let context: AccountContext private let availableReactions: AvailableReactions? let reaction: String? @@ -388,7 +436,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent private var apparentHeight: CGFloat = 0.0 private let listContext: EngineMessageReactionListContext - private var state: EngineMessageReactionListContext.State + private var state: ItemsState private var stateDisposable: Disposable? private var itemNodes: [Int: ItemNode] = [:] @@ -401,6 +449,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent availableReactions: AvailableReactions?, message: EngineMessage, reaction: String?, + readStats: MessageReadStats?, requestUpdate: @escaping (ReactionsTabNode, ContainedViewLayoutTransition) -> Void, requestUpdateApparentHeight: @escaping (ReactionsTabNode, ContainedViewLayoutTransition) -> Void, openPeer: @escaping (PeerId) -> Void @@ -413,7 +462,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.openPeer = openPeer self.listContext = context.engine.messages.messageReactionList(message: message, reaction: reaction) - self.state = EngineMessageReactionListContext.State(message: message, reaction: reaction) + self.state = ItemsState(listState: EngineMessageReactionListContext.State(message: message, reaction: reaction), readStats: readStats) self.scrollNode = ASScrollNode() self.scrollNode.canCancelAllTouchesInViews = true @@ -436,11 +485,12 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent guard let strongSelf = self else { return } + let updatedState = ItemsState(listState: state, readStats: strongSelf.state.readStats) var animateIn = false - if strongSelf.state.items.isEmpty && !state.items.isEmpty { + if strongSelf.state.item(at: 0) == nil && updatedState.item(at: 0) != nil { animateIn = true } - strongSelf.state = state + strongSelf.state = updatedState strongSelf.requestUpdate(strongSelf, animateIn ? .animated(duration: 0.2, curve: .easeInOut) : .immediate) if animateIn { for (_, itemNode) in strongSelf.itemNodes { @@ -492,7 +542,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent for index in minVisibleIndex ... maxVisibleIndex { let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: CGFloat(index) * itemHeight), size: CGSize(width: size.width, height: itemHeight)) - if index < self.state.items.count { + if let item = self.state.item(at: index) { validIds.insert(index) let itemNode: ItemNode @@ -500,7 +550,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent itemNode = current } else { let openPeer = self.openPeer - let peerId = self.state.items[index].peer.id + let peerId = item.peer.id itemNode = ItemNode(context: self.context, availableReactions: self.availableReactions, action: { openPeer(peerId) }) @@ -508,7 +558,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent 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.update(size: itemFrame.size, presentationData: presentationData, item: item, isLast: self.state.item(at: index + 1) == nil, syncronousLoad: syncronousLoad) itemNode.frame = itemFrame } else if index < self.state.totalCount { validPlaceholderIds.insert(index) @@ -558,7 +608,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.placeholderLayers.removeValue(forKey: id) } - if self.state.canLoadMore && maxVisibleIndex >= self.state.items.count - 16 { + if self.state.canLoadMore && maxVisibleIndex >= self.state.listState.items.count - 16 { self.listContext.loadMore() } } @@ -653,6 +703,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent availableReactions: AvailableReactions?, message: EngineMessage, reaction: String?, + readStats: MessageReadStats?, requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void, back: (() -> Void)?, @@ -701,6 +752,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent availableReactions: availableReactions, message: message, reaction: reaction, + readStats: readStats, requestUpdate: { tab, transition in requestUpdateTab?(tab, transition) }, @@ -740,6 +792,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent availableReactions: availableReactions, message: message, reaction: reaction, + readStats: nil, requestUpdate: { tab, transition in requestUpdateTab?(tab, transition) }, @@ -833,14 +886,24 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let availableReactions: AvailableReactions? let message: EngineMessage let reaction: String? + let readStats: MessageReadStats? let back: (() -> Void)? let openPeer: (PeerId) -> Void - public init(context: AccountContext, availableReactions: AvailableReactions?, message: EngineMessage, reaction: String?, back: (() -> Void)?, openPeer: @escaping (PeerId) -> Void) { + public init( + context: AccountContext, + availableReactions: AvailableReactions?, + message: EngineMessage, + reaction: String?, + readStats: MessageReadStats?, + back: (() -> Void)?, + openPeer: @escaping (PeerId) -> Void + ) { self.context = context self.availableReactions = availableReactions self.message = message self.reaction = reaction + self.readStats = readStats self.back = back self.openPeer = openPeer } @@ -854,6 +917,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent availableReactions: self.availableReactions, message: self.message, reaction: self.reaction, + readStats: self.readStats, requestUpdate: requestUpdate, requestUpdateApparentHeight: requestUpdateApparentHeight, back: self.back, diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 481ae15f1e..47769179ca 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -148,6 +148,10 @@ public final class ContextMenuActionItem { public protocol ContextMenuCustomNode: ASDisplayNode { func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) func updateTheme(presentationData: PresentationData) + + func canBeHighlighted() -> Bool + func updateIsHighlighted(isHighlighted: Bool) + func performAction() } public protocol ContextMenuCustomItem { @@ -355,23 +359,28 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } if strongSelf.didMoveFromInitialGesturePoint { - let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view) - let actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint) - if strongSelf.highlightedActionNode !== actionNode { - strongSelf.highlightedActionNode?.setIsHighlighted(false) - strongSelf.highlightedActionNode = actionNode - if let actionNode = actionNode { - actionNode.setIsHighlighted(true) - strongSelf.hapticFeedback.tap() + if let presentationNode = strongSelf.presentationNode { + let presentationPoint = strongSelf.view.convert(localPoint, to: presentationNode.view) + presentationNode.highlightGestureMoved(location: presentationPoint) + } else { + let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view) + let actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint) + if strongSelf.highlightedActionNode !== actionNode { + strongSelf.highlightedActionNode?.setIsHighlighted(false) + strongSelf.highlightedActionNode = actionNode + if let actionNode = actionNode { + actionNode.setIsHighlighted(true) + strongSelf.hapticFeedback.tap() + } } - } - - if let reactionContextNode = strongSelf.reactionContextNode { - let reactionPoint = strongSelf.view.convert(localPoint, to: reactionContextNode.view) - let highlightedReaction = reactionContextNode.reaction(at: reactionPoint)?.reaction - if strongSelf.highlightedReaction?.rawValue != highlightedReaction?.rawValue { - strongSelf.highlightedReaction = highlightedReaction - strongSelf.hapticFeedback.tap() + + if let reactionContextNode = strongSelf.reactionContextNode { + let reactionPoint = strongSelf.view.convert(localPoint, to: reactionContextNode.view) + let highlightedReaction = reactionContextNode.reaction(at: reactionPoint)?.reaction + if strongSelf.highlightedReaction?.rawValue != highlightedReaction?.rawValue { + strongSelf.highlightedReaction = highlightedReaction + strongSelf.hapticFeedback.tap() + } } } } @@ -383,18 +392,22 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } recognizer.externalUpdated = nil if strongSelf.didMoveFromInitialGesturePoint { - if let (_, _) = viewAndPoint { - if let highlightedActionNode = strongSelf.highlightedActionNode { - strongSelf.highlightedActionNode = nil - highlightedActionNode.performAction() - } - if let highlightedReaction = strongSelf.highlightedReaction { - strongSelf.reactionContextNode?.performReactionSelection(reaction: highlightedReaction) - } + if let presentationNode = strongSelf.presentationNode { + presentationNode.highlightGestureFinished(performAction: viewAndPoint != nil) } else { - if let highlightedActionNode = strongSelf.highlightedActionNode { - strongSelf.highlightedActionNode = nil - highlightedActionNode.setIsHighlighted(false) + if let (_, _) = viewAndPoint { + if let highlightedActionNode = strongSelf.highlightedActionNode { + strongSelf.highlightedActionNode = nil + highlightedActionNode.performAction() + } + if let highlightedReaction = strongSelf.highlightedReaction { + strongSelf.reactionContextNode?.performReactionSelection(reaction: highlightedReaction) + } + } else { + if let highlightedActionNode = strongSelf.highlightedActionNode { + strongSelf.highlightedActionNode = nil + highlightedActionNode.setIsHighlighted(false) + } } } } @@ -420,27 +433,32 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } if strongSelf.didMoveFromInitialGesturePoint { - let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view) - var actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint) - if let actionNodeValue = actionNode, !actionNodeValue.isActionEnabled { - actionNode = nil - } - - if strongSelf.highlightedActionNode !== actionNode { - strongSelf.highlightedActionNode?.setIsHighlighted(false) - strongSelf.highlightedActionNode = actionNode - if let actionNode = actionNode { - actionNode.setIsHighlighted(true) - strongSelf.hapticFeedback.tap() + if let presentationNode = strongSelf.presentationNode { + let presentationPoint = strongSelf.view.convert(localPoint, to: presentationNode.view) + presentationNode.highlightGestureMoved(location: presentationPoint) + } else { + let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view) + var actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint) + if let actionNodeValue = actionNode, !actionNodeValue.isActionEnabled { + actionNode = nil } - } - - if let reactionContextNode = strongSelf.reactionContextNode { - let reactionPoint = strongSelf.view.convert(localPoint, to: reactionContextNode.view) - let highlightedReaction = reactionContextNode.reaction(at: reactionPoint)?.reaction - if strongSelf.highlightedReaction?.rawValue != highlightedReaction?.rawValue { - strongSelf.highlightedReaction = highlightedReaction - strongSelf.hapticFeedback.tap() + + if strongSelf.highlightedActionNode !== actionNode { + strongSelf.highlightedActionNode?.setIsHighlighted(false) + strongSelf.highlightedActionNode = actionNode + if let actionNode = actionNode { + actionNode.setIsHighlighted(true) + strongSelf.hapticFeedback.tap() + } + } + + if let reactionContextNode = strongSelf.reactionContextNode { + let reactionPoint = strongSelf.view.convert(localPoint, to: reactionContextNode.view) + let highlightedReaction = reactionContextNode.reaction(at: reactionPoint)?.reaction + if strongSelf.highlightedReaction?.rawValue != highlightedReaction?.rawValue { + strongSelf.highlightedReaction = highlightedReaction + strongSelf.hapticFeedback.tap() + } } } } @@ -452,19 +470,23 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } gesture.externalUpdated = nil if strongSelf.didMoveFromInitialGesturePoint { - if let (_, _) = viewAndPoint { - if let highlightedActionNode = strongSelf.highlightedActionNode { - strongSelf.highlightedActionNode = nil - highlightedActionNode.performAction() - } - - if let highlightedReaction = strongSelf.highlightedReaction { - strongSelf.reactionContextNode?.performReactionSelection(reaction: highlightedReaction) - } + if let presentationNode = strongSelf.presentationNode { + presentationNode.highlightGestureFinished(performAction: viewAndPoint != nil) } else { - if let highlightedActionNode = strongSelf.highlightedActionNode { - strongSelf.highlightedActionNode = nil - highlightedActionNode.setIsHighlighted(false) + if let (_, _) = viewAndPoint { + if let highlightedActionNode = strongSelf.highlightedActionNode { + strongSelf.highlightedActionNode = nil + highlightedActionNode.performAction() + } + + if let highlightedReaction = strongSelf.highlightedReaction { + strongSelf.reactionContextNode?.performReactionSelection(reaction: highlightedReaction) + } + } else { + if let highlightedActionNode = strongSelf.highlightedActionNode { + strongSelf.highlightedActionNode = nil + highlightedActionNode.setIsHighlighted(false) + } } } } diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 13d67fbeba..48ba044a90 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -16,6 +16,9 @@ public protocol ContextControllerActionsStackItemNode: ASDisplayNode { standardWidth: CGFloat, transition: ContainedViewLayoutTransition ) -> (size: CGSize, apparentHeight: CGFloat) + + func highlightGestureMoved(location: CGPoint) + func highlightGestureFinished(performAction: Bool) } public protocol ContextControllerActionsStackItem: AnyObject { @@ -31,6 +34,10 @@ public protocol ContextControllerActionsStackItem: AnyObject { protocol ContextControllerActionsListItemNode: ASDisplayNode { func update(presentationData: PresentationData, constrainedSize: CGSize) -> (minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void) + + func canBeHighlighted() -> Bool + func updateIsHighlighted(isHighlighted: Bool) + func performAction() } private final class ContextControllerActionsListActionItemNode: HighlightTrackingButtonNode, ContextControllerActionsListItemNode { @@ -44,6 +51,8 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin private let subtitleNode: ImmediateTextNode private let iconNode: ASImageNode + private var iconDisposable: Disposable? + init( getController: @escaping () -> ContextControllerProtocol?, requestDismiss: @escaping (ContextMenuActionResult) -> Void, @@ -93,6 +102,10 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) } + deinit { + self.iconDisposable?.dispose() + } + @objc private func pressed() { guard let controller = self.getController() else { return @@ -115,6 +128,18 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin )) } + func canBeHighlighted() -> Bool { + return true + } + + func updateIsHighlighted(isHighlighted: Bool) { + self.highlightBackgroundNode.alpha = isHighlighted ? 1.0 : 0.0 + } + + func performAction() { + self.pressed() + } + func update(presentationData: PresentationData, constrainedSize: CGSize) -> (minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void) { let sideInset: CGFloat = 16.0 let verticalInset: CGFloat = 11.0 @@ -170,12 +195,29 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin ) } - let iconImage = self.iconNode.image ?? self.item.icon(presentationData.theme) + let iconSize: CGSize? + if let iconSource = self.item.iconSource { + iconSize = iconSource.size + if self.iconDisposable == nil { + self.iconDisposable = (iconSource.signal |> deliverOnMainQueue).start(next: { [weak self] image in + guard let strongSelf = self else { + return + } + strongSelf.iconNode.image = image + }) + } + } else if let image = self.iconNode.image { + iconSize = image.size + } else { + let iconImage = self.item.icon(presentationData.theme) + self.iconNode.image = iconImage + iconSize = iconImage?.size + } var maxTextWidth: CGFloat = constrainedSize.width maxTextWidth -= sideInset - if let iconImage = iconImage { - maxTextWidth -= max(standardIconWidth, iconImage.size.width) + if let iconSize = iconSize { + maxTextWidth -= max(standardIconWidth, iconSize.width) } else { maxTextWidth -= sideInset } @@ -187,8 +229,8 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin var minSize = CGSize() minSize.width += sideInset minSize.width += max(titleSize.width, subtitleSize.width) - if let iconImage = iconImage { - minSize.width += max(standardIconWidth, iconImage.size.width) + if let iconSize = iconSize { + minSize.width += max(standardIconWidth, iconSize.width) minSize.width += iconSideInset } else { minSize.width += sideInset @@ -208,12 +250,9 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin transition.updateFrameAdditive(node: self.titleLabelNode, frame: titleFrame) transition.updateFrameAdditive(node: self.subtitleNode, frame: subtitleFrame) - if let iconImage = iconImage { - if self.iconNode.image !== iconImage { - self.iconNode.image = iconImage - } - let iconWidth = max(standardIconWidth, iconImage.size.width) - let iconFrame = CGRect(origin: CGPoint(x: size.width - iconSideInset - iconWidth + floor((iconWidth - iconImage.size.width) / 2.0), y: floor((size.height - iconImage.size.height) / 2.0)), size: iconImage.size) + if let iconSize = iconSize { + let iconWidth = max(standardIconWidth, iconSize.width) + let iconFrame = CGRect(origin: CGPoint(x: size.width - iconSideInset - iconWidth + floor((iconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize) transition.updateFrame(node: self.iconNode, frame: iconFrame) } }) @@ -221,6 +260,16 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin } private final class ContextControllerActionsListSeparatorItemNode: ASDisplayNode, ContextControllerActionsListItemNode { + func canBeHighlighted() -> Bool { + return false + } + + func updateIsHighlighted(isHighlighted: Bool) { + } + + func performAction() { + } + override init() { super.init() } @@ -233,6 +282,26 @@ private final class ContextControllerActionsListSeparatorItemNode: ASDisplayNode } private final class ContextControllerActionsListCustomItemNode: ASDisplayNode, ContextControllerActionsListItemNode { + func canBeHighlighted() -> Bool { + if let itemNode = self.itemNode { + return itemNode.canBeHighlighted() + } else { + return false + } + } + + func updateIsHighlighted(isHighlighted: Bool) { + if let itemNode = self.itemNode { + itemNode.updateIsHighlighted(isHighlighted: isHighlighted) + } + } + + func performAction() { + if let itemNode = self.itemNode { + itemNode.performAction() + } + } + private let getController: () -> ContextControllerProtocol? private let item: ContextMenuCustomItem @@ -297,6 +366,9 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack private var items: [ContextMenuItem] private var itemNodes: [Item] + private var hapticFeedback: HapticFeedback? + private var highlightedItemNode: Item? + init( getController: @escaping () -> ContextControllerProtocol?, requestDismiss: @escaping (ContextMenuActionResult) -> Void, @@ -445,6 +517,39 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack return (combinedSize, combinedSize.height) } + + func highlightGestureMoved(location: CGPoint) { + var highlightedItemNode: Item? + for itemNode in self.itemNodes { + if itemNode.node.frame.contains(location) { + if itemNode.node.canBeHighlighted() { + highlightedItemNode = itemNode + } + break + } + } + if self.highlightedItemNode !== highlightedItemNode { + self.highlightedItemNode?.node.updateIsHighlighted(isHighlighted: false) + highlightedItemNode?.node.updateIsHighlighted(isHighlighted: true) + + self.highlightedItemNode = highlightedItemNode + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + self.hapticFeedback?.tap() + } + } + + func highlightGestureFinished(performAction: Bool) { + if let highlightedItemNode = self.highlightedItemNode { + self.highlightedItemNode = nil + if performAction { + highlightedItemNode.node.performAction() + } else { + highlightedItemNode.node.updateIsHighlighted(isHighlighted: false) + } + } + } } private let items: [ContextMenuItem] @@ -513,6 +618,12 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta return (contentLayout.cleanSize, contentLayout.apparentHeight) } + + func highlightGestureMoved(location: CGPoint) { + } + + func highlightGestureFinished(performAction: Bool) { + } } private let content: ContextControllerItemsContent @@ -643,6 +754,8 @@ final class ContextControllerActionsStackNode: ASDisplayNode { super.init() + self.clipsToBounds = true + self.addSubnode(self.node) self.addSubnode(self.dimNode) } @@ -665,7 +778,8 @@ final class ContextControllerActionsStackNode: ASDisplayNode { 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)) + let transitionOffset = (1.0 - transitionFraction) * size.width / 2.0 + transition.updatePosition(node: self.node, position: CGPoint(x: size.width / 2.0 + scaleOffset / 2.0 + transitionOffset, 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) @@ -678,6 +792,14 @@ final class ContextControllerActionsStackNode: ASDisplayNode { transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: size)) transition.updateAlpha(node: self.dimNode, alpha: 1.0 - transitionFraction) } + + func highlightGestureMoved(location: CGPoint) { + self.node.highlightGestureMoved(location: self.view.convert(location, to: self.node.view)) + } + + func highlightGestureFinished(performAction: Bool) { + self.node.highlightGestureFinished(performAction: performAction) + } } private let getController: () -> ContextControllerProtocol? @@ -886,9 +1008,13 @@ final class ContextControllerActionsStackNode: ASDisplayNode { if itemLayouts[i].transitionFraction < 0.0 { xOffset = itemLayouts[i].transitionFraction * itemLayouts[i].size.width } else { - xOffset = itemLayouts[i].transitionFraction * topItemWidth + if i != 0 { + xOffset = itemLayouts[i].transitionFraction * itemLayouts[i - 1].size.width + } else { + xOffset = itemLayouts[i].transitionFraction * topItemWidth + } } - let itemFrame = CGRect(origin: CGPoint(x: xOffset, y: 0.0), size: itemLayouts[i].size) + let itemFrame = CGRect(origin: CGPoint(x: xOffset, y: 0.0), size: CGSize(width: itemLayouts[i].size.width, height: navigationContainerFrame.height)) itemLayouts[i].itemTransition.updateFrame(node: self.itemContainers[i], frame: itemFrame) if itemLayouts[i].animateAppearingContainer { @@ -899,7 +1025,13 @@ final class ContextControllerActionsStackNode: ASDisplayNode { } 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 + var position = itemContainer.position + if isPopped { + position.x = itemContainer.bounds.width / 2.0 + topItemWidth + } else { + position.x = itemContainer.bounds.width / 2.0 - topItemWidth + } + transition.updatePosition(node: itemContainer, position: position, completion: { [weak itemContainer] _ in itemContainer?.removeFromSupernode() }) } @@ -907,4 +1039,16 @@ final class ContextControllerActionsStackNode: ASDisplayNode { return CGSize(width: topItemWidth, height: topItemSize.height) } + + func highlightGestureMoved(location: CGPoint) { + if let topItemContainer = self.itemContainers.last { + topItemContainer.highlightGestureMoved(location: self.view.convert(location, to: topItemContainer.view)) + } + } + + func highlightGestureFinished(performAction: Bool) { + if let topItemContainer = self.itemContainers.last { + topItemContainer.highlightGestureFinished(performAction: performAction) + } + } } diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index efa227ca95..00f2486bd3 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -157,6 +157,22 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } } + func highlightGestureMoved(location: CGPoint) { + self.actionsStackNode.highlightGestureMoved(location: self.view.convert(location, to: self.actionsStackNode.view)) + + if let reactionContextNode = self.reactionContextNode { + reactionContextNode.highlightGestureMoved(location: self.view.convert(location, to: reactionContextNode.view)) + } + } + + func highlightGestureFinished(performAction: Bool) { + self.actionsStackNode.highlightGestureFinished(performAction: performAction) + + if let reactionContextNode = self.reactionContextNode { + reactionContextNode.highlightGestureFinished(performAction: performAction) + } + } + func replaceItems(items: ContextController.Items, animated: Bool) { self.actionsStackNode.replace(item: makeContextControllerActionsStackItem(items: items), animated: animated) } @@ -471,7 +487,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo animatingOutState.currentContentScreenFrame = updatedContentScreenFrame }*/ } else { - //strongSelf.requestUpdate(animation.transition) + strongSelf.requestUpdate(animation.transition) /*let updatedContentScreenFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: strongSelf.view) if let storedGlobalFrame = contentNode.storedGlobalFrame { diff --git a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift index a05fef8cb9..a45bdcfaac 100644 --- a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift @@ -25,4 +25,7 @@ protocol ContextControllerPresentationNode: ASDisplayNode { ) func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) + + func highlightGestureMoved(location: CGPoint) + func highlightGestureFinished(performAction: Bool) } diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 0dddbade98..2a4b4e5d7c 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -105,7 +105,7 @@ public extension CGRect { } public extension ContainedViewLayoutTransition { - func updateFrame(node: ASDisplayNode, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { + func updateFrame(node: ASDisplayNode, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = true, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { if frame.origin.x.isNaN { return } @@ -157,7 +157,7 @@ public extension ContainedViewLayoutTransition { } } - func updateFrameAsPositionAndBounds(node: ASDisplayNode, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { + func updateFrameAsPositionAndBounds(node: ASDisplayNode, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = true, completion: ((Bool) -> Void)? = nil) { if node.frame.equalTo(frame) && !force { completion?(true) } else { @@ -190,7 +190,7 @@ public extension ContainedViewLayoutTransition { } } - func updateFrameAsPositionAndBounds(layer: CALayer, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { + func updateFrameAsPositionAndBounds(layer: CALayer, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = true, completion: ((Bool) -> Void)? = nil) { if layer.frame.equalTo(frame) && !force { completion?(true) } else { @@ -305,7 +305,7 @@ public extension ContainedViewLayoutTransition { } } - func updatePosition(node: ASDisplayNode, position: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { + func updatePosition(node: ASDisplayNode, position: CGPoint, beginWithCurrentState: Bool = true, completion: ((Bool) -> Void)? = nil) { if node.position.equalTo(position) { completion?(true) } else { diff --git a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift index ab833042ec..305d26d051 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift @@ -17,6 +17,8 @@ public enum ItemListCheckboxItemColor { public class ItemListCheckboxItem: ListViewItem, ItemListItem { let presentationData: ItemListPresentationData + let icon: UIImage? + let iconSize: CGSize? let title: String let style: ItemListCheckboxItemStyle let color: ItemListCheckboxItemColor @@ -25,8 +27,10 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem { public let sectionId: ItemListSectionId let action: () -> Void - public init(presentationData: ItemListPresentationData, title: String, style: ItemListCheckboxItemStyle, color: ItemListCheckboxItemColor = .accent, checked: Bool, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { + public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, iconSize: CGSize? = nil, title: String, style: ItemListCheckboxItemStyle, color: ItemListCheckboxItemColor = .accent, checked: Bool, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { self.presentationData = presentationData + self.icon = icon + self.iconSize = iconSize self.title = title self.style = style self.color = color @@ -86,6 +90,7 @@ public class ItemListCheckboxItemNode: ListViewItemNode { private let activateArea: AccessibilityAreaNode + private let imageNode: ASImageNode private let iconNode: ASImageNode private let titleNode: TextNode @@ -103,6 +108,11 @@ public class ItemListCheckboxItemNode: ListViewItemNode { self.maskNode = ASImageNode() + self.imageNode = ASImageNode() + self.imageNode.isLayerBacked = true + self.imageNode.displayWithoutProcessing = true + self.imageNode.displaysAsynchronously = false + self.iconNode = ASImageNode() self.iconNode.isLayerBacked = true self.iconNode.displayWithoutProcessing = true @@ -120,6 +130,7 @@ public class ItemListCheckboxItemNode: ListViewItemNode { super.init(layerBacked: false, dynamicBounce: false) + self.addSubnode(self.imageNode) self.addSubnode(self.iconNode) self.addSubnode(self.titleNode) self.addSubnode(self.activateArea) @@ -145,6 +156,11 @@ public class ItemListCheckboxItemNode: ListViewItemNode { leftInset += 16.0 } + let iconInset: CGFloat = 44.0 + if item.icon != nil { + leftInset += iconInset + } + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) 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 - leftInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -253,6 +269,12 @@ public class ItemListCheckboxItemNode: ListViewItemNode { strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) + if let icon = item.icon { + let iconSize = item.iconSize ?? icon.size + strongSelf.imageNode.image = icon + strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - iconSize.width) / 2.0), y: floor((layout.contentSize.height - iconSize.height) / 2.0)), size: iconSize) + } + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel)) } }) diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index cdda64ab67..2857b9f912 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -23,6 +23,7 @@ public enum ItemListDisclosureLabelStyle { case multilineDetailText case badge(UIColor) case color(UIColor) + case image(image: UIImage, size: CGSize) } public class ItemListDisclosureItem: ListViewItem, ItemListItem { @@ -234,6 +235,9 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { updatedLabelImage = generateFilledCircleImage(diameter: 17.0, color: color) } } + if case let .image(image, _) = item.labelStyle { + updatedLabelImage = image + } let badgeDiameter: CGFloat = 20.0 if currentItem?.presentationData.theme !== item.presentationData.theme { @@ -468,7 +472,16 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { } strongSelf.labelNode.frame = labelFrame - if case .color = item.labelStyle { + if case let .image(_, size) = item.labelStyle { + if let updatedLabelImage = updatedLabelImage { + strongSelf.labelImageNode.image = updatedLabelImage + } + if strongSelf.labelImageNode.supernode == nil { + strongSelf.addSubnode(strongSelf.labelImageNode) + } + + strongSelf.labelImageNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - size.width - 30.0, y: floor((layout.contentSize.height - size.height) / 2.0)), size: size) + } else if case .color = item.labelStyle { if let updatedLabelImage = updatedLabelImage { strongSelf.labelImageNode.image = updatedLabelImage } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 7686ab1d2d..1776369416 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -117,7 +117,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { public var reactionSelected: ((ReactionContextItem) -> Void)? - private let hapticFeedback = HapticFeedback() + private var hapticFeedback: HapticFeedback? public init(context: AccountContext, theme: PresentationTheme, items: [ReactionContextItem]) { self.theme = theme @@ -239,12 +239,12 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { contentSize.width = max(52.0, contentSize.width) contentSize.height = 52.0 - let sideInset: CGFloat = 12.0 + let sideInset: CGFloat = 11.0 let backgroundOffset: CGPoint = CGPoint(x: 22.0, y: -7.0) var rect: CGRect let isLeftAligned: Bool - if anchorRect.maxX < containerSize.width - backgroundOffset.x - sideInset { + if anchorRect.minX < containerSize.width - anchorRect.maxX { rect = CGRect(origin: CGPoint(x: anchorRect.maxX - contentSize.width + backgroundOffset.x, y: anchorRect.minY - contentSize.height + backgroundOffset.y), size: contentSize) isLeftAligned = true } else { @@ -306,7 +306,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private func updateLayout(size: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, transition: ContainedViewLayoutTransition, animateInFromAnchorRect: CGRect?, animateOutToAnchorRect: CGRect?, animateReactionHighlight: Bool = false) { self.validLayout = (size, insets, anchorRect) - let sideInset: CGFloat = 14.0 + let sideInset: CGFloat = 11.0 let itemSpacing: CGFloat = 9.0 let itemSize: CGFloat = 40.0 let shadowBlur: CGFloat = 5.0 @@ -326,7 +326,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { backgroundInsets.left += sideInset backgroundInsets.right += sideInset - let (backgroundFrame, isLeftAligned, cloudSourcePoint) = self.calculateBackgroundFrame(containerSize: CGSize(width: size.width - sideInset * 2.0, height: size.height), insets: backgroundInsets, anchorRect: anchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight)) + let (backgroundFrame, isLeftAligned, cloudSourcePoint) = self.calculateBackgroundFrame(containerSize: CGSize(width: size.width, height: size.height), insets: backgroundInsets, anchorRect: anchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight)) self.isLeftAligned = isLeftAligned transition.updateFrame(node: self.contentContainer, frame: backgroundFrame) @@ -340,10 +340,13 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { let itemOffsetY: CGFloat = -1.0 - let itemFrame = CGRect(origin: CGPoint(x: sideInset + column * (itemSize + itemSpacing), y: verticalInset + floor((rowHeight - itemSize) / 2.0) + itemOffsetY), size: CGSize(width: itemSize, height: itemSize)) + var itemFrame = CGRect(origin: CGPoint(x: sideInset + column * (itemSize + itemSpacing), y: verticalInset + floor((rowHeight - itemSize) / 2.0) + itemOffsetY), size: CGSize(width: itemSize, height: itemSize)) + if self.highlightedReaction == self.items[i].reaction { + itemFrame = itemFrame.insetBy(dx: -6.0, dy: -6.0) + } if !self.itemNodes[i].isExtracted { transition.updateFrame(node: self.itemNodes[i], frame: itemFrame, beginWithCurrentState: true) - self.itemNodes[i].updateLayout(size: CGSize(width: itemSize, height: itemSize), isExpanded: false, transition: transition) + self.itemNodes[i].updateLayout(size: itemFrame.size, isExpanded: false, transition: transition) } } @@ -465,6 +468,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { return } + let sourceFrame = itemNode.view.convert(itemNode.bounds, to: self.view) let targetFrame = self.view.convert(targetView.convert(targetView.bounds, to: nil), from: nil) targetSnapshotView.frame = targetFrame @@ -479,11 +483,12 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } let targetPosition = targetFrame.center - let _ = targetPosition let duration: Double = 0.16 itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.9, removeOnCompletion: false) + itemNode.layer.animatePosition(from: itemNode.layer.position, to: targetPosition, duration: duration, removeOnCompletion: false) targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.8) + targetSnapshotView.layer.animatePosition(from: sourceFrame.center, to: targetPosition, duration: duration, removeOnCompletion: false) targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 1.0, duration: duration, removeOnCompletion: false, completion: { [weak self, weak targetSnapshotView] _ in if let _ = self { //strongSelf.hapticFeedback.tap() @@ -534,7 +539,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { let expandedScale: CGFloat = 3.0 let expandedSize = CGSize(width: floor(selfSourceRect.width * expandedScale), height: floor(selfSourceRect.height * expandedScale)) - let expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize) + var expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize) + if expandedFrame.minX < -floor(expandedFrame.width * 0.05) { + expandedFrame.origin.x = -floor(expandedFrame.width * 0.05) + } let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear) @@ -604,6 +612,34 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } + public func highlightGestureMoved(location: CGPoint) { + let highlightedReaction = self.reaction(at: location)?.reaction + if self.highlightedReaction != highlightedReaction { + self.highlightedReaction = highlightedReaction + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + self.hapticFeedback?.tap() + + if let (size, insets, anchorRect) = self.validLayout { + self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: .animated(duration: 0.18, curve: .easeInOut), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true) + } + } + } + + public func highlightGestureFinished(performAction: Bool) { + if let highlightedReaction = self.highlightedReaction { + self.highlightedReaction = nil + if performAction { + self.performReactionSelection(reaction: highlightedReaction) + } else { + if let (size, insets, anchorRect) = self.validLayout { + self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: .animated(duration: 0.18, curve: .easeInOut), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true) + } + } + } + } + public func reaction(at point: CGPoint) -> ReactionContextItem? { for i in 0 ..< 2 { let touchInset: CGFloat = i == 0 ? 0.0 : 8.0 @@ -671,7 +707,10 @@ public final class StandaloneReactionAnimation: ASDisplayNode { let expandedScale: CGFloat = 3.0 let expandedSize = CGSize(width: floor(sourceItemSize * expandedScale), height: floor(sourceItemSize * expandedScale)) - let expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize) + var expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize) + if expandedFrame.minX < -floor(expandedFrame.width * 0.05) { + expandedFrame.origin.x = -floor(expandedFrame.width * 0.05) + } sourceSnapshotView.frame = selfTargetRect self.view.addSubview(sourceSnapshotView) @@ -734,6 +773,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { return } + let sourceFrame = itemNode.view.convert(itemNode.bounds, to: self.view) let targetFrame = self.view.convert(targetView.convert(targetView.bounds, to: nil), from: nil) targetSnapshotView.frame = targetFrame @@ -748,15 +788,16 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } let targetPosition = targetFrame.center - let _ = targetPosition let duration: Double = 0.16 itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.9, removeOnCompletion: false) + itemNode.layer.animatePosition(from: itemNode.layer.position, to: targetPosition, duration: duration, removeOnCompletion: false) targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.8) - targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 1.0, duration: duration, removeOnCompletion: false, completion: { [weak self, weak targetSnapshotView] _ in - if let strongSelf = self { + targetSnapshotView.layer.animatePosition(from: sourceFrame.center, to: targetPosition, duration: duration, removeOnCompletion: false) + targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 1.0, duration: duration, removeOnCompletion: false, completion: { [weak targetSnapshotView] _ in + /*if let strongSelf = self { strongSelf.hapticFeedback.tap() - } + }*/ completedTarget = true intermediateCompletion() diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index b6644e6dc7..f6c6f2a719 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -131,9 +131,11 @@ final class ReactionNode: ASDisplayNode { if self.validSize != size { self.validSize = size - self.staticImageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, file: item.stillAnimation, small: false, size: CGSize(width: animationDisplaySize.width * UIScreenScale, height: animationDisplaySize.height * UIScreenScale), fitzModifier: nil, fetched: false, onlyFullSize: false, thumbnail: false, synchronousLoad: false)) - let imageApply = self.staticImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: animationDisplaySize, boundingSize: animationDisplaySize, intrinsicInsets: UIEdgeInsets())) - imageApply() + if !self.staticImageNode.isHidden { + self.staticImageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, file: item.stillAnimation, small: false, size: CGSize(width: animationDisplaySize.width * UIScreenScale, height: animationDisplaySize.height * UIScreenScale), fitzModifier: nil, fetched: false, onlyFullSize: false, thumbnail: false, synchronousLoad: false)) + let imageApply = self.staticImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: animationDisplaySize, boundingSize: animationDisplaySize, intrinsicInsets: UIEdgeInsets())) + imageApply() + } transition.updateFrame(node: self.staticImageNode, frame: animationFrame) } @@ -141,15 +143,15 @@ final class ReactionNode: ASDisplayNode { if self.animationNode == nil { self.didSetupStillAnimation = true - self.stillAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .loop, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id))) + self.stillAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource), width: Int(animationDisplaySize.width * 2.5), height: Int(animationDisplaySize.height * 2.5), playbackMode: .loop, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id))) self.stillAnimationNode.position = animationFrame.center self.stillAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size) self.stillAnimationNode.updateLayout(size: animationFrame.size) self.stillAnimationNode.visibility = true } } else { - transition.updatePosition(node: self.stillAnimationNode, position: animationFrame.center) - transition.updateTransformScale(node: self.stillAnimationNode, scale: animationFrame.size.width / self.stillAnimationNode.bounds.width) + transition.updatePosition(node: self.stillAnimationNode, position: animationFrame.center, beginWithCurrentState: true) + transition.updateTransformScale(node: self.stillAnimationNode, scale: animationFrame.size.width / self.stillAnimationNode.bounds.width, beginWithCurrentState: true) } } diff --git a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift index 54590b1f19..6f5db68462 100644 --- a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift +++ b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift @@ -178,6 +178,12 @@ public func combineLatest(queue: Queue? = nil, _ s1: Signal, _ s2: Signal, _ s3: Signal, _ s4: Signal, _ s5: Signal, _ s6: Signal, _ s7: Signal, _ s8: Signal, _ s9: Signal, _ s10: Signal, _ s11: Signal, _ s12: Signal, _ s13: Signal, _ s14: Signal, _ s15: Signal, _ s16: Signal) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16), E> { + return combineLatestAny([signalOfAny(s1), signalOfAny(s2), signalOfAny(s3), signalOfAny(s4), signalOfAny(s5), signalOfAny(s6), signalOfAny(s7), signalOfAny(s8), signalOfAny(s9), signalOfAny(s10), signalOfAny(s11), signalOfAny(s12), signalOfAny(s13), signalOfAny(s14), signalOfAny(s15), signalOfAny(s16)], combine: { values in + return (values[0] as! T1, values[1] as! T2, values[2] as! T3, values[3] as! T4, values[4] as! T5, values[5] as! T6, values[6] as! T7, values[7] as! T8, values[8] as! T9, values[9] as! T10, values[10] as! T11, values[11] as! T12, values[12] as! T13, values[13] as! T14, values[14] as! T15, values[15] as! T16) + }, initialValues: [:], queue: queue) +} + public func combineLatest(queue: Queue? = nil, _ signals: [Signal]) -> Signal<[T], E> { if signals.count == 0 { return single([T](), E.self) diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index d2036f4f16..55a781e9c6 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -91,6 +91,7 @@ swift_library( "//submodules/UIKitRuntimeUtils:UIKitRuntimeUtils", "//submodules/DebugSettingsUI:DebugSettingsUI", "//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode", + "//submodules/WebPBinding:WebPBinding", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift index a45d4764e9..12e6053bb8 100644 --- a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift +++ b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift @@ -170,20 +170,20 @@ private final class BubbleSettingsControllerNode: ASDisplayNode, UIScrollViewDel messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil)) let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil)) let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)] let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) let message3 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil)) let message4 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message4], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message4], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil)) let width: CGFloat if case .regular = layout.metrics.widthClass { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift index 2bab48b1c3..6746aa4469 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift @@ -149,7 +149,7 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode { let forwardInfo = MessageForwardInfo(author: item.linkEnabled ? peers[peerId] : nil, source: nil, sourceMessageId: nil, date: 0, authorSignature: item.linkEnabled ? nil : item.peerName, psaType: nil, flags: []) - let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: forwardInfo, author: nil, text: item.strings.Privacy_Forwards_PreviewMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode) + let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: forwardInfo, author: nil, text: item.strings.Privacy_Forwards_PreviewMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil) var node: ListViewItemNode? if let current = currentNode { diff --git a/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift b/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift new file mode 100644 index 0000000000..8322d574c5 --- /dev/null +++ b/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift @@ -0,0 +1,338 @@ +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 WebPBinding + +private final class QuickReactionSetupControllerArguments { + let context: AccountContext + let selectItem: (String) -> Void + + init( + context: AccountContext, + selectItem: @escaping (String) -> Void + ) { + self.context = context + self.selectItem = selectItem + } +} + +private enum QuickReactionSetupControllerSection: Int32 { + case demo + case items +} + +private enum QuickReactionSetupControllerEntry: ItemListNodeEntry { + enum StableId: Hashable { + case demoHeader + case demoMessage + case demoDescription + case itemsHeader + case item(String) + } + + case demoHeader(String) + case demoMessage(wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, availableReactions: AvailableReactions?, reaction: String?) + case demoDescription(String) + case itemsHeader(String) + case item(index: Int, value: String, image: UIImage?, text: String, isSelected: Bool) + + var section: ItemListSectionId { + switch self { + case .demoHeader, .demoMessage, .demoDescription: + return QuickReactionSetupControllerSection.demo.rawValue + case .itemsHeader, .item: + return QuickReactionSetupControllerSection.items.rawValue + } + } + + var stableId: StableId { + switch self { + case .demoHeader: + return .demoHeader + case .demoMessage: + return .demoMessage + case .demoDescription: + return .demoDescription + case .itemsHeader: + return .itemsHeader + case let .item(_, value, _, _, _): + return .item(value) + } + } + + var sortId: Int { + switch self { + case .demoHeader: + return 0 + case .demoMessage: + return 1 + case .demoDescription: + return 2 + case .itemsHeader: + return 3 + case let .item(index, _, _, _, _): + return 100 + index + } + } + + static func ==(lhs: QuickReactionSetupControllerEntry, rhs: QuickReactionSetupControllerEntry) -> Bool { + switch lhs { + case let .demoHeader(text): + if case .demoHeader(text) = rhs { + return true + } else { + return false + } + case let .demoMessage(lhsWallpaper, lhsFontSize, lhsBubbleCorners, lhsDateTimeFormat, lhsNameDisplayOrder, lhsAvailableReactions, lhsReaction): + if case let .demoMessage(rhsWallpaper, rhsFontSize, rhsBubbleCorners, rhsDateTimeFormat, rhsNameDisplayOrder, rhsAvailableReactions, rhsReaction) = rhs, lhsWallpaper == rhsWallpaper, lhsFontSize == rhsFontSize, lhsBubbleCorners == rhsBubbleCorners, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, lhsAvailableReactions == rhsAvailableReactions, lhsReaction == rhsReaction { + return true + } else { + return false + } + case let .demoDescription(text): + if case .demoDescription(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: QuickReactionSetupControllerEntry, rhs: QuickReactionSetupControllerEntry) -> Bool { + return lhs.sortId < rhs.sortId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! QuickReactionSetupControllerArguments + switch self { + case let .demoHeader(text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .demoMessage(wallpaper, fontSize, chatBubbleCorners, dateTimeFormat, nameDisplayOrder, availableReactions, reaction): + return ReactionChatPreviewItem( + context: arguments.context, + theme: presentationData.theme, + strings: presentationData.strings, + sectionId: self.section, + fontSize: fontSize, + chatBubbleCorners: chatBubbleCorners, + wallpaper: wallpaper, + dateTimeFormat: dateTimeFormat, + nameDisplayOrder: nameDisplayOrder, + availableReactions: availableReactions, + reaction: reaction + ) + case let .demoDescription(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, image, text, isSelected): + return ItemListCheckboxItem( + presentationData: presentationData, + icon: image, + iconSize: image?.size.aspectFitted(CGSize(width: 30.0, height: 30.0)), + title: text, + style: .right, + color: .accent, + checked: isSelected, + zeroSeparatorInsets: false, + sectionId: self.section, + action: { + arguments.selectItem(value) + } + ) + } + } +} + +private struct QuickReactionSetupControllerState: Equatable { +} + +private func quickReactionSetupControllerEntries( + presentationData: PresentationData, + availableReactions: AvailableReactions?, + images: [String: UIImage], + reactionSettings: ReactionSettings +) -> [QuickReactionSetupControllerEntry] { + var entries: [QuickReactionSetupControllerEntry] = [] + + if let availableReactions = availableReactions { + //TODO:localize + entries.append(.demoHeader("DOUBLE TAP ON MESSAGE TO REACT")) + entries.append(.demoMessage( + wallpaper: presentationData.chatWallpaper, + fontSize: presentationData.chatFontSize, + bubbleCorners: presentationData.chatBubbleCorners, + dateTimeFormat: presentationData.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + availableReactions: availableReactions, + reaction: reactionSettings.quickReaction + )) + entries.append(.demoDescription("You can double tap on message for a quick reaction.")) + + entries.append(.itemsHeader("QUICK REACTION")) + var index = 0 + for availableReaction in availableReactions.reactions { + entries.append(.item( + index: index, + value: availableReaction.value, + image: images[availableReaction.value], + text: availableReaction.title, + isSelected: reactionSettings.quickReaction == availableReaction.value + )) + index += 1 + } + } + + return entries +} + +public func quickReactionSetupController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil +) -> ViewController { + let statePromise = ValuePromise(QuickReactionSetupControllerState(), ignoreRepeated: true) + let stateValue = Atomic(value: QuickReactionSetupControllerState()) + let updateState: ((QuickReactionSetupControllerState) -> QuickReactionSetupControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var dismissImpl: (() -> Void)? + let _ = dismissImpl + + let _ = updateState + + let actionsDisposable = DisposableSet() + + let arguments = QuickReactionSetupControllerArguments( + context: context, + selectItem: { reaction in + let _ = updateReactionSettingsInteractively(postbox: context.account.postbox, { settings in + var settings = settings + settings.quickReaction = reaction + return settings + }).start() + } + ) + + let settings = context.account.postbox.preferencesView(keys: [PreferencesKeys.reactionSettings]) + |> map { preferencesView -> ReactionSettings in + let reactionSettings: ReactionSettings + if let entry = preferencesView.values[PreferencesKeys.reactionSettings], let value = entry.get(ReactionSettings.self) { + reactionSettings = value + } else { + reactionSettings = .default + } + return reactionSettings + } + + let images: Signal<[String: UIImage], NoError> = context.engine.stickers.availableReactions() + |> mapToSignal { availableReactions -> Signal<[String: UIImage], NoError> in + var signals: [Signal<(String, UIImage?), NoError>] = [] + + if let availableReactions = availableReactions { + for availableReaction in availableReactions.reactions { + let signal: Signal<(String, UIImage?), NoError> = context.account.postbox.mediaBox.resourceData(availableReaction.staticIcon.resource) + |> distinctUntilChanged(isEqual: { lhs, rhs in + return lhs.complete == rhs.complete + }) + |> map { data -> (String, UIImage?) in + guard data.complete else { + return (availableReaction.value, nil) + } + guard let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else { + return (availableReaction.value, nil) + } + guard let image = WebP.convert(fromWebP: dataValue) else { + return (availableReaction.value, nil) + } + return (availableReaction.value, image) + } + signals.append(signal) + } + } + + return combineLatest(queue: .mainQueue(), signals) + |> map { values -> [String: UIImage] in + var dict: [String: UIImage] = [:] + for (key, image) in values { + if let image = image { + dict[key] = image + } + } + return dict + } + } + + let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData + let signal = combineLatest(queue: .mainQueue(), + presentationData, + statePromise.get(), + context.engine.stickers.availableReactions(), + settings, + images + ) + |> deliverOnMainQueue + |> map { presentationData, _, availableReactions, settings, images -> (ItemListControllerState, (ItemListNodeState, Any)) in + //TODO:localize + let title: String = "Quick Reaction" + + let entries = quickReactionSetupControllerEntries( + presentationData: presentationData, + availableReactions: availableReactions, + images: images, + reactionSettings: settings + ) + + 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) + dismissImpl = { [weak controller] in + guard let controller = controller else { + return + } + controller.dismiss() + } + + return controller +} + diff --git a/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift new file mode 100644 index 0000000000..f746d7aa62 --- /dev/null +++ b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift @@ -0,0 +1,274 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import Postbox +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AccountContext +import WallpaperBackgroundNode +import AvatarNode + +class ReactionChatPreviewItem: ListViewItem, ItemListItem { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let sectionId: ItemListSectionId + let fontSize: PresentationFontSize + let chatBubbleCorners: PresentationChatBubbleCorners + let wallpaper: TelegramWallpaper + let dateTimeFormat: PresentationDateTimeFormat + let nameDisplayOrder: PresentationPersonNameOrder + let availableReactions: AvailableReactions? + let reaction: String? + + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, wallpaper: TelegramWallpaper, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, availableReactions: AvailableReactions?, reaction: String?) { + self.context = context + self.theme = theme + self.strings = strings + self.sectionId = sectionId + self.fontSize = fontSize + self.chatBubbleCorners = chatBubbleCorners + self.wallpaper = wallpaper + self.dateTimeFormat = dateTimeFormat + self.nameDisplayOrder = nameDisplayOrder + self.availableReactions = availableReactions + self.reaction = reaction + } + + 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 = ReactionChatPreviewItemNode() + 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() }) + }) + } + } + } + + 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? ReactionChatPreviewItemNode { + let makeLayout = nodeValue.asyncLayout() + + 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() + }) + } + } + } + } + } +} + +class ReactionChatPreviewItemNode: ListViewItemNode { + private var backgroundNode: WallpaperBackgroundNode? + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + private let avatarNode: ASImageNode + + private let containerNode: ASDisplayNode + + private var messageNode: ListViewItemNode? + + private var item: ReactionChatPreviewItem? + + init() { + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + + self.avatarNode = ASImageNode() + + self.containerNode = ASDisplayNode() + self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + + super.init(layerBacked: false, dynamicBounce: false) + + self.clipsToBounds = true + + self.addSubnode(self.avatarNode) + self.addSubnode(self.containerNode) + } + + func asyncLayout() -> (_ item: ReactionChatPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentNode = self.messageNode + + var currentBackgroundNode = self.backgroundNode + + return { item, params, neighbors in + if currentBackgroundNode == nil { + currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false) + } + currentBackgroundNode?.update(wallpaper: item.wallpaper) + currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners) + + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + let chatPeerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(1)) + let userPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)) + + var peers = SimpleDictionary() + let messages = SimpleDictionary() + + peers[chatPeerId] = TelegramGroup(id: chatPeerId, title: "Chat", photo: [], participantCount: 1, role: .member, membership: .Member, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 1, version: 1) + //TODO:localize + peers[userPeerId] = TelegramUser(id: userPeerId, accessHash: nil, firstName: "Dino", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + + //TODO:localize + let messageText = "I hope you're enjoying your day as much as I am." + + var attributes: [MessageAttribute] = [] + if let reaction = item.reaction { + attributes.append(ReactionsMessageAttribute(reactions: [MessageReaction(value: reaction, count: 1, isSelected: true)], recentPeers: [])) + } + + let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: chatPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[userPeerId], text: messageText, attributes: attributes, media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: item.availableReactions) + + var node: ListViewItemNode? + if let current = currentNode { + node = current + messageItem.updateNode(async: { $0() }, node: { return current }, params: params, previousItem: nil, nextItem: nil, animation: .None, completion: { (layout, apply) in + let nodeFrame = CGRect(origin: current.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height)) + + current.contentSize = layout.contentSize + current.insets = layout.insets + current.frame = nodeFrame + + apply(ListViewItemApply(isOnScreen: true)) + }) + } else { + messageItem.nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { messageNode, apply in + node = messageNode + apply().1(ListViewItemApply(isOnScreen: true)) + }) + } + + var contentSize = CGSize(width: params.width, height: 8.0 + 8.0) + if let node = node { + contentSize.height += node.frame.size.height + } + insets = itemListNeighborsGroupedInsets(neighbors, params) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) + + var topOffset: CGFloat = 8.0 + if let node = node { + strongSelf.messageNode = node + if node.supernode == nil { + strongSelf.containerNode.addSubnode(node) + } + node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: node.frame.size), within: layout.contentSize) + + let avatarSize: CGFloat = 34.0 + if strongSelf.avatarNode.image == nil { + strongSelf.avatarNode.image = generateImage(CGSize(width: avatarSize, height: avatarSize), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.addEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.clip() + UIGraphicsPushContext(context) + if let image = UIImage(bundleImageName: "Avatar/SampleAvatar1") { + image.draw(in: CGRect(origin: CGPoint(), size: size)) + } + UIGraphicsPopContext() + }) + } + + topOffset += node.frame.size.height + + strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 7.0, y: topOffset - avatarSize), size: CGSize(width: avatarSize, height: avatarSize)) + } + + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + + if let currentBackgroundNode = currentBackgroundNode, strongSelf.backgroundNode !== currentBackgroundNode { + strongSelf.backgroundNode = currentBackgroundNode + strongSelf.insertSubnode(currentBackgroundNode, 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, at: 3) + } + 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 = 0.0 + bottomStripeOffset = -separatorHeight + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + let backgroundFrame = 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))) + + if let backgroundNode = strongSelf.backgroundNode { + backgroundNode.frame = backgroundFrame.insetBy(dx: 0.0, dy: -100.0) + backgroundNode.update(wallpaper: item.wallpaper) + backgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners) + backgroundNode.updateLayout(size: backgroundNode.bounds.size, transition: .immediate) + } + + strongSelf.maskNode.frame = backgroundFrame.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 + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + } + }) + } + } + + 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) + } +} diff --git a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift index cfaf3f9c61..ccfaa4c005 100644 --- a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift @@ -15,6 +15,7 @@ import ItemListStickerPackItem import ItemListPeerActionItem import UndoUI import ShareController +import WebPBinding private final class InstalledStickerPacksControllerArguments { let account: Account @@ -24,6 +25,7 @@ private final class InstalledStickerPacksControllerArguments { let removePack: (ArchivedStickerPackItem) -> Void let openStickersBot: () -> Void let openMasks: () -> Void + let openQuickReaction: () -> Void let openFeatured: () -> Void let openArchived: ([ArchivedStickerPackItem]?) -> Void let openSuggestOptions: () -> Void @@ -32,13 +34,14 @@ private final class InstalledStickerPacksControllerArguments { let expandTrendingPacks: () -> Void let addPack: (StickerPackCollectionInfo) -> Void - init(account: Account, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, removePack: @escaping (ArchivedStickerPackItem) -> Void, openStickersBot: @escaping () -> Void, openMasks: @escaping () -> Void, openFeatured: @escaping () -> Void, openArchived: @escaping ([ArchivedStickerPackItem]?) -> Void, openSuggestOptions: @escaping () -> Void, toggleAnimatedStickers: @escaping (Bool) -> Void, togglePackSelected: @escaping (ItemCollectionId) -> Void, expandTrendingPacks: @escaping () -> Void, addPack: @escaping (StickerPackCollectionInfo) -> Void) { + init(account: Account, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, removePack: @escaping (ArchivedStickerPackItem) -> Void, openStickersBot: @escaping () -> Void, openMasks: @escaping () -> Void, openQuickReaction: @escaping () -> Void, openFeatured: @escaping () -> Void, openArchived: @escaping ([ArchivedStickerPackItem]?) -> Void, openSuggestOptions: @escaping () -> Void, toggleAnimatedStickers: @escaping (Bool) -> Void, togglePackSelected: @escaping (ItemCollectionId) -> Void, expandTrendingPacks: @escaping () -> Void, addPack: @escaping (StickerPackCollectionInfo) -> Void) { self.account = account self.openStickerPack = openStickerPack self.setPackIdWithRevealedOptions = setPackIdWithRevealedOptions self.removePack = removePack self.openStickersBot = openStickersBot self.openMasks = openMasks + self.openQuickReaction = openQuickReaction self.openFeatured = openFeatured self.openArchived = openArchived self.openSuggestOptions = openSuggestOptions @@ -79,6 +82,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { case trending(PresentationTheme, String, Int32) case archived(PresentationTheme, String, Int32, [ArchivedStickerPackItem]?) case masks(PresentationTheme, String) + case quickReaction(String, UIImage?) case animatedStickers(PresentationTheme, String, Bool) case animatedStickersInfo(PresentationTheme, String) case trendingPacksTitle(PresentationTheme, String) @@ -90,7 +94,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { var section: ItemListSectionId { switch self { - case .suggestOptions, .trending, .masks, .archived, .animatedStickers, .animatedStickersInfo: + case .suggestOptions, .trending, .masks, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo: return InstalledStickerPacksSection.service.rawValue case .trendingPacksTitle, .trendingPack, .trendingExpand: return InstalledStickerPacksSection.trending.rawValue @@ -109,22 +113,24 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { return .index(2) case .masks: return .index(3) - case .animatedStickers: + case .quickReaction: return .index(4) - case .animatedStickersInfo: + case .animatedStickers: return .index(5) - case .trendingPacksTitle: + case .animatedStickersInfo: return .index(6) + case .trendingPacksTitle: + return .index(7) case let .trendingPack(_, _, _, info, _, _, _, _, _): return .trendingPack(info.id) case .trendingExpand: - return .index(7) - case .packsTitle: return .index(8) + case .packsTitle: + return .index(9) case let .pack(_, _, _, info, _, _, _, _, _, _): return .pack(info.id) case .packsInfo: - return .index(9) + return .index(10) } } @@ -148,6 +154,12 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { } else { return false } + case let .quickReaction(lhsText, lhsImage): + if case let .quickReaction(rhsText, rhsImage) = rhs, lhsText == rhsText, lhsImage === rhsImage { + return true + } else { + return false + } case let .archived(lhsTheme, lhsText, lhsCount, _): if case let .archived(rhsTheme, rhsText, rhsCount, _) = rhs, lhsTheme === rhsTheme, lhsCount == rhsCount, lhsText == rhsText { return true @@ -292,23 +304,30 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { default: return true } + case .quickReaction: + switch rhs { + case .suggestOptions, .trending, .archived, .masks, .quickReaction: + return false + default: + return true + } case .animatedStickers: switch rhs { - case .suggestOptions, .trending, .archived, .masks, .animatedStickers: + case .suggestOptions, .trending, .archived, .masks, .quickReaction, .animatedStickers: return false default: return true } case .animatedStickersInfo: switch rhs { - case .suggestOptions, .trending, .archived, .masks, .animatedStickers, .animatedStickersInfo: + case .suggestOptions, .trending, .archived, .masks, .quickReaction, .animatedStickers, .animatedStickersInfo: return false default: return true } case .trendingPacksTitle: switch rhs { - case .suggestOptions, .trending, .masks, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle: + case .suggestOptions, .trending, .masks, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle: return false default: return true @@ -324,14 +343,14 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { } case .trendingExpand: switch rhs { - case .suggestOptions, .trending, .masks, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle, .trendingPack, .trendingExpand: + case .suggestOptions, .trending, .masks, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle, .trendingPack, .trendingExpand: return false default: return true } case .packsTitle: switch rhs { - case .suggestOptions, .trending, .masks, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle, .trendingPack, .trendingExpand, .packsTitle: + case .suggestOptions, .trending, .masks, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle, .trendingPack, .trendingExpand, .packsTitle: return false default: return true @@ -370,6 +389,16 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openMasks() }) + case let .quickReaction(title, image): + let labelStyle: ItemListDisclosureLabelStyle + if let image = image { + labelStyle = .image(image: image, size: image.size.aspectFitted(CGSize(width: 30.0, height: 30.0))) + } else { + labelStyle = .text + } + return ItemListDisclosureItem(presentationData: presentationData, title: title, label: "", labelStyle: labelStyle, sectionId: self.section, style: .blocks, action: { + arguments.openQuickReaction() + }) case let .archived(_, text, count, archived): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: count == 0 ? "" : "\(count)", sectionId: self.section, style: .blocks, action: { arguments.openArchived(archived) @@ -480,7 +509,7 @@ private func namespaceForMode(_ mode: InstalledStickerPacksControllerMode) -> It private let maxTrendingPacksDisplayedLimit: Int32 = 3 -private func installedStickerPacksControllerEntries(presentationData: PresentationData, state: InstalledStickerPacksControllerState, mode: InstalledStickerPacksControllerMode, view: CombinedView, temporaryPackOrder: [ItemCollectionId]?, featured: [FeaturedStickerPackItem], archived: [ArchivedStickerPackItem]?, stickerSettings: StickerSettings) -> [InstalledStickerPacksEntry] { +private func installedStickerPacksControllerEntries(presentationData: PresentationData, state: InstalledStickerPacksControllerState, mode: InstalledStickerPacksControllerMode, view: CombinedView, temporaryPackOrder: [ItemCollectionId]?, featured: [FeaturedStickerPackItem], archived: [ArchivedStickerPackItem]?, stickerSettings: StickerSettings, quickReactionImage: UIImage?) -> [InstalledStickerPacksEntry] { var entries: [InstalledStickerPacksEntry] = [] var installedPacks = Set() @@ -514,6 +543,9 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati } entries.append(.masks(presentationData.theme, presentationData.strings.MaskStickerSettings_Title)) + //TODO:localize + entries.append(.quickReaction("Quick Reaction", quickReactionImage)) + entries.append(.animatedStickers(presentationData.theme, presentationData.strings.StickerPacksSettings_AnimatedStickers, stickerSettings.loopAnimatedStickers)) entries.append(.animatedStickersInfo(presentationData.theme, presentationData.strings.StickerPacksSettings_AnimatedStickersInfo)) @@ -699,6 +731,10 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta })) }, openMasks: { pushControllerImpl?(installedStickerPacksController(context: context, mode: .masks, archivedPacks: archivedPacks, updatedPacks: { _ in})) + }, openQuickReaction: { + pushControllerImpl?(quickReactionSetupController( + context: context + )) }, openFeatured: { pushControllerImpl?(featuredStickerPacksController(context: context)) }, openArchived: { archived in @@ -783,14 +819,63 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta let temporaryPackOrder = Promise<[ItemCollectionId]?>(nil) let featured = Promise<[FeaturedStickerPackItem]>() + let quickReactionImage: Signal switch mode { case .general, .modal: featured.set(context.account.viewTracker.featuredStickerPacks()) archivedPromise.set(.single(archivedPacks) |> then(context.engine.stickers.archivedStickerPacks() |> map(Optional.init))) + quickReactionImage = combineLatest( + context.engine.stickers.availableReactions(), + context.account.postbox.preferencesView(keys: [PreferencesKeys.reactionSettings]) + ) + |> map { availableReactions, preferencesView -> TelegramMediaFile? in + guard let availableReactions = availableReactions else { + return nil + } + + let reactionSettings: ReactionSettings + if let entry = preferencesView.values[PreferencesKeys.reactionSettings], let value = entry.get(ReactionSettings.self) { + reactionSettings = value + } else { + reactionSettings = .default + } + + for reaction in availableReactions.reactions { + if reaction.value == reactionSettings.quickReaction { + return reaction.staticIcon + } + } + + return nil + } + |> distinctUntilChanged + |> mapToSignal { file -> Signal in + guard let file = file else { + return .single(nil) + } + + return context.account.postbox.mediaBox.resourceData(file.resource) + |> distinctUntilChanged(isEqual: { lhs, rhs in + return lhs.complete == rhs.complete + }) + |> map { data -> UIImage? in + guard data.complete else { + return nil + } + guard let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else { + return nil + } + guard let image = WebP.convert(fromWebP: dataValue) else { + return nil + } + return image + } + } case .masks: featured.set(.single([])) archivedPromise.set(.single(nil) |> then(context.engine.stickers.archivedStickerPacks(namespace: .masks) |> map(Optional.init))) + quickReactionImage = .single(nil) } var previousPackCount: Int? @@ -799,9 +884,11 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta stickerPacks.get(), temporaryPackOrder.get(), combineLatest(queue: .mainQueue(), featured.get(), archivedPromise.get()), - context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings])) + context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings]), + quickReactionImage + ) |> deliverOnMainQueue - |> map { presentationData, state, view, temporaryPackOrder, featuredAndArchived, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, state, view, temporaryPackOrder, featuredAndArchived, sharedData, quickReactionImage -> (ItemListControllerState, (ItemListNodeState, Any)) in var stickerSettings = StickerSettings.defaultSettings if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.stickerSettings]?.get(StickerSettings.self) { stickerSettings = value @@ -944,7 +1031,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: installedStickerPacksControllerEntries(presentationData: presentationData, state: state, mode: mode, view: view, temporaryPackOrder: temporaryPackOrder, featured: featuredAndArchived.0, archived: featuredAndArchived.1, stickerSettings: stickerSettings), style: .blocks, ensureVisibleItemTag: focusOnItemTag, toolbarItem: toolbarItem, animateChanges: previous != nil && packCount != nil && (previous! != 0 && previous! >= packCount! - 10)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: installedStickerPacksControllerEntries(presentationData: presentationData, state: state, mode: mode, view: view, temporaryPackOrder: temporaryPackOrder, featured: featuredAndArchived.0, archived: featuredAndArchived.1, stickerSettings: stickerSettings, quickReactionImage: quickReactionImage), style: .blocks, ensureVisibleItemTag: focusOnItemTag, toolbarItem: toolbarItem, animateChanges: previous != nil && packCount != nil && (previous! != 0 && previous! >= packCount! - 10)) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index f3db9bc29d..f87e239b95 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -413,20 +413,20 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil)) let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil)) let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)] let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) let message3 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil)) let message4 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message4], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message4], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil)) let width: CGFloat if case .regular = layout.metrics.widthClass { diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index ddac2cdc46..c651a07d05 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -1043,7 +1043,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate return state }, animated: true) }, clickThroughMessage: { - }, backgroundNode: self.backgroundNode) + }, backgroundNode: self.backgroundNode, availableReactions: nil) return item } diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 850fad8014..e1a3c2e2b6 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -593,7 +593,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { sampleMessages.append(message8) items = sampleMessages.reversed().map { message in - self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message], theme: self.previewTheme, strings: self.presentationData.strings, wallpaper: self.wallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: !message.media.isEmpty ? FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local) : nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperNode) + self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message], theme: self.previewTheme, strings: self.presentationData.strings, wallpaper: self.wallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: !message.media.isEmpty ? FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local) : nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperNode, availableReactions: nil) } let width: CGFloat diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift index bbbfedb957..478ae7ec0d 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift @@ -160,7 +160,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { } let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: messageItem.outgoing ? otherPeerId : peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: messageItem.outgoing ? TelegramUser(id: otherPeerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) : nil, text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)] : [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode)) + items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil)) } var nodes: [ListViewItemNode] = [] diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift index 68f9a32b3b..38a1c2c6fd 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift @@ -1103,10 +1103,10 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let theme = self.presentationData.theme.withUpdated(preview: true) let message1 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: bottomMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil)) let message2 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: topMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil)) let params = ListViewItemLayoutParams(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height) if let messageNodes = self.messageNodes { diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 5e12d63613..ac4db6b397 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -1016,7 +1016,7 @@ final class MessageStoryRenderer { let theme = self.presentationData.theme.withUpdated(preview: true) let headerItem = self.context.sharedContext.makeChatMessageDateHeaderItem(context: self.context, timestamp: self.messages.first?.timestamp ?? 0, theme: theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder) - let items: [ListViewItem] = [self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: self.messages, theme: theme, strings: self.presentationData.strings, wallpaper: self.presentationData.theme.chat.defaultWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: nil)] + let items: [ListViewItem] = [self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: self.messages, theme: theme, strings: self.presentationData.strings, wallpaper: self.presentationData.theme.chat.defaultWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: nil, availableReactions: nil)] let inset: CGFloat = 16.0 let width = layout.size.width - inset * 2.0 diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatInfoContextItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatInfoContextItem.swift index 0e7e115a85..1354709d5b 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatInfoContextItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatInfoContextItem.swift @@ -97,4 +97,14 @@ private final class VoiceChatInfoContextItemNode: ASDisplayNode, ContextMenuCust let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0) self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor) } + + func canBeHighlighted() -> Bool { + return false + } + + func updateIsHighlighted(isHighlighted: Bool) { + } + + func performAction() { + } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatRecordingContextItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatRecordingContextItem.swift index 9843fbcda4..3d90146154 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatRecordingContextItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatRecordingContextItem.swift @@ -266,6 +266,14 @@ private final class VoiceChatRecordingContextItemNode: ASDisplayNode, ContextMen self.performAction() } + func canBeHighlighted() -> Bool { + return true + } + + func updateIsHighlighted(isHighlighted: Bool) { + self.setIsHighlighted(isHighlighted) + } + func performAction() { guard let controller = self.getController() else { return diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatShareScreenContextItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatShareScreenContextItem.swift index 67531e289e..d3227b10b1 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatShareScreenContextItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatShareScreenContextItem.swift @@ -168,6 +168,14 @@ private final class VoiceChatShareScreenContextItemNode: ASDisplayNode, ContextM self.performAction() } + func canBeHighlighted() -> Bool { + return true + } + + func updateIsHighlighted(isHighlighted: Bool) { + self.setIsHighlighted(isHighlighted) + } + func performAction() { guard let controller = self.getController() else { return diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatVolumeContextItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatVolumeContextItem.swift index 448fcbca10..69aa80c1b8 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatVolumeContextItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatVolumeContextItem.swift @@ -194,4 +194,14 @@ private final class VoiceChatVolumeContextItemNode: ASDisplayNode, ContextMenuCu self.value = max(self.minValue, min(2.0, location.x / self.bounds.width * 2.0)) self.valueChanged(self.value, true) } + + func canBeHighlighted() -> Bool { + return false + } + + func updateIsHighlighted(isHighlighted: Bool) { + } + + func performAction() { + } } diff --git a/submodules/TelegramCore/Sources/Settings/ReactionSettings.swift b/submodules/TelegramCore/Sources/Settings/ReactionSettings.swift new file mode 100644 index 0000000000..974e2c8a14 --- /dev/null +++ b/submodules/TelegramCore/Sources/Settings/ReactionSettings.swift @@ -0,0 +1,23 @@ +import Postbox +import SwiftSignalKit + +public struct ReactionSettings: Equatable, Codable { + public static var `default` = ReactionSettings(quickReaction: "👍") + + public var quickReaction: String + + public init(quickReaction: String) { + self.quickReaction = quickReaction + } +} + +public func updateReactionSettingsInteractively(postbox: Postbox, _ f: @escaping (ReactionSettings) -> ReactionSettings) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: PreferencesKeys.reactionSettings, { current in + let previous = current?.get(ReactionSettings.self) ?? ReactionSettings.default + let updated = f(previous) + return PreferencesEntry(updated) + }) + } + |> ignoreValues +} diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index 3d44d900cd..494caf4978 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -812,13 +812,13 @@ public final class AccountViewTracker { } } - public func updateReactionsForMessageIds(messageIds: Set) { + public func updateReactionsForMessageIds(messageIds: Set, force: Bool = false) { self.queue.async { var addedMessageIds: [MessageId] = [] let timestamp = Int32(CFAbsoluteTimeGetCurrent()) for messageId in messageIds { let messageTimestamp = self.updatedReactionsMessageIdsAndTimestamps[messageId] - if messageTimestamp == nil || messageTimestamp! < timestamp - 5 * 60 { + if messageTimestamp == nil || messageTimestamp! < timestamp - 1 * 20 || force { self.updatedReactionsMessageIdsAndTimestamps[messageId] = timestamp addedMessageIds.append(messageId) } diff --git a/submodules/TelegramCore/Sources/State/MessageReactions.swift b/submodules/TelegramCore/Sources/State/MessageReactions.swift index 7168e62eb0..85b5c2f41f 100644 --- a/submodules/TelegramCore/Sources/State/MessageReactions.swift +++ b/submodules/TelegramCore/Sources/State/MessageReactions.swift @@ -242,7 +242,7 @@ public extension EngineMessageReactionListContext.State { self.init( totalCount: totalCount, items: [], - canLoadMore: true + canLoadMore: totalCount != 0 ) } } @@ -250,11 +250,11 @@ public extension EngineMessageReactionListContext.State { public final class EngineMessageReactionListContext { public final class Item: Equatable { public let peer: EnginePeer - public let reaction: String + public let reaction: String? - init( + public init( peer: EnginePeer, - reaction: String + reaction: String? ) { self.peer = peer self.reaction = reaction @@ -317,7 +317,9 @@ public final class EngineMessageReactionListContext { let initialState = EngineMessageReactionListContext.State(message: message, reaction: reaction) self.state = InternalState(totalCount: initialState.totalCount, items: initialState.items, canLoadMore: true, nextOffset: nil) - self.loadMore() + if initialState.canLoadMore { + self.loadMore() + } } deinit { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 0dca08190e..bc1b8723cb 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -222,6 +222,7 @@ private enum PreferencesKeyValues: Int32 { case peersNearby = 21 case chatListFiltersFeaturedState = 22 case secretChatSettings = 23 + case reactionSettings = 24 } public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey { @@ -344,6 +345,12 @@ public struct PreferencesKeys { key.setInt32(0, value: PreferencesKeyValues.chatListFiltersFeaturedState.rawValue) return key }() + + public static let reactionSettings: ValueBoxKey = { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: PreferencesKeyValues.reactionSettings.rawValue) + return key + }() } private enum SharedDataKeyValues: Int32 { diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Avatar8.pdf b/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Avatar8.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6c97d03a3088528ce8a695d45cfb867f3ef6af75 GIT binary patch literal 7034 zcmai(1yEaCyZ4dcP~0u0K}#V)0<<^*iWG<95Fj`N*U}c3hT=t9+_e-b?k=aewFQd1 zQ?!&D&U?&#LwbzrG{C~69HDC&lAbf&^?A>d}YdeJ}S)<*(gn~dY z5NTmgC@v1FBiOZ%9O06z6l}+`JKnB1EsSY-FyjVrth)H|J zvwiuT<1zc=#yU5hQoE-*k|@srOh$M}a_)#Jwo-XAhrwEwI(T_y^#W8>^7nO{#M^S0b(PRWz8IMOZ(w! zd&J6Sm>~vx;cyS8+gd!3x(|2Uki$h*$C456Z7^B&`y#zQ;-lKMCac8y>YW4{yqOv*eaqh?Dq6&Z_p^1GAT`Q(-Kn;Qv;x&b zNo8zw@5;uF(p9Flhu?JC{0R5kw$7}~fA1}x?fJkrgx*Hn!Ygm+5kPya!3;8hy|OZD zutn~T(^cpirmpg=)Qu;P!C5`>I|0ZtR+@)!r=qMAI7@4G-5q!TmR^C6ET^m#@am%D z?a1sn$wVBoY^Nc8$DTVBjK!F1?)ii#P9>ha-laztH;%Qxvh37K_hEt#Jju20vq#r` ziBm8;vdlhAv({cUROH$(L?j~FQALm6U@IisJo^1_b;xQ?D)EZaZn!vwVccLSg!_|& zXw}X;V1Ad5teeOwp55nT49B#T*c}FA#vELpp5Au_p&<6QX%}W6JSED$P|!HFNH{yk zV|PWJPs;tFMI*CSty9^KiLqEoy^n*HWLX?j<(}_oiw~77xJ{*|@O6*OHS-1gK1(-W z5)`ST(0qGepka!#mm8#`gg4Wt475H>BI74$J4I#Bb5wO#7gH3PH^^~YDC|e2&TUAJ z;)IAYVO=@OC4#OTzaT=X@naYz&5Ll>%U$KBYc&a6DwRhWISC&Ap$Ob$8jo0AW^`~% ze_ImiqI3CUSXq_S8CS1hn5HTNPA&O{NLs{FxsIo?Yh>4?0c6EttAI~FXBquWOB%oV z!9i6?D&;~acu2p>FDN`zvp3?o9yh~sH-J05YyswJC|6v+HGSakc5Wqn;yk9mL#Bixwf z51EMk+Nh#JI_-%lZ9olf>XH-nHgDMd!79pKE*ynORJw%jG(qZmcZH=;ZVADW*c+?b zhGU#E#Iji_k12{svaeCOc)D0Cq>j-GH=+3nu^W#oCv|rO*f5@xuGTCdg4M1>ppSX| z6w1!;8O1k9?y-#VI%-{(cj7Z3{?ajZ`aVg23`;~JIr%5P$~_kCmhf~ob?RfonaBre zDNhe&KHvte1y6GbNB@A6Tn5gWbLV<(534~qUj|;mwkdSig4+GuCT%o>XRxx3x;TNA z;h?zhC0~J+Pe(QK7b5F3<#J)^p@MzJ(+7&?6kai7+;Ck{iZIfh>U{#}QsgK#PZY*> zACvI@Q`ZCv7Y_Ou(093JLh=ECfWRfoS0m#j-320f;Ym`}oPBU76>_PcxT72~0@OZw zK&3*&vZjHj1v4fiApm9Rf;hZ?^cQM;agx7%ov;!mB<|$*PRNkHwh;L4ooOO78%f@3 z)QSEI2=<)VXGMxr*sb7cZ~(Oc~t0(7z+qENFWNmjrwF-XnFXH;v5FJt^1&{1H>2OeU+?rmH$ z@aM+&)G$e53FOVan2^f|x82&r}Xg+OKfTtt8Bc1vT!D-!k4h zsd}E0muuBrL|coxB_5F&>I=$qmNAxfa}~xZSk8LEk+cQZ>D7I;=l3S+3pFdeofqjP z?8E4y*7gDpDwE?sq)DPAO?&$Ai3`Y4VV{mocQCiidsQI_I`VX!FaT9}s@6l3ZfQ?H z^4B?Y=7s947JppCmH;H;elwYe{?@E0sL_zvGL9gjn-J+S4}(qSjUN0!mD$_-NIz2g z$t9pl_rOwGS8$MdjpSgoEn8Bv7t|H&e&5vrJlO^=c=ADcBg&&dE-1R7wP~Ur0#6Ii zl|M~_9_jYppXM$gG~pt5&$fNu16nV%KYz~X{x#Wl_Vxnd&$LWU0$^Qx>Q(yd4dyhs zjaQjP<9HJh#^FDyOX>wr`Q2%XkM+BYfnj50{(O_3+&J?dUK3?EjLrJNc)cN1i;(!d z^gJs&dzS1vZE~_c7`&}%CA6gGDyFG|J1(E#WG`nIPgSK&@**^Ub^!|O8eiq#nC|u| za9ZXtbh;79DYq8OWl9e|``HK>ZRB>+z175ge&~?vgt;QRS%)`HG)bil=Z^g4)`DmA zE3?>Oi$mzkE=g7F?Wg2k0;J9YBg|p}$iAMHPV>?1$wB21$@hHlq?t1i5+`$w3kj$X z?5En9Z@_%~Jo$WpWD?U*=j@S);~^EM-v(|Z2TJGo;Ct3v(vRVcQI5~bCh#BDT;gOY z(q{u*nXY6Bd}8FmkJ^1>)}JlO(Wi+M+jc{CVky`6 zn**iYAFH(##D;h|66K%9egczZHxk2&N3-jO0{oj7fs**ZZH|kBRiZ6p(abT$eDk?b;lbaHlpQ%}G4ehg1&^AoYYZ zVA6+plmyp0U(6lonXi@tVxDMsA?QAj-3;Wieg5X6(oc&C^3=h7)=u!4+}bV9BWH#rzau&E_#_gs09{*W@%a|GS_^ERI$7!_|;Vp@5dc z+8S0G5av80D?&_Uma^REd(pYMF-^Kje7fU$nJ2rTn0mmq_{wcpic4X}=%ntTp2b^S z=+R=^nCU3-;Yko&%fmhq9>G9uw@9bt^Je;ce;a1p1HXdD_EV3=3dhx{x=*GlhrlV4 zj;v&+`7%YS^}jI{Tw-8b1?mMQKNMKB{QpP<{2cgDtd{7EEe9zMw!>KYnUHnAbn?a6 zSciU6vJt~fq^Wex3i0*skfM+1B^E9fl2R0;z4j14<7-cN__WJd28+ z5iA=8Sx~QkY-}${BVN;XEGfp(X#8ZzF6oy(Y`IQo)>Y z$cZE`86;6AsA9YknNJfGS}B6m>p&1uDpASq%Z*1C%}I@1x2u&)f=tq+z8@fQ+m@rB zOKdOvsO$7*@*qCb(CTKv zW>HTJCI6s67t%+pklgemyCj~IhY~=n7$My8K_-(UYJ+uQFqaiS#60~4BG-B4yQ0ae zQgnSzIR)YnEI<3jSQy9RaSJPP3KDKooxd%iTj5As0KLC;8C+(&O@gCwDnO99<=nrw zd6trz1p=FXvgnQSG=IM^O0S;#PO~v}W>RtKYE#e`kKO+#U#TK?v0n*0%DyO-{HO1u zk4DlcQI%^1bAg}j`y)>0Zew*k0Ov{BAHxi1%fdpqmHF^d7kQ0CX{XiutiVetqoc^DLYG}TV3ErFN{LCz`eknCAGwDdMNSv2_F+B z*T=LM^tdp=&Zl_-Pv?dEjDu4@)ZyHJG1;T@f}d6$TBnPR+%$+L3S+_w3hV*P0e6I2 z_&pYwCUr9nO|nBik!63(z*mznEUoPxJi@vN{E+RjFE_n1d(xclBY6hP-cEZ=Kb?XK z_p=B~jmV(vN}@fJ4JvVMlz@=+IJV+FsJ_LLXPc!aL^xahg*@oDJ5YlB1DJGtUC@N2 z0k`?{?uKyo01BYpT0nj+ggesH%@W}b6#ADz4(aTH_PYb`fJ^I+r2eXR=l>hPG~AGu z+6WJz5xOW$0cZl`mqR)t-LzfcmI&Y-p~<;JfWm(j0P@TGcqnRnpjqutpCZ}^z4H-D zO8%q%o$2rT|61?=r14wAfnYvCG3cLY_t%0%{*j=DUM|1P9Ths?O~VgEc-dJZv=n9k zn+pY?5P|=j9RCT6>F>OCwC8BTM_NQ$Mk&~U?57NV1iE2@K^8Lv$7CA49hxxtwRw~S!9lt3$wJyqpTXkXM%~Pu%|XCS zBZkJWAV8;wKIUtoY<;TzoepCP%AxLaEIKmGd-|B|OV-u^#S0kLc=hBr8}9U}$gsp$73kFhZD6T8Kl-R`})6tm61@F}oajRHg}Srm}Zq}Z)( z_FG>aRcl{bKprM`JzI+KQ5`w0A$|*{ef<&nP9Brpz$w)>Zm*naZ_i+Q-S)C=Zjt4s z_Kifp-?i=$$L>gm3@XE5(Q&tozl8?l3}pD8xws@@4RV(9!wT3jB7+e2x1#?s<375_2>AWpxMC$T zj&fd7m0{CfKO>H@a%0corO#aqaqPgNh{f~BA`}Yb17q=;lQ6v&birN#VuZYYN{0Oo zo9`)>oiw0IMsgN|PzExK=@o46f-xEBHwsV-g1X=u1D>~&xDXM)R+J_T>req>E(F@) z2OW}8=g5@A(&Uk*%1)Cp8shuP$!LgDVfV;I#*)QLUygDV(eVVAWXowtoIdbiXv8-O zzRJQI1q$KbUnEF(p&siqrMF1s#AN{CX&pgK5s51Ez z_AGq&3Ddp4cKT%|mzVW)#sbd56@WU@8C28joi36kjw7FDo&~CdAkq%^LyH&pTM1t= zy<%C6VeM_>j3qDxI0TX2e+L9Pf^^6j@Hg;pLdZLXGy{v&J=ns?e7lV;xIev}m1$OD z;!sPVQVIjnYJs>@YXWtY?Ac5>Bsf~`Ck#BcQgov;kTzAX0A&Kr*h%B1G!nTusMM92 zBk5*Bx2wOxw<{YX{pzpme|Bw4Y$q_J zF|;z!Gw3oT3-t=qr#Yt$q^+fG2=&$28WPn44A%@dYE8jOsr$n^*6h`9#InmP_6i@3 zN#@O$N|aj`RqNa2s#SlXG>PXk5|ODhuG99GRafEH=QsPPd4Q&p%<{~iXK9=3rtO&=+REVp;vAV1B)}y!g?gSKD5|lQfgcd737*rjfII zXKdFT*AP63aG&s&d*`zrUR+Hf0aN5Vn@jayj!-Ljoc^4jCX@;n(+$3Ge(m2I8 zeTjt$8w?{vrV+^>pT|A_SQme8abSN?DEU}2QZibSvem1#G=TpCeuH)6bv1u7bGUIe zbFB^#1(XqN66OIE0D5?mL;?g9IG%v64$O|GAXffyGm!Bl4VLULIj>N00%2kk>MM6T zK3<_Rj$Pk+U7wde-hf9I@-VsXrl&Fe+>2^ zuPUGnQNRKT8dgSXe~_13@%i|?O8Cm|GU3WTpP=!(>fNtywQI#gBNee`9R{vEER3wo zi)6Qvl%2(EsuibZ4c=>jnS`eB>-de{ha1sHCKrfbae(BIs*o{Qa<@sOrBa0wwbBZf zx=ig4&nmOyz}4c!*u-3J!Q!#vKkPzkE@~!Z+2K)@4^0FmI9vF;gmS@hje~Ym`?{1l z(qnO|T1p%3|zkywifpG zv}yJF9rmk3c;De(dFXV7@wWywZzbbLw(7x+T+K=CVyt3n1d6I! zlt0=lP8DwFZ3i;Mq^-AX``7KIf9?A=)3)gJY63GFy9ZB_g^H>9RsXq$L%EDo5B&R5 zS=Y!;&URy)%`3B4u)U{8aU+VMiurw=F`iRTCTB#7L=vH$(eb^XR$Tj#H$Q&L`BRo| zX$a;vTmVGxiybZZU9cWx5R1?GMg^2?ao%K}eI7N8GR&U-J{|cv4{GPH>c6=4dLo_q z3-cqX_qQ)jQD>=X)q^%FBXM7-KfD*q^nQ6P+Z%fHCXzP%@pBK!#+udf^oq)gg`vRY zkETZrZpGhPu7Bw!m)cZ_nLG(d4`|s7yNRA=Y07wKw)@g5SNKaIg0HTK#WLze{WdEZmIY6_c7u4f~1S*xp#~uHMZak``YrfFK%mB{X#t{Gg`{~1;MXl9}Xw$^~Lj$lk5K!-gmn9PROFgv67seEZiMo z1-#R$T0oP(we+3%{&%f=r_J4g{Lc_pc5qpw56}pV4uRf{KgG2tTCqcb{3>=ml6S?U2quehA+`rL@>T($U)9%F_~kyu#%K zw^Y{#{+$AW@CooifW}<1o_3B_KmjmJM97#MsObrJ^Y8J|KB11gC9O_2x~$x5CSFy|Mvxi z3JVJg1FeC7*#rf}&|h+Q0iFN0K?Oz7+x71@FjxTHfq%CN3W@w5xq@ga`CoCuBIpMH zFB?<NkbnJd+&$23`*TmB PP!Ul=c6NDn1;YOVB)XKG literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Contents.json new file mode 100644 index 0000000000..3b6cd02302 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Avatar8.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 6428918210..d65e57448a 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1106,7 +1106,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G 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 + let items = ContextController.Items(content: .custom(ReactionListContextMenuContent(context: strongSelf.context, availableReactions: availableReactions, message: EngineMessage(message), reaction: value, readStats: nil, back: nil, openPeer: { id in dismissController?({ guard let strongSelf = self else { return @@ -1149,8 +1149,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - let _ = allowedReactions - strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in guard let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item else { return diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 890beafc65..de90518046 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -312,7 +312,7 @@ private final class ChatHistoryTransactionOpaqueState { } } -private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, animatedEmojiStickers: [String: [StickerPackItem]], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]], subject: ChatControllerSubject?, currentlyPlayingMessageId: MessageIndex?, isCopyProtectionEnabled: Bool, availableReactions: AvailableReactions?) -> ChatMessageItemAssociatedData { +private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, animatedEmojiStickers: [String: [StickerPackItem]], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]], subject: ChatControllerSubject?, currentlyPlayingMessageId: MessageIndex?, isCopyProtectionEnabled: Bool, availableReactions: AvailableReactions?, defaultReaction: String?) -> ChatMessageItemAssociatedData { var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel var contactsPeerIds: Set = Set() var channelDiscussionGroup: ChatMessageItemAssociatedData.ChannelDiscussionGroupStatus = .unknown @@ -361,7 +361,7 @@ private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHist } } - return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions) + return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction) } private extension ChatHistoryLocationInput { @@ -479,7 +479,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() private let messageProcessingManager = ChatMessageThrottledProcessingManager() - private let messageWithReactionsProcessingManager = ChatMessageThrottledProcessingManager() + private let messageWithReactionsProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 4.0) let adSeenProcessingManager = ChatMessageThrottledProcessingManager() private let seenLiveLocationProcessingManager = ChatMessageThrottledProcessingManager() private let unsupportedMessageProcessingManager = ChatMessageThrottledProcessingManager() @@ -573,6 +573,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private let adMessagesContext: AdMessagesHistoryContext? private var preloadAdPeerId: PeerId? private let preloadAdPeerDisposable = MetaDisposable() + + private var refreshDisplayedItemRangeTimer: SwiftSignalKit.Timer? /*var historyScrollingArea: SparseDiscreteScrollingArea? { didSet { @@ -983,6 +985,18 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let availableReactions = context.engine.stickers.availableReactions() + let defaultReaction = context.account.postbox.preferencesView(keys: [PreferencesKeys.reactionSettings]) + |> map { preferencesView -> String? in + let reactionSettings: ReactionSettings + if let entry = preferencesView.values[PreferencesKeys.reactionSettings], let value = entry.get(ReactionSettings.self) { + reactionSettings = value + } else { + reactionSettings = .default + } + return reactionSettings.quickReaction + } + |> distinctUntilChanged + let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue, historyViewUpdate, self.chatPresentationDataPromise.get(), @@ -998,8 +1012,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { customThreadOutgoingReadState, self.currentlyPlayingMessageIdPromise.get(), adMessages, - availableReactions - ).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, currentlyPlayingMessageId, adMessages, availableReactions in + availableReactions, + defaultReaction + ).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, currentlyPlayingMessageId, adMessages, availableReactions, defaultReaction in func applyHole() { Queue.mainQueue().async { if let strongSelf = self { @@ -1120,7 +1135,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { isCopyProtectionEnabled = peer.isCopyProtectionEnabled } } - let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions) + let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction) let filteredEntries = chatHistoryEntriesForView( location: chatLocation, @@ -1353,6 +1368,14 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } + self.refreshDisplayedItemRangeTimer = SwiftSignalKit.Timer(timeout: 10.0, repeat: true, completion: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateVisibleItemRange(force: true) + }, queue: .mainQueue()) + self.refreshDisplayedItemRangeTimer?.start() + let appConfiguration = context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) |> take(1) |> map { view in @@ -1511,7 +1534,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.canReadHistoryDisposable?.dispose() self.loadedMessagesFromCachedDataDisposable?.dispose() self.preloadAdPeerDisposable.dispose() - //self.scrollNavigationDisposable.dispose() + self.refreshDisplayedItemRangeTimer?.invalidate() } public func setLoadStateUpdated(_ f: @escaping (ChatHistoryNodeLoadState, Bool) -> Void) { @@ -1842,7 +1865,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } if !hasAction { - messageIdsWithPossibleReactions.append(message.id) + switch message.id.peerId.namespace { + case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel: + messageIdsWithPossibleReactions.append(message.id) + default: + break + } } if contentRequiredValidation { messageIdsWithUnsupportedMedia.append(message.id) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index d9dc7d04ec..1ae675a1ec 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1216,51 +1216,18 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } actions.insert(.custom(ChatReadReportContextItem(context: context, message: message, stats: readStats, action: { c, f, stats in - if reactionCount == 0 && stats.peers.count == 1 { + if reactionCount == 0, let stats = stats, stats.peers.count == 1 { c.dismiss(completion: { controllerInteraction.openPeer(stats.peers[0].id, .default, nil) }) - } 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), reaction: nil, back: { [weak c] in - c?.popItems() - }, openPeer: { [weak c] id in - c?.dismiss(completion: { - controllerInteraction.openPeer(id, .default, nil) - }) - })), tip: nil))) - } else { - var subActions: [ContextMenuItem] = [] - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - subActions.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, textColor: .primary, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, action: { controller, _ in - controller.setItems(contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: chatPresentationInterfaceState, context: context, messages: messages, controllerInteraction: controllerInteraction, selectAll: selectAll, interfaceInteraction: interfaceInteraction, readStats: stats), minHeight: nil, previousActionsTransition: .slide(forward: false)) - }))) - - subActions.append(.separator) - - for peer in stats.peers { - let avatarSignal = peerAvatarCompleteImage(account: context.account, peer: peer, size: CGSize(width: 30.0, height: 30.0)) - - subActions.append(.action(ContextMenuActionItem(text: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), textLayout: .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: CGSize(width: 30.0, height: 30.0), signal: avatarSignal), action: { _, f in - c.dismiss(completion: { - controllerInteraction.openPeer(peer.id, .default, nil) - }) - }))) - } - - var tip: ContextController.Tip? - if messageViewsPrivacyTips < 3 { - tip = .messageViewsPrivacy - let _ = ApplicationSpecificNotice.incrementMessageViewsPrivacyTips(accountManager: context.sharedContext.accountManager).start() - } - - let minHeight = c.getActionsMinHeight() - c.setItems(.single(ContextController.Items(content: .list(subActions), tip: tip)), minHeight: minHeight, previousActionsTransition: .slide(forward: true)) - } + } else if (stats != nil && !stats!.peers.isEmpty) || reactionCount != 0 { + c.pushItems(items: .single(ContextController.Items(content: .custom(ReactionListContextMenuContent(context: context, availableReactions: availableReactions, message: EngineMessage(message), reaction: nil, readStats: stats, back: { [weak c] in + c?.popItems() + }, openPeer: { [weak c] id in + c?.dismiss(completion: { + controllerInteraction.openPeer(id, .default, nil) + }) + })), tip: nil))) } else { f(.default) } @@ -1768,6 +1735,14 @@ private final class ChatDeleteMessageContextItemNode: ASDisplayNode, ContextMenu } } + func canBeHighlighted() -> Bool { + return self.isActionEnabled + } + + func updateIsHighlighted(isHighlighted: Bool) { + self.setIsHighlighted(isHighlighted) + } + func actionNode(at point: CGPoint) -> ContextActionNodeProtocol { return self } @@ -1777,9 +1752,9 @@ final class ChatReadReportContextItem: ContextMenuCustomItem { fileprivate let context: AccountContext fileprivate let message: Message fileprivate let stats: MessageReadStats? - fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats) -> Void + fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats?) -> Void - init(context: AccountContext, message: Message, stats: MessageReadStats?, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats) -> Void) { + init(context: AccountContext, message: Message, stats: MessageReadStats?, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats?) -> Void) { self.context = context self.message = message self.stats = stats @@ -1910,6 +1885,8 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus } }) } + + item.context.account.viewTracker.updateReactionsForMessageIds(messageIds: [item.message.id], force: true) } deinit { @@ -2121,6 +2098,14 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus } private var actionTemporarilyDisabled: Bool = false + + func canBeHighlighted() -> Bool { + return self.isActionEnabled + } + + func updateIsHighlighted(isHighlighted: Bool) { + self.setIsHighlighted(isHighlighted) + } func performAction() { if self.actionTemporarilyDisabled { @@ -2131,15 +2116,22 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus self?.actionTemporarilyDisabled = false } - guard let controller = self.getController(), let currentStats = self.currentStats else { + guard let controller = self.getController() else { return } self.item.action(controller, { [weak self] result in self?.actionSelected(result) - }, currentStats) + }, self.currentStats) } var isActionEnabled: Bool { + var reactionCount = 0 + for reaction in mergedMessageReactionsAndPeers(message: self.item.message).reactions { + reactionCount += Int(reaction.count) + } + if reactionCount >= 0 { + return true + } guard let currentStats = self.currentStats else { return false } diff --git a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift index d41796e3cd..133d59024f 100644 --- a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift @@ -195,7 +195,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { animation.animator.updateFrame(layer: buttonView.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0)), completion: nil) } if let iconNode = node.iconNode { - animation.animator.updateFrame(layer: iconNode.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0)), completion: nil) + animation.animator.updateFrame(layer: iconNode.layer, frame: CGRect(x: width - 16.0, y: 4.0, width: 12.0, height: 12.0), completion: nil) } node.accessibilityArea.accessibilityLabel = title diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index a2e3bf89ae..41fc48ffca 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -565,7 +565,26 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } - let (_, refineLayout) = contentFileLayout(context, presentationData, message, message, associatedData, chatLocation, attributes, message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, false, file, automaticDownload, message.effectivelyIncoming(context.account.peerId), false, associatedData.forcedResourceStatus, statusType, nil, CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)) + let (_, refineLayout) = contentFileLayout(ChatMessageInteractiveFileNode.Arguments( + context: context, + presentationData: presentationData, + message: message, + topMessage: message, + associatedData: associatedData, + chatLocation: chatLocation, + attributes: attributes, + isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, + forcedIsEdited: false, + file: file, + automaticDownload: automaticDownload, + incoming: message.effectivelyIncoming(context.account.peerId), + isRecentActions: false, + forcedResourceStatus: associatedData.forcedResourceStatus, + dateAndStatusType: statusType, + displayReactions: false, + messageSelection: nil, + constrainedSize: CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height) + )) refineContentFileLayout = refineLayout } } else if let image = media as? TelegramMediaImage { diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift index f1a4707ab8..302fb56e16 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift @@ -70,7 +70,7 @@ private final class StatusReactionNode: ASDisplayNode { if self.value != value { self.value = value - let defaultImageSize = CGSize(width: 19.0, height: 19.0) + let defaultImageSize = CGSize(width: 17.0, height: 17.0) let imageSize: CGSize if let file = file { self.iconImageDisposable.set((context.account.postbox.mediaBox.resourceData(file.resource) @@ -576,7 +576,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { var replyCountLayoutAndApply: (TextNodeLayout, () -> TextNode)? - let reactionSize: CGFloat = 19.0 + let reactionSize: CGFloat = 17.0 var reactionCountLayoutAndApply: (TextNodeLayout, () -> TextNode)? let reactionSpacing: CGFloat = 2.0 let reactionTrailingSpacing: CGFloat = 6.0 diff --git a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift index f09a642e57..ffe13fabdd 100644 --- a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift @@ -117,7 +117,26 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!) - let (initialWidth, refineLayout) = interactiveFileLayout(item.context, item.presentationData, item.message, item.topMessage, item.associatedData, item.chatLocation, item.attributes, item.isItemPinned, item.isItemEdited, selectedFile!, automaticDownload, item.message.effectivelyIncoming(item.context.account.peerId), item.associatedData.isRecentActions, item.associatedData.forcedResourceStatus, statusType, item.message.groupingKey != nil ? selection : nil, CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height)) + let (initialWidth, refineLayout) = interactiveFileLayout(ChatMessageInteractiveFileNode.Arguments( + context: item.context, + presentationData: item.presentationData, + message: item.message, + topMessage: item.topMessage, + associatedData: item.associatedData, + chatLocation: item.chatLocation, + attributes: item.attributes, + isPinned: item.isItemPinned, + forcedIsEdited: item.isItemEdited, + file: selectedFile!, + automaticDownload: automaticDownload, + incoming: item.message.effectivelyIncoming(item.context.account.peerId), + isRecentActions: item.associatedData.isRecentActions, + forcedResourceStatus: item.associatedData.forcedResourceStatus, + dateAndStatusType: statusType, + displayReactions: true, + messageSelection: item.message.groupingKey != nil ? selection : nil, + constrainedSize: CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height) + )) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index d872cd0c39..903c5aef09 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -588,13 +588,6 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in if let strongSelf = self { - let transition: ContainedViewLayoutTransition - if animation.isAnimated { - transition = .animated(duration: 0.2, curve: .spring) - } else { - transition = .immediate - } - strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize) @@ -724,14 +717,14 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD let deliveryFailedFrame = CGRect(origin: CGPoint(x: videoFrame.maxX + deliveryFailedInset - deliveryFailedSize.width, y: videoFrame.maxY - deliveryFailedSize.height), size: deliveryFailedSize) if isAppearing { deliveryFailedNode.frame = deliveryFailedFrame - transition.animatePositionAdditive(node: deliveryFailedNode, offset: CGPoint(x: deliveryFailedInset, y: 0.0)) + animation.transition.animatePositionAdditive(node: deliveryFailedNode, offset: CGPoint(x: deliveryFailedInset, y: 0.0)) } else { - transition.updateFrame(node: deliveryFailedNode, frame: deliveryFailedFrame) + animation.animator.updateFrame(layer: deliveryFailedNode.layer, frame: deliveryFailedFrame, completion: nil) } } else if let deliveryFailedNode = strongSelf.deliveryFailedNode { strongSelf.deliveryFailedNode = nil - transition.updateAlpha(node: deliveryFailedNode, alpha: 0.0) - transition.updateFrame(node: deliveryFailedNode, frame: deliveryFailedNode.frame.offsetBy(dx: 24.0, dy: 0.0), completion: { [weak deliveryFailedNode] _ in + animation.animator.updateAlpha(layer: deliveryFailedNode.layer, alpha: 0.0, completion: nil) + animation.animator.updateFrame(layer: deliveryFailedNode.layer, frame: deliveryFailedNode.frame.offsetBy(dx: 24.0, dy: 0.0), completion: { [weak deliveryFailedNode] _ in deliveryFailedNode?.removeFromSupernode() }) } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 393f87cc06..8d4d30b2c4 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -24,6 +24,67 @@ private struct FetchControls { } final class ChatMessageInteractiveFileNode: ASDisplayNode { + final class Arguments { + let context: AccountContext + let presentationData: ChatPresentationData + let message: Message + let topMessage: Message + let associatedData: ChatMessageItemAssociatedData + let chatLocation: ChatLocation + let attributes: ChatMessageEntryAttributes + let isPinned: Bool + let forcedIsEdited: Bool + let file: TelegramMediaFile + let automaticDownload: Bool + let incoming: Bool + let isRecentActions: Bool + let forcedResourceStatus: FileMediaResourceStatus? + let dateAndStatusType: ChatMessageDateAndStatusType? + let displayReactions: Bool + let messageSelection: Bool? + let constrainedSize: CGSize + + init( + context: AccountContext, + presentationData: ChatPresentationData, + message: Message, + topMessage: Message, + associatedData: ChatMessageItemAssociatedData, + chatLocation: ChatLocation, + attributes: ChatMessageEntryAttributes, + isPinned: Bool, + forcedIsEdited: Bool, + file: TelegramMediaFile, + automaticDownload: Bool, + incoming: Bool, + isRecentActions: Bool, + forcedResourceStatus: FileMediaResourceStatus?, + dateAndStatusType: ChatMessageDateAndStatusType?, + displayReactions: Bool, + messageSelection: Bool?, + constrainedSize: CGSize + ) { + self.context = context + self.presentationData = presentationData + self.message = message + self.topMessage = topMessage + self.associatedData = associatedData + self.chatLocation = chatLocation + self.attributes = attributes + self.isPinned = isPinned + self.forcedIsEdited = forcedIsEdited + self.file = file + self.automaticDownload = automaticDownload + self.incoming = incoming + self.isRecentActions = isRecentActions + self.forcedResourceStatus = forcedResourceStatus + self.dateAndStatusType = dateAndStatusType + self.displayReactions = displayReactions + self.messageSelection = messageSelection + self.constrainedSize = constrainedSize + } + } + private var selectionNode: FileMessageSelectionNode? private let titleNode: TextNode @@ -213,7 +274,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } } - func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ topMessage: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ isPinned: Bool, _ forcedIsEdited: Bool, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> Void))) { + func asyncLayout() -> (Arguments) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> Void))) { let currentFile = self.file let titleAsyncLayout = TextNode.asyncLayout(self.titleNode) @@ -223,11 +284,11 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { let currentMessage = self.message - return { context, presentationData, message, topMessage, associatedData, chatLocation, attributes, isPinned, forcedIsEdited, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize in + return { arguments in return (CGFloat.greatestFiniteMagnitude, { constrainedSize in - let titleFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 16.0 / 17.0)) - let descriptionFont = Font.with(size: floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0), design: .regular, weight: .regular, traits: [.monospacedNumbers]) - let durationFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 11.0 / 17.0)) + let titleFont = Font.regular(floor(arguments.presentationData.fontSize.baseDisplaySize * 16.0 / 17.0)) + let descriptionFont = Font.with(size: floor(arguments.presentationData.fontSize.baseDisplaySize * 13.0 / 17.0), design: .regular, weight: .regular, traits: [.monospacedNumbers]) + let durationFont = Font.regular(floor(arguments.presentationData.fontSize.baseDisplaySize * 11.0 / 17.0)) var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? var updatedStatusSignal: Signal<(FileMediaResourceStatus, MediaResourceStatus?), NoError>? @@ -237,58 +298,58 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { var mediaUpdated = false if let currentFile = currentFile { - mediaUpdated = file != currentFile + mediaUpdated = arguments.file != currentFile } else { mediaUpdated = true } var statusUpdated = mediaUpdated - if currentMessage?.id != message.id || currentMessage?.flags != message.flags { + if currentMessage?.id != arguments.message.id || currentMessage?.flags != arguments.message.flags { statusUpdated = true } - let hasThumbnail = (!file.previewRepresentations.isEmpty || file.immediateThumbnailData != nil) && !file.isMusic && !file.isVoice + let hasThumbnail = (!arguments.file.previewRepresentations.isEmpty || arguments.file.immediateThumbnailData != nil) && !arguments.file.isMusic && !arguments.file.isVoice if mediaUpdated { - if largestImageRepresentation(file.previewRepresentations) != nil || file.immediateThumbnailData != nil { - updateImageSignal = chatMessageImageFile(account: context.account, fileReference: .message(message: MessageReference(message), media: file), thumbnail: true) + if largestImageRepresentation(arguments.file.previewRepresentations) != nil || arguments.file.immediateThumbnailData != nil { + updateImageSignal = chatMessageImageFile(account: arguments.context.account, fileReference: .message(message: MessageReference(arguments.message), media: arguments.file), thumbnail: true) } updatedFetchControls = FetchControls(fetch: { [weak self] in if let strongSelf = self { - strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, message: message, file: file, userInitiated: true).start()) + strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: arguments.context, message: arguments.message, file: arguments.file, userInitiated: true).start()) } }, cancel: { - messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: file) + messageMediaFileCancelInteractiveFetch(context: arguments.context, messageId: arguments.message.id, file: arguments.file) }) } if statusUpdated { - if message.flags.isSending { - updatedStatusSignal = combineLatest(messageFileMediaResourceStatus(context: context, file: file, message: message, isRecentActions: isRecentActions), messageMediaFileStatus(context: context, messageId: message.id, file: file)) + if arguments.message.flags.isSending { + updatedStatusSignal = combineLatest(messageFileMediaResourceStatus(context: arguments.context, file: arguments.file, message: arguments.message, isRecentActions: arguments.isRecentActions), messageMediaFileStatus(context: arguments.context, messageId: arguments.message.id, file: arguments.file)) |> map { resourceStatus, actualFetchStatus -> (FileMediaResourceStatus, MediaResourceStatus?) in return (resourceStatus, actualFetchStatus) } - updatedAudioLevelEventsSignal = messageFileMediaPlaybackAudioLevelEvents(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: false) + updatedAudioLevelEventsSignal = messageFileMediaPlaybackAudioLevelEvents(context: arguments.context, file: arguments.file, message: arguments.message, isRecentActions: arguments.isRecentActions, isGlobalSearch: false) } else { - updatedStatusSignal = messageFileMediaResourceStatus(context: context, file: file, message: message, isRecentActions: isRecentActions) + updatedStatusSignal = messageFileMediaResourceStatus(context: arguments.context, file: arguments.file, message: arguments.message, isRecentActions: arguments.isRecentActions) |> map { resourceStatus -> (FileMediaResourceStatus, MediaResourceStatus?) in return (resourceStatus, nil) } - updatedAudioLevelEventsSignal = messageFileMediaPlaybackAudioLevelEvents(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: false) + updatedAudioLevelEventsSignal = messageFileMediaPlaybackAudioLevelEvents(context: arguments.context, file: arguments.file, message: arguments.message, isRecentActions: arguments.isRecentActions, isGlobalSearch: false) } - updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: false) + updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: arguments.context, file: arguments.file, message: arguments.message, isRecentActions: arguments.isRecentActions, isGlobalSearch: false) } var consumableContentIcon: UIImage? - for attribute in message.attributes { + for attribute in arguments.message.attributes { if let attribute = attribute as? ConsumableContentMessageAttribute { let isConsumed = attribute.consumed if !isConsumed { - if incoming { - consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentIncomingIcon(presentationData.theme.theme) + if arguments.incoming { + consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentIncomingIcon(arguments.presentationData.theme.theme) } else { - consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentOutgoingIcon(presentationData.theme.theme) + consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentOutgoingIcon(arguments.presentationData.theme.theme) } } break @@ -303,20 +364,20 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { var isVoice = false var audioDuration: Int32 = 0 - let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing + let messageTheme = arguments.incoming ? arguments.presentationData.theme.theme.chat.message.incoming : arguments.presentationData.theme.theme.chat.message.outgoing - for attribute in file.attributes { + for attribute in arguments.file.attributes { if case let .Audio(voice, duration, title, performer, waveform) = attribute { isAudio = true - if let forcedResourceStatus = forcedResourceStatus, statusUpdated { + if let forcedResourceStatus = arguments.forcedResourceStatus, statusUpdated { updatedStatusSignal = .single((forcedResourceStatus, nil)) } else if let currentUpdatedStatusSignal = updatedStatusSignal { updatedStatusSignal = currentUpdatedStatusSignal |> map { status, _ in switch status.mediaStatus { case let .fetchStatus(fetchStatus): - if !voice && !message.flags.isSending { + if !voice && !arguments.message.flags.isSending { return (FileMediaResourceStatus(mediaStatus: .fetchStatus(.Local), fetchStatus: status.fetchStatus), nil) } else { return (FileMediaResourceStatus(mediaStatus: .fetchStatus(fetchStatus), fetchStatus: status.fetchStatus), nil) @@ -336,12 +397,12 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { audioWaveform = AudioWaveform(bitstream: waveform, bitsPerSample: 5) } } else { - candidateTitleString = NSAttributedString(string: title ?? (file.fileName ?? "Unknown Track"), font: titleFont, textColor: messageTheme.fileTitleColor) + candidateTitleString = NSAttributedString(string: title ?? (arguments.file.fileName ?? "Unknown Track"), font: titleFont, textColor: messageTheme.fileTitleColor) let descriptionText: String if let performer = performer { descriptionText = performer - } else if let size = file.size { - descriptionText = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: presentationData)) + } else if let size = arguments.file.size { + descriptionText = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: arguments.presentationData)) } else { descriptionText = "" } @@ -357,15 +418,15 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { if let candidateTitleString = candidateTitleString { titleString = candidateTitleString } else if !isVoice { - titleString = NSAttributedString(string: file.fileName ?? "File", font: titleFont, textColor: messageTheme.fileTitleColor) + titleString = NSAttributedString(string: arguments.file.fileName ?? "File", font: titleFont, textColor: messageTheme.fileTitleColor) } if let candidateDescriptionString = candidateDescriptionString { descriptionString = candidateDescriptionString } else if !isVoice { let descriptionText: String - if let size = file.size { - descriptionText = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: presentationData)) + if let size = arguments.file.size { + descriptionText = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: arguments.presentationData)) } else { descriptionText = "" } @@ -383,7 +444,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let fileSizeString: String - if let _ = file.size { + if let _ = arguments.file.size { fileSizeString = "000.0 MB" } else { fileSizeString = "" @@ -423,47 +484,54 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))? - if let statusType = dateAndStatusType { + if let statusType = arguments.dateAndStatusType { var edited = false - if attributes.updatingMedia != nil { + if arguments.attributes.updatingMedia != nil { edited = true } var viewCount: Int? var dateReplies = 0 - let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: topMessage) - for attribute in message.attributes { + let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: arguments.topMessage) + for attribute in arguments.message.attributes { if let attribute = attribute as? EditedMessageAttribute { edited = !attribute.isHidden } else if let attribute = attribute as? ViewCountMessageAttribute { viewCount = attribute.count - } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = chatLocation { - if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { + } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = arguments.chatLocation { + if let channel = arguments.message.peers[arguments.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } } } - if forcedIsEdited { + if arguments.forcedIsEdited { edited = true } - let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings) + let dateText = stringForMessageTimestampStatus(accountPeerId: arguments.context.account.peerId, message: arguments.message, dateTimeFormat: arguments.presentationData.dateTimeFormat, nameDisplayOrder: arguments.presentationData.nameDisplayOrder, strings: arguments.presentationData.strings) + + let displayReactionsInline = shouldDisplayInlineDateReactions(message: arguments.message) + var reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings? + + if displayReactionsInline || arguments.displayReactions { + reactionSettings = ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: displayReactionsInline, preferAdditionalInset: !displayReactionsInline) + } statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( - context: context, - presentationData: presentationData, + context: arguments.context, + presentationData: arguments.presentationData, edited: edited, impressionCount: viewCount, dateText: dateText, type: statusType, - layoutInput: .trailingContent(contentWidth: iconFrame == nil ? 1000.0 : controlAreaWidth, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message), preferAdditionalInset: !shouldDisplayInlineDateReactions(message: message))), + layoutInput: .trailingContent(contentWidth: iconFrame == nil ? 1000.0 : controlAreaWidth, reactionSettings: reactionSettings), constrainedSize: constrainedSize, - availableReactions: associatedData.availableReactions, + availableReactions: arguments.associatedData.availableReactions, reactions: dateReactionsAndPeers.reactions, reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, - isPinned: isPinned && !associatedData.isInPinnedListMode, - hasAutoremove: message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: message) + isPinned: arguments.isPinned && !arguments.associatedData.isInPinnedListMode, + hasAutoremove: arguments.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: arguments.message) )) } @@ -488,9 +556,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { if hasThumbnail { fileIconImage = nil } else { - let principalGraphics = PresentationResourcesChat.principalGraphics(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) + let principalGraphics = PresentationResourcesChat.principalGraphics(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, bubbleCorners: arguments.presentationData.chatBubbleCorners) - fileIconImage = incoming ? principalGraphics.radialIndicatorFileIconIncoming : principalGraphics.radialIndicatorFileIconOutgoing + fileIconImage = arguments.incoming ? principalGraphics.radialIndicatorFileIconIncoming : principalGraphics.radialIndicatorFileIconOutgoing } return (minLayoutWidth, { boundingWidth in @@ -537,7 +605,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } let streamingCacheStatusFrame: CGRect - if (isAudio && !isVoice) || file.previewRepresentations.isEmpty { + if (isAudio && !isVoice) || arguments.file.previewRepresentations.isEmpty { streamingCacheStatusFrame = CGRect(origin: CGPoint(x: progressFrame.maxX - streamingProgressDiameter + 2.0, y: progressFrame.maxY - streamingProgressDiameter + 2.0), size: CGSize(width: streamingProgressDiameter, height: streamingProgressDiameter)) } else { streamingCacheStatusFrame = CGRect() @@ -545,10 +613,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { return (fittedLayoutSize, { [weak self] synchronousLoads, animation in if let strongSelf = self { - strongSelf.context = context - strongSelf.presentationData = presentationData - strongSelf.message = message - strongSelf.file = file + strongSelf.context = arguments.context + strongSelf.presentationData = arguments.presentationData + strongSelf.message = arguments.message + strongSelf.file = arguments.file let _ = titleApply() let _ = descriptionApply() @@ -604,7 +672,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } strongSelf.waveformScrubbingNode?.frame = CGRect(origin: CGPoint(x: 57.0, y: 1.0), size: CGSize(width: boundingWidth - 60.0, height: 15.0)) let waveformColor: UIColor - if incoming { + if arguments.incoming { if consumableContentIcon != nil { waveformColor = messageTheme.mediaActiveControlColor } else { @@ -680,8 +748,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { })) } - strongSelf.waveformNode.displaysAsynchronously = !presentationData.isPreview - strongSelf.statusNode?.displaysAsynchronously = !presentationData.isPreview + strongSelf.waveformNode.displaysAsynchronously = !arguments.presentationData.isPreview + strongSelf.statusNode?.displaysAsynchronously = !arguments.presentationData.isPreview strongSelf.statusNode?.frame = CGRect(origin: CGPoint(), size: progressFrame.size) strongSelf.statusContainerNode.frame = progressFrame @@ -695,14 +763,14 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { if let updatedFetchControls = updatedFetchControls { let _ = strongSelf.fetchControls.swap(updatedFetchControls) - if automaticDownload { + if arguments.automaticDownload { updatedFetchControls.fetch() } } let isAnimated = !synchronousLoads let transition: ContainedViewLayoutTransition = isAnimated ? .animated(duration: 0.2, curve: .spring) : .immediate - if let selection = messageSelection { + if let selection = arguments.messageSelection { if let streamingStatusNode = strongSelf.streamingStatusNode { transition.updateAlpha(node: streamingStatusNode, alpha: 0.0) transition.updateTransformScale(node: streamingStatusNode, scale: 0.2) @@ -713,14 +781,14 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { selectionNode.updateSelected(selection, animated: isAnimated) } else { let type: FileMessageSelectionNode.NodeType - if file.isVoice { + if arguments.file.isVoice { type = .voice - } else if file.isMusic || file.previewRepresentations.isEmpty { + } else if arguments.file.isMusic || arguments.file.previewRepresentations.isEmpty { type = .file } else { type = .media } - let selectionNode = FileMessageSelectionNode(theme: presentationData.theme.theme, incoming: incoming, type: type, toggle: { [weak self] value in + let selectionNode = FileMessageSelectionNode(theme: arguments.presentationData.theme.theme, incoming: arguments.incoming, type: type, toggle: { [weak self] value in self?.toggleSelection(value) }) strongSelf.selectionNode = selectionNode @@ -750,7 +818,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { strongSelf.updateStatus(animated: isAnimated) - if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported) { + if let forwardInfo = arguments.message.forwardInfo, forwardInfo.flags.contains(.isImported) { strongSelf.dateAndStatusNode.pressed = { guard let strongSelf = self else { return @@ -1062,12 +1130,12 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { self.fetchingCompactTextNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: fetchingCompactSize) } - static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ topMessage: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ isPinned: Bool, _ forcedIsEdited: Bool, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> ChatMessageInteractiveFileNode))) { + static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (Arguments) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> ChatMessageInteractiveFileNode))) { let currentAsyncLayout = node?.asyncLayout() - return { context, presentationData, message, topMessage, associatedData, chatLocation, attributes, isPinned, forcedIsEdited, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize in + return { arguments in var fileNode: ChatMessageInteractiveFileNode - var fileLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ topMessage: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ isPinned: Bool, _ forcedIsEdited: Bool, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> Void))) + var fileLayout: (Arguments) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { fileNode = node @@ -1077,7 +1145,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { fileLayout = fileNode.asyncLayout() } - let (initialWidth, continueLayout) = fileLayout(context, presentationData, message, topMessage, associatedData, chatLocation, attributes, isPinned, forcedIsEdited, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize) + let (initialWidth, continueLayout) = fileLayout(arguments) return (initialWidth, { constrainedSize in let (finalWidth, finalLayout) = continueLayout(constrainedSize) diff --git a/submodules/TelegramUI/Sources/ChatMessageThrottledProcessingManager.swift b/submodules/TelegramUI/Sources/ChatMessageThrottledProcessingManager.swift index 712db909a9..5816f49a00 100644 --- a/submodules/TelegramUI/Sources/ChatMessageThrottledProcessingManager.swift +++ b/submodules/TelegramUI/Sources/ChatMessageThrottledProcessingManager.swift @@ -7,16 +7,18 @@ final class ChatMessageThrottledProcessingManager { private let queue = Queue() private let delay: Double + private let submitInterval: Double? var process: ((Set) -> Void)? private var timer: SwiftSignalKit.Timer? private var processedList: [MessageId] = [] - private var processed = Set() + private var processed: [MessageId: Double] = [:] private var buffer = Set() - init(delay: Double = 1.0) { + init(delay: Double = 1.0, submitInterval: Double? = nil) { self.delay = delay + self.submitInterval = submitInterval } func setProcess(process: @escaping (Set) -> Void) { @@ -27,9 +29,17 @@ final class ChatMessageThrottledProcessingManager { func add(_ messageIds: [MessageId]) { self.queue.async { + let timestamp = CFAbsoluteTimeGetCurrent() + for id in messageIds { - if !self.processed.contains(id) { - self.processed.insert(id) + if let processedTimestamp = self.processed[id] { + if let submitInterval = self.submitInterval, (timestamp - processedTimestamp) >= submitInterval { + self.processed[id] = timestamp + self.processedList.append(id) + self.buffer.insert(id) + } + } else { + self.processed[id] = timestamp self.processedList.append(id) self.buffer.insert(id) } @@ -37,7 +47,7 @@ final class ChatMessageThrottledProcessingManager { if self.processedList.count > 1000 { for i in 0 ..< 200 { - self.processed.remove(self.processedList[i]) + self.processed.removeValue(forKey: self.processedList[i]) } self.processedList.removeSubrange(0 ..< 200) } diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift index 0118afeb5e..8867084c92 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift @@ -114,7 +114,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.titleUpdated(title: new) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .changeAbout(prev, new): var peers = SimpleDictionary() var author: Peer? @@ -145,14 +145,14 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: let peers = SimpleDictionary() let attributes: [MessageAttribute] = [] let prevMessage = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: prev, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: new, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.isEmpty ? .eventLogPreviousDescription(prevMessage) : nil) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.isEmpty ? .eventLogPreviousDescription(prevMessage) : nil) } case let .changeUsername(prev, new): var peers = SimpleDictionary() @@ -183,7 +183,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action: TelegramMediaActionType = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var previousAttributes: [MessageAttribute] = [] var attributes: [MessageAttribute] = [] @@ -202,7 +202,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let prevMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: prevText, attributes: previousAttributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.isEmpty ? .eventLogPreviousLink(prevMessage) : nil) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.isEmpty ? .eventLogPreviousLink(prevMessage) : nil) } case let .changePhoto(_, new): var peers = SimpleDictionary() @@ -221,7 +221,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.photoUpdated(image: photo) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .toggleInvites(value): var peers = SimpleDictionary() var author: Peer? @@ -248,7 +248,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .toggleSignatures(value): var peers = SimpleDictionary() var author: Peer? @@ -275,7 +275,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .updatePinned(message): switch self.id.contentIndex { case .header: @@ -306,7 +306,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: if let message = message { var peers = SimpleDictionary() @@ -324,7 +324,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } else { var peers = SimpleDictionary() var author: Peer? @@ -346,7 +346,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 0), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } } case let .editMessage(prev, message): @@ -391,7 +391,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -408,7 +408,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: filterOriginalMessageFlags(message), read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.text.isEmpty || !message.text.isEmpty ? .eventLogPreviousMessage(filterOriginalMessageFlags(prev)) : nil) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: filterOriginalMessageFlags(message), read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.text.isEmpty || !message.text.isEmpty ? .eventLogPreviousMessage(filterOriginalMessageFlags(prev)) : nil) } case let .deleteMessage(message): switch self.id.contentIndex { @@ -434,7 +434,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -458,7 +458,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } case .participantJoin, .participantLeave: var peers = SimpleDictionary() @@ -476,7 +476,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { action = TelegramMediaActionType.removedMembers(peerIds: [self.entry.event.peerId]) } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .participantInvite(participant): var peers = SimpleDictionary() var author: Peer? @@ -493,7 +493,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action: TelegramMediaActionType action = TelegramMediaActionType.addedMembers(peerIds: [participant.peer.id]) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .participantToggleBan(prev, new): var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -623,7 +623,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .participantToggleAdmin(prev, new): var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -856,7 +856,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .changeStickerPack(_, new): var peers = SimpleDictionary() var author: Peer? @@ -885,7 +885,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .togglePreHistoryHidden(value): var peers = SimpleDictionary() var author: Peer? @@ -915,7 +915,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .updateDefaultBannedRights(prev, new): var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -973,7 +973,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .pollStopped(message): switch self.id.contentIndex { case .header: @@ -1001,7 +1001,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -1018,7 +1018,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.author, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: filterOriginalMessageFlags(message), read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: nil) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: filterOriginalMessageFlags(message), read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: nil) } case let .linkedPeerUpdated(previous, updated): var peers = SimpleDictionary() @@ -1074,7 +1074,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .changeGeoLocation(_, updated): var peers = SimpleDictionary() var author: Peer? @@ -1096,12 +1096,12 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let mediaMap = TelegramMediaMap(latitude: updated.latitude, longitude: updated.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: [], media: [mediaMap], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } else { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } case let .updateSlowmode(_, newValue): var peers = SimpleDictionary() @@ -1132,7 +1132,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .startGroupCall, .endGroupCall: var peers = SimpleDictionary() var author: Peer? @@ -1169,7 +1169,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .groupCallUpdateParticipantMuteStatus(participantId, isMuted): var peers = SimpleDictionary() var author: Peer? @@ -1203,7 +1203,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .updateGroupCallSettings(joinMuted): var peers = SimpleDictionary() var author: Peer? @@ -1232,7 +1232,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .groupCallUpdateParticipantVolume(participantId, volume): var peers = SimpleDictionary() var author: Peer? @@ -1263,7 +1263,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .deleteExportedInvitation(invite): var peers = SimpleDictionary() var author: Peer? @@ -1289,7 +1289,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .revokeExportedInvitation(invite): var peers = SimpleDictionary() var author: Peer? @@ -1315,7 +1315,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .editExportedInvitation(_, updatedInvite): var peers = SimpleDictionary() var author: Peer? @@ -1341,7 +1341,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .participantJoinedViaInvite(invite): var peers = SimpleDictionary() var author: Peer? @@ -1367,7 +1367,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .changeHistoryTTL(_, updatedValue): var peers = SimpleDictionary() var author: Peer? @@ -1398,7 +1398,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .changeTheme(_, updatedValue): var peers = SimpleDictionary() var author: Peer? @@ -1429,7 +1429,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .participantJoinByRequest(invite, approvedBy): var peers = SimpleDictionary() var author: Peer? @@ -1462,7 +1462,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .toggleCopyProtection(value): var peers = SimpleDictionary() var author: Peer? @@ -1489,7 +1489,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .sendMessage(message): switch self.id.contentIndex { case .header: @@ -1514,7 +1514,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -1531,7 +1531,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } } } diff --git a/submodules/TelegramUI/Sources/ChatSendAsPeerListContextItem.swift b/submodules/TelegramUI/Sources/ChatSendAsPeerListContextItem.swift index cd4293db3b..92fb7d3a96 100644 --- a/submodules/TelegramUI/Sources/ChatSendAsPeerListContextItem.swift +++ b/submodules/TelegramUI/Sources/ChatSendAsPeerListContextItem.swift @@ -215,6 +215,14 @@ private final class ChatSendAsPeerListContextItemNode: ASDisplayNode, ContextMen func setIsHighlighted(_ value: Bool) { } + func canBeHighlighted() -> Bool { + return self.isActionEnabled + } + + func updateIsHighlighted(isHighlighted: Bool) { + self.setIsHighlighted(isHighlighted) + } + func actionNode(at point: CGPoint) -> ContextActionNodeProtocol { for actionNode in self.actionNodes { let frame = actionNode.convert(actionNode.bounds, to: self) diff --git a/submodules/TelegramUI/Sources/ChatSendAsPeerTitleContextItem.swift b/submodules/TelegramUI/Sources/ChatSendAsPeerTitleContextItem.swift index 67375e0284..b8f843f98a 100644 --- a/submodules/TelegramUI/Sources/ChatSendAsPeerTitleContextItem.swift +++ b/submodules/TelegramUI/Sources/ChatSendAsPeerTitleContextItem.swift @@ -75,4 +75,14 @@ private final class ChatSendAsPeerTitleContextItemNode: ASDisplayNode, ContextMe let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 12.0 / 17.0) self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.secondaryColor) } + + func canBeHighlighted() -> Bool { + return false + } + + func updateIsHighlighted(isHighlighted: Bool) { + } + + func performAction() { + } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 7d6789dbe6..fc84b8c5f4 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -716,7 +716,7 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p } else { stickersLabel = "" } - items[.advanced]!.append(PeerInfoScreenDisclosureItem(id: 5, label: .badge(stickersLabel, presentationData.theme.list.itemAccentColor), text: presentationData.strings.ChatSettings_Stickers, icon: PresentationResourcesSettings.stickers, action: { + items[.advanced]!.append(PeerInfoScreenDisclosureItem(id: 5, label: .badge(stickersLabel, presentationData.theme.list.itemAccentColor), text: presentationData.strings.ChatSettings_StickersAndReactions, icon: PresentationResourcesSettings.stickers, action: { interaction.openSettings(.stickers) })) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 7fd0a6c3d5..27ee669a26 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1235,7 +1235,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return PeerSelectionControllerImpl(params) } - public func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)? = nil, clickThroughMessage: (() -> Void)? = nil, backgroundNode: ASDisplayNode?) -> ListViewItem { + public func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)? = nil, clickThroughMessage: (() -> Void)? = nil, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?) -> ListViewItem { let controllerInteraction: ChatControllerInteraction controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in @@ -1309,7 +1309,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { chatLocation = .peer(messages.first!.id.peerId) } - return ChatMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: true), context: context, chatLocation: chatLocation, associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact, automaticDownloadNetworkType: .cellular, isRecentActions: false, subject: nil, contactsPeerIds: Set(), animatedEmojiStickers: [:], forcedResourceStatus: forcedResourceStatus, availableReactions: nil), controllerInteraction: controllerInteraction, content: content, disableDate: true, additionalContent: nil) + return ChatMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: true), context: context, chatLocation: chatLocation, associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact, automaticDownloadNetworkType: .cellular, isRecentActions: false, subject: nil, contactsPeerIds: Set(), animatedEmojiStickers: [:], forcedResourceStatus: forcedResourceStatus, availableReactions: availableReactions, defaultReaction: nil), controllerInteraction: controllerInteraction, content: content, disableDate: true, additionalContent: nil) } public func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader { From ee7873b8f2d67964c4d215ae5ad79efc795e4355 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 21 Dec 2021 04:29:31 +0400 Subject: [PATCH 16/35] Implement reaction preview --- .../Sources/ChatController.swift | 4 ++ .../Sources/ItemListController.swift | 9 +++ .../QuickReactionSetupController.swift | 12 ++++ .../Reactions/ReactionChatPreviewItem.swift | 69 +++++++++++++++++++ .../Sources/ChatMessageItemView.swift | 4 +- 5 files changed, 96 insertions(+), 2 deletions(-) diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index c7e32e93fb..f82c3d695e 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -551,3 +551,7 @@ public enum FileMediaResourceMediaStatus: Equatable { case fetchStatus(MediaResourceStatus) case playbackStatus(FileMediaResourcePlaybackStatus) } + +public protocol ChatMessageItemNodeProtocol: ListViewItemNode { + func targetReactionView(value: String) -> UIView? +} diff --git a/submodules/ItemListUI/Sources/ItemListController.swift b/submodules/ItemListUI/Sources/ItemListController.swift index e504a7d712..88607d6f23 100644 --- a/submodules/ItemListUI/Sources/ItemListController.swift +++ b/submodules/ItemListUI/Sources/ItemListController.swift @@ -206,6 +206,14 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable } } + public var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?) -> Void)? { + didSet { + if self.isNodeLoaded { + (self.displayNode as! ItemListControllerNode).listNode.didScrollWithOffset = self.didScrollWithOffset + } + } + } + public var willScrollToTop: (() -> Void)? public func setReorderEntry(_ f: @escaping (Int, Int, [T]) -> Signal) { @@ -471,6 +479,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable displayNode.reorderEntry = self.reorderEntry displayNode.reorderCompleted = self.reorderCompleted displayNode.listNode.experimentalSnapScrollToItem = self.experimentalSnapScrollToItem + displayNode.listNode.didScrollWithOffset = self.didScrollWithOffset displayNode.requestLayout = { [weak self] transition in self?.requestLayout(transition: transition) } diff --git a/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift b/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift index 8322d574c5..808f1d3101 100644 --- a/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift +++ b/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift @@ -326,6 +326,18 @@ public func quickReactionSetupController( } let controller = ItemListController(context: context, state: signal) + + controller.didScrollWithOffset = { [weak controller] offset, transition, _ in + guard let controller = controller else { + return + } + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ReactionChatPreviewItemNode { + itemNode.standaloneReactionAnimation?.addRelativeContentOffset(CGPoint(x: 0.0, y: offset), transition: transition) + } + } + } + dismissImpl = { [weak controller] in guard let controller = controller else { return diff --git a/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift index f746d7aa62..ed105b9b1f 100644 --- a/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift @@ -12,6 +12,7 @@ import PresentationDataUtils import AccountContext import WallpaperBackgroundNode import AvatarNode +import ReactionSelectionNode class ReactionChatPreviewItem: ListViewItem, ItemListItem { let context: AccountContext @@ -86,6 +87,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode { private var messageNode: ListViewItemNode? private var item: ReactionChatPreviewItem? + private(set) weak var standaloneReactionAnimation: StandaloneReactionAnimation? init() { self.topStripeNode = ASDisplayNode() @@ -109,6 +111,63 @@ class ReactionChatPreviewItemNode: ListViewItemNode { self.addSubnode(self.containerNode) } + override func didLoad() { + super.didLoad() + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { _ in + return .waitForDoubleTap + } + self.view.addGestureRecognizer(recognizer) + } + + @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .doubleTap: + if let item = self.item, let updatedReaction = item.reaction, let availableReactions = item.availableReactions, let messageNode = self.messageNode as? ChatMessageItemNodeProtocol { + if let targetView = messageNode.targetReactionView(value: updatedReaction) { + for reaction in availableReactions.reactions { + if reaction.value == updatedReaction { + if let standaloneReactionAnimation = self.standaloneReactionAnimation { + standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in + standaloneReactionAnimation?.removeFromSupernode() + }) + self.standaloneReactionAnimation = nil + } + + if let supernode = self.supernode { + let standaloneReactionAnimation = StandaloneReactionAnimation(context: item.context, theme: item.theme, reaction: ReactionContextItem( + reaction: ReactionContextItem.Reaction(rawValue: reaction.value), + stillAnimation: reaction.selectAnimation, + listAnimation: reaction.activateAnimation, + applicationAnimation: reaction.effectAnimation + )) + self.standaloneReactionAnimation = standaloneReactionAnimation + + supernode.addSubnode(standaloneReactionAnimation) + standaloneReactionAnimation.frame = supernode.bounds + standaloneReactionAnimation.animateReactionSelection(targetView: targetView, hideNode: true, completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + }) + } + + break + } + } + } + } + default: + break + } + } + default: + break + } + } + func asyncLayout() -> (_ item: ReactionChatPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let currentNode = self.messageNode @@ -161,6 +220,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode { node = messageNode apply().1(ListViewItemApply(isOnScreen: true)) }) + node?.isUserInteractionEnabled = false } var contentSize = CGSize(width: params.width, height: 8.0 + 8.0) @@ -174,6 +234,15 @@ class ReactionChatPreviewItemNode: ListViewItemNode { return (layout, { [weak self] in if let strongSelf = self { + if let previousItem = strongSelf.item, previousItem.reaction != item.reaction { + if let standaloneReactionAnimation = strongSelf.standaloneReactionAnimation { + standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in + standaloneReactionAnimation?.removeFromSupernode() + }) + strongSelf.standaloneReactionAnimation = nil + } + } + strongSelf.item = item strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) diff --git a/submodules/TelegramUI/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Sources/ChatMessageItemView.swift index 212c3f51ca..f08b5e9318 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItemView.swift @@ -675,7 +675,7 @@ final class ChatMessageAccessibilityData { } } -public class ChatMessageItemView: ListViewItemNode { +public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { let layoutConstants = (ChatMessageItemLayoutConstants.compact, ChatMessageItemLayoutConstants.regular) var item: ChatMessageItem? @@ -872,7 +872,7 @@ public class ChatMessageItemView: ListViewItemNode { func openMessageContextMenu() { } - func targetReactionView(value: String) -> UIView? { + public func targetReactionView(value: String) -> UIView? { return nil } From fc8787d7320a1cc6e654105e7eb4a2a0390be312 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 21 Dec 2021 05:20:53 +0400 Subject: [PATCH 17/35] Various Improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 11 ++ .../LocalizationListController.swift | 3 +- .../LocalizationListControllerNode.swift | 97 ++++++++++-- .../TranslatonSettingsController.swift | 147 ++++++++++++++++++ .../ChatInterfaceStateContextMenus.swift | 22 ++- .../ChatMessageInstantVideoItemNode.swift | 28 +++- .../Sources/PostboxKeys.swift | 2 + .../Sources/TranslationSettings.swift | 58 +++++++ submodules/Translate/Sources/Translate.swift | 46 ++++++ 9 files changed, 389 insertions(+), 25 deletions(-) create mode 100644 submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift create mode 100644 submodules/TelegramUIPreferences/Sources/TranslationSettings.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 5074a8ff8e..e6bd91b35e 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7149,3 +7149,14 @@ Sorry for the inconvenience."; "Conversation.ContextMenuTranslate" = "Translate"; "ClearCache.ClearDescription" = "All media will stay in the Telegram cloud and can be re-downloaded if you need it again."; + +"Localization.TranslateMessages" = "Translate Messages"; +"Localization.ShowTranslate" = "Show Translate Button"; +"Localization.ShowTranslateInfo" = "Show 'Translate' button in the message action menu."; +"Localization.DoNotTranslate" = "Do Not Translate"; +"Localization.DoNotTranslateInfo" = "Do not show 'Translate' button in the message action menu for this language"; +"Localization.DoNotTranslateManyInfo" = "Do not show 'Translate' button in the message action menu for this languages"; + +"Localization.InterfaceLanguage" = "Interface Language"; + +"DoNotTranslate.Title" = "Do Not Translate"; diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift index fc42531e3e..82d7fef26d 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift @@ -92,7 +92,6 @@ public class LocalizationListController: ViewController { self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.controllerNode.updatePresentationData(self.presentationData) - let editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.editPressed)) let doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) if self.navigationItem.rightBarButtonItem === self.editItem { @@ -124,6 +123,8 @@ public class LocalizationListController: ViewController { } }, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) + }, push: { [weak self] c in + self?.push(c) }) self.controllerNode.listNode.visibleContentOffsetChanged = { [weak self] offset in diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift index 909cb6c91b..5ef5d5d003 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift @@ -14,14 +14,18 @@ import ShareController import SearchBarNode import SearchUI import UndoUI +import TelegramUIPreferences private enum LanguageListSection: ItemListSectionId { + case translate case official case unofficial } private enum LanguageListEntryId: Hashable { case search + case translate(Int) + case localizationTitle case localization(String) } @@ -31,10 +35,26 @@ private enum LanguageListEntryType { } private enum LanguageListEntry: Comparable, Identifiable { + case translateTitle(text: String) + case translate(text: String, value: Bool) + case doNotTranslate(text: String, value: String) + case translateInfo(text: String) + + case localizationTitle(text: String, section: ItemListSectionId) case localization(index: Int, info: LocalizationInfo?, type: LanguageListEntryType, selected: Bool, activity: Bool, revealed: Bool, editing: Bool) var stableId: LanguageListEntryId { switch self { + case .translateTitle: + return .translate(0) + case .translate: + return .translate(1) + case .doNotTranslate: + return .translate(2) + case .translateInfo: + return .translate(3) + case .localizationTitle: + return .localizationTitle case let .localization(index, info, _, _, _, _, _): return .localization(info?.languageCode ?? "\(index)") } @@ -42,8 +62,18 @@ private enum LanguageListEntry: Comparable, Identifiable { private func index() -> Int { switch self { + case .translateTitle: + return 0 + case .translate: + return 1 + case .doNotTranslate: + return 2 + case .translateInfo: + return 3 + case .localizationTitle: + return 1000 case let .localization(index, _, _, _, _, _, _): - return index + return 1001 + index } } @@ -51,8 +81,22 @@ private enum LanguageListEntry: Comparable, Identifiable { return lhs.index() < rhs.index() } - func item(presentationData: PresentationData, searchMode: Bool, openSearch: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) -> ListViewItem { + func item(presentationData: PresentationData, searchMode: Bool, openSearch: @escaping () -> Void, toggleShowTranslate: @escaping (Bool) -> Void, openDoNotTranslate: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) -> ListViewItem { switch self { + case let .translateTitle(text): + return ItemListSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), text: text, sectionId: LanguageListSection.translate.rawValue) + case let .translate(text, value): + return ItemListSwitchItem(presentationData: ItemListPresentationData(presentationData), title: text, value: value, sectionId: LanguageListSection.translate.rawValue, style: .blocks, updated: { value in + toggleShowTranslate(value) + }) + case let .doNotTranslate(text, value): + return ItemListDisclosureItem(presentationData: ItemListPresentationData(presentationData), title: text, label: value, sectionId: LanguageListSection.translate.rawValue, style: .blocks, action: { + openDoNotTranslate() + }) + case let .translateInfo(text): + return ItemListTextItem(presentationData: ItemListPresentationData(presentationData), text: .plain(text), sectionId: LanguageListSection.translate.rawValue) + case let .localizationTitle(text, section): + return ItemListSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), text: text, sectionId: section) case let .localization(_, info, type, selected, activity, revealed, editing): return LocalizationListItem(presentationData: ItemListPresentationData(presentationData), id: info?.languageCode ?? "", title: info?.title ?? " ", subtitle: info?.localizedTitle ?? " ", checked: selected, activity: activity, loading: info == nil, editing: LocalizationListItemEditing(editable: !selected && !searchMode && !(info?.isOfficial ?? true), editing: editing, revealed: !selected && revealed, reorderable: false), sectionId: type == .official ? LanguageListSection.official.rawValue : LanguageListSection.unofficial.rawValue, alwaysPlain: searchMode, action: { if let info = info { @@ -74,8 +118,8 @@ private func preparedLanguageListSearchContainerTransition(presentationData: Pre let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, openSearch: {}, selectLocalization: selectLocalization, setItemWithRevealedOptions: { _, _ in }, removeItem: { _ in }), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, openSearch: {}, selectLocalization: selectLocalization, setItemWithRevealedOptions: { _, _ in }, removeItem: { _ in }), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, openSearch: {}, toggleShowTranslate: { _ in }, openDoNotTranslate: {}, selectLocalization: selectLocalization, setItemWithRevealedOptions: { _, _ in }, removeItem: { _ in }), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, openSearch: {}, toggleShowTranslate: { _ in }, openDoNotTranslate: {}, selectLocalization: selectLocalization, setItemWithRevealedOptions: { _, _ in }, removeItem: { _ in }), directionHint: nil) } return LocalizationListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) } @@ -262,12 +306,12 @@ private struct LanguageListNodeTransition { let crossfade: Bool } -private func preparedLanguageListNodeTransition(presentationData: PresentationData, from fromEntries: [LanguageListEntry], to toEntries: [LanguageListEntry], openSearch: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void, firstTime: Bool, isLoading: Bool, forceUpdate: Bool, animated: Bool, crossfade: Bool) -> LanguageListNodeTransition { +private func preparedLanguageListNodeTransition(presentationData: PresentationData, from fromEntries: [LanguageListEntry], to toEntries: [LanguageListEntry], openSearch: @escaping () -> Void, toggleShowTranslate: @escaping (Bool) -> Void, openDoNotTranslate: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void, firstTime: Bool, isLoading: Bool, forceUpdate: Bool, animated: Bool, crossfade: Bool) -> LanguageListNodeTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, toggleShowTranslate: toggleShowTranslate, openDoNotTranslate: openDoNotTranslate, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, toggleShowTranslate: toggleShowTranslate, openDoNotTranslate: openDoNotTranslate, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) } return LanguageListNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, isLoading: isLoading, animated: animated, crossfade: crossfade) } @@ -279,6 +323,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { private let requestActivateSearch: () -> Void private let requestDeactivateSearch: () -> Void private let present: (ViewController, Any?) -> Void + private let push: (ViewController) -> Void private var didSetReady = false let _ready = ValuePromise() @@ -304,7 +349,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { } } - init(context: AccountContext, presentationData: PresentationData, navigationBar: NavigationBar, requestActivateSearch: @escaping () -> Void, requestDeactivateSearch: @escaping () -> Void, updateCanStartEditing: @escaping (Bool?) -> Void, present: @escaping (ViewController, Any?) -> Void) { + init(context: AccountContext, presentationData: PresentationData, navigationBar: NavigationBar, requestActivateSearch: @escaping () -> Void, requestDeactivateSearch: @escaping () -> Void, updateCanStartEditing: @escaping (Bool?) -> Void, present: @escaping (ViewController, Any?) -> Void, push: @escaping (ViewController) -> Void) { self.context = context self.presentationData = presentationData self.presentationDataValue.set(.single(presentationData)) @@ -312,6 +357,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { self.requestActivateSearch = requestActivateSearch self.requestDeactivateSearch = requestDeactivateSearch self.present = present + self.push = push self.listNode = ListView() self.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.list.blocksBackgroundColor, direction: true) @@ -373,7 +419,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { let preferencesKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.localizationListState])) let previousState = Atomic(value: nil) let previousEntriesHolder = Atomic<([LanguageListEntry], PresentationTheme, PresentationStrings)?>(value: nil) - self.listDisposable = combineLatest(queue: .mainQueue(), context.account.postbox.combinedView(keys: [preferencesKey]), context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.localizationSettings]), self.presentationDataValue.get(), self.applyingCode.get(), revealedCode.get(), self.isEditing.get()).start(next: { [weak self] view, sharedData, presentationData, applyingCode, revealedCode, isEditing in + self.listDisposable = combineLatest(queue: .mainQueue(), context.account.postbox.combinedView(keys: [preferencesKey]), context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.localizationSettings, ApplicationSpecificSharedDataKeys.translationSettings]), self.presentationDataValue.get(), self.applyingCode.get(), revealedCode.get(), self.isEditing.get()).start(next: { [weak self] view, sharedData, presentationData, applyingCode, revealedCode, isEditing in guard let strongSelf = self else { return } @@ -385,10 +431,26 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { } var existingIds = Set() + var showTranslate = true + if let translationSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) { + showTranslate = translationSettings.showTranslate + } + let localizationListState = (view.views[preferencesKey] as? PreferencesView)?.values[PreferencesKeys.localizationListState]?.get(LocalizationListState.self) if let localizationListState = localizationListState, !localizationListState.availableOfficialLocalizations.isEmpty { strongSelf.currentListState = localizationListState + if #available(iOS 15.0, *) { + entries.append(.translateTitle(text: presentationData.strings.Localization_TranslateMessages.uppercased())) + entries.append(.translate(text: presentationData.strings.Localization_ShowTranslate, value: showTranslate)) + if showTranslate { + entries.append(.doNotTranslate(text: presentationData.strings.Localization_DoNotTranslate, value: "")) + entries.append(.translateInfo(text: presentationData.strings.Localization_DoNotTranslateInfo)) + } else { + entries.append(.translateInfo(text: presentationData.strings.Localization_ShowTranslateInfo)) + } + } + let availableSavedLocalizations = localizationListState.availableSavedLocalizations.filter({ info in !localizationListState.availableOfficialLocalizations.contains(where: { $0.languageCode == info.languageCode }) }) if availableSavedLocalizations.isEmpty { updateCanStartEditing(nil) @@ -396,6 +458,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { updateCanStartEditing(isEditing) } if !availableSavedLocalizations.isEmpty { + entries.append(.localizationTitle(text: presentationData.strings.Localization_InterfaceLanguage.uppercased(), section: LanguageListSection.unofficial.rawValue)) for info in availableSavedLocalizations { if existingIds.contains(info.languageCode) { continue @@ -403,6 +466,8 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { existingIds.insert(info.languageCode) entries.append(.localization(index: entries.count, info: info, type: .unofficial, selected: info.languageCode == activeLanguageCode, activity: applyingCode == info.languageCode, revealed: revealedCode == info.languageCode, editing: isEditing)) } + } else { + entries.append(.localizationTitle(text: presentationData.strings.Localization_InterfaceLanguage.uppercased(), section: LanguageListSection.official.rawValue)) } for info in localizationListState.availableOfficialLocalizations { if existingIds.contains(info.languageCode) { @@ -420,7 +485,19 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { let previousState = previousState.swap(localizationListState) let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings)) - let transition = preparedLanguageListNodeTransition(presentationData: presentationData, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, openSearch: openSearch, selectLocalization: { [weak self] info in self?.selectLocalization(info) }, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem, firstTime: previousEntriesAndPresentationData == nil, isLoading: entries.isEmpty, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings, animated: (previousEntriesAndPresentationData?.0.count ?? 0) >= entries.count, crossfade: (previousState == nil) != (localizationListState == nil)) + let transition = preparedLanguageListNodeTransition(presentationData: presentationData, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, openSearch: openSearch, toggleShowTranslate: { value in + let _ = updateTranslationSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + var updated = current.withUpdatedShowTranslate(value) + if !value { + updated = updated.withUpdatedIgnoredLanguages(nil) + } + return updated + }).start() + }, openDoNotTranslate: { [weak self] in + if let strongSelf = self { + strongSelf.push(translationSettingsController(context: strongSelf.context)) + } + }, selectLocalization: { [weak self] info in self?.selectLocalization(info) }, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem, firstTime: previousEntriesAndPresentationData == nil, isLoading: entries.isEmpty, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings, animated: (previousEntriesAndPresentationData?.0.count ?? 0) >= entries.count, crossfade: (previousState == nil) != (localizationListState == nil)) strongSelf.enqueueTransition(transition) }) self.updatedDisposable = context.engine.localization.synchronizedLocalizationListState().start() diff --git a/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift b/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift new file mode 100644 index 0000000000..d3df163669 --- /dev/null +++ b/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift @@ -0,0 +1,147 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import TelegramStringFormatting +import AccountContext +import Translate + +private final class TranslationSettingsControllerArguments { + let context: AccountContext + let updateLanguageSelected: (String, Bool) -> Void + + init(context: AccountContext, updateLanguageSelected: @escaping (String, Bool) -> Void) { + self.context = context + self.updateLanguageSelected = updateLanguageSelected + } +} + +private enum TranslationSettingsControllerSection: Int32 { + case languages +} + +private enum TranslationSettingsControllerEntry: ItemListNodeEntry { + case language(Int32, PresentationTheme, String, String, Bool, String) + + var section: ItemListSectionId { + switch self { + case .language: + return TranslationSettingsControllerSection.languages.rawValue + } + } + + var stableId: Int32 { + switch self { + case let .language(index, _, _, _, _, _): + return index + } + } + + static func ==(lhs: TranslationSettingsControllerEntry, rhs: TranslationSettingsControllerEntry) -> Bool { + switch lhs { + case let .language(lhsIndex, lhsTheme, lhsTitle, lhsSubtitle, lhsValue, lhsCode): + if case let .language(rhsIndex, rhsTheme, rhsTitle, rhsSubtitle, rhsValue, rhsCode) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsValue == rhsValue, lhsCode == rhsCode { + return true + } else { + return false + } + } + } + + static func <(lhs: TranslationSettingsControllerEntry, rhs: TranslationSettingsControllerEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! TranslationSettingsControllerArguments + switch self { + case let .language(_, _, title, subtitle, value, code): + return LocalizationListItem(presentationData: presentationData, id: code, title: title, subtitle: subtitle, checked: value, activity: false, loading: false, editing: LocalizationListItemEditing(editable: false, editing: false, revealed: false, reorderable: false), sectionId: self.section, alwaysPlain: false, action: { + arguments.updateLanguageSelected(code, !value) + }, setItemWithRevealedOptions: { _, _ in }, removeItem: { _ in }) + } + } +} + +private func translationSettingsControllerEntries(theme: PresentationTheme, strings: PresentationStrings, settings: TranslationSettings, languages: [(String, String, String)]) -> [TranslationSettingsControllerEntry] { + var entries: [TranslationSettingsControllerEntry] = [] + + var index: Int32 = 0 + var selectedLanguages: Set + if let ignoredLanguages = settings.ignoredLanguages { + selectedLanguages = Set(ignoredLanguages) + } else { + selectedLanguages = Set([strings.baseLanguageCode]) + } + for (code, title, subtitle) in languages { + entries.append(.language(index, theme, title, subtitle, selectedLanguages.contains(code), code)) + index += 1 + } + + return entries +} + +public func translationSettingsController(context: AccountContext) -> ViewController { + let actionsDisposable = DisposableSet() + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let interfaceLanguageCode = presentationData.strings.baseLanguageCode + + let arguments = TranslationSettingsControllerArguments(context: context, updateLanguageSelected: { code, value in + let _ = updateTranslationSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + var updated = current + var updatedIgnoredLanguages = updated.ignoredLanguages ?? [] + if value { + if current.ignoredLanguages == nil { + updatedIgnoredLanguages.append(interfaceLanguageCode) + } + if !updatedIgnoredLanguages.contains(code) { + updatedIgnoredLanguages.append(code) + } + } else { + updatedIgnoredLanguages.removeAll(where: { $0 == code }) + } + updated = updated.withUpdatedIgnoredLanguages(updatedIgnoredLanguages) + return updated + }).start() + }) + + + let enLocale = Locale(identifier: "en") + var languages: [(String, String, String)] = [] + for code in supportedTranslationLanguages { + if let title = enLocale.localizedString(forLanguageCode: code) { + let languageLocale = Locale(identifier: code) + let subtitle = languageLocale.localizedString(forLanguageCode: code) ?? title + let value = (code, title.capitalized, subtitle.capitalized) + if code == interfaceLanguageCode { + languages.insert(value, at: 0) + } else { + languages.append(value) + } + } + } + + let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) + let signal = combineLatest(queue: Queue.mainQueue(), context.sharedContext.presentationData, sharedData) + |> map { presentationData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in + let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) ?? TranslationSettings.defaultSettings + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.DoNotTranslate_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: translationSettingsControllerEntries(theme: presentationData.theme, strings: presentationData.strings, settings: settings, languages: languages), style: .blocks, animateChanges: false) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(context: context, state: signal) + controller.alwaysSynchronous = true + return controller +} diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 54fcc55573..33dedbdcc5 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -24,6 +24,8 @@ import AvatarNode import AdUI import TelegramNotices import ReactionListContextMenuContent +import TelegramUIPreferences +import Translate private struct MessageContextMenuData { let starStatus: Bool? @@ -543,7 +545,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState return transaction.getCombinedPeerReadState(messages[0].id.peerId) } - let dataSignal: Signal<(MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool, Int32, AvailableReactions?), NoError> = combineLatest( + let dataSignal: Signal<(MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool, Int32, AvailableReactions?, TranslationSettings), NoError> = combineLatest( loadLimits, loadStickerSaveStatusSignal, loadResourceStatusSignal, @@ -553,9 +555,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState cachedData, readState, ApplicationSpecificNotice.getMessageViewsPrivacyTips(accountManager: context.sharedContext.accountManager), - context.engine.stickers.availableReactions() + context.engine.stickers.availableReactions(), + context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) ) - |> map { limitsAndAppConfig, stickerSaveStatus, resourceStatus, messageActions, updatingMessageMedia, cachedData, readState, messageViewsPrivacyTips, availableReactions -> (MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool, Int32, AvailableReactions?) in + |> map { limitsAndAppConfig, stickerSaveStatus, resourceStatus, messageActions, updatingMessageMedia, cachedData, readState, messageViewsPrivacyTips, availableReactions, sharedData -> (MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool, Int32, AvailableReactions?, TranslationSettings) in let (limitsConfiguration, appConfig) = limitsAndAppConfig var canEdit = false if !isAction { @@ -568,12 +571,19 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState isMessageRead = readState.isOutgoingMessageIndexRead(message.index) } - return (MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, resourceStatus: resourceStatus, messageActions: messageActions), updatingMessageMedia, cachedData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions) + let translationSettings: TranslationSettings + if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) { + translationSettings = current + } else { + translationSettings = TranslationSettings.defaultSettings + } + + return (MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, resourceStatus: resourceStatus, messageActions: messageActions), updatingMessageMedia, cachedData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions, translationSettings) } return dataSignal |> deliverOnMainQueue - |> map { data, updatingMessageMedia, cachedData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions -> ContextController.Items in + |> map { data, updatingMessageMedia, cachedData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions, translationSettings -> ContextController.Items in var actions: [ContextMenuItem] = [] var isPinnedMessages = false @@ -760,7 +770,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState f(.default) }))) - if #available(iOS 15.0, *), !message.text.isEmpty { + if canTranslateText(context: context, text: message.text, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages) { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuTranslate, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index d872cd0c39..f2476857f7 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -1248,14 +1248,20 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD let animationProgress: CGFloat = (currentValue - initialHeight) / (targetHeight - initialHeight) let scaleProgress: CGFloat var effectiveAvatarInset = avatarInset - if currentValue < targetHeight { - initialSize = displaySize - targetSize = maximumDisplaySize - scaleProgress = animationProgress - } else if currentValue > targetHeight { - initialSize = maximumDisplaySize - targetSize = displaySize - scaleProgress = 1.0 - animationProgress + if abs(targetHeight - initialHeight) > 100.0 { + if currentValue < targetHeight { + initialSize = displaySize + targetSize = maximumDisplaySize + scaleProgress = animationProgress + } else if currentValue > targetHeight { + initialSize = maximumDisplaySize + targetSize = displaySize + scaleProgress = 1.0 - animationProgress + } else { + initialSize = isPlaying ? maximumDisplaySize : displaySize + targetSize = initialSize + scaleProgress = isPlaying ? 1.0 : 0.0 + } } else { initialSize = isPlaying ? maximumDisplaySize : displaySize targetSize = initialSize @@ -1324,6 +1330,12 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD let actionButtonsFrame = CGRect(origin: CGPoint(x: videoFrame.minX, y: videoFrame.maxY), size: actionButtonsSize) actionButtonsNode.frame = actionButtonsFrame } + + if let reactionButtonsNode = self.reactionButtonsNode { + let reactionButtonsSize = reactionButtonsNode.frame.size + let reactionButtonsFrame = CGRect(origin: CGPoint(x: videoFrame.minX, y: videoFrame.maxY + 6.0), size: reactionButtonsSize) + reactionButtonsNode.frame = reactionButtonsFrame + } } override func openMessageContextMenu() { diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index 73e2effe84..5fde289241 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -35,6 +35,7 @@ private enum ApplicationSpecificSharedDataKeyValues: Int32 { case contactSynchronizationSettings = 15 case webBrowserSettings = 16 case intentsSettings = 17 + case translationSettings = 18 } public struct ApplicationSpecificSharedDataKeys { @@ -56,6 +57,7 @@ public struct ApplicationSpecificSharedDataKeys { public static let contactSynchronizationSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.contactSynchronizationSettings.rawValue) public static let webBrowserSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.webBrowserSettings.rawValue) public static let intentsSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.intentsSettings.rawValue) + public static let translationSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.translationSettings.rawValue) } private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 { diff --git a/submodules/TelegramUIPreferences/Sources/TranslationSettings.swift b/submodules/TelegramUIPreferences/Sources/TranslationSettings.swift new file mode 100644 index 0000000000..51c6a77818 --- /dev/null +++ b/submodules/TelegramUIPreferences/Sources/TranslationSettings.swift @@ -0,0 +1,58 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit + +public struct TranslationSettings: Codable, Equatable { + public var showTranslate: Bool + public var ignoredLanguages: [String]? + + public static var defaultSettings: TranslationSettings { + return TranslationSettings(showTranslate: true, ignoredLanguages: nil) + } + + init(showTranslate: Bool, ignoredLanguages: [String]?) { + self.showTranslate = showTranslate + self.ignoredLanguages = ignoredLanguages + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.showTranslate = try container.decodeIfPresent(Bool.self, forKey: "showTranslate") ?? true + self.ignoredLanguages = try container.decodeIfPresent([String].self, forKey: "ignoredLanguages") + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.showTranslate, forKey: "showTranslate") + try container.encodeIfPresent(self.ignoredLanguages, forKey: "ignoredLanguages") + } + + public static func ==(lhs: TranslationSettings, rhs: TranslationSettings) -> Bool { + return lhs.showTranslate == rhs.showTranslate && lhs.ignoredLanguages == rhs.ignoredLanguages + } + + public func withUpdatedShowTranslate(_ showTranslate: Bool) -> TranslationSettings { + return TranslationSettings(showTranslate: showTranslate, ignoredLanguages: self.ignoredLanguages) + } + + public func withUpdatedIgnoredLanguages(_ ignoredLanguages: [String]?) -> TranslationSettings { + return TranslationSettings(showTranslate: self.showTranslate, ignoredLanguages: ignoredLanguages) + } +} + +public func updateTranslationSettingsInteractively(accountManager: AccountManager, _ f: @escaping (TranslationSettings) -> TranslationSettings) -> Signal { + return accountManager.transaction { transaction -> Void in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.translationSettings, { entry in + let currentSettings: TranslationSettings + if let entry = entry?.get(TranslationSettings.self) { + currentSettings = entry + } else { + currentSettings = TranslationSettings.defaultSettings + } + return PreferencesEntry(f(currentSettings)) + }) + } +} diff --git a/submodules/Translate/Sources/Translate.swift b/submodules/Translate/Sources/Translate.swift index 42334de9a4..984f805382 100644 --- a/submodules/Translate/Sources/Translate.swift +++ b/submodules/Translate/Sources/Translate.swift @@ -2,11 +2,57 @@ import Foundation import UIKit import Display import AccountContext +import NaturalLanguage // Incuding at least one Objective-C class in a swift file ensures that it doesn't get stripped by the linker private final class LinkHelperClass: NSObject { } +public var supportedTranslationLanguages = [ + "en", + "ar", + "zh", + "fr", + "de", + "it", + "jp", + "ko", + "pt", + "ru", + "es" +] + +@available(iOS 12.0, *) +private let languageRecognizer = NLLanguageRecognizer() + +public func canTranslateText(context: AccountContext, text: String, showTranslate: Bool, ignoredLanguages: [String]?) -> Bool { + guard showTranslate, text.count > 0 else { + return false + } + + if #available(iOS 15.0, *) { + var dontTranslateLanguages: [String] = [] + if let ignoredLanguages = ignoredLanguages { + dontTranslateLanguages = ignoredLanguages + } else { + dontTranslateLanguages = [context.sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode] + } + + let text = String(text.prefix(64)) + languageRecognizer.processString(text) + let hypotheses = languageRecognizer.languageHypotheses(withMaximum: 2) + languageRecognizer.reset() + + if let language = hypotheses.first(where: { supportedTranslationLanguages.contains($0.key.rawValue) }) { + return !dontTranslateLanguages.contains(language.key.rawValue) + } else { + return false + } + } else { + return false + } +} + public func translateText(context: AccountContext, text: String) { guard !text.isEmpty else { return From 50060d9da2d1d211e76cace929b24603fc3e3983 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 21 Dec 2021 05:48:40 +0400 Subject: [PATCH 18/35] Fix build --- .../Sources/InvisibleInkDustNode.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift index 4f7d3316cb..82980fab56 100644 --- a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift @@ -143,6 +143,24 @@ public class InvisibleInkDustNode: ASDisplayNode { self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap(_:)))) } + public func update(revealed: Bool) { + guard self.isRevealed != revealed, let textNode = self.textNode else { + return + } + + self.isRevealed = revealed + + if revealed { + let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear) + transition.updateAlpha(node: self, alpha: 0.0) + transition.updateAlpha(node: textNode, alpha: 1.0) + } else { + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .linear) + transition.updateAlpha(node: self, alpha: 1.0) + transition.updateAlpha(node: textNode, alpha: 0.0) + } + } + @objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) { guard let (size, _, _) = self.currentParams, let textNode = self.textNode, !self.isRevealed else { return From 8235fc6755c316d0d54369765df85fea48d0ea1c Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 21 Dec 2021 12:44:45 +0400 Subject: [PATCH 19/35] Context menu improvements --- .../ContextControllerActionsStackNode.swift | 58 +++++++++++++------ ...tControllerExtractedPresentationNode.swift | 25 +++++--- .../ContainedViewLayoutTransition.swift | 17 ++++-- 3 files changed, 69 insertions(+), 31 deletions(-) diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 48ba044a90..c3ae419655 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -93,9 +93,7 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin if highlighted { strongSelf.highlightBackgroundNode.alpha = 1.0 } else { - let previousAlpha = strongSelf.highlightBackgroundNode.alpha strongSelf.highlightBackgroundNode.alpha = 0.0 - strongSelf.highlightBackgroundNode.layer.animateAlpha(from: previousAlpha, to: 0.0, duration: 0.2) } } @@ -246,14 +244,14 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin let titleFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: titleSize) let subtitleFrame = CGRect(origin: CGPoint(x: sideInset, y: titleFrame.maxY + titleSubtitleSpacing), size: subtitleSize) - transition.updateFrame(node: self.highlightBackgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrame(node: self.highlightBackgroundNode, frame: CGRect(origin: CGPoint(), size: size), beginWithCurrentState: true) transition.updateFrameAdditive(node: self.titleLabelNode, frame: titleFrame) transition.updateFrameAdditive(node: self.subtitleNode, frame: subtitleFrame) if let iconSize = iconSize { let iconWidth = max(standardIconWidth, iconSize.width) let iconFrame = CGRect(origin: CGPoint(x: size.width - iconSideInset - iconWidth + floor((iconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize) - transition.updateFrame(node: self.iconNode, frame: iconFrame) + transition.updateFrame(node: self.iconNode, frame: iconFrame, beginWithCurrentState: true) } }) } @@ -344,7 +342,7 @@ private final class ContextControllerActionsListCustomItemNode: ASDisplayNode, C let itemLayoutAndApply = itemNode.updateLayout(constrainedWidth: constrainedSize.width, constrainedHeight: constrainedSize.height) return (minSize: itemLayoutAndApply.0, apply: { size, transition in - transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: size), beginWithCurrentState: true) itemLayoutAndApply.1(size, transition) }) } @@ -493,10 +491,10 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack let itemSize = CGSize(width: combinedSize.width, height: itemNodeLayout.minSize.height) let itemFrame = CGRect(origin: nextItemOrigin, size: itemSize) - itemTransition.updateFrame(node: item.node, frame: itemFrame) + itemTransition.updateFrame(node: item.node, frame: itemFrame, beginWithCurrentState: true) if let separatorNode = item.separatorNode { - itemTransition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.maxY), size: CGSize(width: itemFrame.width, height: UIScreenPixel))) + itemTransition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.maxY), size: CGSize(width: itemFrame.width, height: UIScreenPixel)), beginWithCurrentState: true) if i != self.itemNodes.count - 1 { switch self.items[i + 1] { case .separator: @@ -543,10 +541,9 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack func highlightGestureFinished(performAction: Bool) { if let highlightedItemNode = self.highlightedItemNode { self.highlightedItemNode = nil + highlightedItemNode.node.updateIsHighlighted(isHighlighted: false) if performAction { highlightedItemNode.node.performAction() - } else { - highlightedItemNode.node.updateIsHighlighted(isHighlighted: false) } } } @@ -614,7 +611,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta bottomInset: 0.0, transition: transition ) - transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: contentLayout.cleanSize)) + transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: contentLayout.cleanSize), beginWithCurrentState: true) return (contentLayout.cleanSize, contentLayout.apparentHeight) } @@ -779,9 +776,9 @@ final class ContextControllerActionsStackNode: ASDisplayNode { let scale: CGFloat = (size.width - scaleOffset) / size.width let yOffset: CGFloat = size.height * (1.0 - scale) let transitionOffset = (1.0 - transitionFraction) * size.width / 2.0 - transition.updatePosition(node: self.node, position: CGPoint(x: size.width / 2.0 + scaleOffset / 2.0 + transitionOffset, 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) + transition.updatePosition(node: self.node, position: CGPoint(x: size.width / 2.0 + scaleOffset / 2.0 + transitionOffset, y: size.height / 2.0 - yOffset / 2.0), beginWithCurrentState: true) + transition.updateBounds(node: self.node, bounds: CGRect(origin: CGPoint(), size: size), beginWithCurrentState: true) + transition.updateTransformScale(node: self.node, scale: scale, beginWithCurrentState: true) return (size, apparentHeight) } @@ -789,8 +786,8 @@ final class ContextControllerActionsStackNode: ASDisplayNode { 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) + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: size), beginWithCurrentState: true) + transition.updateAlpha(node: self.dimNode, alpha: 1.0 - transitionFraction, beginWithCurrentState: true) } func highlightGestureMoved(location: CGPoint) { @@ -810,6 +807,8 @@ final class ContextControllerActionsStackNode: ASDisplayNode { private var itemContainers: [ItemContainer] = [] private var dismissingItemContainers: [(container: ItemContainer, isPopped: Bool)] = [] + private var selectionPanGesture: UIPanGestureRecognizer? + var topReactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])? { return self.itemContainers.last?.reactionItems } @@ -850,6 +849,25 @@ final class ContextControllerActionsStackNode: ASDisplayNode { } strongSelf.pop() } + + let selectionPanGesture = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + self.selectionPanGesture = selectionPanGesture + self.view.addGestureRecognizer(selectionPanGesture) + selectionPanGesture.isEnabled = false + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .changed: + let location = recognizer.location(in: self.view) + self.highlightGestureMoved(location: location) + case .ended: + self.highlightGestureFinished(performAction: true) + case .cancelled: + self.highlightGestureFinished(performAction: false) + default: + break + } } func replace(item: ContextControllerActionsStackItem, animated: Bool) { @@ -1000,7 +1018,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { } let navigationContainerFrame = CGRect(origin: CGPoint(), size: CGSize(width: topItemWidth, height: max(14 * 2.0, topItemApparentHeight))) - transition.updateFrame(node: self.navigationContainer, frame: navigationContainerFrame) + transition.updateFrame(node: self.navigationContainer, frame: navigationContainerFrame, beginWithCurrentState: true) self.navigationContainer.update(presentationData: presentationData, size: navigationContainerFrame.size, transition: transition) for i in 0 ..< self.itemContainers.count { @@ -1016,7 +1034,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { } let itemFrame = CGRect(origin: CGPoint(x: xOffset, y: 0.0), size: CGSize(width: itemLayouts[i].size.width, height: navigationContainerFrame.height)) - itemLayouts[i].itemTransition.updateFrame(node: self.itemContainers[i], frame: itemFrame) + itemLayouts[i].itemTransition.updateFrame(node: self.itemContainers[i], frame: itemFrame, beginWithCurrentState: true) if itemLayouts[i].animateAppearingContainer { transition.animatePositionAdditive(node: self.itemContainers[i], offset: CGPoint(x: itemFrame.width, y: 0.0)) } @@ -1051,4 +1069,10 @@ final class ContextControllerActionsStackNode: ASDisplayNode { topItemContainer.highlightGestureFinished(performAction: performAction) } } + + func updatePanSelection(isEnabled: Bool) { + if let selectionPanGesture = self.selectionPanGesture { + selectionPanGesture.isEnabled = isEnabled + } + } } diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 00f2486bd3..748e2630e8 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -23,12 +23,17 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo super.init() self.addSubnode(self.offsetContainerNode) - self.offsetContainerNode.addSubnode(self.containingNode.contentNode) } func update(presentationData: PresentationData, size: CGSize, transition: ContainedViewLayoutTransition) { } + func takeContainingNode() { + if self.containingNode.contentNode.supernode !== self.offsetContainerNode { + self.offsetContainerNode.addSubnode(self.containingNode.contentNode) + } + } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.bounds.contains(point) { return nil @@ -217,12 +222,12 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo transition: .immediate ) - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) self.backgroundNode.update(size: layout.size, transition: transition) - transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) if self.scrollNode.frame != CGRect(origin: CGPoint(), size: layout.size) { - transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) } if let current = self.contentNode { @@ -328,7 +333,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if reactionContextNode.frame.isEmpty { reactionContextNodeTransition = .immediate } - reactionContextNodeTransition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + reactionContextNodeTransition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) reactionContextNode.updateLayout(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), anchorRect: contentRect, transition: reactionContextNodeTransition) } if let removedReactionContextNode = removedReactionContextNode { @@ -338,7 +343,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo }) } - transition.updateFrame(node: self.contentRectDebugNode, frame: contentRect) + transition.updateFrame(node: self.contentRectDebugNode, frame: contentRect, beginWithCurrentState: true) var actionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: contentRect.maxY + contentActionsSpacing), size: actionsSize) if self.source.keepInPlace { @@ -365,9 +370,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo actionsFrame.origin.x = actionsEdgeInset } } - transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame) + transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame, beginWithCurrentState: true) - 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)) + 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), beginWithCurrentState: true) let contentHeight: CGFloat if self.actionsStackNode.topPositionLock != nil { @@ -391,6 +396,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } } + self.actionsStackNode.updatePanSelection(isEnabled: contentSize.height <= layout.size.height) + defaultScrollY = contentSize.height - layout.size.height if defaultScrollY < 0.0 { defaultScrollY = 0.0 @@ -401,6 +408,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo switch stateTransition { case .animateIn: + contentNode.takeContainingNode() + let duration: Double = 0.42 let springDamping: CGFloat = 104.0 diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 2a4b4e5d7c..74eec36301 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -105,7 +105,7 @@ public extension CGRect { } public extension ContainedViewLayoutTransition { - func updateFrame(node: ASDisplayNode, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = true, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { + func updateFrame(node: ASDisplayNode, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { if frame.origin.x.isNaN { return } @@ -157,7 +157,7 @@ public extension ContainedViewLayoutTransition { } } - func updateFrameAsPositionAndBounds(node: ASDisplayNode, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = true, completion: ((Bool) -> Void)? = nil) { + func updateFrameAsPositionAndBounds(node: ASDisplayNode, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.frame.equalTo(frame) && !force { completion?(true) } else { @@ -190,7 +190,7 @@ public extension ContainedViewLayoutTransition { } } - func updateFrameAsPositionAndBounds(layer: CALayer, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = true, completion: ((Bool) -> Void)? = nil) { + func updateFrameAsPositionAndBounds(layer: CALayer, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if layer.frame.equalTo(frame) && !force { completion?(true) } else { @@ -261,7 +261,7 @@ public extension ContainedViewLayoutTransition { } } - func updateBounds(node: ASDisplayNode, bounds: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + func updateBounds(node: ASDisplayNode, bounds: CGRect, force: Bool = false, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.bounds.equalTo(bounds) && !force { completion?(true) } else { @@ -272,7 +272,12 @@ public extension ContainedViewLayoutTransition { completion(true) } case let .animated(duration, curve): - let previousBounds = node.bounds + let previousBounds: CGRect + if beginWithCurrentState, let presentation = node.layer.presentation() { + previousBounds = presentation.bounds + } else { + previousBounds = node.bounds + } node.bounds = bounds node.layer.animateBounds(from: previousBounds, to: bounds, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in if let completion = completion { @@ -305,7 +310,7 @@ public extension ContainedViewLayoutTransition { } } - func updatePosition(node: ASDisplayNode, position: CGPoint, beginWithCurrentState: Bool = true, completion: ((Bool) -> Void)? = nil) { + func updatePosition(node: ASDisplayNode, position: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.position.equalTo(position) { completion?(true) } else { From fb6f97202f2a5b72cd9c64894fd9babb96f27833 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 21 Dec 2021 14:19:16 +0400 Subject: [PATCH 20/35] Context menu improvements --- .../ContextUI/Sources/ContextController.swift | 13 ++ ...tControllerExtractedPresentationNode.swift | 11 ++ .../ContextControllerPresentationNode.swift | 3 + .../Sources/ReactionContextNode.swift | 122 +++++++++++++----- .../Reactions/ReactionChatPreviewItem.swift | 22 ++-- .../TelegramUI/Sources/ChatController.swift | 82 ++++++------ .../Sources/ChatMessageTransitionNode.swift | 110 ++++++++++++++++ 7 files changed, 281 insertions(+), 82 deletions(-) diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 47769179ca..0ac7f7abd5 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -1229,12 +1229,21 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { + if let presentationNode = self.presentationNode { + presentationNode.addRelativeContentOffset(offset, transition: transition) + } if self.reactionContextNodeIsAnimatingOut, let reactionContextNode = self.reactionContextNode { reactionContextNode.bounds = reactionContextNode.bounds.offsetBy(dx: 0.0, dy: offset.y) transition.animateOffsetAdditive(node: reactionContextNode, offset: -offset.y) } } + func cancelReactionAnimation() { + if let presentationNode = self.presentationNode { + presentationNode.cancelReactionAnimation() + } + } + func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { if let presentationNode = self.presentationNode { presentationNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, completion: completion) @@ -2372,6 +2381,10 @@ public final class ContextController: ViewController, StandalonePresentableContr } } + public func cancelReactionAnimation() { + self.controllerNode.cancelReactionAnimation() + } + public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { self.controllerNode.addRelativeContentOffset(offset, transition: transition) } diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 748e2630e8..4e63a212a6 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -653,4 +653,15 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo intermediateCompletion() }) } + + func cancelReactionAnimation() { + self.reactionContextNode?.cancelReactionAnimation() + } + + func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { + if self.reactionContextNodeIsAnimatingOut, let reactionContextNode = self.reactionContextNode { + reactionContextNode.bounds = reactionContextNode.bounds.offsetBy(dx: 0.0, dy: offset.y) + transition.animateOffsetAdditive(node: reactionContextNode, offset: -offset.y) + } + } } diff --git a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift index a45bdcfaac..cd27640916 100644 --- a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift @@ -25,7 +25,10 @@ protocol ContextControllerPresentationNode: ASDisplayNode { ) func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) + func cancelReactionAnimation() func highlightGestureMoved(location: CGPoint) func highlightGestureFinished(performAction: Bool) + + func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 1776369416..1dadfc99ca 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -7,11 +7,6 @@ import TelegramPresentationData import AccountContext import TelegramAnimatedStickerNode -public enum ReactionGestureItem { - case like - case unlike -} - public final class ReactionContextItem { public struct Reaction: Equatable { public var rawValue: String @@ -118,6 +113,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { public var reactionSelected: ((ReactionContextItem) -> Void)? private var hapticFeedback: HapticFeedback? + private var standaloneReactionAnimation: StandaloneReactionAnimation? + + private weak var animationTargetView: UIView? + private var animationHideNode: Bool = false public init(context: AccountContext, theme: PresentationTheme, items: [ReactionContextItem]) { self.theme = theme @@ -489,10 +488,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { itemNode.layer.animatePosition(from: itemNode.layer.position, to: targetPosition, duration: duration, removeOnCompletion: false) targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.8) targetSnapshotView.layer.animatePosition(from: sourceFrame.center, to: targetPosition, duration: duration, removeOnCompletion: false) - targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 1.0, duration: duration, removeOnCompletion: false, completion: { [weak self, weak targetSnapshotView] _ in - if let _ = self { - //strongSelf.hapticFeedback.tap() - } + targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 1.0, duration: duration, removeOnCompletion: false, completion: { [weak targetSnapshotView] _ in completedTarget = true intermediateCompletion() @@ -500,11 +496,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if hideNode { targetView.isHidden = false - /*targetView.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0, completion: { _ in*/ - targetSnapshotView?.isHidden = true - targetScaleCompleted = true - intermediateCompletion() - //}) + targetSnapshotView?.isHidden = true + targetScaleCompleted = true + intermediateCompletion() } else { targetScaleCompleted = true intermediateCompletion() @@ -528,6 +522,18 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if itemNode.item.reaction.rawValue != value { continue } + + self.animationTargetView = targetView + self.animationHideNode = hideNode + + /*let standaloneReactionAnimation = StandaloneReactionAnimation() + self.standaloneReactionAnimation = standaloneReactionAnimation + standaloneReactionAnimation.frame = self.bounds + self.addSubnode(standaloneReactionAnimation) + standaloneReactionAnimation.animateReactionSelection(context: itemNode.context, theme: self.theme, reaction: itemNode.item, targetView: targetView, currentItemNode: itemNode, hideNode: hideNode, completion: completion) + + return*/ + if hideNode { targetView.isHidden = true } @@ -662,6 +668,14 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } + public func cancelReactionAnimation() { + self.standaloneReactionAnimation?.cancel() + + if let animationTargetView = self.animationTargetView, self.animationHideNode { + animationTargetView.isHidden = false + } + } + public func setHighlightedReaction(_ value: ReactionContextItem.Reaction?) { self.highlightedReaction = value if let (size, insets, anchorRect) = self.validLayout { @@ -678,29 +692,47 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } public final class StandaloneReactionAnimation: ASDisplayNode { - private let itemNode: ReactionNode + private var itemNode: ReactionNode? = nil private let hapticFeedback = HapticFeedback() + private var isCancelled: Bool = false - public init(context: AccountContext, theme: PresentationTheme, reaction: ReactionContextItem) { - self.itemNode = ReactionNode(context: context, theme: theme, item: reaction) - + private weak var targetView: UIView? + private var hideNode: Bool = false + + override public init() { super.init() self.isUserInteractionEnabled = false - - self.addSubnode(self.itemNode) } - public func animateReactionSelection(targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { + public func animateReactionSelection(context: AccountContext, theme: PresentationTheme, reaction: ReactionContextItem, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { + self.animateReactionSelection(context: context, theme: theme, reaction: reaction, targetView: targetView, currentItemNode: nil, hideNode: hideNode, completion: completion) + } + + func animateReactionSelection(context: AccountContext, theme: PresentationTheme, reaction: ReactionContextItem, targetView: UIView, currentItemNode: ReactionNode?, hideNode: Bool, completion: @escaping () -> Void) { guard let sourceSnapshotView = targetView.snapshotContentTree() else { completion() return } + + self.targetView = targetView + self.hideNode = hideNode + + let itemNode: ReactionNode + if let currentItemNode = currentItemNode { + itemNode = currentItemNode + } else { + itemNode = ReactionNode(context: context, theme: theme, item: reaction) + } + self.itemNode = itemNode + + self.addSubnode(itemNode) + if hideNode { targetView.isHidden = true } - self.itemNode.isExtracted = true + itemNode.isExtracted = true let sourceItemSize: CGFloat = 40.0 let selfTargetRect = self.view.convert(targetView.bounds, from: targetView) @@ -732,7 +764,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { let animationFrame = expandedFrame.insetBy(dx: -expandedFrame.width * 0.5, dy: -expandedFrame.height * 0.5) .offsetBy(dx: incomingMessage ? (expandedFrame.width - 50.0) : (-expandedFrame.width + 50.0), dy: 0.0) - additionalAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.itemNode.context.account, resource: self.itemNode.item.applicationAnimation.resource), width: Int(animationFrame.width * 2.0), height: Int(animationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.itemNode.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.itemNode.item.applicationAnimation.resource.id))) + additionalAnimationNode.setup(source: AnimatedStickerResourceSource(account: itemNode.context.account, resource: itemNode.item.applicationAnimation.resource), width: Int(animationFrame.width * 2.0), height: Int(animationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: itemNode.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(itemNode.item.applicationAnimation.resource.id))) additionalAnimationNode.frame = animationFrame if incomingMessage { additionalAnimationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) @@ -750,9 +782,28 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } } + var didBeginDismissAnimation = false + let beginDismissAnimation: () -> Void = { [weak self] in + if !didBeginDismissAnimation { + didBeginDismissAnimation = true + + guard let strongSelf = self else { + mainAnimationCompleted = true + intermediateCompletion() + return + } + strongSelf.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: hideNode, completion: { + mainAnimationCompleted = true + intermediateCompletion() + }) + } + } + additionalAnimationNode.completed = { _ in additionalAnimationCompleted = true intermediateCompletion() + + beginDismissAnimation() } DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1 * UIView.animationDurationFactor(), execute: { @@ -760,11 +811,9 @@ public final class StandaloneReactionAnimation: ASDisplayNode { }) DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0, execute: { - self.animateFromItemNodeToReaction(itemNode: self.itemNode, targetView: targetView, hideNode: hideNode, completion: { - mainAnimationCompleted = true - intermediateCompletion() - }) + beginDismissAnimation() }) + } private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { @@ -795,9 +844,6 @@ public final class StandaloneReactionAnimation: ASDisplayNode { targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.8) targetSnapshotView.layer.animatePosition(from: sourceFrame.center, to: targetPosition, duration: duration, removeOnCompletion: false) targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 1.0, duration: duration, removeOnCompletion: false, completion: { [weak targetSnapshotView] _ in - /*if let strongSelf = self { - strongSelf.hapticFeedback.tap() - }*/ completedTarget = true intermediateCompletion() @@ -805,11 +851,9 @@ public final class StandaloneReactionAnimation: ASDisplayNode { if hideNode { targetView.isHidden = false - /*targetView.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0, completion: { _ in*/ - targetSnapshotView?.isHidden = true - targetScaleCompleted = true - intermediateCompletion() - //}) + targetSnapshotView?.isHidden = true + targetScaleCompleted = true + intermediateCompletion() } else { targetScaleCompleted = true intermediateCompletion() @@ -823,6 +867,14 @@ public final class StandaloneReactionAnimation: ASDisplayNode { self.bounds = self.bounds.offsetBy(dx: 0.0, dy: offset.y) transition.animateOffsetAdditive(node: self, offset: -offset.y) } + + public func cancel() { + self.isCancelled = true + + if let targetView = self.targetView, self.hideNode { + targetView.isHidden = false + } + } } public final class StandaloneDismissReactionAnimation: ASDisplayNode { diff --git a/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift index ed105b9b1f..906d2f538b 100644 --- a/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift @@ -132,6 +132,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode { for reaction in availableReactions.reactions { if reaction.value == updatedReaction { if let standaloneReactionAnimation = self.standaloneReactionAnimation { + standaloneReactionAnimation.cancel() standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in standaloneReactionAnimation?.removeFromSupernode() }) @@ -139,19 +140,24 @@ class ReactionChatPreviewItemNode: ListViewItemNode { } if let supernode = self.supernode { - let standaloneReactionAnimation = StandaloneReactionAnimation(context: item.context, theme: item.theme, reaction: ReactionContextItem( - reaction: ReactionContextItem.Reaction(rawValue: reaction.value), - stillAnimation: reaction.selectAnimation, - listAnimation: reaction.activateAnimation, - applicationAnimation: reaction.effectAnimation - )) + let standaloneReactionAnimation = StandaloneReactionAnimation() self.standaloneReactionAnimation = standaloneReactionAnimation supernode.addSubnode(standaloneReactionAnimation) standaloneReactionAnimation.frame = supernode.bounds - standaloneReactionAnimation.animateReactionSelection(targetView: targetView, hideNode: true, completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation.animateReactionSelection( + context: item.context, theme: item.theme, reaction: ReactionContextItem( + reaction: ReactionContextItem.Reaction(rawValue: reaction.value), + stillAnimation: reaction.selectAnimation, + listAnimation: reaction.activateAnimation, + applicationAnimation: reaction.effectAnimation + ), + targetView: targetView, + hideNode: true, + completion: { [weak standaloneReactionAnimation] in standaloneReactionAnimation?.removeFromSupernode() - }) + } + ) } break diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index d65e57448a..90cdd71d0a 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -429,12 +429,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private weak var currentPinchController: PinchController? private weak var currentPinchSourceItemNode: ListViewItemNode? - private weak var currentReactionContextController: ContextController? - private weak var currentReactionContextItemNode: ListViewItemNode? - - private weak var currentStandaloneReactionAnimation: StandaloneReactionAnimation? - private weak var currentStandaloneReactionItemNode: ListViewItemNode? - private var screenCaptureManager: ScreenCaptureDetectionManager? private let chatAdditionalDataDisposable = MetaDisposable() @@ -594,6 +588,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return true } + + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + if strongSelf.presentVoiceMessageDiscardAlert(action: action, performAction: false) { return false } @@ -1025,6 +1022,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, selectAll: selectAll)), items: .single(actions), recognizer: recognizer, gesture: gesture) strongSelf.currentContextController = controller @@ -1059,8 +1058,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } if let itemNode = itemNode, let targetView = itemNode.targetReactionView(value: updatedReaction) { - strongSelf.currentReactionContextController = controller - strongSelf.currentReactionContextItemNode = itemNode + strongSelf.chatDisplayNode.messageTransitionNode.addMessageContextController(messageId: item.message.id, contextController: controller) controller.dismissWithReaction(value: updatedReaction, targetView: targetView, hideNode: true, completion: { [weak itemNode, weak targetView] in guard let strongSelf = self, let itemNode = itemNode, let targetView = targetView else { @@ -1116,6 +1114,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }))) + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + 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 @@ -1238,21 +1238,27 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G 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 - )) + let standaloneReactionAnimation = StandaloneReactionAnimation() - strongSelf.currentStandaloneReactionAnimation = standaloneReactionAnimation - strongSelf.currentStandaloneReactionItemNode = itemNode + strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds - standaloneReactionAnimation.animateReactionSelection(targetView: targetView, hideNode: true, completion: { [weak standaloneReactionAnimation] in - standaloneReactionAnimation?.removeFromSupernode() - }) + standaloneReactionAnimation.animateReactionSelection( + context: strongSelf.context, + theme: strongSelf.presentationData.theme, + reaction: ReactionContextItem( + reaction: ReactionContextItem.Reaction(rawValue: reaction.value), + stillAnimation: reaction.selectAnimation, + listAnimation: reaction.activateAnimation, + applicationAnimation: reaction.effectAnimation + ), + targetView: targetView, + hideNode: true, + completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + } + ) break } @@ -2471,6 +2477,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }))) + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil) strongSelf.currentContextController = controller strongSelf.forEachController({ controller in @@ -2548,6 +2556,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G f(.dismissWithoutContent) }))) + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: topMessage, selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil) strongSelf.currentContextController = controller strongSelf.forEachController({ controller in @@ -2981,6 +2991,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return items } + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) }) @@ -3287,6 +3299,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return items } + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } @@ -4888,26 +4902,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - if let reactionItemNode = strongSelf.currentReactionContextItemNode, let currentReactionContextController = strongSelf.currentReactionContextController { - if let itemNode = itemNode { - if itemNode === reactionItemNode { - currentReactionContextController.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) - } - } else { - currentReactionContextController.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) - } - } - - if let standaloneReactionItemNode = strongSelf.currentStandaloneReactionItemNode, let currentStandaloneReactionAnimation = strongSelf.currentStandaloneReactionAnimation { - if let itemNode = itemNode { - if itemNode === standaloneReactionItemNode { - currentStandaloneReactionAnimation.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) - } - } else { - currentStandaloneReactionAnimation.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) - } - } - strongSelf.chatDisplayNode.messageTransitionNode.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode) } @@ -6271,6 +6265,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return items } + + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), items: items |> map { ContextController.Items(content: .list($0)) }) contextController.dismissedForCancel = { [weak chatController] in @@ -7750,6 +7746,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peerId), subject: .pinnedMessages(id: pinnedMessage.message.id), botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) + + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) }, joinGroupCall: { [weak self] activeCall in @@ -7812,6 +7811,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G items.append(.custom(ChatSendAsPeerTitleContextItem(text: strongSelf.presentationInterfaceState.strings.Conversation_SendMesageAs.uppercased()), false)) items.append(.custom(ChatSendAsPeerListContextItem(context: strongSelf.context, chatPeerId: peerId, peers: peers, selectedPeerId: myPeerId), false)) + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(ChatControllerContextReferenceContentSource(controller: strongSelf, sourceNode: node, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) contextController.dismissed = { [weak self] in if let strongSelf = self { @@ -11998,6 +11999,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peerId), subject: .message(id: .timestamp(timestamp), highlight: false, timecode: nil), botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) + + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } diff --git a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift index cf4d579b2c..92493c11a6 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift @@ -5,6 +5,10 @@ import Display import ContextUI import AnimatedStickerNode import SwiftSignalKit +import ContextUI +import Postbox +import TelegramCore +import ReactionSelectionNode private final class OverlayTransitionContainerNode: ViewControllerTracingNode { override init() { @@ -595,6 +599,53 @@ public final class ChatMessageTransitionNode: ASDisplayNode { } } } + + private final class MessageReactionContext { + private(set) weak var itemNode: ListViewItemNode? + private(set) weak var contextController: ContextController? + private(set) weak var standaloneReactionAnimation: StandaloneReactionAnimation? + + var isEmpty: Bool { + return self.contextController == nil && self.standaloneReactionAnimation == nil + } + + init(itemNode: ListViewItemNode, contextController: ContextController?, standaloneReactionAnimation: StandaloneReactionAnimation?) { + self.itemNode = itemNode + self.contextController = contextController + self.standaloneReactionAnimation = standaloneReactionAnimation + } + + func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, itemNode: ListViewItemNode?) { + guard let currentItemNode = self.itemNode else { + return + } + if itemNode == nil || itemNode === currentItemNode { + if let contextController = self.contextController { + contextController.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) + } + if let standaloneReactionAnimation = self.standaloneReactionAnimation { + standaloneReactionAnimation.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) + } + } + } + + func addContentOffset(offset: CGFloat, itemNode: ListViewItemNode?) { + } + + func dismiss() { + if let contextController = self.contextController { + contextController.cancelReactionAnimation() + contextController.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + }) + } + if let standaloneReactionAnimation = self.standaloneReactionAnimation { + standaloneReactionAnimation.cancel() + standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in + standaloneReactionAnimation?.removeFromSupernode() + }) + } + } + } private let listNode: ChatHistoryListNode private let getContentAreaInScreenSpace: () -> CGRect @@ -604,6 +655,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode { private var animatingItemNodes: [AnimatingItemNode] = [] private var decorationItemNodes: [DecorationItemNode] = [] + private var messageReactionContexts: [MessageReactionContext] = [] var hasScheduledTransitions: Bool { return self.currentPendingItem != nil @@ -714,6 +766,58 @@ public final class ChatMessageTransitionNode: ASDisplayNode { override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { return nil } + + private func removeEmptyMessageReactionContexts() { + for i in (0 ..< self.messageReactionContexts.count).reversed() { + if self.messageReactionContexts[i].isEmpty { + self.messageReactionContexts.remove(at: i) + } + } + } + + func dismissMessageReactionContexts() { + for messageReactionContext in self.messageReactionContexts { + messageReactionContext.dismiss() + } + self.messageReactionContexts.removeAll() + } + + func addMessageContextController(messageId: MessageId, contextController: ContextController) { + self.addMessageReactionContextContext(messageId: messageId, contextController: contextController, standaloneReactionAnimation: nil) + } + + func addMessageStandaloneReactionAnimation(messageId: MessageId, standaloneReactionAnimation: StandaloneReactionAnimation) { + self.addMessageReactionContextContext(messageId: messageId, contextController: nil, standaloneReactionAnimation: standaloneReactionAnimation) + } + + private func addMessageReactionContextContext(messageId: MessageId, contextController: ContextController?, standaloneReactionAnimation: StandaloneReactionAnimation?) { + self.removeEmptyMessageReactionContexts() + + var messageItemNode: ListViewItemNode? + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if let item = itemNode.item { + for (message, _) in item.content { + if message.id == messageId { + messageItemNode = itemNode + break + } + } + } + } + } + + if let messageItemNode = messageItemNode { + for i in 0 ..< self.messageReactionContexts.count { + if self.messageReactionContexts[i].itemNode === messageItemNode { + self.messageReactionContexts[i].dismiss() + self.messageReactionContexts.remove(at: i) + break + } + } + self.messageReactionContexts.append(MessageReactionContext(itemNode: messageItemNode, contextController: contextController, standaloneReactionAnimation: standaloneReactionAnimation)) + } + } func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, itemNode: ListViewItemNode?) { for animatingItemNode in self.animatingItemNodes { @@ -724,6 +828,9 @@ public final class ChatMessageTransitionNode: ASDisplayNode { decorationItemNode.addExternalOffset(offset: offset, transition: transition) } } + for messageReactionContext in self.messageReactionContexts { + messageReactionContext.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode) + } } func addContentOffset(offset: CGFloat, itemNode: ListViewItemNode?) { @@ -735,6 +842,9 @@ public final class ChatMessageTransitionNode: ASDisplayNode { decorationItemNode.addContentOffset(offset: offset) } } + for messageReactionContext in self.messageReactionContexts { + messageReactionContext.addContentOffset(offset: offset, itemNode: itemNode) + } } func isAnimatingMessage(stableId: UInt32) -> Bool { From 5f66e325a73ae36eb389b7e656b80de9304e7ff4 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 21 Dec 2021 14:46:28 +0400 Subject: [PATCH 21/35] Disable removing corrupt database in non-main processes --- .../Sources/NotificationService.swift | 2 +- Telegram/SiriIntents/IntentHandler.swift | 2 +- submodules/Postbox/Sources/Postbox.swift | 10 +-- .../Postbox/Sources/SqliteValueBox.swift | 83 ++++++++++--------- .../Sources/Account/Account.swift | 6 +- .../AccountManager/AccountManagerImpl.swift | 10 +-- ...yncCore_StandaloneAccountTransaction.swift | 4 +- .../TelegramUI/Sources/AppDelegate.swift | 2 +- .../Sources/NotificationContentContext.swift | 2 +- .../Sources/ShareExtensionContext.swift | 2 +- 10 files changed, 66 insertions(+), 57 deletions(-) diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index 3e50ecf6e1..2ba4482b8c 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -613,7 +613,7 @@ private final class NotificationServiceHandler { let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown" - self.accountManager = AccountManager(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false) + self.accountManager = AccountManager(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false, removeDatabaseOnError: false) let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId) self.encryptionParameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: deviceSpecificEncryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: deviceSpecificEncryptionParameters.salt)!) diff --git a/Telegram/SiriIntents/IntentHandler.swift b/Telegram/SiriIntents/IntentHandler.swift index 075cd25ffd..b9e62875fb 100644 --- a/Telegram/SiriIntents/IntentHandler.swift +++ b/Telegram/SiriIntents/IntentHandler.swift @@ -121,7 +121,7 @@ class DefaultIntentHandler: INExtension, INSendMessageIntentHandling, INSearchFo let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown" initializeAccountManagement() - let accountManager = AccountManager(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false) + let accountManager = AccountManager(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false, removeDatabaseOnError: false) self.accountManager = accountManager let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId) diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 28d04fa502..452cf92738 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -1170,7 +1170,7 @@ func debugRestoreState(basePath:String, name: String) { private let sharedQueue = Queue(name: "org.telegram.postbox.Postbox") -public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, encryptionParameters: ValueBoxEncryptionParameters, timestampForAbsoluteTimeBasedOperations: Int32, isTemporary: Bool, isReadOnly: Bool, useCopy: Bool, useCaches: Bool) -> Signal { +public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, encryptionParameters: ValueBoxEncryptionParameters, timestampForAbsoluteTimeBasedOperations: Int32, isTemporary: Bool, isReadOnly: Bool, useCopy: Bool, useCaches: Bool, removeDatabaseOnError: Bool) -> Signal { let queue = sharedQueue return Signal { subscriber in queue.async { @@ -1214,7 +1214,7 @@ public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, postboxLog("openPostbox, initialize SqliteValueBox") - guard var valueBox = SqliteValueBox(basePath: dbBasePath, queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, encryptionParameters: encryptionParameters, upgradeProgress: { progress in + guard var valueBox = SqliteValueBox(basePath: dbBasePath, queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: encryptionParameters, upgradeProgress: { progress in postboxLog("openPostbox, SqliteValueBox upgrading progress \(progress)") subscriber.putNext(.upgrading(progress)) }) else { @@ -1242,7 +1242,7 @@ public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, postboxLog("Version \(userVersion) is newer than supported") assertionFailure("Version \(userVersion) is newer than supported") valueBox.drop() - guard let updatedValueBox = SqliteValueBox(basePath: dbBasePath, queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, encryptionParameters: encryptionParameters, upgradeProgress: { progress in + guard let updatedValueBox = SqliteValueBox(basePath: dbBasePath, queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: encryptionParameters, upgradeProgress: { progress in subscriber.putNext(.upgrading(progress)) }) else { subscriber.putNext(.error) @@ -1266,7 +1266,7 @@ public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, valueBox.internalClose() let _ = try? FileManager.default.removeItem(atPath: dbBasePath) let _ = try? FileManager.default.moveItem(atPath: updatedPath, toPath: dbBasePath) - guard let updatedValueBox = SqliteValueBox(basePath: dbBasePath, queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, encryptionParameters: encryptionParameters, upgradeProgress: { progress in + guard let updatedValueBox = SqliteValueBox(basePath: dbBasePath, queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: encryptionParameters, upgradeProgress: { progress in subscriber.putNext(.upgrading(progress)) }) else { subscriber.putNext(.error) @@ -1280,7 +1280,7 @@ public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, assertionFailure("Couldn't find any upgrade for \(userVersion)") postboxLog("Couldn't find any upgrade for \(userVersion)") valueBox.drop() - guard let updatedValueBox = SqliteValueBox(basePath: dbBasePath, queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, encryptionParameters: encryptionParameters, upgradeProgress: { progress in + guard let updatedValueBox = SqliteValueBox(basePath: dbBasePath, queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: encryptionParameters, upgradeProgress: { progress in subscriber.putNext(.upgrading(progress)) }) else { subscriber.putNext(.error) diff --git a/submodules/Postbox/Sources/SqliteValueBox.swift b/submodules/Postbox/Sources/SqliteValueBox.swift index 133996cc46..088f89623d 100644 --- a/submodules/Postbox/Sources/SqliteValueBox.swift +++ b/submodules/Postbox/Sources/SqliteValueBox.swift @@ -46,7 +46,7 @@ struct SqlitePreparedStatement { sqlite3_clear_bindings(statement) } - func step(handle: OpaquePointer?, _ initial: Bool = false, path: String?) -> Bool { + func step(handle: OpaquePointer?, _ initial: Bool = false, pathToRemoveOnError: String?) -> Bool { let res = sqlite3_step(statement) if res != SQLITE_ROW && res != SQLITE_DONE { if let error = sqlite3_errmsg(handle), let str = NSString(utf8String: error) { @@ -56,7 +56,7 @@ struct SqlitePreparedStatement { } if res == SQLITE_CORRUPT { - if let path = path { + if let path = pathToRemoveOnError { postboxLog("Corrupted DB at step, dropping") try? FileManager.default.removeItem(atPath: path) preconditionFailure() @@ -70,7 +70,7 @@ struct SqlitePreparedStatement { var code: Int32 } - func tryStep(handle: OpaquePointer?, _ initial: Bool = false, path: String?) -> Result { + func tryStep(handle: OpaquePointer?, _ initial: Bool = false, pathToRemoveOnError: String?) -> Result { let res = sqlite3_step(statement) if res != SQLITE_ROW && res != SQLITE_DONE { if let error = sqlite3_errmsg(handle), let str = NSString(utf8String: error) { @@ -80,7 +80,7 @@ struct SqlitePreparedStatement { } if res == SQLITE_CORRUPT { - if let path = path { + if let path = pathToRemoveOnError { postboxLog("Corrupted DB at step, dropping") try? FileManager.default.removeItem(atPath: path) preconditionFailure() @@ -167,6 +167,7 @@ public final class SqliteValueBox: ValueBox { private let inMemory: Bool private let encryptionParameters: ValueBoxEncryptionParameters? private let databasePath: String + private let removeDatabaseOnError: Bool private var database: Database! private var tables: [Int32: SqliteValueBoxTable] = [:] private var fullTextTables: [Int32: ValueBoxFullTextTable] = [:] @@ -202,11 +203,12 @@ public final class SqliteValueBox: ValueBox { private let queue: Queue - public init?(basePath: String, queue: Queue, isTemporary: Bool, isReadOnly: Bool, useCaches: Bool, encryptionParameters: ValueBoxEncryptionParameters?, upgradeProgress: (Float) -> Void, inMemory: Bool = false) { + public init?(basePath: String, queue: Queue, isTemporary: Bool, isReadOnly: Bool, useCaches: Bool, removeDatabaseOnError: Bool, encryptionParameters: ValueBoxEncryptionParameters?, upgradeProgress: (Float) -> Void, inMemory: Bool = false) { self.basePath = basePath self.isTemporary = isTemporary self.isReadOnly = isReadOnly self.useCaches = useCaches + self.removeDatabaseOnError = removeDatabaseOnError self.inMemory = inMemory self.encryptionParameters = encryptionParameters self.databasePath = basePath + "/db_sqlite" @@ -294,7 +296,9 @@ public final class SqliteValueBox: ValueBox { preconditionFailure("Don't have write access to database folder") } - let _ = try? FileManager.default.removeItem(atPath: path) + if self.removeDatabaseOnError { + let _ = try? FileManager.default.removeItem(atPath: path) + } preconditionFailure("Couldn't open database") } @@ -328,7 +332,7 @@ public final class SqliteValueBox: ValueBox { if self.isEncrypted(database) { postboxLog("Encryption key is invalid") - if isTemporary || isReadOnly { + if isTemporary || isReadOnly || !self.removeDatabaseOnError { return nil } @@ -347,7 +351,7 @@ public final class SqliteValueBox: ValueBox { } } else { postboxLog("Encryption key is required") - if isReadOnly { + if isReadOnly || !self.removeDatabaseOnError { return nil } @@ -548,13 +552,16 @@ public final class SqliteValueBox: ValueBox { postboxLog("isEncrypted prepare...") let allIsOk = Atomic(value: false) + let removeDatabaseOnError = self.removeDatabaseOnError let databasePath = self.databasePath DispatchQueue.global().asyncAfter(deadline: .now() + 5.0, execute: { if allIsOk.with({ $0 }) == false { postboxLog("Timeout reached, discarding database") - try? FileManager.default.removeItem(atPath: databasePath) + if removeDatabaseOnError { + try? FileManager.default.removeItem(atPath: databasePath) + } - exit(0) + preconditionFailure() } }) let status = sqlite3_prepare_v2(database.handle, "SELECT * FROM sqlite_master LIMIT 1", -1, &statement, nil) @@ -569,7 +576,7 @@ public final class SqliteValueBox: ValueBox { return true } let preparedStatement = SqlitePreparedStatement(statement: statement) - switch preparedStatement.tryStep(handle: database.handle, path: self.databasePath) { + switch preparedStatement.tryStep(handle: database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { case .success: break case let .failure(error): @@ -588,7 +595,7 @@ public final class SqliteValueBox: ValueBox { let status = sqlite3_prepare_v2(database.handle, "PRAGMA user_version", -1, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) - let _ = preparedStatement.step(handle: database.handle, path: self.databasePath) + let _ = preparedStatement.step(handle: database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) let value = preparedStatement.int64At(0) preparedStatement.destroy() return value @@ -601,7 +608,7 @@ public final class SqliteValueBox: ValueBox { precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) var result: String? - if preparedStatement.step(handle: database.handle, path: self.databasePath) { + if preparedStatement.step(handle: database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { result = preparedStatement.stringAt(0) } preparedStatement.destroy() @@ -616,7 +623,7 @@ public final class SqliteValueBox: ValueBox { let preparedStatement = SqlitePreparedStatement(statement: statement) var tables: [SqliteValueBoxTable] = [] - while preparedStatement.step(handle: database.handle, true, path: self.databasePath) { + while preparedStatement.step(handle: database.handle, true, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { guard let name = preparedStatement.stringAt(0) else { assertionFailure() continue @@ -663,7 +670,7 @@ public final class SqliteValueBox: ValueBox { let preparedStatement = SqlitePreparedStatement(statement: statement) var tables: [ValueBoxFullTextTable] = [] - while preparedStatement.step(handle: database.handle, true, path: self.databasePath) { + while preparedStatement.step(handle: database.handle, true, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let value = preparedStatement.int64At(0) tables.append(ValueBoxFullTextTable(id: Int32(value))) } @@ -1475,7 +1482,7 @@ public final class SqliteValueBox: ValueBox { var buffer: ReadBuffer? - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { buffer = statement.valueAt(0) break } @@ -1495,7 +1502,7 @@ public final class SqliteValueBox: ValueBox { if let _ = self.tables[table.id] { let statement = self.getRowIdStatement(table, key: key) - if statement.step(handle: self.database.handle, path: self.databasePath) { + if statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let rowId = statement.int64At(0) var blobHandle: OpaquePointer? sqlite3_blob_open(database.handle, "main", "t\(table.id)", "value", rowId, 0, &blobHandle) @@ -1515,7 +1522,7 @@ public final class SqliteValueBox: ValueBox { if let _ = self.tables[table.id] { let statement = self.getRowIdStatement(table, key: key) - if statement.step(handle: self.database.handle, path: self.databasePath) { + if statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let rowId = statement.int64At(0) var blobHandle: OpaquePointer? sqlite3_blob_open(database.handle, "main", "t\(table.id)", "value", rowId, 1, &blobHandle) @@ -1566,7 +1573,7 @@ public final class SqliteValueBox: ValueBox { } } - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let key = statement.keyAt(0) let value = statement.valueAt(1) @@ -1591,7 +1598,7 @@ public final class SqliteValueBox: ValueBox { } } - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let key = statement.int64KeyAt(0) let value = statement.valueAt(1) @@ -1702,7 +1709,7 @@ public final class SqliteValueBox: ValueBox { } } - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let key = statement.keyAt(0) if !keys(key) { @@ -1726,7 +1733,7 @@ public final class SqliteValueBox: ValueBox { } } - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let key = statement.int64KeyAt(0) if !keys(key) { @@ -1748,7 +1755,7 @@ public final class SqliteValueBox: ValueBox { if let _ = self.tables[table.id] { let statement: SqlitePreparedStatement = self.scanStatement(table) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let key = statement.keyAt(0) let value = statement.valueAt(1) @@ -1767,7 +1774,7 @@ public final class SqliteValueBox: ValueBox { if let _ = self.tables[table.id] { let statement: SqlitePreparedStatement = self.scanKeysStatement(table) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let key = statement.keyAt(0) if !keys(key) { @@ -1785,7 +1792,7 @@ public final class SqliteValueBox: ValueBox { if let _ = self.tables[table.id] { let statement: SqlitePreparedStatement = self.scanStatement(table) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let key = statement.int64KeyValueAt(0) let value = statement.valueAt(1) @@ -1804,7 +1811,7 @@ public final class SqliteValueBox: ValueBox { if let _ = self.tables[table.id] { let statement: SqlitePreparedStatement = self.scanKeysStatement(table) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let key = statement.int64KeyValueAt(0) if !keys(key) { @@ -1822,18 +1829,18 @@ public final class SqliteValueBox: ValueBox { if sqliteTable.hasPrimaryKey { let statement = self.insertOrReplaceStatement(sqliteTable, key: key, value: value) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { } statement.reset() } else { if self.exists(table, key: key) { let statement = self.updateStatement(table, key: key, value: value) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { } statement.reset() } else { let statement = self.insertOrReplaceStatement(sqliteTable, key: key, value: value) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { } statement.reset() } @@ -1850,7 +1857,7 @@ public final class SqliteValueBox: ValueBox { } let statement = self.deleteStatement(table, key: key) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { } statement.reset() } @@ -1860,7 +1867,7 @@ public final class SqliteValueBox: ValueBox { precondition(self.queue.isCurrent()) if let _ = self.tables[table.id] { let statement = self.rangeDeleteStatement(table, start: min(start, end), end: max(start, end)) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { } statement.reset() } @@ -1870,7 +1877,7 @@ public final class SqliteValueBox: ValueBox { precondition(self.queue.isCurrent()) if let _ = self.tables[table.id] { let statement = self.moveStatement(table, from: previousKey, to: updatedKey) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { } statement.reset() } @@ -1880,7 +1887,7 @@ public final class SqliteValueBox: ValueBox { precondition(self.queue.isCurrent()) if let _ = self.tables[fromTable.id] { let statement = self.copyStatement(fromTable: fromTable, fromKey: fromKey, toTable: toTable, toKey: toKey) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { } statement.reset() } @@ -1916,7 +1923,7 @@ public final class SqliteValueBox: ValueBox { } if let statement = statement { - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let resultCollectionId = statement.stringAt(0) let resultItemId = statement.stringAt(1) @@ -1942,7 +1949,7 @@ public final class SqliteValueBox: ValueBox { } let statement = self.fullTextInsertStatement(table, collectionId: collectionIdData, itemId: itemIdData, contents: contentsData, tags: tagsData) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { } statement.reset() } @@ -1960,7 +1967,7 @@ public final class SqliteValueBox: ValueBox { } let statement = self.fullTextDeleteStatement(table, itemId: itemIdData) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { } statement.reset() } @@ -1987,7 +1994,7 @@ public final class SqliteValueBox: ValueBox { } var result = 0 - while statement.step(handle: database.handle, true, path: self.databasePath) { + while statement.step(handle: database.handle, true, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let value = statement.int32At(0) result = Int(value) } @@ -2005,7 +2012,7 @@ public final class SqliteValueBox: ValueBox { let statement = SqlitePreparedStatement(statement: statementImpl) var result = 0 - while statement.step(handle: database.handle, true, path: self.databasePath) { + while statement.step(handle: database.handle, true, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let value = statement.int32At(0) result = Int(value) } diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index 8012a5d5ae..f0b6e79b90 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -173,7 +173,8 @@ public func accountWithId(accountManager: AccountManager { } } - fileprivate init?(queue: Queue, basePath: String, isTemporary: Bool, isReadOnly: Bool, useCaches: Bool, temporarySessionId: Int64) { + fileprivate init?(queue: Queue, basePath: String, isTemporary: Bool, isReadOnly: Bool, useCaches: Bool, removeDatabaseOnError: Bool, temporarySessionId: Int64) { let startTime = CFAbsoluteTimeGetCurrent() self.queue = queue @@ -82,11 +82,11 @@ final class AccountManagerImpl { self.loginTokensPath = "\(basePath)/login-tokens" self.temporarySessionId = temporarySessionId let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil) - guard let guardValueBox = SqliteValueBox(basePath: basePath + "/guard_db", queue: queue, isTemporary: isTemporary, isReadOnly: false, useCaches: useCaches, encryptionParameters: nil, upgradeProgress: { _ in }) else { + guard let guardValueBox = SqliteValueBox(basePath: basePath + "/guard_db", queue: queue, isTemporary: isTemporary, isReadOnly: false, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: nil, upgradeProgress: { _ in }) else { return nil } self.guardValueBox = guardValueBox - guard let valueBox = SqliteValueBox(basePath: basePath + "/db", queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, encryptionParameters: nil, upgradeProgress: { _ in }) else { + guard let valueBox = SqliteValueBox(basePath: basePath + "/db", queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: nil, upgradeProgress: { _ in }) else { return nil } self.valueBox = valueBox @@ -510,7 +510,7 @@ public final class AccountManager { return AccountManagerImpl.getCurrentRecords(basePath: basePath) } - public init(basePath: String, isTemporary: Bool, isReadOnly: Bool, useCaches: Bool) { + public init(basePath: String, isTemporary: Bool, isReadOnly: Bool, useCaches: Bool, removeDatabaseOnError: Bool) { self.queue = sharedQueue self.basePath = basePath var temporarySessionId: Int64 = 0 @@ -518,7 +518,7 @@ public final class AccountManager { self.temporarySessionId = temporarySessionId let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { - if let value = AccountManagerImpl(queue: queue, basePath: basePath, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, temporarySessionId: temporarySessionId) { + if let value = AccountManagerImpl(queue: queue, basePath: basePath, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, temporarySessionId: temporarySessionId) { return value } else { preconditionFailure() diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift index ea3f266d0e..18ace49fe7 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift @@ -97,9 +97,9 @@ public enum AccountTransactionError { case couldNotOpen } -public func accountTransaction(rootPath: String, id: AccountRecordId, encryptionParameters: ValueBoxEncryptionParameters, isReadOnly: Bool, useCopy: Bool = false, useCaches: Bool = true, transaction: @escaping (Postbox, Transaction) -> T) -> Signal { +public func accountTransaction(rootPath: String, id: AccountRecordId, encryptionParameters: ValueBoxEncryptionParameters, isReadOnly: Bool, useCopy: Bool = false, useCaches: Bool = true, removeDatabaseOnError: Bool = true, transaction: @escaping (Postbox, Transaction) -> T) -> Signal { let path = "\(rootPath)/\(accountRecordIdPathName(id))" - let postbox = openPostbox(basePath: path + "/postbox", seedConfiguration: telegramPostboxSeedConfiguration, encryptionParameters: encryptionParameters, timestampForAbsoluteTimeBasedOperations: Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970), isTemporary: true, isReadOnly: isReadOnly, useCopy: useCopy, useCaches: useCaches) + let postbox = openPostbox(basePath: path + "/postbox", seedConfiguration: telegramPostboxSeedConfiguration, encryptionParameters: encryptionParameters, timestampForAbsoluteTimeBasedOperations: Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970), isTemporary: true, isReadOnly: isReadOnly, useCopy: useCopy, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError) return postbox |> castError(AccountTransactionError.self) |> mapToSignal { value -> Signal in diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 58ce4b2f49..a43ff5cb3d 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -705,7 +705,7 @@ private func extractAccountManagerState(records: AccountRecordsView(basePath: rootPath + "/accounts-metadata", isTemporary: false, isReadOnly: false, useCaches: true) + let accountManager = AccountManager(basePath: rootPath + "/accounts-metadata", isTemporary: false, isReadOnly: false, useCaches: true, removeDatabaseOnError: true) self.accountManager = accountManager telegramUIDeclareEncodables() diff --git a/submodules/TelegramUI/Sources/NotificationContentContext.swift b/submodules/TelegramUI/Sources/NotificationContentContext.swift index 0925c65132..7d6151ecf1 100644 --- a/submodules/TelegramUI/Sources/NotificationContentContext.swift +++ b/submodules/TelegramUI/Sources/NotificationContentContext.swift @@ -94,7 +94,7 @@ public final class NotificationViewControllerImpl { if sharedAccountContext == nil { initializeAccountManagement() - let accountManager = AccountManager(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false) + let accountManager = AccountManager(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false, removeDatabaseOnError: false) var initialPresentationDataAndSettings: InitialPresentationDataAndSettings? let semaphore = DispatchSemaphore(value: 0) diff --git a/submodules/TelegramUI/Sources/ShareExtensionContext.swift b/submodules/TelegramUI/Sources/ShareExtensionContext.swift index af225ff735..9d4fefba03 100644 --- a/submodules/TelegramUI/Sources/ShareExtensionContext.swift +++ b/submodules/TelegramUI/Sources/ShareExtensionContext.swift @@ -205,7 +205,7 @@ public class ShareRootControllerImpl { let internalContext: InternalContext - let accountManager = AccountManager(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false) + let accountManager = AccountManager(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false, removeDatabaseOnError: false) if let globalInternalContext = globalInternalContext { internalContext = globalInternalContext From 36485e344f38a5cd5214a3e12b26f4fccd07211e Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 21 Dec 2021 15:07:29 +0400 Subject: [PATCH 22/35] Fix context menu layout on iPad --- submodules/ContextUI/Sources/ContextController.swift | 6 +++++- .../ContextControllerExtractedPresentationNode.swift | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 0ac7f7abd5..f94be5a65c 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -1412,10 +1412,14 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi if let presentationNode = self.presentationNode { transition.updateFrame(node: presentationNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + var updatedTransition = transition + if case .animateIn = presentationStateTransition { + updatedTransition = .immediate + } presentationNode.update( presentationData: self.presentationData, layout: layout, - transition: transition, + transition: updatedTransition, stateTransition: presentationStateTransition ) return diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 4e63a212a6..54686eafdf 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -334,7 +334,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo reactionContextNodeTransition = .immediate } reactionContextNodeTransition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) - reactionContextNode.updateLayout(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), anchorRect: contentRect, transition: reactionContextNodeTransition) + reactionContextNode.updateLayout(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), anchorRect: contentRect.offsetBy(dx: contentParentGlobalFrame.minX, dy: 0.0), transition: reactionContextNodeTransition) } if let removedReactionContextNode = removedReactionContextNode { removedReactionContextNode.animateOut(to: contentRect, animatingOutToReaction: false) @@ -418,7 +418,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) if let animateClippingFromContentAreaInScreenSpace = contentNode.animateClippingFromContentAreaInScreenSpace { - self.clippingNode.layer.animateFrame(from: animateClippingFromContentAreaInScreenSpace, to: CGRect(origin: CGPoint(), size: layout.size), duration: 0.2) + self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(x: 0.0, y: animateClippingFromContentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: animateClippingFromContentAreaInScreenSpace.height)), to: CGRect(origin: CGPoint(), size: layout.size), duration: 0.2) self.clippingNode.layer.animateBoundsOriginYAdditive(from: animateClippingFromContentAreaInScreenSpace.minY, to: 0.0, duration: 0.2) } @@ -528,7 +528,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo let putBackInfo = self.source.putBack() if let putBackInfo = putBackInfo { - self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(), size: layout.size), to: putBackInfo.contentAreaInScreenSpace, duration: duration, removeOnCompletion: false) + self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(), size: layout.size), to: CGRect(origin: CGPoint(x: 0.0, y: putBackInfo.contentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: putBackInfo.contentAreaInScreenSpace.height)), duration: duration, removeOnCompletion: false) self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: duration, removeOnCompletion: false) } From 3a93b4e86f9eb5aa07fe7bc1dd1c9476abdc4b74 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 21 Dec 2021 15:55:08 +0400 Subject: [PATCH 23/35] Fix sticker and instant video reaction layout --- .../ChatMessageAnimatedStickerItemNode.swift | 7 +++- .../ChatMessageInstantVideoItemNode.swift | 37 ++++++++++++++++++- .../Sources/ChatMessageStickerItemNode.swift | 7 +++- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index f7beb9ea94..ad2c8bfc1a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1144,7 +1144,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { layoutSize.height += actionButtonsSizeAndApply.0.height } if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { - layoutSize.height += reactionButtonsSizeAndApply.0.height + layoutSize.height += 4.0 + reactionButtonsSizeAndApply.0.height } return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in @@ -1411,7 +1411,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { let reactionButtonsNode = reactionButtonsSizeAndApply.1(animation) - let reactionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY), size: reactionButtonsSizeAndApply.0) + var reactionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY), size: reactionButtonsSizeAndApply.0) + if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { + reactionButtonsFrame.origin.y += 4.0 + actionButtonsSizeAndApply.0.height + } if reactionButtonsNode !== strongSelf.reactionButtonsNode { strongSelf.reactionButtonsNode = reactionButtonsNode reactionButtonsNode.reactionSelected = { value in diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index ba8455156d..ff168a8ec3 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -787,7 +787,10 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { let reactionButtonsNode = reactionButtonsSizeAndApply.1(animation) - let reactionButtonsFrame = CGRect(origin: CGPoint(x: videoFrame.minX, y: videoFrame.maxY + 6.0), size: reactionButtonsSizeAndApply.0) + var reactionButtonsFrame = CGRect(origin: CGPoint(x: videoFrame.minX, y: videoFrame.maxY + 6.0), size: reactionButtonsSizeAndApply.0) + if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { + reactionButtonsFrame.origin.y += 4.0 + actionButtonsSizeAndApply.0.height + } if reactionButtonsNode !== strongSelf.reactionButtonsNode { strongSelf.reactionButtonsNode = reactionButtonsNode reactionButtonsNode.reactionSelected = { value in @@ -805,6 +808,16 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) } reactionButtonsNode.frame = reactionButtonsFrame + if let (rect, containerSize) = strongSelf.absoluteRect { + var rect = rect + rect.origin.y = containerSize.height - rect.maxY + strongSelf.insets.top + + var reactionButtonsNodeFrame = reactionButtonsFrame + reactionButtonsNodeFrame.origin.x += rect.minX + reactionButtonsNodeFrame.origin.y += rect.minY + + reactionButtonsNode.update(rect: rect, within: containerSize, transition: .immediate) + } strongSelf.addSubnode(reactionButtonsNode) if animation.isAnimated { reactionButtonsNode.animateIn(animation: animation) @@ -1338,6 +1351,28 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.interactiveVideoNode.frame, nil) } + private var absoluteRect: (CGRect, CGSize)? + override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.absoluteRect = (rect, containerSize) + + var rect = rect + rect.origin.y = containerSize.height - rect.maxY + self.insets.top + + if let reactionButtonsNode = self.reactionButtonsNode { + var reactionButtonsNodeFrame = reactionButtonsNode.frame + reactionButtonsNodeFrame.origin.x += rect.minX + reactionButtonsNodeFrame.origin.y += rect.minY + + reactionButtonsNode.update(rect: rect, within: containerSize, transition: .immediate) + } + } + + override func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + if let reactionButtonsNode = self.reactionButtonsNode { + reactionButtonsNode.offset(value: value, animationCurve: animationCurve, duration: duration) + } + } + override func targetReactionView(value: String) -> UIView? { if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { return result diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index 619222d1f8..c16ce72c85 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -689,7 +689,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { layoutSize.height += dateAndStatusSize.height } if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { - layoutSize.height += reactionButtonsSizeAndApply.0.height + layoutSize.height += 4.0 + reactionButtonsSizeAndApply.0.height } if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { layoutSize.height += actionButtonsSizeAndApply.0.height @@ -986,7 +986,10 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { let reactionButtonsNode = reactionButtonsSizeAndApply.1(animation) - let reactionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY - 10.0), size: reactionButtonsSizeAndApply.0) + var reactionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY - 10.0), size: reactionButtonsSizeAndApply.0) + if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { + reactionButtonsFrame.origin.y += 4.0 + actionButtonsSizeAndApply.0.height + } if reactionButtonsNode !== strongSelf.reactionButtonsNode { strongSelf.reactionButtonsNode = reactionButtonsNode reactionButtonsNode.reactionSelected = { value in From e38e44f7586ff522e22b319100b6fb75e9defd61 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 21 Dec 2021 16:00:54 +0400 Subject: [PATCH 24/35] Fix context menu animation --- submodules/ContextUI/Sources/ContextController.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index f94be5a65c..0ac7f7abd5 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -1412,14 +1412,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi if let presentationNode = self.presentationNode { transition.updateFrame(node: presentationNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - var updatedTransition = transition - if case .animateIn = presentationStateTransition { - updatedTransition = .immediate - } presentationNode.update( presentationData: self.presentationData, layout: layout, - transition: updatedTransition, + transition: transition, stateTransition: presentationStateTransition ) return From 73b49b6039ee9f8aeb95b560b9aef7da878bdb7e Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 21 Dec 2021 17:31:20 +0400 Subject: [PATCH 25/35] Reaction improvements --- .../Sources/ReactionListContextMenuContent.swift | 2 +- .../TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index ce1b2c0593..0ae1d4ae53 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -741,7 +741,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent reactions.insert((nil, totalCount), at: 0) } - if reactions.count > 2 { + if reactions.count > 2 && totalCount > 10 { self.tabListNode = ReactionTabListNode(context: context, availableReactions: availableReactions, reactions: reactions, message: message) } diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index ff168a8ec3..0857dcdb14 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -558,7 +558,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD if !reactions.reactions.isEmpty { let totalInset = params.leftInset + layoutConstants.bubble.edgeInset * 2.0 + avatarInset + layoutConstants.bubble.contentInsets.left + params.rightInset + layoutConstants.bubble.contentInsets.right - let maxReactionsWidth = params.width - totalInset + let maxReactionsWidth = params.width - totalInset - 8.0 let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments( context: item.context, presentationData: item.presentationData, From f830972d325915a4a7abc1593eacd276cd326092 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 21 Dec 2021 17:56:56 +0400 Subject: [PATCH 26/35] Reaction improvements --- .../Sources/ReactionButtonListComponent.swift | 8 ++--- .../Sources/DefaultPresentationStrings.swift | 28 ++++++++++++++++ .../TelegramUI/Sources/ChatController.swift | 32 +++++++++++-------- .../Sources/ChatMessageTransitionNode.swift | 11 ++++--- 4 files changed, 57 insertions(+), 22 deletions(-) diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index 437824c587..a98a6f642c 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -153,7 +153,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode { } var counterComponents: [String] = [] - for character in "\(spec.component.count)" { + for character in countString(Int64(spec.component.count)) { counterComponents.append(String(character)) } @@ -164,12 +164,12 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode { var previousDisplayCounter: String? if let currentLayout = currentLayout { if currentLayout.spec.component.avatarPeers.isEmpty { - previousDisplayCounter = "\(spec.component.count)" + previousDisplayCounter = countString(Int64(spec.component.count)) } } var currentDisplayCounter: String? if spec.component.avatarPeers.isEmpty { - currentDisplayCounter = "\(spec.component.count)" + currentDisplayCounter = countString(Int64(spec.component.count)) } let backgroundImage: UIImage @@ -669,7 +669,7 @@ public final class ReactionButtonComponent: Component { self.iconView.frame = CGRect(origin: CGPoint(x: sideInsets, y: floorToScreenPixels((height - imageSize.height) / 2.0)), size: imageSize) - let text = "\(component.count)" + let text = countString(Int64(component.count)) var measureText = "" for _ in 0 ..< text.count { measureText.append("0") diff --git a/submodules/TelegramPresentationData/Sources/DefaultPresentationStrings.swift b/submodules/TelegramPresentationData/Sources/DefaultPresentationStrings.swift index 22191fc8e1..53da3d2101 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultPresentationStrings.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultPresentationStrings.swift @@ -95,3 +95,31 @@ public func dataSizeString(_ size: Int64, forceDecimal: Bool = false, formatting return formatting.byte("\(size)").string } } + +public func countString(_ count: Int64, forceDecimal: Bool = false) -> String { + let decimalSeparator = "." + if count >= 1000 * 1000 * 1000 { + let remainder = Int64((Double(count % (1000 * 1000 * 1000)) / (1000 * 1000 * 100.0)).rounded(.down)) + if remainder != 0 || forceDecimal { + return "\(count / (1000 * 1000 * 1000))\(decimalSeparator)\(remainder)T" + } else { + return "\(count / (1000 * 1000 * 1000))T" + } + } else if count >= 1000 * 1000 { + let remainder = Int64((Double(count % (1000 * 1000)) / (1000.0 * 100.0)).rounded(.down)) + if remainder != 0 || forceDecimal { + return "\(count / (1000 * 1000))\(decimalSeparator)\(remainder)M" + } else { + return "\(count / (1000 * 1000))M" + } + } else if count >= 1000 { + let remainder = (count % (1000)) / (102) + if remainder != 0 || forceDecimal { + return "\(count / 1000)\(decimalSeparator)\(remainder)K" + } else { + return "\(count / 1000)K" + } + } else { + return "\(count)" + } +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 90cdd71d0a..98ca92e802 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1265,23 +1265,27 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } }) - } 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 { + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts(itemNode: itemNode) + + 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() diff --git a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift index 92493c11a6..62e47ed088 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift @@ -775,11 +775,14 @@ public final class ChatMessageTransitionNode: ASDisplayNode { } } - func dismissMessageReactionContexts() { - for messageReactionContext in self.messageReactionContexts { - messageReactionContext.dismiss() + func dismissMessageReactionContexts(itemNode: ListViewItemNode? = nil) { + for i in (0 ..< self.messageReactionContexts.count).reversed() { + let messageReactionContext = self.messageReactionContexts[i] + if itemNode == nil || messageReactionContext.itemNode === itemNode { + self.messageReactionContexts.remove(at: i) + messageReactionContext.dismiss() + } } - self.messageReactionContexts.removeAll() } func addMessageContextController(messageId: MessageId, contextController: ContextController) { From e8f4cbae768d0e5fe1733d6c9b77e2d094b3b967 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 21 Dec 2021 18:28:03 +0400 Subject: [PATCH 27/35] Retry upload --- buildbox/deploy-appcenter.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/buildbox/deploy-appcenter.sh b/buildbox/deploy-appcenter.sh index 141a43bf48..0c5195fe3f 100644 --- a/buildbox/deploy-appcenter.sh +++ b/buildbox/deploy-appcenter.sh @@ -9,5 +9,11 @@ DSYM_PATH="build/artifacts/Telegram.DSYMs.zip" APPCENTER="/usr/local/bin/appcenter" $APPCENTER login --token "$API_TOKEN" -$APPCENTER distribute release --app "$API_USER_NAME/$API_APP_NAME" -f "$IPA_PATH" -g Internal + +NEXT_WAIT_TIME=0 +until [ $NEXT_WAIT_TIME -eq 5 ] || $APPCENTER distribute release --app "$API_USER_NAME/$API_APP_NAME" -f "$IPA_PATH" -g Internal; do + sleep $(( NEXT_WAIT_TIME++ )) +done +[ $NEXT_WAIT_TIME -lt 10 ] + $APPCENTER crashes upload-symbols --app "$API_USER_NAME/$API_APP_NAME" --symbol "$DSYM_PATH" From 6035a52fd3f0b63863dc7354fa50a3d08e3ef902 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 21 Dec 2021 18:36:44 +0400 Subject: [PATCH 28/35] Various Improvements --- .../Sources/Node/ChatListItem.swift | 8 + submodules/SettingsUI/BUILD | 1 + .../LocalizationListControllerNode.swift | 27 +++- .../ChatPinnedMessageTitlePanelNode.swift | 44 +++++- .../Sources/ChatTextInputMenu.swift | 2 +- .../Sources/ChatTextInputPanelNode.swift | 143 +++++++++++++++++- .../Sources/ReplyAccessoryPanelNode.swift | 69 ++++++--- .../Sources/ChatTextInputAttributes.swift | 18 ++- 8 files changed, 286 insertions(+), 26 deletions(-) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index cef20e0fe2..5b0a72e542 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1786,11 +1786,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.inputActivitiesNode.alpha = 1.0 strongSelf.textNode.alpha = 0.0 strongSelf.authorNode.alpha = 0.0 + strongSelf.dustNode?.alpha = 0.0 if animated || animateContent { strongSelf.inputActivitiesNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) strongSelf.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) strongSelf.authorNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) + strongSelf.dustNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) } } } else { @@ -1798,6 +1800,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.inputActivitiesNode.alpha = 0.0 strongSelf.textNode.alpha = 1.0 strongSelf.authorNode.alpha = 1.0 + strongSelf.dustNode?.alpha = 1.0 if animated || animateContent { strongSelf.inputActivitiesNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, completion: { value in if let strongSelf = self, value { @@ -1806,6 +1809,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { }) strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) strongSelf.authorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + strongSelf.dustNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } else { strongSelf.inputActivitiesNode.removeFromSupernode() } @@ -2016,6 +2020,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { textFrame.origin.x = contentRect.origin.x transition.updateFrameAdditive(node: self.textNode, frame: textFrame) + if let dustNode = self.dustNode { + transition.updateFrameAdditive(node: dustNode, frame: textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)) + } + var mediaPreviewOffsetX = textFrame.origin.x + 1.0 let contentImageSpacing: CGFloat = 2.0 for (_, media, mediaSize) in self.currentMediaPreviewSpecs { diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 55a781e9c6..aa68c921f7 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -92,6 +92,7 @@ swift_library( "//submodules/DebugSettingsUI:DebugSettingsUI", "//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode", "//submodules/WebPBinding:WebPBinding", + "//submodules/Translate:Translate", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift index 5ef5d5d003..c3a778db4b 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift @@ -15,6 +15,7 @@ import SearchBarNode import SearchUI import UndoUI import TelegramUIPreferences +import Translate private enum LanguageListSection: ItemListSectionId { case translate @@ -432,8 +433,20 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { var existingIds = Set() var showTranslate = true + var ignoredLanguages: [String] = [] if let translationSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) { showTranslate = translationSettings.showTranslate + if let languages = translationSettings.ignoredLanguages { + ignoredLanguages = languages + } else { + if let activeLanguageCode = activeLanguageCode, supportedTranslationLanguages.contains(activeLanguageCode) { + ignoredLanguages = [activeLanguageCode] + } + } + } else { + if let activeLanguageCode = activeLanguageCode, supportedTranslationLanguages.contains(activeLanguageCode) { + ignoredLanguages = [activeLanguageCode] + } } let localizationListState = (view.views[preferencesKey] as? PreferencesView)?.values[PreferencesKeys.localizationListState]?.get(LocalizationListState.self) @@ -444,8 +457,18 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { entries.append(.translateTitle(text: presentationData.strings.Localization_TranslateMessages.uppercased())) entries.append(.translate(text: presentationData.strings.Localization_ShowTranslate, value: showTranslate)) if showTranslate { - entries.append(.doNotTranslate(text: presentationData.strings.Localization_DoNotTranslate, value: "")) - entries.append(.translateInfo(text: presentationData.strings.Localization_DoNotTranslateInfo)) + var value = "" + if ignoredLanguages.count > 1 { + value = ignoredLanguages.joined(separator: ", ") + } else if let code = ignoredLanguages.first { + let enLocale = Locale(identifier: "en") + if let title = enLocale.localizedString(forLanguageCode: code) { + value = title + } + } + + entries.append(.doNotTranslate(text: presentationData.strings.Localization_DoNotTranslate, value: value)) + entries.append(.translateInfo(text: ignoredLanguages.count > 1 ? presentationData.strings.Localization_DoNotTranslateManyInfo : presentationData.strings.Localization_DoNotTranslateInfo)) } else { entries.append(.translateInfo(text: presentationData.strings.Localization_ShowTranslateInfo)) } diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index d4158f8c69..15cedf05cc 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -15,6 +15,8 @@ import AnimatedCountLabelNode import AnimatedNavigationStripeNode import ContextUI import RadialStatusNode +import InvisibleInkDustNode +import TextFormat private enum PinnedMessageAnimation { case slideToTop @@ -50,6 +52,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { private let lineNode: AnimatedNavigationStripeNode private let titleNode: AnimatedCountLabelNode private let textNode: TextNode + private var dustNode: InvisibleInkDustNode? private let imageNode: TransformImageNode private let imageNodeContainer: ASDisplayNode @@ -451,7 +454,25 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { } let (titleLayout, titleApply) = makeTitleLayout(CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), titleStrings) - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: foldLineBreaks(descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId).0), font: Font.regular(15.0), textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) + let (textString, _, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId) + + let messageText: NSAttributedString + let textFont = Font.regular(15.0) + if isText { + let entities = (message.textEntitiesAttribute?.entities ?? []).filter { entity in + if case .Spoiler = entity.type { + return true + } else { + return false + } + } + let textColor = theme.chat.inputPanel.primaryTextColor + messageText = stringWithAppliedEntities(message.text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false) + } else { + messageText = NSAttributedString(string: foldLineBreaks(textString), font: textFont, textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor) + } + + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) Queue.mainQueue().async { if let strongSelf = self { @@ -463,7 +484,26 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { animationTransition.updateFrameAdditive(node: strongSelf.contentTextContainer, frame: CGRect(origin: CGPoint(x: contentLeftInset + textLineInset, y: 0.0), size: CGSize())) strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 5.0), size: titleLayout.size) - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 23.0), size: textLayout.size) + + let textFrame = CGRect(origin: CGPoint(x: 0.0, y: 23.0), size: textLayout.size) + strongSelf.textNode.frame = textFrame + + if !textLayout.spoilers.isEmpty { + let dustNode: InvisibleInkDustNode + if let current = strongSelf.dustNode { + dustNode = current + } else { + dustNode = InvisibleInkDustNode(textNode: nil) + dustNode.isUserInteractionEnabled = false + strongSelf.dustNode = dustNode + strongSelf.contentTextContainer.insertSubnode(dustNode, aboveSubnode: strongSelf.textNode) + } + dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) + dustNode.update(size: dustNode.frame.size, color: theme.chat.inputPanel.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + } else if let dustNode = strongSelf.dustNode { + dustNode.removeFromSupernode() + strongSelf.dustNode = nil + } let lineFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: 0.0), size: CGSize(width: 2.0, height: panelHeight)) animationTransition.updateFrame(node: strongSelf.lineNode, frame: lineFrame) diff --git a/submodules/TelegramUI/Sources/ChatTextInputMenu.swift b/submodules/TelegramUI/Sources/ChatTextInputMenu.swift index 630f06b617..81fedc3d29 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputMenu.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputMenu.swift @@ -27,11 +27,11 @@ final class ChatTextInputMenu { UIMenuController.shared.menuItems = [] case .format: UIMenuController.shared.menuItems = [ + UIMenuItem(title: self.stringSpoiler, action: Selector(("formatAttributesSpoiler:"))), UIMenuItem(title: self.stringBold, action: Selector(("formatAttributesBold:"))), UIMenuItem(title: self.stringItalic, action: Selector(("formatAttributesItalic:"))), UIMenuItem(title: self.stringMonospace, action: Selector(("formatAttributesMonospace:"))), UIMenuItem(title: self.stringLink, action: Selector(("formatAttributesLink:"))), - UIMenuItem(title: self.stringSpoiler, action: Selector(("formatAttributesSpoiler:"))), UIMenuItem(title: self.stringStrikethrough, action: Selector(("formatAttributesStrikethrough:"))), UIMenuItem(title: self.stringUnderline, action: Selector(("formatAttributesUnderline:"))) ] diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 1ef0d2ffde..6f28bbcbda 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -352,7 +352,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + private var currentState: ChatTextInputState? func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, accessoryItems: [ChatTextInputAccessoryItem], animated: Bool) { + self.currentState = state + if state.inputText.length != 0 && self.textInputNode == nil { self.loadTextInputNode() } @@ -713,6 +716,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } textInputNode.frame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) + textInputNode.view.layoutIfNeeded() + self.updateSpoiler() } self.textInputBackgroundNode.isUserInteractionEnabled = false @@ -1793,10 +1798,143 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } private func updateSpoiler() { + guard let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState else { + return + } + + let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor + + var rects: [CGRect] = [] + + if let attributedText = textInputNode.attributedText { + let beginning = textInputNode.textView.beginningOfDocument + attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in + if let _ = attributes[ChatTextInputAttributes.spoiler] { + func addSpoiler(startIndex: Int, endIndex: Int) { + if let start = textInputNode.textView.position(from: beginning, offset: startIndex), let end = textInputNode.textView.position(from: start, offset: endIndex - startIndex), let textRange = textInputNode.textView.textRange(from: start, to: end) { + let textRects = textInputNode.textView.selectionRects(for: textRange) + for textRect in textRects { + rects.append(textRect.rect.insetBy(dx: 1.0, dy: 1.0).offsetBy(dx: 0.0, dy: 1.0)) + } + } + } + + var startIndex: Int? + var currentIndex: Int? + + let nsString = (attributedText.string as NSString) + nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in + if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil { + if let currentStartIndex = startIndex { + startIndex = nil + let endIndex = range.location + addSpoiler(startIndex: currentStartIndex, endIndex: endIndex) + } + } else if startIndex == nil { + startIndex = range.location + } + currentIndex = range.location + range.length + } + + if let currentStartIndex = startIndex, let currentIndex = currentIndex { + startIndex = nil + let endIndex = currentIndex + addSpoiler(startIndex: currentStartIndex, endIndex: endIndex) + } + } + }) + } + + if !rects.isEmpty { + let dustNode: InvisibleInkDustNode + if let current = self.dustNode { + dustNode = current + } else { + dustNode = InvisibleInkDustNode(textNode: nil) + dustNode.alpha = self.spoilersRevealed ? 0.0 : 1.0 + dustNode.isUserInteractionEnabled = false + textInputNode.textView.addSubview(dustNode.view) + self.dustNode = dustNode + } + dustNode.frame = CGRect(origin: CGPoint(), size: textInputNode.textView.contentSize) + dustNode.update(size: textInputNode.textView.contentSize, color: textColor, rects: rects) + } else if let dustNode = self.dustNode { + dustNode.removeFromSupernode() + self.dustNode = nil + } + } + + private func updateSpoilersRevealed() { guard let textInputNode = self.textInputNode else { return } - print(textInputNode.attributedText?.description ?? "") + + let selectionRange = textInputNode.textView.selectedRange + + var revealed = false + if let attributedText = textInputNode.attributedText { + attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in + if let _ = attributes[ChatTextInputAttributes.spoiler] { + if let _ = selectionRange.intersection(range) { + revealed = true + } + } + }) + } + + guard self.spoilersRevealed != revealed else { + return + } + self.spoilersRevealed = revealed + + if revealed { + self.updateInternalSpoilersRevealed(true) + } else { + Queue.mainQueue().after(1.5, { + self.updateInternalSpoilersRevealed(false) + }) + } + } + + private func updateInternalSpoilersRevealed(_ revealed: Bool) { + guard self.spoilersRevealed == revealed, let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState else { + return + } + + let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor + let accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor + let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) + + refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed) + + if let state = self.currentState { + textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed) + } + + if textInputNode.textView.subviews.count > 1 { + let containerView = textInputNode.textView.subviews[1] + if let canvasView = containerView.subviews.first { + if let snapshotView = canvasView.snapshotView(afterScreenUpdates: false) { + textInputNode.view.insertSubview(snapshotView, at: 0) + canvasView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + } + + if revealed { + let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear) + if let dustNode = self.dustNode { + transition.updateAlpha(node: dustNode, alpha: 0.0) + } + } else { + let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear) + if let dustNode = self.dustNode { + transition.updateAlpha(node: dustNode, alpha: 1.0) + } + } } private func updateCounterTextNode(transition: ContainedViewLayoutTransition) { @@ -2069,6 +2207,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + + self.updateSpoilersRevealed() } } @@ -2193,6 +2333,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.spoiler), inputMode) } + self.updateSpoilersRevealed() } @objc func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { diff --git a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift index 57550bec1e..5018cf30ef 100644 --- a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift @@ -11,6 +11,8 @@ import AccountContext import LocalizedPeerData import PhotoResources import TelegramStringFormatting +import InvisibleInkDustNode +import TextFormat final class ReplyAccessoryPanelNode: AccessoryPanelNode { private let messageDisposable = MetaDisposable() @@ -23,6 +25,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { let iconNode: ASImageNode let titleNode: ImmediateTextNode let textNode: ImmediateTextNode + var dustNode: InvisibleInkDustNode? let imageNode: TransformImageNode private let actionArea: AccessibilityAreaNode @@ -96,6 +99,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { var authorName = "" var text = "" + var isText = true if let forwardInfo = message?.forwardInfo, forwardInfo.flags.contains(.isImported) { if let author = forwardInfo.author { authorName = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder) @@ -105,8 +109,34 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { } else if let author = message?.effectiveAuthor { authorName = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder) } + + let isMedia: Bool if let message = message { - (text, _, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) + switch messageContentKind(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) { + case .text: + isMedia = false + default: + isMedia = true + } + (text, _, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) + } else { + isMedia = false + } + + let textFont = Font.regular(14.0) + let messageText: NSAttributedString + if isText, let message = message { + let entities = (message.textEntitiesAttribute?.entities ?? []).filter { entity in + if case .Spoiler = entity.type { + return true + } else { + return false + } + } + let textColor = strongSelf.theme.chat.inputPanel.primaryTextColor + messageText = stringWithAppliedEntities(message.text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false) + } else { + messageText = NSAttributedString(string: text, font: textFont, textColor: isMedia ? strongSelf.theme.chat.inputPanel.secondaryTextColor : strongSelf.theme.chat.inputPanel.primaryTextColor) } var updatedMediaReference: AnyMediaReference? @@ -169,22 +199,10 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { updateImageSignal = .single({ _ in return nil }) } } - - let isMedia: Bool - if let message = message { - switch messageContentKind(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) { - case .text: - isMedia = false - default: - isMedia = true - } - } else { - isMedia = false - } - + strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.strings.Conversation_ReplyMessagePanelTitle(authorName).string, font: Font.medium(14.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) - strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: isMedia ? strongSelf.theme.chat.inputPanel.secondaryTextColor : strongSelf.theme.chat.inputPanel.primaryTextColor) - + strongSelf.textNode.attributedText = messageText + let headerString: String if let message = message, message.flags.contains(.Incoming), let author = message.author { headerString = "Reply to message. From: \(EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder))" @@ -295,8 +313,25 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { } let textSize = self.textNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset - imageTextInset, height: bounds.size.height)) + let textFrame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset - self.textNode.insets.left, y: 25.0 - self.textNode.insets.top), size: textSize) if self.textNode.supernode == self { - self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset - self.textNode.insets.left, y: 25.0 - self.textNode.insets.top), size: textSize) + self.textNode.frame = textFrame + } + + if let textLayout = self.textNode.cachedLayout, !textLayout.spoilers.isEmpty { + if self.dustNode == nil { + let dustNode = InvisibleInkDustNode(textNode: nil) + self.dustNode = dustNode + self.textNode.supernode?.insertSubnode(dustNode, aboveSubnode: self.textNode) + + } + if let dustNode = self.dustNode { + dustNode.update(size: textFrame.size, color: self.theme.chat.inputPanel.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) + } + } else if let dustNode = self.dustNode { + self.dustNode = nil + dustNode.removeFromSupernode() } } diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 597539aaf2..2c1bd90901 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -86,7 +86,11 @@ public func textAttributedStringForStateText(_ stateText: NSAttributedString, fo result.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) } else if key == ChatTextInputAttributes.spoiler { result.addAttribute(key, value: value, range: range) - result.addAttribute(NSAttributedString.Key.backgroundColor, value: textColor.withAlphaComponent(0.15), range: range) + if spoilersRevealed { + result.addAttribute(NSAttributedString.Key.backgroundColor, value: textColor.withAlphaComponent(0.15), range: range) + } else { + result.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range) + } } } @@ -472,7 +476,11 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme textNode.textView.textStorage.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) } else if key == ChatTextInputAttributes.spoiler { textNode.textView.textStorage.addAttribute(key, value: value, range: range) - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: theme.chat.inputPanel.primaryTextColor.withAlphaComponent(0.15), range: range) + if spoilersRevealed { + textNode.textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: theme.chat.inputPanel.primaryTextColor.withAlphaComponent(0.15), range: range) + } else { + textNode.textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range) + } } } @@ -564,7 +572,11 @@ public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, th textNode.textView.textStorage.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) } else if key == ChatTextInputAttributes.spoiler { textNode.textView.textStorage.addAttribute(key, value: value, range: range) - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: UIColor.clear, range: range) + if spoilersRevealed { + textNode.textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: theme.chat.inputPanel.primaryTextColor.withAlphaComponent(0.15), range: range) + } else { + textNode.textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range) + } } } From d60d2beaa4a2dc643d1b4d45869353b3fcd8bada Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 21 Dec 2021 18:48:21 +0400 Subject: [PATCH 29/35] Don't show Spoiler format option in secret chats --- submodules/TelegramUI/Sources/ChatController.swift | 2 +- .../TelegramUI/Sources/ChatTextInputMenu.swift | 12 +++++++++--- .../TelegramUI/Sources/ChatTextInputPanelNode.swift | 8 +++++++- .../Sources/PeerSelectionTextInputPanelNode.swift | 8 +++++++- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 98ca92e802..da9ccc66f7 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -9798,7 +9798,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private func getCaptionPanelView() -> TGCaptionPanelView { let presentationData = self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) - var presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: presentationData.chatFontSize, bubbleCorners: presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil) + var presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: presentationData.chatFontSize, bubbleCorners: presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: self.presentationInterfaceState.chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil) var updateChatPresentationInterfaceStateImpl: (((ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void)? var ensureFocusedImpl: (() -> Void)? diff --git a/submodules/TelegramUI/Sources/ChatTextInputMenu.swift b/submodules/TelegramUI/Sources/ChatTextInputMenu.swift index 81fedc3d29..402145b38e 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputMenu.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputMenu.swift @@ -17,6 +17,8 @@ final class ChatTextInputMenu { private var stringUnderline: String = "Underline" private var stringSpoiler: String = "Spoiler" + private let hasSpoilers: Bool + private(set) var state: ChatTextInputMenuState = .inactive { didSet { if self.state != oldValue { @@ -26,8 +28,7 @@ final class ChatTextInputMenu { case .general: UIMenuController.shared.menuItems = [] case .format: - UIMenuController.shared.menuItems = [ - UIMenuItem(title: self.stringSpoiler, action: Selector(("formatAttributesSpoiler:"))), + var menuItems: [UIMenuItem] = [ UIMenuItem(title: self.stringBold, action: Selector(("formatAttributesBold:"))), UIMenuItem(title: self.stringItalic, action: Selector(("formatAttributesItalic:"))), UIMenuItem(title: self.stringMonospace, action: Selector(("formatAttributesMonospace:"))), @@ -35,6 +36,10 @@ final class ChatTextInputMenu { UIMenuItem(title: self.stringStrikethrough, action: Selector(("formatAttributesStrikethrough:"))), UIMenuItem(title: self.stringUnderline, action: Selector(("formatAttributesUnderline:"))) ] + if self.hasSpoilers { + menuItems.insert(UIMenuItem(title: self.stringSpoiler, action: Selector(("formatAttributesSpoiler:"))), at: 0) + } + UIMenuController.shared.menuItems = menuItems } } @@ -43,7 +48,8 @@ final class ChatTextInputMenu { private var observer: NSObjectProtocol? - init() { + init(hasSpoilers: Bool = false) { + self.hasSpoilers = hasSpoilers self.observer = NotificationCenter.default.addObserver(forName: UIMenuController.didHideMenuNotification, object: nil, queue: nil, using: { [weak self] _ in self?.back() }) diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 6f28bbcbda..38ca07e01b 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -296,7 +296,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var isMediaDeleted: Bool = false - private let inputMenu = ChatTextInputMenu() + private let inputMenu: ChatTextInputMenu private var theme: PresentationTheme? private var strings: PresentationStrings? @@ -451,6 +451,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { init(presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) { self.presentationInterfaceState = presentationInterfaceState + var hasSpoilers = true + if presentationInterfaceState.chatLocation.peerId.namespace == Namespaces.Peer.SecretChat { + hasSpoilers = false + } + self.inputMenu = ChatTextInputMenu(hasSpoilers: hasSpoilers) + self.clippingNode = ASDisplayNode() self.clippingNode.clipsToBounds = true diff --git a/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift b/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift index f1a3b97b39..775e22681c 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift @@ -141,7 +141,7 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A private var presentationInterfaceState: ChatPresentationInterfaceState? private var initializedPlaceholder = false - private let inputMenu = ChatTextInputMenu() + private let inputMenu: ChatTextInputMenu private var theme: PresentationTheme? private var strings: PresentationStrings? @@ -241,6 +241,12 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A self.presentationInterfaceState = presentationInterfaceState self.isCaption = isCaption + var hasSpoilers = true + if presentationInterfaceState.chatLocation.peerId.namespace == Namespaces.Peer.SecretChat { + hasSpoilers = false + } + self.inputMenu = ChatTextInputMenu(hasSpoilers: hasSpoilers) + self.textInputContainerBackgroundNode = ASImageNode() self.textInputContainerBackgroundNode.isUserInteractionEnabled = false self.textInputContainerBackgroundNode.displaysAsynchronously = false From 52cfe331ac08eadc383c085d1854746b6eacdc52 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 21 Dec 2021 19:35:39 +0400 Subject: [PATCH 30/35] Various Improvements --- .../ChatInterfaceStateContextMenus.swift | 26 ++++++--- .../Sources/ChatTextInputPanelNode.swift | 54 +++++++++++-------- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 1f04a3f9c3..77ed83fa29 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -698,7 +698,19 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState resourceAvailable = false } - if (!messages[0].text.isEmpty || resourceAvailable || diceEmoji != nil) && !chatPresentationInterfaceState.copyProtectionEnabled { + var messageText: String = "" + for message in messages { + if !message.text.isEmpty { + if messageText.isEmpty { + messageText = message.text + } else { + messageText = "" + break + } + } + } + + if (!messageText.isEmpty || resourceAvailable || diceEmoji != nil) && !chatPresentationInterfaceState.copyProtectionEnabled { let message = messages[0] var isExpired = false for media in message.media { @@ -728,7 +740,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if let restrictedText = restrictedText { storeMessageTextInPasteboard(restrictedText, entities: nil) } else { - storeMessageTextInPasteboard(message.text, entities: messageEntities) + storeMessageTextInPasteboard(messageText, entities: messageEntities) } Queue.mainQueue().after(0.2, { @@ -744,7 +756,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState |> deliverOnMainQueue).start(next: { data in if data.complete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { if let image = UIImage(data: imageData) { - if !message.text.isEmpty { + if !messageText.isEmpty { copyTextWithEntities() } else { UIPasteboard.general.image = image @@ -770,20 +782,20 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState f(.default) }))) - if canTranslateText(context: context, text: message.text, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages) { + if canTranslateText(context: context, text: messageText, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages) { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuTranslate, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in - controllerInteraction.performTextSelectionAction(0, NSAttributedString(string: message.text), .translate) + controllerInteraction.performTextSelectionAction(0, NSAttributedString(string: messageText), .translate) f(.default) }))) } - if isSpeakSelectionEnabled() && !message.text.isEmpty { + if isSpeakSelectionEnabled() && !messageText.isEmpty { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuSpeak, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Message"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in - controllerInteraction.performTextSelectionAction(0, NSAttributedString(string: message.text), .speak) + controllerInteraction.performTextSelectionAction(0, NSAttributedString(string: messageText), .speak) f(.default) }))) } diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 38ca07e01b..266761ef42 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -352,10 +352,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - private var currentState: ChatTextInputState? func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, accessoryItems: [ChatTextInputAccessoryItem], animated: Bool) { - self.currentState = state - if state.inputText.length != 0 && self.textInputNode == nil { self.loadTextInputNode() } @@ -1870,7 +1867,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - private func updateSpoilersRevealed() { + private func updateSpoilersRevealed(animated: Bool = true) { guard let textInputNode = self.textInputNode else { return } @@ -1894,15 +1891,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.spoilersRevealed = revealed if revealed { - self.updateInternalSpoilersRevealed(true) + self.updateInternalSpoilersRevealed(true, animated: animated) } else { Queue.mainQueue().after(1.5, { - self.updateInternalSpoilersRevealed(false) + self.updateInternalSpoilersRevealed(false, animated: true) }) } } - private func updateInternalSpoilersRevealed(_ revealed: Bool) { + private func updateInternalSpoilersRevealed(_ revealed: Bool, animated: Bool) { guard self.spoilersRevealed == revealed, let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState else { return } @@ -1913,11 +1910,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed) - if let state = self.currentState { - textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed) - } - - if textInputNode.textView.subviews.count > 1 { + textInputNode.attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed) + + if textInputNode.textView.subviews.count > 1, animated { let containerView = textInputNode.textView.subviews[1] if let canvasView = containerView.subviews.first { if let snapshotView = canvasView.snapshotView(afterScreenUpdates: false) { @@ -1930,16 +1925,20 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - if revealed { - let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear) - if let dustNode = self.dustNode { - transition.updateAlpha(node: dustNode, alpha: 0.0) - } - } else { - let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear) - if let dustNode = self.dustNode { - transition.updateAlpha(node: dustNode, alpha: 1.0) + if animated { + if revealed { + let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear) + if let dustNode = self.dustNode { + transition.updateAlpha(node: dustNode, alpha: 0.0) + } + } else { + let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear) + if let dustNode = self.dustNode { + transition.updateAlpha(node: dustNode, alpha: 1.0) + } } + } else if let dustNode = self.dustNode { + dustNode.alpha = revealed ? 0.0 : 1.0 } } @@ -2336,10 +2335,21 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { @objc func formatAttributesSpoiler(_ sender: Any) { self.inputMenu.back() + + var animated = false + if let attributedText = self.textInputNode?.attributedText { + attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, _, _ in + if let _ = attributes[ChatTextInputAttributes.spoiler] { + animated = true + } + }) + } + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.spoiler), inputMode) } - self.updateSpoilersRevealed() + + self.updateSpoilersRevealed(animated: animated) } @objc func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { From 1a8b151446d3a088e7c5431f24c660210a22f968 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 21 Dec 2021 21:02:38 +0400 Subject: [PATCH 31/35] Various Improvements --- .../Sources/Node/ChatListItem.swift | 2 +- submodules/Display/Source/TextNode.swift | 51 +++++++++++++++---- .../ChatItemGalleryFooterContentNode.swift | 2 +- .../Sources/InvisibleInkDustNode.swift | 19 +++---- .../ChatInterfaceStateContextMenus.swift | 6 ++- .../Sources/ChatMessageNotificationItem.swift | 2 +- .../Sources/ChatMessageReplyInfoNode.swift | 2 +- .../ChatMessageTextBubbleContentNode.swift | 2 +- .../ChatPinnedMessageTitlePanelNode.swift | 2 +- .../Sources/ChatTextInputPanelNode.swift | 2 +- .../PeerInfoScreenLabeledValueItem.swift | 2 +- .../Sources/ReplyAccessoryPanelNode.swift | 2 +- 12 files changed, 62 insertions(+), 32 deletions(-) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 5b0a72e542..72502f1f39 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1757,7 +1757,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.dustNode = dustNode strongSelf.contextContainer.insertSubnode(dustNode, aboveSubnode: strongSelf.textNode) } - dustNode.update(size: textNodeFrame.size, color: theme.messageTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) + dustNode.update(size: textNodeFrame.size, color: theme.messageTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) dustNode.frame = textNodeFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) } else if let dustNode = strongSelf.dustNode { diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 8871464fdf..5cff2c7bbd 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -44,14 +44,16 @@ private final class TextNodeLine { let isRTL: Bool let strikethroughs: [TextNodeStrikethrough] let spoilers: [TextNodeSpoiler] + let spoilerWords: [TextNodeSpoiler] - init(line: CTLine, frame: CGRect, range: NSRange, isRTL: Bool, strikethroughs: [TextNodeStrikethrough], spoilers: [TextNodeSpoiler]) { + init(line: CTLine, frame: CGRect, range: NSRange, isRTL: Bool, strikethroughs: [TextNodeStrikethrough], spoilers: [TextNodeSpoiler], spoilerWords: [TextNodeSpoiler]) { self.line = line self.frame = frame self.range = range self.isRTL = isRTL self.strikethroughs = strikethroughs self.spoilers = spoilers + self.spoilerWords = spoilerWords } } @@ -174,6 +176,7 @@ public final class TextNodeLayout: NSObject { fileprivate let displaySpoilers: Bool public let hasRTL: Bool public let spoilers: [(NSRange, CGRect)] + public let spoilerWords: [(NSRange, CGRect)] fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, explicitAlignment: NSTextAlignment, resolvedAlignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, rawTextSize: CGSize, truncated: Bool, firstLineOffset: CGFloat, lines: [TextNodeLine], blockQuotes: [TextNodeBlockQuote], backgroundColor: UIColor?, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool) { self.attributedString = attributedString @@ -199,14 +202,17 @@ public final class TextNodeLayout: NSObject { self.displaySpoilers = displaySpoilers var hasRTL = false var spoilers: [(NSRange, CGRect)] = [] + var spoilerWords: [(NSRange, CGRect)] = [] for line in lines { if line.isRTL { hasRTL = true } spoilers.append(contentsOf: line.spoilers.map { ( $0.range, $0.frame.offsetBy(dx: line.frame.minX, dy: line.frame.minY)) }) + spoilerWords.append(contentsOf: line.spoilerWords.map { ( $0.range, $0.frame.offsetBy(dx: line.frame.minX, dy: line.frame.minY)) }) } self.hasRTL = hasRTL self.spoilers = spoilers + self.spoilerWords = spoilerWords } public func areLinesEqual(to other: TextNodeLayout) -> Bool { @@ -952,6 +958,7 @@ public class TextNode: ASDisplayNode { while true { var strikethroughs: [TextNodeStrikethrough] = [] var spoilers: [TextNodeSpoiler] = [] + var spoilerWords: [TextNodeSpoiler] = [] var lineConstrainedWidth = constrainedSize.width var lineConstrainedWidthDelta: CGFloat = 0.0 @@ -973,7 +980,7 @@ public class TextNode: ASDisplayNode { let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, lastLineCharacterIndex, Double(lineConstrainedWidth)) - func addSpoiler(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { + func addSpoiler(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int) { var secondaryLeftOffset: CGFloat = 0.0 let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) var leftOffset = floor(rawLeftOffset) @@ -988,7 +995,25 @@ public class TextNode: ASDisplayNode { rightOffset = ceil(secondaryRightOffset) } - spoilers.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent))) + spoilers.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset), height: ascent + descent))) + } + + func addSpoilerWord(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { + var secondaryLeftOffset: CGFloat = 0.0 + let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) + var leftOffset = floor(rawLeftOffset) + if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { + leftOffset = floor(secondaryLeftOffset) + } + + var secondaryRightOffset: CGFloat = 0.0 + let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) + var rightOffset = ceil(rawRightOffset) + if !rawRightOffset.isEqual(to: secondaryRightOffset) { + rightOffset = ceil(secondaryRightOffset) + } + + spoilerWords.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent))) } var isLastLine = false @@ -1056,7 +1081,7 @@ public class TextNode: ASDisplayNode { if let currentStartIndex = startIndex { startIndex = nil let endIndex = range.location - addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) + addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) } } else if startIndex == nil { startIndex = range.location @@ -1067,8 +1092,10 @@ public class TextNode: ASDisplayNode { if let currentStartIndex = startIndex, let currentIndex = currentIndex { startIndex = nil let endIndex = currentIndex - addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex, rightInset: truncated ? 12.0 : 0.0) + addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex, rightInset: truncated ? 12.0 : 0.0) } + + addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length - 1) } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) @@ -1098,7 +1125,7 @@ public class TextNode: ASDisplayNode { } } - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers)) + lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords)) break } else { if lineCharacterCount > 0 { @@ -1135,7 +1162,7 @@ public class TextNode: ASDisplayNode { if let currentStartIndex = startIndex { startIndex = nil let endIndex = range.location - addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) + addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) } } else if startIndex == nil { startIndex = range.location @@ -1146,8 +1173,10 @@ public class TextNode: ASDisplayNode { if let currentStartIndex = startIndex, let currentIndex = currentIndex { startIndex = nil let endIndex = currentIndex - addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) + addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) } + + addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length - 1) } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) @@ -1176,7 +1205,7 @@ public class TextNode: ASDisplayNode { } } - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers)) + lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords)) } else { if !lines.isEmpty { layoutSize.height += fontLineSpacing @@ -1293,7 +1322,7 @@ public class TextNode: ASDisplayNode { if layout.displaySpoilers && !line.spoilers.isEmpty { context.saveGState() var clipRects: [CGRect] = [] - for spoiler in line.spoilers { + for spoiler in line.spoilerWords { clipRects.append(spoiler.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY)) } context.clip(to: clipRects) @@ -1328,7 +1357,7 @@ public class TextNode: ASDisplayNode { if layout.displaySpoilers { context.restoreGState() } else { - for spoiler in line.spoilers { + for spoiler in line.spoilerWords { clearRects.append(spoiler.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY)) } } diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 070be199d5..df4179027e 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -737,7 +737,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } if let dustNode = self.dustNode { - dustNode.update(size: textFrame.size, color: .white, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) + dustNode.update(size: textFrame.size, color: .white, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) } } else { diff --git a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift index 82980fab56..db2f20a0b6 100644 --- a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift @@ -45,7 +45,7 @@ private let emitterMaskImage: UIImage = { }() public class InvisibleInkDustNode: ASDisplayNode { - private var currentParams: (size: CGSize, color: UIColor, rects: [CGRect])? + private var currentParams: (size: CGSize, color: UIColor, rects: [CGRect], wordRects: [CGRect])? private weak var textNode: TextNode? private let textMaskNode: ASDisplayNode @@ -162,7 +162,7 @@ public class InvisibleInkDustNode: ASDisplayNode { } @objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) { - guard let (size, _, _) = self.currentParams, let textNode = self.textNode, !self.isRevealed else { + guard let (size, _, _, _) = self.currentParams, let textNode = self.textNode, !self.isRevealed else { return } @@ -192,8 +192,6 @@ public class InvisibleInkDustNode: ASDisplayNode { self?.emitterNode.view.mask = nil }) self.emitterMaskFillNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) - - self.isRevealedUpdated(true) } Queue.mainQueue().after(0.8 * UIView.animationDurationFactor()) { @@ -209,7 +207,6 @@ public class InvisibleInkDustNode: ASDisplayNode { let timeToRead = min(45.0, ceil(max(4.0, textLength * 0.04))) Queue.mainQueue().after(timeToRead * UIView.animationDurationFactor()) { self.isRevealed = false - self.isRevealedUpdated(false) let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .linear) transition.updateAlpha(node: self, alpha: 1.0) @@ -218,12 +215,12 @@ public class InvisibleInkDustNode: ASDisplayNode { } private func updateEmitter() { - guard let (size, color, rects) = self.currentParams else { + guard let (size, color, _, wordRects) = self.currentParams else { return } self.emitter?.color = color.cgColor - self.emitterLayer?.setValue(rects, forKey: "emitterRects") + self.emitterLayer?.setValue(wordRects, forKey: "emitterRects") self.emitterLayer?.frame = CGRect(origin: CGPoint(), size: size) let radius = max(size.width, size.height) @@ -231,15 +228,15 @@ public class InvisibleInkDustNode: ASDisplayNode { self.emitterLayer?.setValue(radius * -0.5, forKeyPath: "emitterBehaviors.fingerAttractor.falloff") var square: Float = 0.0 - for rect in rects { + for rect in wordRects { square += Float(rect.width * rect.height) } self.emitter?.birthRate = square * 0.4 } - public func update(size: CGSize, color: UIColor, rects: [CGRect]) { - self.currentParams = (size, color, rects) + public func update(size: CGSize, color: UIColor, rects: [CGRect], wordRects: [CGRect]) { + self.currentParams = (size, color, rects, wordRects) self.emitterNode.frame = CGRect(origin: CGPoint(), size: size) self.emitterMaskNode.frame = self.emitterNode.bounds @@ -252,7 +249,7 @@ public class InvisibleInkDustNode: ASDisplayNode { } public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - if let (_, _, rects) = self.currentParams { + if let (_, _, rects, _) = self.currentParams { for rect in rects { if rect.contains(point) { return true diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 77ed83fa29..ddb7994b10 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -82,7 +82,11 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: LimitsCo } } } else if let author = message.author, message.author?.id != message.id.peerId, author.id.namespace == Namespaces.Peer.CloudChannel && message.id.peerId.namespace == Namespaces.Peer.CloudChannel, !message.flags.contains(.Incoming) { - hasEditRights = true + if message.media.contains(where: { $0 is TelegramMediaInvoice }) { + hasEditRights = false + } else { + hasEditRights = true + } } else if message.author?.id == message.id.peerId, let peer = message.peers[message.id.peerId] { if let peer = peer as? TelegramChannel { switch peer.info { diff --git a/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift b/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift index 6ea076b896..589d06b240 100644 --- a/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift +++ b/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift @@ -410,7 +410,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { self.insertSubnode(dustNode, aboveSubnode: self.textNode) } dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) - dustNode.update(size: dustNode.frame.size, color: presentationData.theme.inAppNotification.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + dustNode.update(size: dustNode.frame.size, color: presentationData.theme.inAppNotification.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) } else if let dustNode = self.dustNode { dustNode.removeFromSupernode() self.dustNode = nil diff --git a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift index e4b5d79f78..504c9a139f 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift @@ -254,7 +254,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { node.contentNode.insertSubnode(dustNode, aboveSubnode: textNode) } dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) - dustNode.update(size: dustNode.frame.size, color: dustColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + dustNode.update(size: dustNode.frame.size, color: dustColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) } else if let dustNode = node.dustNode { dustNode.removeFromSupernode() node.dustNode = nil diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 45d9529641..0360664c40 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -405,7 +405,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.insertSubnode(dustNode, aboveSubnode: spoilerTextNode) } dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) - dustNode.update(size: dustNode.frame.size, color: messageTheme.secondaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + dustNode.update(size: dustNode.frame.size, color: messageTheme.secondaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) } else if let spoilerTextNode = strongSelf.spoilerTextNode { strongSelf.spoilerTextNode = nil spoilerTextNode.removeFromSupernode() diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 15cedf05cc..e84f1a03aa 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -499,7 +499,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { strongSelf.contentTextContainer.insertSubnode(dustNode, aboveSubnode: strongSelf.textNode) } dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) - dustNode.update(size: dustNode.frame.size, color: theme.chat.inputPanel.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + dustNode.update(size: dustNode.frame.size, color: theme.chat.inputPanel.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) } else if let dustNode = strongSelf.dustNode { dustNode.removeFromSupernode() strongSelf.dustNode = nil diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 266761ef42..d6900ef29b 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -1860,7 +1860,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.dustNode = dustNode } dustNode.frame = CGRect(origin: CGPoint(), size: textInputNode.textView.contentSize) - dustNode.update(size: textInputNode.textView.contentSize, color: textColor, rects: rects) + dustNode.update(size: textInputNode.textView.contentSize, color: textColor, rects: rects, wordRects: rects) } else if let dustNode = self.dustNode { dustNode.removeFromSupernode() self.dustNode = nil diff --git a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift index 5ab17d132e..67b0302480 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift @@ -279,7 +279,7 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { textColorValue = presentationData.theme.list.itemAccentColor } - self.expandNode.attributedText = NSAttributedString(string: presentationData.strings.PeerInfo_BioExpand.uppercased(), font: Font.medium(15.0), textColor: presentationData.theme.list.itemAccentColor) + self.expandNode.attributedText = NSAttributedString(string: presentationData.strings.PeerInfo_BioExpand, font: Font.medium(15.0), textColor: presentationData.theme.list.itemAccentColor) let expandSize = self.expandNode.updateLayout(CGSize(width: width, height: 100.0)) self.labelNode.attributedText = NSAttributedString(string: item.label, font: Font.regular(14.0), textColor: presentationData.theme.list.itemPrimaryTextColor) diff --git a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift index 5018cf30ef..89553036a2 100644 --- a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift @@ -326,7 +326,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { } if let dustNode = self.dustNode { - dustNode.update(size: textFrame.size, color: self.theme.chat.inputPanel.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + dustNode.update(size: textFrame.size, color: self.theme.chat.inputPanel.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) } } else if let dustNode = self.dustNode { From 9505dcf9f459dd34d00d2daab7a0297461b9023c Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 21 Dec 2021 22:15:05 +0400 Subject: [PATCH 32/35] Context menu improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 34 +++ ...tControllerExtractedPresentationNode.swift | 11 +- .../ReactionContextBackgroundNode.swift | 195 ++++++++++++++++++ .../Sources/ReactionContextNode.swift | 183 ++-------------- .../ChatInterfaceStateContextMenus.swift | 14 +- 5 files changed, 270 insertions(+), 167 deletions(-) create mode 100644 submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 4b63357a04..08524f9e6c 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -217,6 +217,40 @@ "PUSH_CHAT_MESSAGE_GAME_SCORE" = "%1$@ scored %4$@ in game %3$@ in the group %2$@"; "PUSH_CHAT_MESSAGE_VIDEOS" = "%1$@ sent %3$@ videos to the group %2$@"; +"PUSH_REACT_TEXT" = "%1$@|%2$@ to your %3$@"; +"PUSH_REACT_NOTEXT" = "%1$@|%2$@ to your message"; +"PUSH_REACT_PHOTO" = "%1$@|%2$@ to your photo"; +"PUSH_REACT_VIDEO" = "%1$@|%2$@ to your video"; +"PUSH_REACT_ROUND" = "%1$@|%2$@ to your video message"; +"PUSH_REACT_DOC" = "%1$@|%2$@ to your file"; +"PUSH_REACT_STICKER" = "%1$@|%2$@ to your %3$@sticker"; +"PUSH_REACT_AUDIO" = "%1$@|%2$@ to your voice message"; +"PUSH_REACT_CONTACT" = "%1$@|%2$@ to your contact %3$@"; +"PUSH_REACT_GEO" = "%1$@|%2$@ to your map"; +"PUSH_REACT_GEOLIVE" = "%1$@|%2$@ to your live location"; +"PUSH_REACT_POLL" = "%1$@|%2$@ to your poll %3$@"; +"PUSH_REACT_QUIZ" = "%1$@|%2$@ to your quiz %3$@"; +"PUSH_REACT_GAME" = "%1$@|%2$@ to your game"; +"PUSH_REACT_INVOICE" = "%1$@|%2$@ to your invoice"; +"PUSH_REACT_GIF" = "%1$@|%2$@ to your GIF"; + +"PUSH_CHAT_REACT_TEXT" = "%2$@|%1$@ %3$@ to your %4$@"; +"PUSH_CHAT_REACT_NOTEXT" = "%2$@|%1$@ %3$@ to your message"; +"PUSH_CHAT_REACT_PHOTO" = "%2$@|%1$@ %3$@ to your photo"; +"PUSH_CHAT_REACT_VIDEO" = "%2$@|%1$@ %3$@ to your video"; +"PUSH_CHAT_REACT_ROUND" = "%2$@|%1$@ %3$@ to your video message"; +"PUSH_CHAT_REACT_DOC" = "%2$@|%1$@ %3$@ to your file"; +"PUSH_CHAT_REACT_STICKER" = "%2$@|%1$@ %3$@ to your %4$@sticker"; +"PUSH_CHAT_REACT_AUDIO" = "%2$@|%1$@ %3$@ to your voice message"; +"PUSH_CHAT_REACT_CONTACT" = "%2$@|%1$@ %3$@ to your contact %4$@"; +"PUSH_CHAT_REACT_GEO" = "%2$@|%1$@ %3$@ to your map"; +"PUSH_CHAT_REACT_GEOLIVE" = "%2$@|%1$@ %3$@ to your live location"; +"PUSH_CHAT_REACT_POLL" = "%2$@|%1$@ %3$@ to your poll %4$@"; +"PUSH_CHAT_REACT_QUIZ" = "%2$@|%1$@ %3$@ to your quiz %4$@"; +"PUSH_CHAT_REACT_GAME" = "%2$@|%1$@ %3$@ to your game"; +"PUSH_CHAT_REACT_INVOICE" = "%2$@|%1$@ %3$@ to your invoice"; +"PUSH_CHAT_REACT_GIF" = "%2$@|%1$@ %3$@ to your GIF"; + "PUSH_REMINDER_TITLE" = "🗓 Reminder"; "PUSH_SENDER_YOU" = "📅 You"; diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 54686eafdf..b6ccc1f53f 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -8,7 +8,7 @@ import TelegramCore import SwiftSignalKit import ReactionSelectionNode -final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextControllerPresentationNode { +final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextControllerPresentationNode, UIScrollViewDelegate { private final class ContentNode: ASDisplayNode { let offsetContainerNode: ASDisplayNode let containingNode: ContextExtractedContentContainingNode @@ -126,6 +126,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo self.scrollNode.addSubnode(self.contentRectDebugNode) #endif*/ + self.scrollNode.view.delegate = self + self.dismissTapNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dismissTapGesture(_:)))) } @@ -162,6 +164,13 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } } + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if let reactionContextNode = self.reactionContextNode { + let isIntersectingContent = scrollView.contentOffset.y >= 10.0 + reactionContextNode.updateIsIntersectingContent(isIntersectingContent: isIntersectingContent, transition: .animated(duration: 0.25, curve: .easeInOut)) + } + } + func highlightGestureMoved(location: CGPoint) { self.actionsStackNode.highlightGestureMoved(location: self.view.convert(location, to: self.actionsStackNode.view)) diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift new file mode 100644 index 0000000000..6fe7b5158f --- /dev/null +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift @@ -0,0 +1,195 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramPresentationData +import AccountContext + +private func generateBackgroundImage(foreground: UIColor, diameter: CGFloat, sideInset: CGFloat) -> UIImage? { + return generateImage(CGSize(width: diameter + sideInset * 2.0, height: diameter + sideInset * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(foreground.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: sideInset, y: sideInset), size: CGSize(width: diameter, height: diameter))) + })?.stretchableImage(withLeftCapWidth: Int(sideInset + diameter / 2.0), topCapHeight: Int(sideInset + diameter / 2.0)) +} + +private func generateBubbleImage(foreground: UIColor, diameter: CGFloat, sideInset: CGFloat) -> UIImage? { + return generateImage(CGSize(width: diameter + sideInset * 2.0, height: diameter + sideInset * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(foreground.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: sideInset, y: sideInset), size: CGSize(width: diameter, height: diameter))) + })?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0 + sideInset / 2.0), topCapHeight: Int(diameter / 2.0 + sideInset / 2.0)) +} + +private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? { + return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(shadow.cgColor) + context.setShadow(offset: CGSize(), blur: shadowBlur, color: shadow.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + context.setShadow(offset: CGSize(), blur: 1.0, color: shadow.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + })?.stretchableImage(withLeftCapWidth: Int(shadowBlur + diameter / 2.0), topCapHeight: Int(shadowBlur + diameter / 2.0)) +} + + +final class ReactionContextBackgroundNode: ASDisplayNode { + private let largeCircleSize: CGFloat + private let smallCircleSize: CGFloat + + private let backgroundNode: NavigationBackgroundNode + + private let maskLayer: SimpleLayer + private let backgroundLayer: SimpleLayer + private let backgroundShadowLayer: SimpleLayer + private let largeCircleLayer: SimpleLayer + private let largeCircleShadowLayer: SimpleLayer + private let smallCircleLayer: SimpleLayer + private let smallCircleShadowLayer: SimpleLayer + + private var theme: PresentationTheme? + + init(largeCircleSize: CGFloat, smallCircleSize: CGFloat) { + self.largeCircleSize = largeCircleSize + self.smallCircleSize = smallCircleSize + + self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: true) + + self.maskLayer = SimpleLayer() + self.backgroundLayer = SimpleLayer() + self.backgroundShadowLayer = SimpleLayer() + self.largeCircleLayer = SimpleLayer() + self.largeCircleShadowLayer = SimpleLayer() + self.smallCircleLayer = SimpleLayer() + self.smallCircleShadowLayer = SimpleLayer() + + self.backgroundLayer.backgroundColor = UIColor.black.cgColor + self.backgroundLayer.masksToBounds = true + self.backgroundLayer.cornerRadius = 52.0 / 2.0 + + self.largeCircleLayer.backgroundColor = UIColor.black.cgColor + self.largeCircleLayer.masksToBounds = true + self.largeCircleLayer.cornerRadius = largeCircleSize / 2.0 + + self.smallCircleLayer.backgroundColor = UIColor.black.cgColor + self.smallCircleLayer.masksToBounds = true + self.smallCircleLayer.cornerRadius = smallCircleSize / 2.0 + + if #available(iOS 13.0, *) { + self.backgroundLayer.cornerCurve = .circular + self.largeCircleLayer.cornerCurve = .circular + self.smallCircleLayer.cornerCurve = .circular + } + + super.init() + + self.layer.addSublayer(self.backgroundShadowLayer) + self.layer.addSublayer(self.smallCircleShadowLayer) + self.layer.addSublayer(self.largeCircleShadowLayer) + + self.backgroundShadowLayer.opacity = 0.0 + self.largeCircleShadowLayer.opacity = 0.0 + self.smallCircleShadowLayer.opacity = 0.0 + + self.addSubnode(self.backgroundNode) + + self.maskLayer.addSublayer(self.smallCircleLayer) + self.maskLayer.addSublayer(self.largeCircleLayer) + self.maskLayer.addSublayer(self.backgroundLayer) + + self.backgroundNode.layer.mask = self.maskLayer + } + + func updateIsIntersectingContent(isIntersectingContent: Bool, transition: ContainedViewLayoutTransition) { + let shadowAlpha: CGFloat = isIntersectingContent ? 1.0 : 0.0 + transition.updateAlpha(layer: self.backgroundShadowLayer, alpha: shadowAlpha) + transition.updateAlpha(layer: self.smallCircleShadowLayer, alpha: shadowAlpha) + transition.updateAlpha(layer: self.largeCircleShadowLayer, alpha: shadowAlpha) + } + + func update( + theme: PresentationTheme, + size: CGSize, + cloudSourcePoint: CGFloat, + isLeftAligned: Bool, + transition: ContainedViewLayoutTransition + ) { + let shadowInset: CGFloat = 15.0 + + if self.theme !== theme { + self.theme = theme + + self.backgroundNode.updateColor(color: theme.contextMenu.backgroundColor, transition: .immediate) + + let shadowColor = UIColor(white: 0.0, alpha: 0.4) + + if let image = generateBubbleShadowImage(shadow: shadowColor, diameter: 52.0, shadowBlur: shadowInset) { + ASDisplayNodeSetResizableContents(self.backgroundShadowLayer, image) + } + if let image = generateBubbleShadowImage(shadow: shadowColor, diameter: self.largeCircleSize, shadowBlur: shadowInset) { + ASDisplayNodeSetResizableContents(self.largeCircleShadowLayer, image) + } + if let image = generateBubbleShadowImage(shadow: shadowColor, diameter: self.smallCircleSize, shadowBlur: shadowInset) { + ASDisplayNodeSetResizableContents(self.smallCircleShadowLayer, image) + } + } + + let backgroundFrame = CGRect(origin: CGPoint(), size: size) + + let largeCircleFrame: CGRect + let smallCircleFrame: CGRect + if isLeftAligned { + largeCircleFrame = CGRect(origin: CGPoint(x: cloudSourcePoint - floor(largeCircleSize / 2.0), y: size.height - largeCircleSize / 2.0), size: CGSize(width: largeCircleSize, height: largeCircleSize)) + smallCircleFrame = CGRect(origin: CGPoint(x: largeCircleFrame.maxX - 3.0, y: largeCircleFrame.maxY + 2.0), size: CGSize(width: smallCircleSize, height: smallCircleSize)) + } else { + largeCircleFrame = CGRect(origin: CGPoint(x: cloudSourcePoint - floor(largeCircleSize / 2.0), y: size.height - largeCircleSize / 2.0), size: CGSize(width: largeCircleSize, height: largeCircleSize)) + smallCircleFrame = CGRect(origin: CGPoint(x: largeCircleFrame.minX + 3.0 - smallCircleSize, y: largeCircleFrame.maxY + 2.0), size: CGSize(width: smallCircleSize, height: smallCircleSize)) + } + + let contentBounds = backgroundFrame.insetBy(dx: -10.0, dy: -10.0).union(largeCircleFrame).union(smallCircleFrame) + + transition.updateFrame(layer: self.backgroundLayer, frame: backgroundFrame.offsetBy(dx: -contentBounds.minX, dy: -contentBounds.minY)) + transition.updateFrame(layer: self.largeCircleLayer, frame: largeCircleFrame.offsetBy(dx: -contentBounds.minX, dy: -contentBounds.minY)) + transition.updateFrame(layer: self.smallCircleLayer, frame: smallCircleFrame.offsetBy(dx: -contentBounds.minX, dy: -contentBounds.minY)) + + transition.updateFrame(layer: self.backgroundShadowLayer, frame: backgroundFrame.insetBy(dx: -shadowInset, dy: -shadowInset)) + transition.updateFrame(layer: self.largeCircleShadowLayer, frame: largeCircleFrame.insetBy(dx: -shadowInset, dy: -shadowInset)) + transition.updateFrame(layer: self.smallCircleShadowLayer, frame: smallCircleFrame.insetBy(dx: -shadowInset, dy: -shadowInset)) + + transition.updateFrame(node: self.backgroundNode, frame: contentBounds) + self.backgroundNode.update(size: contentBounds.size, transition: transition) + } + + func animateIn() { + let smallCircleDuration: Double = 0.5 + let largeCircleDuration: Double = 0.5 + let largeCircleDelay: Double = 0.08 + let mainCircleDuration: Double = 0.5 + let mainCircleDelay: Double = 0.1 + + self.smallCircleLayer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: smallCircleDuration) + + self.largeCircleLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: largeCircleDelay) + self.largeCircleLayer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: largeCircleDuration, delay: largeCircleDelay) + + self.backgroundLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: mainCircleDelay) + self.backgroundLayer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: mainCircleDelay) + } + + func animateInFromAnchorRect(size: CGSize, sourceBackgroundFrame: CGRect) { + let springDuration: Double = 0.42 + let springDamping: CGFloat = 104.0 + let springDelay: Double = 0.22 + + self.backgroundLayer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - size.width / 2.0, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) + self.backgroundLayer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) + } + + func animateOut() { + self.backgroundLayer.animateAlpha(from: CGFloat(self.backgroundLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false) + self.largeCircleLayer.animateAlpha(from: CGFloat(self.largeCircleLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false) + self.smallCircleLayer.animateAlpha(from: CGFloat(self.smallCircleLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false) + } +} diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 1dadfc99ca..12f421c12a 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -37,68 +37,11 @@ public final class ReactionContextItem { private let largeCircleSize: CGFloat = 16.0 private let smallCircleSize: CGFloat = 8.0 -private func generateBackgroundImage(foreground: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? { - return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(foreground.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - })?.stretchableImage(withLeftCapWidth: Int(shadowBlur + diameter / 2.0), topCapHeight: Int(shadowBlur + diameter / 2.0)) -} - -private func generateBackgroundShadowImage(shadow: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? { - return generateImage(CGSize(width: diameter * 2.0 + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(shadow.cgColor) - context.setShadow(offset: CGSize(), blur: shadowBlur, color: shadow.cgColor) - - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur + diameter, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - context.fill(CGRect(origin: CGPoint(x: shadowBlur + diameter / 2.0, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - - context.setFillColor(UIColor.clear.cgColor) - context.setBlendMode(.copy) - - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur + diameter, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - context.fill(CGRect(origin: CGPoint(x: shadowBlur + diameter / 2.0, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - })?.stretchableImage(withLeftCapWidth: Int(diameter + shadowBlur / 2.0), topCapHeight: Int(diameter / 2.0 + shadowBlur / 2.0)) -} - -private func generateBubbleImage(foreground: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? { - return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(foreground.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - })?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0 + shadowBlur / 2.0), topCapHeight: Int(diameter / 2.0 + shadowBlur / 2.0)) -} - -private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? { - return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(shadow.cgColor) - context.setShadow(offset: CGSize(), blur: shadowBlur, color: shadow.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - context.setShadow(offset: CGSize(), blur: 1.0, color: shadow.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - context.setFillColor(UIColor.clear.cgColor) - context.setBlendMode(.copy) - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - })?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0 + shadowBlur / 2.0), topCapHeight: Int(diameter / 2.0 + shadowBlur / 2.0)) -} - public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private let theme: PresentationTheme private let items: [ReactionContextItem] - private let backgroundNode: ASImageNode - private let backgroundShadowNode: ASImageNode - private let backgroundContainerNode: ASDisplayNode - - private let largeCircleNode: ASImageNode - private let largeCircleShadowNode: ASImageNode - - private let smallCircleNode: ASImageNode - private let smallCircleShadowNode: ASImageNode + private let backgroundNode: ReactionContextBackgroundNode private let contentContainer: ASDisplayNode private let contentContainerMask: UIImageView @@ -122,44 +65,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.theme = theme self.items = items - let shadowBlur: CGFloat = 5.0 - - self.backgroundNode = ASImageNode() - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.displaysAsynchronously = false - - self.backgroundShadowNode = ASImageNode() - self.backgroundShadowNode.displayWithoutProcessing = true - self.backgroundShadowNode.displaysAsynchronously = false - - self.backgroundContainerNode = ASDisplayNode() - self.backgroundContainerNode.allowsGroupOpacity = true - - self.largeCircleNode = ASImageNode() - self.largeCircleNode.displayWithoutProcessing = true - self.largeCircleNode.displaysAsynchronously = false - - self.largeCircleShadowNode = ASImageNode() - self.largeCircleShadowNode.displayWithoutProcessing = true - self.largeCircleShadowNode.displaysAsynchronously = false - - self.smallCircleNode = ASImageNode() - self.smallCircleNode.displayWithoutProcessing = true - self.smallCircleNode.displaysAsynchronously = false - - self.smallCircleShadowNode = ASImageNode() - self.smallCircleShadowNode.displayWithoutProcessing = true - self.smallCircleShadowNode.displaysAsynchronously = false - - self.backgroundNode.image = generateBackgroundImage(foreground: theme.contextMenu.backgroundColor.withAlphaComponent(1.0), diameter: 52.0, shadowBlur: shadowBlur) - - self.backgroundShadowNode.image = generateBackgroundShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: 52.0, shadowBlur: shadowBlur) - - self.largeCircleNode.image = generateBubbleImage(foreground: theme.contextMenu.backgroundColor.withAlphaComponent(1.0), diameter: largeCircleSize, shadowBlur: shadowBlur) - self.smallCircleNode.image = generateBubbleImage(foreground: theme.contextMenu.backgroundColor.withAlphaComponent(1.0), diameter: smallCircleSize, shadowBlur: shadowBlur) - - self.largeCircleShadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: largeCircleSize, shadowBlur: shadowBlur) - self.smallCircleShadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: smallCircleSize, shadowBlur: shadowBlur) + self.backgroundNode = ReactionContextBackgroundNode(largeCircleSize: largeCircleSize, smallCircleSize: smallCircleSize) self.scrollNode = ASScrollNode() self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true @@ -200,18 +106,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { context.fill(CGRect(origin: CGPoint(x: maskGradientWidth, y: 0.0), size: CGSize(width: 1.0, height: size.height))) })?.stretchableImage(withLeftCapWidth: Int(maskGradientWidth), topCapHeight: 0) self.contentContainer.view.mask = self.contentContainerMask - //self.contentContainer.view.addSubview(self.contentContainerMask) super.init() - self.addSubnode(self.smallCircleShadowNode) - self.addSubnode(self.largeCircleShadowNode) - self.addSubnode(self.backgroundShadowNode) - - self.backgroundContainerNode.addSubnode(self.smallCircleNode) - self.backgroundContainerNode.addSubnode(self.largeCircleNode) - self.backgroundContainerNode.addSubnode(self.backgroundNode) - self.addSubnode(self.backgroundContainerNode) + self.addSubnode(self.backgroundNode) self.scrollNode.view.delegate = self @@ -233,6 +131,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: transition, animateInFromAnchorRect: nil, animateOutToAnchorRect: nil) } + public func updateIsIntersectingContent(isIntersectingContent: Bool, transition: ContainedViewLayoutTransition) { + self.backgroundNode.updateIsIntersectingContent(isIntersectingContent: isIntersectingContent, transition: transition) + } + private func calculateBackgroundFrame(containerSize: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, contentSize: CGSize) -> (backgroundFrame: CGRect, isLeftAligned: Bool, cloudSourcePoint: CGFloat) { var contentSize = contentSize contentSize.width = max(52.0, contentSize.width) @@ -308,7 +210,6 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { let sideInset: CGFloat = 11.0 let itemSpacing: CGFloat = 9.0 let itemSize: CGFloat = 40.0 - let shadowBlur: CGFloat = 5.0 let verticalInset: CGFloat = 13.0 let rowHeight: CGFloat = 30.0 @@ -350,34 +251,15 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } self.updateScrolling(transition: transition) - - let isInOverflow = backgroundFrame.maxY > anchorRect.minY - let backgroundAlpha: CGFloat = isInOverflow ? 1.0 : 0.8 - let shadowAlpha: CGFloat = isInOverflow ? 1.0 : 0.0 - transition.updateAlpha(node: self.backgroundContainerNode, alpha: backgroundAlpha) - transition.updateAlpha(node: self.backgroundShadowNode, alpha: shadowAlpha) - transition.updateAlpha(node: self.largeCircleShadowNode, alpha: shadowAlpha) - transition.updateAlpha(node: self.smallCircleShadowNode, alpha: shadowAlpha) - transition.updateFrame(node: self.backgroundContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) - - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame.insetBy(dx: -shadowBlur, dy: -shadowBlur)) - transition.updateFrame(node: self.backgroundShadowNode, frame: backgroundFrame.insetBy(dx: -shadowBlur, dy: -shadowBlur)) - - let largeCircleFrame: CGRect - let smallCircleFrame: CGRect - if isLeftAligned { - largeCircleFrame = CGRect(origin: CGPoint(x: cloudSourcePoint - floor(largeCircleSize / 2.0), y: backgroundFrame.maxY - largeCircleSize / 2.0), size: CGSize(width: largeCircleSize, height: largeCircleSize)) - smallCircleFrame = CGRect(origin: CGPoint(x: largeCircleFrame.maxX - 3.0, y: largeCircleFrame.maxY + 2.0), size: CGSize(width: smallCircleSize, height: smallCircleSize)) - } else { - largeCircleFrame = CGRect(origin: CGPoint(x: cloudSourcePoint - floor(largeCircleSize / 2.0), y: backgroundFrame.maxY - largeCircleSize / 2.0), size: CGSize(width: largeCircleSize, height: largeCircleSize)) - smallCircleFrame = CGRect(origin: CGPoint(x: largeCircleFrame.minX + 3.0 - smallCircleSize, y: largeCircleFrame.maxY + 2.0), size: CGSize(width: smallCircleSize, height: smallCircleSize)) - } - - transition.updateFrame(node: self.largeCircleNode, frame: largeCircleFrame.insetBy(dx: -shadowBlur, dy: -shadowBlur)) - transition.updateFrame(node: self.largeCircleShadowNode, frame: largeCircleFrame.insetBy(dx: -shadowBlur, dy: -shadowBlur)) - transition.updateFrame(node: self.smallCircleNode, frame: smallCircleFrame.insetBy(dx: -shadowBlur, dy: -shadowBlur)) - transition.updateFrame(node: self.smallCircleShadowNode, frame: smallCircleFrame.insetBy(dx: -shadowBlur, dy: -shadowBlur)) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + self.backgroundNode.update( + theme: self.theme, + size: backgroundFrame.size, + cloudSourcePoint: cloudSourcePoint - backgroundFrame.minX, + isLeftAligned: isLeftAligned, + transition: transition + ) if let animateInFromAnchorRect = animateInFromAnchorRect { let springDuration: Double = 0.42 @@ -386,14 +268,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { let sourceBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: backgroundInsets, anchorRect: animateInFromAnchorRect, contentSize: CGSize(width: backgroundFrame.height, height: contentHeight)).0 - self.backgroundNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - backgroundFrame.midX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) - self.backgroundNode.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size).insetBy(dx: -shadowBlur, dy: -shadowBlur)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: backgroundFrame.size).insetBy(dx: -shadowBlur, dy: -shadowBlur)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) + self.backgroundNode.animateInFromAnchorRect(size: backgroundFrame.size, sourceBackgroundFrame: sourceBackgroundFrame.offsetBy(dx: -backgroundFrame.minX, dy: -backgroundFrame.minY)) self.contentContainer.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - backgroundFrame.midX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) self.contentContainer.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: backgroundFrame.size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) - - //self.contentContainerMask.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - backgroundFrame.midX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) - //self.contentContainerMask.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: backgroundFrame.size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) } else if let animateOutToAnchorRect = animateOutToAnchorRect { let targetBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: backgroundInsets, anchorRect: animateOutToAnchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight)).0 @@ -410,22 +288,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: .immediate, animateInFromAnchorRect: sourceAnchorRect, animateOutToAnchorRect: nil) } - let smallCircleDuration: Double = 0.5 - let largeCircleDuration: Double = 0.5 - let largeCircleDelay: Double = 0.08 let mainCircleDuration: Double = 0.5 let mainCircleDelay: Double = 0.1 - self.smallCircleNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: smallCircleDuration) - self.smallCircleShadowNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: smallCircleDuration) - - self.largeCircleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: largeCircleDelay) - self.largeCircleNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: largeCircleDuration, delay: largeCircleDelay) - self.largeCircleShadowNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: largeCircleDuration, delay: largeCircleDelay) - - self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: mainCircleDelay) - self.backgroundNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: mainCircleDelay) - self.backgroundShadowNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: mainCircleDelay) + self.backgroundNode.animateIn() for i in 0 ..< self.itemNodes.count { let itemNode = self.itemNodes[i] @@ -433,22 +299,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: itemDelay) itemNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: itemDelay, initialVelocity: 0.0) } - - /*if let itemNode = self.itemNodes.first { - itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: mainCircleDelay) - itemNode.didAppear() - itemNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: mainCircleDelay, completion: { _ in - }) - }*/ } public func animateOut(to targetAnchorRect: CGRect?, animatingOutToReaction: Bool) { - self.backgroundNode.layer.animateAlpha(from: self.backgroundNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.backgroundShadowNode.layer.animateAlpha(from: self.backgroundShadowNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.largeCircleNode.layer.animateAlpha(from: self.largeCircleNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.largeCircleShadowNode.layer.animateAlpha(from: self.largeCircleShadowNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.smallCircleNode.layer.animateAlpha(from: self.smallCircleNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.smallCircleShadowNode.layer.animateAlpha(from: self.smallCircleShadowNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.backgroundNode.animateOut() + for itemNode in self.itemNodes { if itemNode.isExtracted { continue diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 1f04a3f9c3..ad4a8f608f 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -146,7 +146,7 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: LimitsCo return false } -private func canViewReadStats(message: Message, isMessageRead: Bool, appConfig: AppConfiguration) -> Bool { +private func canViewReadStats(message: Message, cachedData: CachedPeerData?, isMessageRead: Bool, appConfig: AppConfiguration) -> Bool { guard let peer = message.peers[message.id.peerId] else { return false } @@ -195,6 +195,16 @@ private func canViewReadStats(message: Message, isMessageRead: Bool, appConfig: case let channel as TelegramChannel: if case .broadcast = channel.info { return false + } else if let cachedData = cachedData as? CachedChannelData { + if let memberCount = cachedData.participantsSummary.memberCount { + if Int(memberCount) > maxParticipantCount { + return false + } + } else { + return false + } + } else { + return false } case let group as TelegramGroup: if group.participantCount > maxParticipantCount { @@ -1191,7 +1201,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } - let canViewStats = canViewReadStats(message: message, isMessageRead: isMessageRead, appConfig: appConfig) + let canViewStats = canViewReadStats(message: message, cachedData: cachedData, isMessageRead: isMessageRead, appConfig: appConfig) var reactionCount = 0 for reaction in mergedMessageReactionsAndPeers(message: message).reactions { reactionCount += Int(reaction.count) From 64c94d1fbadd1152681c98427d4c5d48939880d0 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 21 Dec 2021 22:22:17 +0400 Subject: [PATCH 33/35] Fix sticker reaction layout --- submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index c16ce72c85..b34d37ca52 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -986,7 +986,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { let reactionButtonsNode = reactionButtonsSizeAndApply.1(animation) - var reactionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY - 10.0), size: reactionButtonsSizeAndApply.0) + var reactionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY - 4.0), size: reactionButtonsSizeAndApply.0) if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { reactionButtonsFrame.origin.y += 4.0 + actionButtonsSizeAndApply.0.height } From d95219e0b1c69e544b86384ed520b43cf59a3bc7 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 21 Dec 2021 23:26:55 +0400 Subject: [PATCH 34/35] Various Fixes --- .../Sources/Data and Storage/StorageUsageController.swift | 2 +- submodules/UrlWhitelist/Sources/UrlWhitelist.swift | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift index 4939d18b57..893a90de24 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift @@ -507,7 +507,7 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P } else { otherSize = (!otherSize.0, otherSize.1) } - controller?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in + controller?.updateItem(groupIndex: 0, itemIndex: itemIndex + 1, { item in if let item = item as? ActionSheetCheckboxItem { return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action) } diff --git a/submodules/UrlWhitelist/Sources/UrlWhitelist.swift b/submodules/UrlWhitelist/Sources/UrlWhitelist.swift index 3f12ac2e08..47bcf4ec20 100644 --- a/submodules/UrlWhitelist/Sources/UrlWhitelist.swift +++ b/submodules/UrlWhitelist/Sources/UrlWhitelist.swift @@ -2,7 +2,8 @@ import Foundation private let whitelistedHosts: Set = Set([ "t.me", - "telegram.me" + "telegram.me", + "telegra.ph" ]) public func isConcealedUrlWhitelisted(_ url: URL) -> Bool { From a81421bdc9aec141e5aaee21b56a1ec020b96ca8 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Wed, 22 Dec 2021 10:34:52 +0400 Subject: [PATCH 35/35] Update API --- submodules/Postbox/Sources/Coding.swift | 3 +- submodules/TelegramApi/Sources/Api0.swift | 4 +- submodules/TelegramApi/Sources/Api2.swift | 74 ++++++++----- submodules/TelegramApi/Sources/Api4.swift | 14 +++ .../Sources/State/AvailableReactions.swift | 17 ++- .../TelegramEngine/Messages/AdMessages.swift | 100 ++++++++++++------ 6 files changed, 148 insertions(+), 64 deletions(-) diff --git a/submodules/Postbox/Sources/Coding.swift b/submodules/Postbox/Sources/Coding.swift index 2a2252e00a..00544eb6b2 100644 --- a/submodules/Postbox/Sources/Coding.swift +++ b/submodules/Postbox/Sources/Coding.swift @@ -1806,7 +1806,8 @@ public final class PostboxDecoder { let result = try AdaptedPostboxDecoder().decode(T.self, from: innerData) return result } catch let error { - assertionFailure("Decoding error: \(error)") + postboxLog("Decoding error: \(error)") + //assertionFailure("Decoding error: \(error)") return nil } } else { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index e011f5df7c..77f5e6bf04 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -758,7 +758,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1625153079] = { return Api.InputWebFileLocation.parse_inputWebFileGeoPointLocation($0) } dict[-1275374751] = { return Api.EmojiLanguage.parse_emojiLanguage($0) } dict[1601666510] = { return Api.MessageFwdHeader.parse_messageFwdHeader($0) } - dict[-783162982] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } + dict[981691896] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } dict[-1012849566] = { return Api.BaseTheme.parse_baseThemeClassic($0) } dict[-69724536] = { return Api.BaseTheme.parse_baseThemeDay($0) } dict[-1212997976] = { return Api.BaseTheme.parse_baseThemeNight($0) } @@ -791,7 +791,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[594408994] = { return Api.EmojiKeyword.parse_emojiKeywordDeleted($0) } dict[-290921362] = { return Api.upload.CdnFile.parse_cdnFileReuploadNeeded($0) } dict[-1449145777] = { return Api.upload.CdnFile.parse_cdnFile($0) } - dict[1679961905] = { return Api.AvailableReaction.parse_availableReaction($0) } + dict[1424116867] = { return Api.AvailableReaction.parse_availableReaction($0) } dict[415997816] = { return Api.help.InviteText.parse_inviteText($0) } dict[-1826077446] = { return Api.MessageUserReaction.parse_messageUserReaction($0) } dict[1984755728] = { return Api.BotInlineMessage.parse_botInlineMessageMediaAuto($0) } diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index 6dafefb779..ca5bf77426 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -19708,17 +19708,19 @@ public extension Api { } public enum SponsoredMessage: TypeConstructorDescription { - case sponsoredMessage(flags: Int32, randomId: Buffer, fromId: Api.Peer, channelPost: Int32?, startParam: String?, message: String, entities: [Api.MessageEntity]?) + case sponsoredMessage(flags: Int32, randomId: Buffer, fromId: Api.Peer?, chatInvite: Api.ChatInvite?, chatInviteHash: String?, channelPost: Int32?, startParam: String?, message: String, entities: [Api.MessageEntity]?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .sponsoredMessage(let flags, let randomId, let fromId, let channelPost, let startParam, let message, let entities): + case .sponsoredMessage(let flags, let randomId, let fromId, let chatInvite, let chatInviteHash, let channelPost, let startParam, let message, let entities): if boxed { - buffer.appendInt32(-783162982) + buffer.appendInt32(981691896) } serializeInt32(flags, buffer: buffer, boxed: false) serializeBytes(randomId, buffer: buffer, boxed: false) - fromId.serialize(buffer, true) + if Int(flags) & Int(1 << 3) != 0 {fromId!.serialize(buffer, true)} + if Int(flags) & Int(1 << 4) != 0 {chatInvite!.serialize(buffer, true)} + if Int(flags) & Int(1 << 4) != 0 {serializeString(chatInviteHash!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 2) != 0 {serializeInt32(channelPost!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {serializeString(startParam!, buffer: buffer, boxed: false)} serializeString(message, buffer: buffer, boxed: false) @@ -19733,8 +19735,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .sponsoredMessage(let flags, let randomId, let fromId, let channelPost, let startParam, let message, let entities): - return ("sponsoredMessage", [("flags", flags), ("randomId", randomId), ("fromId", fromId), ("channelPost", channelPost), ("startParam", startParam), ("message", message), ("entities", entities)]) + case .sponsoredMessage(let flags, let randomId, let fromId, let chatInvite, let chatInviteHash, let channelPost, let startParam, let message, let entities): + return ("sponsoredMessage", [("flags", flags), ("randomId", randomId), ("fromId", fromId), ("chatInvite", chatInvite), ("chatInviteHash", chatInviteHash), ("channelPost", channelPost), ("startParam", startParam), ("message", message), ("entities", entities)]) } } @@ -19744,28 +19746,36 @@ public extension Api { var _2: Buffer? _2 = parseBytes(reader) var _3: Api.Peer? - if let signature = reader.readInt32() { + if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.Peer - } - var _4: Int32? - if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt32() } + } } + var _4: Api.ChatInvite? + if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.ChatInvite + } } var _5: String? - if Int(_1!) & Int(1 << 0) != 0 {_5 = parseString(reader) } - var _6: String? - _6 = parseString(reader) - var _7: [Api.MessageEntity]? + if Int(_1!) & Int(1 << 4) != 0 {_5 = parseString(reader) } + var _6: Int32? + if Int(_1!) & Int(1 << 2) != 0 {_6 = reader.readInt32() } + var _7: String? + if Int(_1!) & Int(1 << 0) != 0 {_7 = parseString(reader) } + var _8: String? + _8 = parseString(reader) + var _9: [Api.MessageEntity]? if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { - _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + _9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.SponsoredMessage.sponsoredMessage(flags: _1!, randomId: _2!, fromId: _3!, channelPost: _4, startParam: _5, message: _6!, entities: _7) + let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil + let _c8 = _8 != nil + let _c9 = (Int(_1!) & Int(1 << 1) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.SponsoredMessage.sponsoredMessage(flags: _1!, randomId: _2!, fromId: _3, chatInvite: _4, chatInviteHash: _5, channelPost: _6, startParam: _7, message: _8!, entities: _9) } else { return nil @@ -20190,17 +20200,18 @@ public extension Api { } public enum AvailableReaction: TypeConstructorDescription { - case availableReaction(reaction: String, title: String, staticIcon: Api.Document, selectAnimation: Api.Document, activateAnimation: Api.Document, effectAnimation: Api.Document) + case availableReaction(reaction: String, title: String, staticIcon: Api.Document, appearAnimation: Api.Document, selectAnimation: Api.Document, activateAnimation: Api.Document, effectAnimation: Api.Document) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .availableReaction(let reaction, let title, let staticIcon, let selectAnimation, let activateAnimation, let effectAnimation): + case .availableReaction(let reaction, let title, let staticIcon, let appearAnimation, let selectAnimation, let activateAnimation, let effectAnimation): if boxed { - buffer.appendInt32(1679961905) + buffer.appendInt32(1424116867) } serializeString(reaction, buffer: buffer, boxed: false) serializeString(title, buffer: buffer, boxed: false) staticIcon.serialize(buffer, true) + appearAnimation.serialize(buffer, true) selectAnimation.serialize(buffer, true) activateAnimation.serialize(buffer, true) effectAnimation.serialize(buffer, true) @@ -20210,8 +20221,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .availableReaction(let reaction, let title, let staticIcon, let selectAnimation, let activateAnimation, let effectAnimation): - return ("availableReaction", [("reaction", reaction), ("title", title), ("staticIcon", staticIcon), ("selectAnimation", selectAnimation), ("activateAnimation", activateAnimation), ("effectAnimation", effectAnimation)]) + case .availableReaction(let reaction, let title, let staticIcon, let appearAnimation, let selectAnimation, let activateAnimation, let effectAnimation): + return ("availableReaction", [("reaction", reaction), ("title", title), ("staticIcon", staticIcon), ("appearAnimation", appearAnimation), ("selectAnimation", selectAnimation), ("activateAnimation", activateAnimation), ("effectAnimation", effectAnimation)]) } } @@ -20236,14 +20247,19 @@ public extension Api { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.Document } + var _7: Api.Document? + if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.Document + } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.AvailableReaction.availableReaction(reaction: _1!, title: _2!, staticIcon: _3!, selectAnimation: _4!, activateAnimation: _5!, effectAnimation: _6!) + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.AvailableReaction.availableReaction(reaction: _1!, title: _2!, staticIcon: _3!, appearAnimation: _4!, selectAnimation: _5!, activateAnimation: _6!, effectAnimation: _7!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api4.swift b/submodules/TelegramApi/Sources/Api4.swift index 15a5792e21..8bf9c9f2df 100644 --- a/submodules/TelegramApi/Sources/Api4.swift +++ b/submodules/TelegramApi/Sources/Api4.swift @@ -4591,6 +4591,20 @@ public extension Api { return result }) } + + public static func setDefaultReaction(emoji: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1474910882) + serializeString(emoji, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.setDefaultReaction", parameters: [("emoji", emoji)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } } public struct channels { public static func readHistory(channel: Api.InputChannel, maxId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { diff --git a/submodules/TelegramCore/Sources/State/AvailableReactions.swift b/submodules/TelegramCore/Sources/State/AvailableReactions.swift index c041c41fd2..86d24e38de 100644 --- a/submodules/TelegramCore/Sources/State/AvailableReactions.swift +++ b/submodules/TelegramCore/Sources/State/AvailableReactions.swift @@ -9,6 +9,7 @@ public final class AvailableReactions: Equatable, Codable { case value case title case staticIcon + case appearAnimation case selectAnimation case activateAnimation case effectAnimation @@ -17,6 +18,7 @@ public final class AvailableReactions: Equatable, Codable { public let value: String public let title: String public let staticIcon: TelegramMediaFile + public let appearAnimation: TelegramMediaFile public let selectAnimation: TelegramMediaFile public let activateAnimation: TelegramMediaFile public let effectAnimation: TelegramMediaFile @@ -25,6 +27,7 @@ public final class AvailableReactions: Equatable, Codable { value: String, title: String, staticIcon: TelegramMediaFile, + appearAnimation: TelegramMediaFile, selectAnimation: TelegramMediaFile, activateAnimation: TelegramMediaFile, effectAnimation: TelegramMediaFile @@ -32,6 +35,7 @@ public final class AvailableReactions: Equatable, Codable { self.value = value self.title = title self.staticIcon = staticIcon + self.appearAnimation = appearAnimation self.selectAnimation = selectAnimation self.activateAnimation = activateAnimation self.effectAnimation = effectAnimation @@ -47,6 +51,9 @@ public final class AvailableReactions: Equatable, Codable { if lhs.staticIcon != rhs.staticIcon { return false } + if lhs.appearAnimation != rhs.appearAnimation { + return false + } if lhs.selectAnimation != rhs.selectAnimation { return false } @@ -68,6 +75,9 @@ public final class AvailableReactions: Equatable, Codable { let staticIconData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .staticIcon) self.staticIcon = TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: staticIconData.data))) + let appearAnimationData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .appearAnimation) + self.appearAnimation = TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: appearAnimationData.data))) + let selectAnimationData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .selectAnimation) self.selectAnimation = TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: selectAnimationData.data))) @@ -85,6 +95,7 @@ public final class AvailableReactions: Equatable, Codable { try container.encode(self.title, forKey: .title) try container.encode(PostboxEncoder().encodeObjectToRawData(self.staticIcon), forKey: .staticIcon) + try container.encode(PostboxEncoder().encodeObjectToRawData(self.appearAnimation), forKey: .appearAnimation) try container.encode(PostboxEncoder().encodeObjectToRawData(self.selectAnimation), forKey: .selectAnimation) try container.encode(PostboxEncoder().encodeObjectToRawData(self.activateAnimation), forKey: .activateAnimation) try container.encode(PostboxEncoder().encodeObjectToRawData(self.effectAnimation), forKey: .effectAnimation) @@ -135,10 +146,13 @@ public final class AvailableReactions: Equatable, Codable { private extension AvailableReactions.Reaction { convenience init?(apiReaction: Api.AvailableReaction) { switch apiReaction { - case let .availableReaction(reaction, title, staticIcon, selectAnimation, activateAnimation, effectAnimation): + case let .availableReaction(reaction, title, staticIcon, appearAnimation, selectAnimation, activateAnimation, effectAnimation): guard let staticIconFile = telegramMediaFileFromApiDocument(staticIcon) else { return nil } + guard let appearAnimationFile = telegramMediaFileFromApiDocument(appearAnimation) else { + return nil + } guard let selectAnimationFile = telegramMediaFileFromApiDocument(selectAnimation) else { return nil } @@ -152,6 +166,7 @@ private extension AvailableReactions.Reaction { value: reaction, title: title, staticIcon: staticIconFile, + appearAnimation: appearAnimationFile, selectAnimation: selectAnimationFile, activateAnimation: activateAnimationFile, effectAnimation: effectAnimationFile diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index 037d7abd0a..f2f8013690 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -10,16 +10,38 @@ private class AdMessagesHistoryContextImpl { case text case textEntities case media - case authorId + case target case messageId case startParam } + + enum Target: Equatable, Codable { + enum DecodingError: Error { + case generic + } + + enum CodingKeys: String, CodingKey { + case peer + } + + case peer(PeerId) + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let peer = try container.decodeIfPresent(Int64.self, forKey: .peer) { + self = .peer(PeerId(peer)) + } else { + throw DecodingError.generic + } + } + } public let opaqueId: Data public let text: String public let textEntities: [MessageTextEntity] public let media: [Media] - public let authorId: PeerId + public let target: Target public let messageId: MessageId? public let startParam: String? @@ -28,7 +50,7 @@ private class AdMessagesHistoryContextImpl { text: String, textEntities: [MessageTextEntity], media: [Media], - authorId: PeerId, + target: Target, messageId: MessageId?, startParam: String? ) { @@ -36,7 +58,7 @@ private class AdMessagesHistoryContextImpl { self.text = text self.textEntities = textEntities self.media = media - self.authorId = authorId + self.target = target self.messageId = messageId self.startParam = startParam } @@ -54,7 +76,7 @@ private class AdMessagesHistoryContextImpl { return PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? Media } - self.authorId = try container.decode(PeerId.self, forKey: .authorId) + self.target = try container.decode(Target.self, forKey: .target) self.messageId = try container.decodeIfPresent(MessageId.self, forKey: .messageId) self.startParam = try container.decodeIfPresent(String.self, forKey: .startParam) } @@ -73,7 +95,7 @@ private class AdMessagesHistoryContextImpl { } try container.encode(mediaData, forKey: .media) - try container.encode(self.authorId, forKey: .authorId) + try container.encode(self.target, forKey: .target) try container.encodeIfPresent(self.messageId, forKey: .messageId) try container.encodeIfPresent(self.startParam, forKey: .startParam) } @@ -96,7 +118,7 @@ private class AdMessagesHistoryContextImpl { return false } } - if lhs.authorId != rhs.authorId { + if lhs.target != rhs.target { return false } if lhs.messageId != rhs.messageId { @@ -108,7 +130,7 @@ private class AdMessagesHistoryContextImpl { return true } - func toMessage(peerId: PeerId, transaction: Transaction) -> Message { + func toMessage(peerId: PeerId, transaction: Transaction) -> Message? { var attributes: [MessageAttribute] = [] attributes.append(AdMessageAttribute(opaqueId: self.opaqueId, startParam: self.startParam, messageId: self.messageId)) @@ -122,9 +144,18 @@ private class AdMessagesHistoryContextImpl { if let peer = transaction.getPeer(peerId) { messagePeers[peer.id] = peer } - if let peer = transaction.getPeer(self.authorId) { - messagePeers[peer.id] = peer + + let author: Peer + switch self.target { + case let .peer(peerId): + if let peer = transaction.getPeer(peerId) { + author = peer + } else { + return nil + } } + + messagePeers[author.id] = author return Message( stableId: 0, @@ -140,7 +171,7 @@ private class AdMessagesHistoryContextImpl { globalTags: [], localTags: [], forwardInfo: nil, - author: transaction.getPeer(self.authorId), + author: author, text: self.text, attributes: attributes, media: self.media, @@ -270,7 +301,7 @@ private class AdMessagesHistoryContextImpl { |> mapToSignal { cachedState -> Signal in if let cachedState = cachedState, cachedState.timestamp >= Int32(Date().timeIntervalSince1970) - 5 * 60 { return account.postbox.transaction { transaction -> State in - return State(messages: cachedState.messages.map { message in + return State(messages: cachedState.messages.compactMap { message -> Message? in return message.toMessage(peerId: peerId, transaction: transaction) }) } @@ -325,35 +356,42 @@ private class AdMessagesHistoryContextImpl { for message in messages { switch message { - case let .sponsoredMessage(_, randomId, fromId, channelPost, startParam, message, entities): + case let .sponsoredMessage(_, randomId, fromId, chatInvite, chatInviteHash, channelPost, startParam, message, entities): var parsedEntities: [MessageTextEntity] = [] if let entities = entities { parsedEntities = messageTextEntitiesFromApiEntities(entities) } + + let _ = chatInvite + let _ = chatInviteHash + + var target: CachedMessage.Target? + if let fromId = fromId { + target = .peer(fromId.peerId) + } + + var messageId: MessageId? + if let fromId = fromId, let channelPost = channelPost { + messageId = MessageId(peerId: fromId.peerId, namespace: Namespaces.Message.Cloud, id: channelPost) + } - let parsedMedia: [Media] = [] - /*if let media = media { - let (mediaValue, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId) - if let mediaValue = mediaValue { - parsedMedia.append(mediaValue) - } - }*/ - - parsedMessages.append(CachedMessage( - opaqueId: randomId.makeData(), - text: message, - textEntities: parsedEntities, - media: parsedMedia, - authorId: fromId.peerId, - messageId: channelPost.flatMap { MessageId(peerId: fromId.peerId, namespace: Namespaces.Message.Cloud, id: $0) }, - startParam: startParam - )) + if let target = target { + parsedMessages.append(CachedMessage( + opaqueId: randomId.makeData(), + text: message, + textEntities: parsedEntities, + media: [], + target: target, + messageId: messageId, + startParam: startParam + )) + } } } CachedState.setCached(transaction: transaction, peerId: peerId, state: CachedState(timestamp: Int32(Date().timeIntervalSince1970), messages: parsedMessages)) - return parsedMessages.map { message in + return parsedMessages.compactMap { message -> Message? in return message.toMessage(peerId: peerId, transaction: transaction) } }