This commit is contained in:
Isaac 2026-01-20 21:35:04 +08:00
parent f444587197
commit 4f04dc0bb7
12 changed files with 466 additions and 381 deletions

View file

@ -3259,8 +3259,12 @@ public final class ChatListNode: ListView {
default:
continue
}
if case let .index(index) = transition.chatListView.filteredEntries[entryCount - 1 - i].sortIndex, case let .chatList(chatListIndex) = index, chatListIndex.pinningIndex != nil {
pinnedOverscroll = true
if case let .index(index) = transition.chatListView.filteredEntries[entryCount - 1 - i].sortIndex {
if case let .chatList(chatListIndex) = index, chatListIndex.pinningIndex != nil {
pinnedOverscroll = true
} else if case let .forum(pinnedIndex, _, _, _, _) = index, case .index = pinnedIndex {
pinnedOverscroll = true
}
}
}
}

View file

@ -70,6 +70,9 @@ swift_library(
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/AvatarNode",
"//submodules/PhotoResources",
"//submodules/TelegramUI/Components/EdgeEffect",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/GlassControls",
],
visibility = [
"//visibility:public",

View file

@ -31,6 +31,10 @@ import TranslateUI
import TelegramNotices
import SolidRoundedButtonNode
import UrlHandling
import GlassControls
import ComponentFlow
import ComponentDisplayAdapters
import EdgeEffect
private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: .white)
private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: .white)
@ -132,14 +136,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
private var dateTimeFormat: PresentationDateTimeFormat
private let contentNode: ASDisplayNode
private let deleteButton: UIButton
private let fullscreenButton: UIButton
private let actionButton: UIButton
private let editButton: UIButton
private let maskNode: ASDisplayNode
private let textSelectionKnobContainer: UIView
private let textSelectionKnobSurface: UIView
private let scrollWrapperNode: CaptionScrollWrapperNode
private let scrollWrapperEffect: VariableBlurEffect
private let scrollNode: ASScrollNode
private let textNode: ImmediateTextNodeWithEntities
@ -147,6 +148,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
private var dustNode: InvisibleInkDustNode?
private var buttonNode: SolidRoundedButtonNode?
private var buttonIconNode: ASImageNode?
private let buttonPanel = ComponentView<Empty>()
private var textSelectionNode: TextSelectionNode?
@ -174,6 +176,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
private var videoFramePreviewNode: (ASImageNode, ImmediateTextNode)?
private var validLayout: (CGSize, LayoutMetrics, CGFloat, CGFloat, CGFloat, CGFloat)?
private var buttonsState: (displayDeleteButton: Bool, displayFullscreenButton: Bool, displayActionButton: Bool, displayEditButton: Bool)?
private var codeHighlightState: (id: EngineMessage.Id, specs: [CachedMessageSyntaxHighlight.Spec], disposable: Disposable)?
@ -357,21 +360,16 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
self.contentNode = ASDisplayNode()
self.deleteButton = UIButton()
self.fullscreenButton = UIButton()
self.actionButton = UIButton()
self.editButton = UIButton()
self.deleteButton.setImage(deleteImage, for: [.normal])
self.actionButton.setImage(actionImage, for: [.normal])
self.editButton.setImage(editImage, for: [.normal])
self.textSelectionKnobContainer = UIView()
self.textSelectionKnobSurface = UIView()
self.textSelectionKnobContainer.addSubview(self.textSelectionKnobSurface)
self.scrollWrapperNode = CaptionScrollWrapperNode()
self.scrollWrapperNode.layer.allowsGroupOpacity = true
self.scrollWrapperNode.clipsToBounds = true
self.scrollWrapperEffect = VariableBlurEffect(layer: self.scrollWrapperNode.layer, isTransparent: true, maxBlurRadius: 3.0)
self.scrollWrapperNode.backgroundColor = .clear
self.scrollWrapperNode.isOpaque = false
self.scrollNode = ASScrollNode()
self.scrollNode.clipsToBounds = false
@ -604,10 +602,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
self.textSelectionNode = textSelectionNode
self.contentNode.view.addSubview(self.deleteButton)
self.contentNode.view.addSubview(self.fullscreenButton)
self.contentNode.view.addSubview(self.actionButton)
self.contentNode.view.addSubview(self.editButton)
self.contentNode.addSubnode(self.scrollWrapperNode)
self.scrollWrapperNode.addSubnode(self.scrollNode)
self.contentNode.view.addSubview(self.textSelectionKnobContainer)
@ -626,7 +620,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
self.contentNode.addSubnode(self.statusNode)
self.contentNode.addSubnode(self.statusButtonNode)
self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), for: [.touchUpInside])
/*self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), for: [.touchUpInside])
self.deleteButton.accessibilityTraits = [.button]
self.deleteButton.accessibilityLabel = presentationData.strings.Gallery_VoiceOver_Delete
@ -640,7 +634,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
self.editButton.addTarget(self, action: #selector(self.editButtonPressed), for: [.touchUpInside])
self.editButton.accessibilityTraits = [.button]
self.editButton.accessibilityLabel = presentationData.strings.Gallery_VoiceOver_Edit
self.editButton.accessibilityLabel = presentationData.strings.Gallery_VoiceOver_Edit*/
self.backwardButton.addTarget(self, action: #selector(self.backwardButtonPressed), forControlEvents: .touchUpInside)
self.forwardButton.addTarget(self, action: #selector(self.forwardButtonPressed), forControlEvents: .touchUpInside)
@ -812,6 +806,15 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
dateText = nil
}
if origin == nil {
self.buttonsState = (
displayDeleteButton: false,
displayFullscreenButton: false,
displayActionButton: false,
displayEditButton: false
)
}
if self.currentMessageText != caption || self.currentAuthorNameText != titleText || self.currentDateText != dateText {
self.currentMessageText = caption
self.currentAuthorNameText = titleText
@ -842,13 +845,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
self.requestLayout?(.immediate)
}
if origin == nil {
self.editButton.isHidden = true
self.deleteButton.isHidden = true
self.fullscreenButton.isHidden = true
self.editButton.isHidden = true
}
}
func setMessage(_ message: Message, displayInfo: Bool = true, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false) {
@ -1044,8 +1040,13 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
dateText = ""
canEdit = false
}
var displayDeleteButton = false
var displayFullscreenButton = false
var displayActionButton = false
var displayEditButton = false
if self.currentMessageText != messageText || canDelete != !self.deleteButton.isHidden || canFullscreen != !self.fullscreenButton.isHidden || canShare != !self.actionButton.isHidden || canEdit != !self.editButton.isHidden || self.currentAuthorNameText != authorNameText || self.currentDateText != dateText {
do {
self.currentMessageText = messageText
if messageText.length == 0 {
@ -1069,15 +1070,13 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
self.dateNode.accessibilityLabel = self.dateNode.attributedText?.string
if canFullscreen {
self.fullscreenButton.isHidden = false
self.deleteButton.isHidden = true
displayFullscreenButton = true
} else {
self.deleteButton.isHidden = !canDelete
self.fullscreenButton.isHidden = true
displayDeleteButton = canDelete
}
self.actionButton.isHidden = !canShare
self.editButton.isHidden = !canEdit
displayActionButton = canShare
displayEditButton = canEdit
if let adAttribute = message.adAttribute {
if self.buttonNode == nil {
@ -1103,6 +1102,13 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
buttonNode.removeFromSupernode()
}
self.buttonsState = (
displayDeleteButton: displayDeleteButton,
displayFullscreenButton: displayFullscreenButton,
displayActionButton: displayActionButton,
displayEditButton: displayEditButton
)
self.requestLayout?(.immediate)
}
}
@ -1175,17 +1181,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
self.validLayout = (size, metrics, leftInset, rightInset, bottomInset, contentInset)
let width = size.width
var bottomInset = bottomInset
if !bottomInset.isZero && bottomInset < 30.0 {
bottomInset -= 7.0
}
var panelHeight = 44.0 + bottomInset
var panelHeight = 54.0 + bottomInset
panelHeight += contentInset
let isLandscape = size.width > size.height
self.fullscreenButton.setImage(isLandscape ? fullscreenOffImage : fullscreenOnImage, for: [.normal])
let displayCaption: Bool
if case .compact = metrics.widthClass {
displayCaption = !self.textNode.isHidden && !isLandscape
@ -1193,17 +1193,22 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
displayCaption = !self.textNode.isHidden
}
if metrics.isTablet {
self.fullscreenButton.isHidden = true
var buttonPanelInsets = UIEdgeInsets()
buttonPanelInsets.left = 8.0
buttonPanelInsets.right = 8.0
buttonPanelInsets.bottom = bottomInset + 8.0
if bottomInset <= 32.0 {
buttonPanelInsets.left += 18.0
buttonPanelInsets.right += 18.0
}
if !self.textNode.isHidden {
var textFrame = CGRect()
var visibleTextHeight: CGFloat = 0.0
let sideInset: CGFloat = 8.0 + leftInset
let sideInset: CGFloat = 16.0 + leftInset
let topInset: CGFloat = 8.0
let textBottomInset: CGFloat = 8.0
let textBottomInset: CGFloat = 8.0 + 14.0
let constrainSize = CGSize(width: width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)
let textSize = self.textNode.updateLayout(constrainSize)
@ -1233,26 +1238,52 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
maxTextOffset -= 44.0
}
textOffset = min(maxTextOffset, self.scrollNode.view.contentOffset.y)
let originalPanelHeight = panelHeight
panelHeight = max(0.0, panelHeight + visibleTextPanelHeight + textOffset)
if self.scrollNode.view.isScrollEnabled {
if self.scrollWrapperNode.layer.mask == nil, let maskImage = captionMaskImage {
let maskLayer = CALayer()
maskLayer.contents = maskImage.cgImage
maskLayer.contentsScale = maskImage.scale
maskLayer.contentsCenter = CGRect(x: 0.0, y: 0.0, width: 1.0, height: (maskImage.size.height - 16.0) / maskImage.size.height)
self.scrollWrapperNode.layer.mask = maskLayer
if self.scrollWrapperNode.view.mask == nil {
self.scrollWrapperNode.layer.rasterizationScale = UIScreenScale
let maskView = UIImageView()
let height: CGFloat = 70.0
let baseGradientAlpha: CGFloat = 1.0
let numSteps = 8
let firstStep = 0
let firstLocation = 0.0
let colors = (0 ..< numSteps).map { i -> UIColor in
if i < firstStep {
return UIColor(white: 1.0, alpha: 1.0)
} else {
let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1)
let value: CGFloat = 1.0 - bezierPoint(0.42, 0.0, 0.58, 1.0, step)
return UIColor(white: 0.0, alpha: baseGradientAlpha * value)
}
}
let locations = (0 ..< numSteps).map { i -> CGFloat in
if i < firstStep {
return 0.0
} else {
let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1)
return (firstLocation + (1.0 - firstLocation) * step)
}
}
maskView.image = generateGradientImage(size: CGSize(width: 8.0, height: height), colors: colors, locations: locations)!.stretchableImage(withLeftCapWidth: 0, topCapHeight: 1)
//maskView.image = EdgeEffectView.generateEdgeGradient(baseHeight: originalPanelHeight + 20.0, isInverted: false)
self.scrollWrapperNode.view.mask = maskView
}
} else {
self.scrollWrapperNode.layer.mask = nil
self.scrollWrapperNode.view.mask = nil
}
let scrollWrapperNodeFrame = CGRect(x: 0.0, y: 0.0, width: width, height: max(0.0, visibleTextPanelHeight + textOffset))
let scrollWrapperNodeFrame = CGRect(x: 0.0, y: 0.0, width: width, height: max(0.0, visibleTextPanelHeight + textOffset + originalPanelHeight - 14.0))
if self.scrollWrapperNode.frame != scrollWrapperNodeFrame {
self.scrollWrapperNode.frame = scrollWrapperNodeFrame
self.scrollWrapperNode.layer.mask?.frame = self.scrollWrapperNode.bounds
self.scrollWrapperNode.layer.mask?.removeAllAnimations()
self.scrollWrapperNode.view.mask?.frame = self.scrollWrapperNode.bounds
self.scrollWrapperNode.view.mask?.layer.removeAllAnimations()
self.scrollWrapperEffect.update(size: scrollWrapperNodeFrame.size, constantHeight: 80.0, placement: VariableBlurEffect.Placement(position: .bottom, extendsInwards: true), gradient: EdgeEffectView.generateEdgeGradientData(baseHeight: 80.0), transition: transition)
}
if let buttonNode = self.buttonNode {
@ -1294,11 +1325,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
panelHeight += 34.0
}
var scrubberY: CGFloat = 8.0
var scrubberY: CGFloat = 0.0
if self.textNode.isHidden || !displayCaption {
panelHeight += 8.0
} else {
scrubberY = panelHeight - bottomInset - 44.0 - 44.0
scrubberY = panelHeight - buttonPanelInsets.bottom - 44.0 - 44.0 - 8.0
if contentInset > 0.0 {
scrubberY -= contentInset
}
@ -1308,49 +1339,118 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
panelHeight -= 44.0
}
let scrubberFrame = CGRect(origin: CGPoint(x: leftInset, y: scrubberY), size: CGSize(width: width - leftInset - rightInset, height: 34.0))
scrubberView.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
let scrubberFrame = CGRect(origin: CGPoint(x: leftInset + 4.0, y: scrubberY), size: CGSize(width: width - (leftInset + 4.0) * 2.0, height: 34.0))
scrubberView.updateLayout(size: size, leftInset: leftInset + 4.0, rightInset: rightInset, transition: .immediate)
transition.updateBounds(layer: scrubberView.layer, bounds: CGRect(origin: CGPoint(), size: scrubberFrame.size))
transition.updatePosition(layer: scrubberView.layer, position: CGPoint(x: scrubberFrame.midX, y: scrubberFrame.midY))
}
transition.updateAlpha(node: self.scrollWrapperNode, alpha: displayCaption ? 1.0 : 0.0)
self.actionButton.frame = CGRect(origin: CGPoint(x: leftInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
var leftControlItems: [GlassControlGroupComponent.Item] = []
var rightControlItems: [GlassControlGroupComponent.Item] = []
let deleteFrame = CGRect(origin: CGPoint(x: width - 44.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
var editFrame = CGRect(origin: CGPoint(x: width - 44.0 - 50.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
if self.deleteButton.isHidden && self.fullscreenButton.isHidden {
editFrame = deleteFrame
if let buttonsState = self.buttonsState {
if buttonsState.displayActionButton {
leftControlItems.append(GlassControlGroupComponent.Item(
id: AnyHashable("forward"),
content: .icon("Chat/Input/Accessory Panels/MessageSelectionForward"),
action: { [weak self] in
guard let self else {
return
}
self.actionButtonPressed()
}
))
}
if buttonsState.displayEditButton {
rightControlItems.append(GlassControlGroupComponent.Item(
id: AnyHashable("edit"),
content: .icon("Media Gallery/Draw"),
action: { [weak self] in
guard let self else {
return
}
self.editButtonPressed()
}
))
}
if buttonsState.displayFullscreenButton && !metrics.isTablet {
rightControlItems.append(GlassControlGroupComponent.Item(
id: AnyHashable("fullscreen"),
content: .icon(isLandscape ? "Chat/Context Menu/Collapse" : "Chat/Context Menu/Expand"),
action: { [weak self] in
guard let self else {
return
}
self.fullscreenButtonPressed()
}
))
}
if buttonsState.displayDeleteButton {
rightControlItems.append(GlassControlGroupComponent.Item(
id: AnyHashable("delete"),
content: .icon("Chat/Input/Accessory Panels/MessageSelectionTrash"),
action: { [weak self] in
guard let self else {
return
}
self.deleteButtonPressed()
}
))
}
}
let buttonPanelSize = self.buttonPanel.update(
transition: ComponentTransition(transition),
component: AnyComponent(GlassControlPanelComponent(
theme: defaultDarkColorPresentationTheme,
leftItem: GlassControlPanelComponent.Item(
items: leftControlItems,
background: .panel
),
centralItem: nil,
rightItem: GlassControlPanelComponent.Item(
items: rightControlItems,
background: .panel
),
centerAlignmentIfPossible: true
)),
environment: {},
containerSize: CGSize(width: size.width - buttonPanelInsets.left - buttonPanelInsets.right, height: 44.0)
)
let buttonPanelFrame = CGRect(origin: CGPoint(x: buttonPanelInsets.left, y: panelHeight - buttonPanelInsets.bottom - buttonPanelSize.height), size: buttonPanelSize)
if let buttonPanelView = self.buttonPanel.view {
if buttonPanelView.superview == nil {
self.contentNode.view.insertSubview(buttonPanelView, belowSubview: self.authorNameNode.view)
}
ComponentTransition(transition).setFrame(view: buttonPanelView, frame: buttonPanelFrame)
}
self.deleteButton.frame = deleteFrame
self.fullscreenButton.frame = deleteFrame
self.editButton.frame = editFrame
if let image = self.backwardButton.backgroundIconNode.image {
self.backwardButton.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0) - 66.0, y: panelHeight - bottomInset - 44.0 + 7.0), size: image.size)
self.backwardButton.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0) - 66.0, y: panelHeight - buttonPanelInsets.bottom - 44.0 + floorToScreenPixels((44.0 - image.size.height) * 0.5)), size: image.size)
}
if let image = self.forwardButton.backgroundIconNode.image {
self.forwardButton.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0) + 66.0, y: panelHeight - bottomInset - 44.0 + 7.0), size: image.size)
self.forwardButton.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0) + 66.0, y: panelHeight - buttonPanelInsets.bottom - 44.0 + floorToScreenPixels((44.0 - image.size.height) * 0.5)), size: image.size)
}
self.playbackControlButton.frame = CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - bottomInset - 44.0 - 2.0), size: CGSize(width: 44.0, height: 44.0))
self.playPauseIconNode.frame = self.playbackControlButton.bounds.offsetBy(dx: 2.0, dy: 2.0)
self.playbackControlButton.frame = CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - buttonPanelInsets.bottom - 44.0 + floorToScreenPixels((44.0 - 44.0) * 0.5)), size: CGSize(width: 44.0, height: 44.0))
self.playPauseIconNode.frame = self.playbackControlButton.bounds.offsetBy(dx: 2.0, dy: -2.0)
let statusSize = CGSize(width: 28.0, height: 28.0)
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: floor((width - statusSize.width) / 2.0), y: panelHeight - bottomInset - statusSize.height - 8.0), size: statusSize))
self.statusButtonNode.frame = CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
let buttonsSideInset: CGFloat = !self.editButton.isHidden ? 88.0 : 44.0
let authorNameSize = self.authorNameNode.measure(CGSize(width: width - buttonsSideInset * 2.0 - 8.0 * 2.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude))
let buttonsSideInset: CGFloat = 44.0 * CGFloat(max(leftControlItems.count, rightControlItems.count))
let authorNameSize = self.authorNameNode.measure(CGSize(width: width - buttonsSideInset * 2.0 - 8.0 * 2.0 - buttonPanelInsets.left - buttonPanelInsets.right, height: CGFloat.greatestFiniteMagnitude))
let dateSize = self.dateNode.measure(CGSize(width: width - buttonsSideInset * 2.0 - 8.0 * 2.0, height: CGFloat.greatestFiniteMagnitude))
if authorNameSize.height.isZero {
self.dateNode.frame = CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height) / 2.0)), size: dateSize)
self.dateNode.frame = CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - buttonPanelInsets.bottom - 44.0 + floor((44.0 - dateSize.height) / 2.0)), size: dateSize)
} else {
let labelsSpacing: CGFloat = 0.0
self.authorNameNode.frame = CGRect(origin: CGPoint(x: floor((width - authorNameSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0)), size: authorNameSize)
self.dateNode.frame = CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0) + authorNameSize.height + labelsSpacing), size: dateSize)
self.authorNameNode.frame = CGRect(origin: CGPoint(x: floor((width - authorNameSize.width) / 2.0), y: panelHeight - buttonPanelInsets.bottom - 44.0 + floor((44.0 - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0)), size: authorNameSize)
self.dateNode.frame = CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - buttonPanelInsets.bottom - 44.0 + floor((44.0 - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0) + authorNameSize.height + labelsSpacing), size: dateSize)
}
if let (videoFramePreviewNode, videoFrameTextNode) = self.videoFramePreviewNode {
@ -1365,7 +1465,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
if size.width > size.height {
scrubberInset = 58.0
} else {
scrubberInset = 13.0
scrubberInset = 14.0 + 8.0
}
let imageSize = intrinsicImageSize.aspectFitted(fitSize)
@ -1403,10 +1503,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
self.scrollWrapperNode.alpha = 1.0
self.dateNode.alpha = 1.0
self.authorNameNode.alpha = 1.0
self.deleteButton.alpha = 1.0
self.fullscreenButton.alpha = 1.0
self.actionButton.alpha = 1.0
self.editButton.alpha = 1.0
if let buttonPanelView = self.buttonPanel.view {
buttonPanelView.alpha = 1.0
}
self.backwardButton.alpha = self.hasSeekControls ? 1.0 : 0.0
self.forwardButton.alpha = self.hasSeekControls ? 1.0 : 0.0
self.statusNode.alpha = 1.0
@ -1432,10 +1532,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
self.scrollWrapperNode.alpha = 0.0
self.dateNode.alpha = 0.0
self.authorNameNode.alpha = 0.0
self.deleteButton.alpha = 0.0
self.fullscreenButton.alpha = 0.0
self.actionButton.alpha = 0.0
self.editButton.alpha = 0.0
if let buttonPanelView = self.buttonPanel.view {
buttonPanelView.alpha = 0.0
}
self.backwardButton.alpha = 0.0
self.forwardButton.alpha = 0.0
self.statusNode.alpha = 0.0

View file

@ -140,11 +140,11 @@ public func internalDocumentItemSupportsMimeType(_ type: String, fileName: Strin
return false
}
private let textFont = Font.regular(16.0)
private let boldFont = Font.bold(16.0)
private let italicFont = Font.italic(16.0)
private let boldItalicFont = Font.semiboldItalic(16.0)
private let fixedFont = UIFont(name: "Menlo-Regular", size: 15.0) ?? textFont
private let textFont = Font.regular(17.0)
private let boldFont = Font.bold(17.0)
private let italicFont = Font.italic(17.0)
private let boldItalicFont = Font.semiboldItalic(17.0)
private let fixedFont = UIFont(name: "Menlo-Regular", size: 17.0) ?? textFont
public func galleryCaptionStringWithAppliedEntities(context: AccountContext, text: String, entities: [MessageTextEntity], message: Message?, cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil) -> NSAttributedString {
var baseQuoteSecondaryTintColor: UIColor?
@ -576,8 +576,7 @@ private func galleryEntriesForMessageHistoryEntries(_ entries: [MessageHistoryEn
}
public class GalleryController: ViewController, StandalonePresentableController, KeyShortcutResponder, GalleryControllerProtocol {
public static let darkNavigationTheme = NavigationBarTheme(overallDarkAppearance: true, buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), enableBackgroundBlur: false, separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
public static let lightNavigationTheme = NavigationBarTheme(overallDarkAppearance: false, buttonColor: UIColor(rgb: 0x0088ff), disabledButtonColor: UIColor(rgb: 0xd0d0d0), primaryTextColor: .black, backgroundColor: UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0), enableBackgroundBlur: false, separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
public static let darkNavigationTheme = NavigationBarTheme(overallDarkAppearance: true, buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), enableBackgroundBlur: false, separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, edgeEffectColor: .black, style: .glass)
private var galleryNode: GalleryControllerNode {
return self.displayNode as! GalleryControllerNode

View file

@ -4,6 +4,8 @@ import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import ComponentFlow
import ComponentDisplayAdapters
public final class GalleryControllerInteraction {
public let presentController: (ViewController, ViewControllerPresentationArguments?) -> Void
@ -39,14 +41,14 @@ open class GalleryFooterContentNode: ASDisplayNode {
open func animateIn(transition: ContainedViewLayoutTransition) {
self.alpha = 0.0
transition.updateAlpha(node: self, alpha: 1.0)
ComponentTransition(transition).setAlpha(view: self.view, alpha: 1.0)
}
open func animateIn(fromHeight: CGFloat, previousContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition) {
}
open func animateOut(transition: ContainedViewLayoutTransition) {
transition.updateAlpha(node: self, alpha: 0.0)
ComponentTransition(transition).setAlpha(view: self.view, alpha: 0.0)
}
open func animateOut(toHeight: CGFloat, nextContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {

View file

@ -2,9 +2,12 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import EdgeEffect
import ComponentFlow
import ComponentDisplayAdapters
public final class GalleryFooterNode: ASDisplayNode {
private let backgroundNode: ASDisplayNode
private let edgeEffectView: EdgeEffectView
private var currentThumbnailPanelHeight: CGFloat?
private var currentFooterContentNode: GalleryFooterContentNode?
@ -16,25 +19,24 @@ public final class GalleryFooterNode: ASDisplayNode {
public init(controllerInteraction: GalleryControllerInteraction) {
self.controllerInteraction = controllerInteraction
self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = UIColor(white: 0.0, alpha: 0.6)
self.edgeEffectView = EdgeEffectView()
super.init()
self.addSubnode(self.backgroundNode)
self.view.addSubview(self.edgeEffectView)
}
private var visibilityAlpha: CGFloat = 1.0
public func setVisibilityAlpha(_ alpha: CGFloat, animated: Bool) {
self.visibilityAlpha = alpha
self.backgroundNode.alpha = alpha
self.edgeEffectView.alpha = alpha
self.currentFooterContentNode?.setVisibilityAlpha(alpha, animated: true)
self.currentOverlayContentNode?.setVisibilityAlpha(alpha)
}
func animateIn(transition: ContainedViewLayoutTransition) {
self.backgroundNode.alpha = 0.0
transition.updateAlpha(node: self.backgroundNode, alpha: 1.0)
self.edgeEffectView.alpha = 0.0
ComponentTransition(transition).setAlpha(view: self.edgeEffectView, alpha: 1.0)
if let currentFooterContentNode = self.currentFooterContentNode {
currentFooterContentNode.animateIn(transition: transition)
@ -47,7 +49,7 @@ public final class GalleryFooterNode: ASDisplayNode {
}
func animateOut(transition: ContainedViewLayoutTransition) {
transition.updateAlpha(node: self.backgroundNode, alpha: 0.0)
ComponentTransition(transition).setAlpha(view: self.edgeEffectView, alpha: 0.0)
if let currentFooterContentNode = self.currentFooterContentNode {
currentFooterContentNode.animateOut(transition: transition)
@ -104,6 +106,8 @@ public final class GalleryFooterNode: ASDisplayNode {
}
var backgroundHeight: CGFloat = 0.0
let verticalOffset: CGFloat = isHidden ? (layout.size.width > layout.size.height ? 44.0 : (effectiveThumbnailPanelHeight > 0.0 ? 106.0 : 54.0)) : 0.0
let backgroundFrame: CGRect
var edgeEffectTransition = ComponentTransition(transition)
if let footerContentNode = self.currentFooterContentNode {
backgroundHeight = footerContentNode.updateLayout(size: layout.size, metrics: layout.metrics, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, contentInset: effectiveThumbnailPanelHeight, transition: transition)
transition.updateFrame(node: footerContentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight + verticalOffset), size: CGSize(width: layout.size.width, height: backgroundHeight)))
@ -115,18 +119,26 @@ public final class GalleryFooterNode: ASDisplayNode {
dismissedCurrentFooterContentNode.removeFromSupernode()
}
})
contentTransition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight + verticalOffset), size: CGSize(width: layout.size.width, height: backgroundHeight)))
backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight + verticalOffset), size: CGSize(width: layout.size.width, height: backgroundHeight))
edgeEffectTransition = ComponentTransition(contentTransition)
} else {
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight + verticalOffset), size: CGSize(width: layout.size.width, height: backgroundHeight)))
backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight + verticalOffset), size: CGSize(width: layout.size.width, height: backgroundHeight))
}
} else {
if let dismissedCurrentFooterContentNode = dismissedCurrentFooterContentNode {
dismissedCurrentFooterContentNode.removeFromSupernode()
}
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight + verticalOffset), size: CGSize(width: layout.size.width, height: backgroundHeight)))
backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight + verticalOffset), size: CGSize(width: layout.size.width, height: backgroundHeight))
}
var edgeEffectFrame = backgroundFrame
let edgeEffectHeight: CGFloat = 46.0
edgeEffectFrame.origin.y -= edgeEffectHeight
edgeEffectFrame.size.height += edgeEffectHeight
edgeEffectTransition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame)
self.edgeEffectView.update(content: .black, rect: edgeEffectFrame, edge: .bottom, edgeSize: min(90.0, edgeEffectFrame.height), transition: edgeEffectTransition)
let contentTransition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
if let overlayContentNode = self.currentOverlayContentNode {
let insets = UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: isHidden ? layout.intrinsicInsets.bottom : backgroundHeight, right: layout.safeInsets.right)
@ -158,7 +170,7 @@ public final class GalleryFooterNode: ASDisplayNode {
if let overlayResult = self.currentOverlayContentNode?.hitTest(point, with: event) {
return overlayResult
}
if !self.backgroundNode.frame.contains(point) || self.visibilityAlpha < 1.0 {
if !self.edgeEffectView.frame.contains(point) || self.visibilityAlpha < 1.0 {
return nil
}
let result = super.hitTest(point, with: event)

View file

@ -25,6 +25,9 @@ import Pasteboard
import AdUI
import AdsInfoScreen
import AdsReportScreen
import GlassBackgroundComponent
import ComponentFlow
import ComponentDisplayAdapters
enum ChatMediaGalleryThumbnail: Equatable {
case image(ImageMediaReference)
@ -756,7 +759,13 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
guard let controller = self.baseNavigationController()?.topViewController as? ViewController else {
return
}
let contextController = makeContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode, actionsOnTop: false)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
var sourceView = self.moreBarButton.referenceNode.view
if let value = self.galleryController()?.navigationBar?.navigationButtonContextContainer(sourceView: sourceView) {
sourceView = value
}
let contextController = makeContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, actionsOnTop: false)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
controller.presentInGlobalOverlay(contextController)
}
@ -1544,11 +1553,9 @@ private class RecognizedContentContainer: ASDisplayNode {
private class ImageRecognitionOverlayContentNode: GalleryOverlayContentNode {
private let backgroundNode: ASImageNode
private let selectedBackgroundNode: ASImageNode
private let iconNode: ASImageNode
private let selectedIconNode: ASImageNode
private let buttonNode: HighlightTrackingButtonNode
private let backgroundContainer: GlassBackgroundContainerView
private let backgroundView: GlassBackgroundView
private let iconView: UIImageView
var action: ((Bool) -> Void)?
private var appeared = false
@ -1556,128 +1563,111 @@ private class ImageRecognitionOverlayContentNode: GalleryOverlayContentNode {
private var validLayout: (CGSize, LayoutMetrics, UIEdgeInsets)?
private var interfaceIsHidden: Bool = false
private var isSelected: Bool = false
init(theme: PresentationTheme) {
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false
self.backgroundContainer = GlassBackgroundContainerView()
self.backgroundView = GlassBackgroundView()
self.iconView = UIImageView()
self.selectedBackgroundNode = ASImageNode()
self.selectedBackgroundNode.displaysAsynchronously = false
self.selectedBackgroundNode.isHidden = true
self.selectedBackgroundNode.image = generateFilledCircleImage(diameter: 32.0, color: .white)
self.buttonNode = HighlightTrackingButtonNode()
self.buttonNode.alpha = 0.0
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/LiveTextIcon"), color: .white)
self.iconNode.contentMode = .center
self.selectedIconNode = ASImageNode()
self.selectedIconNode.displaysAsynchronously = false
self.selectedIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/LiveTextIcon"), color: .black)
self.selectedIconNode.contentMode = .center
self.selectedIconNode.isHidden = true
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/LiveTextIcon"), color: .white)?.withRenderingMode(.alwaysTemplate)
self.iconView.contentMode = .center
super.init()
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.addSubnode(self.buttonNode)
self.buttonNode.addSubnode(self.backgroundNode)
self.buttonNode.addSubnode(self.selectedBackgroundNode)
self.buttonNode.addSubnode(self.iconNode)
self.buttonNode.addSubnode(self.selectedIconNode)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.iconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.iconNode.alpha = 0.4
} else {
strongSelf.iconNode.alpha = 1.0
strongSelf.iconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.backgroundContainer.contentView.addSubview(self.backgroundView)
self.backgroundView.contentView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:))))
self.view.addSubview(self.backgroundContainer)
self.backgroundView.contentView.addSubview(self.iconView)
}
@objc private func buttonPressed() {
let newValue = !self.buttonNode.isSelected
self.buttonNode.isSelected = newValue
self.selectedBackgroundNode.isHidden = !newValue
self.selectedIconNode.isHidden = !newValue
if !newValue && !self.interfaceIsHidden, let (size, metrics, insets) = self.validLayout {
self.updateLayout(size: size, metrics: metrics, insets: insets, isHidden: self.interfaceIsHidden, transition: .animated(duration: 0.3, curve: .easeInOut))
}
self.action?(newValue)
if self.interfaceIsHidden && !newValue {
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
transition.updateAlpha(node: self.buttonNode, alpha: 0.0)
@objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
let newValue = !self.isSelected
self.isSelected = newValue
if !newValue && !self.interfaceIsHidden, let (size, metrics, insets) = self.validLayout {
self.updateLayout(size: size, metrics: metrics, insets: insets, isHidden: self.interfaceIsHidden, transition: .animated(duration: 0.3, curve: .easeInOut))
}
self.action?(newValue)
if self.interfaceIsHidden && !newValue {
let transition = ComponentTransition.easeInOut(duration: 0.2)
transition.setAlpha(view: self.backgroundContainer, alpha: 0.0)
}
}
}
func transitionIn() {
guard self.buttonNode.alpha.isZero else {
guard self.backgroundContainer.alpha.isZero else {
return
}
self.appeared = true
self.buttonNode.alpha = 1.0
self.buttonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
ComponentTransition.easeInOut(duration: 0.2).setAlpha(view: self.backgroundContainer, alpha: 1.0)
}
override func updateLayout(size: CGSize, metrics: LayoutMetrics, insets: UIEdgeInsets, isHidden: Bool, transition: ContainedViewLayoutTransition) {
let transition = ComponentTransition(transition)
self.validLayout = (size, metrics, insets)
self.interfaceIsHidden = isHidden
let buttonSize = CGSize(width: 32.0, height: 32.0)
self.backgroundNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 12.0), size: buttonSize)
self.selectedBackgroundNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 12.0), size: buttonSize)
self.iconNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 12.0), size: buttonSize)
self.selectedIconNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 12.0), size: buttonSize)
let buttonSize = CGSize(width: 44.0, height: 44.0)
self.backgroundContainer.update(size: buttonSize, isDark: true, transition: transition)
self.backgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: buttonSize)
let tintColor: GlassBackgroundView.TintColor
if self.isSelected {
tintColor = .init(kind: .custom, color: UIColor(white: 1.0, alpha: 1.0))
} else {
tintColor = .init(kind: .panel, color: UIColor(white: 0.0, alpha: 0.6))
}
self.backgroundView.update(size: buttonSize, cornerRadius: buttonSize.height * 0.5, isDark: true, tintColor: tintColor, isInteractive: true, transition: transition)
self.iconView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: buttonSize)
transition.setTintColor(view: self.iconView, color: self.isSelected ? .black : .white)
if self.appeared {
if !self.buttonNode.isSelected && isHidden {
transition.updateAlpha(node: self.buttonNode, alpha: 0.0)
if !self.isSelected && isHidden {
transition.setAlpha(view: self.backgroundContainer, alpha: 0.0)
} else {
transition.updateAlpha(node: self.buttonNode, alpha: 1.0)
transition.setAlpha(view: self.backgroundContainer, alpha: 1.0)
}
}
var buttonPosition: CGPoint
if isHidden && !self.buttonNode.isSelected {
buttonPosition = CGPoint(x: size.width - insets.right - buttonSize.width - 59.0, y: -50.0)
if isHidden && !self.isSelected {
buttonPosition = CGPoint(x: size.width - insets.right - buttonSize.width - 66.0 - 10.0, y: -52.0)
} else {
buttonPosition = CGPoint(x: size.width - insets.right - buttonSize.width - (self.buttonNode.isSelected ? 24.0 : 59.0), y: insets.top - 58.0)
buttonPosition = CGPoint(x: size.width - insets.right - buttonSize.width - (self.isSelected ? 24.0 : 70.0), y: insets.top - 50.0)
}
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: buttonPosition, size: CGSize(width: buttonSize.width + 24.0, height: buttonSize.height + 24.0)))
transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: buttonPosition, size: buttonSize))
}
override func animateIn(previousContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition) {
guard self.appeared && (!self.interfaceIsHidden || self.buttonNode.isSelected) else {
guard self.appeared && (!self.interfaceIsHidden || self.isSelected) else {
return
}
self.buttonNode.alpha = 1.0
if let previousContentNode = previousContentNode as? ImageRecognitionOverlayContentNode, previousContentNode.appeared {
self.backgroundContainer.alpha = 1.0
} else {
self.buttonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.backgroundContainer.alpha = 0.0
ComponentTransition.easeInOut(duration: 0.2).setAlpha(view: self.backgroundContainer, alpha: 1.0)
}
}
override func animateOut(nextContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
let previousAlpha = self.buttonNode.alpha
self.buttonNode.alpha = 0.0
self.buttonNode.layer.animateAlpha(from: previousAlpha, to: 0.0, duration: 0.2)
ComponentTransition.easeInOut(duration: 0.2).setAlpha(view: self.backgroundContainer, alpha: 0.0)
completion()
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if self.buttonNode.alpha > 0.0 && self.buttonNode.frame.contains(point) {
if self.backgroundContainer.alpha > 0.0 && self.backgroundContainer.frame.contains(point) {
return true
} else {
return false

View file

@ -434,14 +434,18 @@ func optionsBackgroundImage(dark: Bool) -> UIImage? {
}
func optionsCircleImage(dark: Bool) -> UIImage? {
return generateImage(CGSize(width: 22.0, height: 22.0), contextGenerator: { size, context in
return generateImage(CGSize(width: 44.0, height: 44.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(UIColor.white.cgColor)
let lineWidth: CGFloat = 1.3
context.setLineWidth(lineWidth)
context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth, dy: lineWidth))
context.setFillColor(UIColor.white.cgColor)
let spacing: CGFloat = 3.66
let width: CGFloat = 4.33
for i in 0 ..< 3 {
let x: CGFloat = floorToScreenPixels((size.width - width * 3.0 - spacing * 2.0) * 0.5) + CGFloat(i) * (width + spacing)
let y: CGFloat = floorToScreenPixels((size.height - width) * 0.5)
context.fillEllipse(in: CGRect(origin: CGPoint(x: x, y: y), size: CGSize(width: width, height: width)))
}
})
}
@ -491,7 +495,6 @@ final class MoreHeaderButton: HighlightableButtonNode {
let referenceNode: ContextReferenceContentNode
let containerNode: ContextControllerSourceNode
private let iconNode: ASImageNode
private var animationNode: AnimationNode?
var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
@ -527,29 +530,17 @@ final class MoreHeaderButton: HighlightableButtonNode {
strongSelf.contextAction?(strongSelf.containerNode, gesture)
}
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 26.0, height: 44.0))
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 44.0, height: 44.0))
self.referenceNode.frame = self.containerNode.bounds
self.iconNode.image = optionsCircleImage(dark: false)
if let image = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
}
self.hitTestSlop = UIEdgeInsets(top: 0.0, left: -4.0, bottom: 0.0, right: -4.0)
}
private var content: Content?
func setContent(_ content: Content, animated: Bool = false) {
if case .more = content, self.animationNode == nil {
let iconColor = UIColor(rgb: 0xffffff)
let animationNode = AnimationNode(animation: "anim_profilemore", colors: ["Point 2.Group 1.Fill 1": iconColor,
"Point 3.Group 1.Fill 1": iconColor,
"Point 1.Group 1.Fill 1": iconColor], scale: 1.0)
let animationSize = CGSize(width: 22.0, height: 22.0)
animationNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - animationSize.width) / 2.0), y: floor((self.containerNode.bounds.height - animationSize.height) / 2.0)), size: animationSize)
self.addSubnode(animationNode)
self.animationNode = animationNode
}
if animated {
if let snapshotView = self.referenceNode.view.snapshotContentTree() {
snapshotView.frame = self.referenceNode.frame
@ -562,9 +553,6 @@ final class MoreHeaderButton: HighlightableButtonNode {
self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.iconNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3)
self.animationNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.animationNode?.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3)
}
switch content {
@ -575,7 +563,6 @@ final class MoreHeaderButton: HighlightableButtonNode {
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = true
case let .more(image):
if let image = image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
@ -583,7 +570,6 @@ final class MoreHeaderButton: HighlightableButtonNode {
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = false
}
} else {
self.content = content
@ -595,7 +581,6 @@ final class MoreHeaderButton: HighlightableButtonNode {
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = true
case let .more(image):
if let image = image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
@ -603,7 +588,6 @@ final class MoreHeaderButton: HighlightableButtonNode {
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = false
}
}
}
@ -614,14 +598,13 @@ final class MoreHeaderButton: HighlightableButtonNode {
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: wide ? 32.0 : 22.0, height: 44.0)
return CGSize(width: 44.0, height: 44.0)
}
func onLayout() {
}
func play() {
self.animationNode?.playOnce()
}
}
@ -688,10 +671,8 @@ final class SettingsHeaderButton: HighlightableButtonNode {
strongSelf.contextAction?(strongSelf.containerNode, gesture)
}
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 26.0, height: 44.0))
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 44.0, height: 44.0))
self.referenceNode.frame = self.containerNode.bounds
self.hitTestSlop = UIEdgeInsets(top: 0.0, left: -4.0, bottom: 0.0, right: -4.0)
if let image = self.gearsLayer.image {
let iconInnerInsets = UIEdgeInsets(top: 4.0, left: 8.0, bottom: 4.0, right: 6.0)
@ -725,7 +706,7 @@ final class SettingsHeaderButton: HighlightableButtonNode {
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: wide ? 32.0 : 22.0, height: 44.0)
return CGSize(width: 44.0, height: 44.0)
}
func onLayout() {
@ -3254,7 +3235,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
})
}
let contextController = makeContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: sourceNode, actionsOnTop: actionsOnTop)), items: items |> map { items in
var sourceView = sourceNode.view
if !isSettings {
if let value = self.galleryController()?.navigationBar?.navigationButtonContextContainer(sourceView: sourceView) {
sourceView = value
}
}
let contextController = makeContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, actionsOnTop: actionsOnTop)), items: items |> map { items in
if !items.topItems.isEmpty {
return ContextController.Items(id: AnyHashable(0), content: .twoLists(items.items, items.topItems))
} else {
@ -4087,17 +4075,17 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
final class HeaderContextReferenceContentSource: ContextReferenceContentSource {
private let controller: ViewController
private let sourceNode: ContextReferenceContentNode
private let sourceView: UIView
private let actionsOnTop: Bool
init(controller: ViewController, sourceNode: ContextReferenceContentNode, actionsOnTop: Bool) {
init(controller: ViewController, sourceView: UIView, actionsOnTop: Bool) {
self.controller = controller
self.sourceNode = sourceNode
self.sourceView = sourceView
self.actionsOnTop = actionsOnTop
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: self.actionsOnTop ? .top : .bottom)
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: self.actionsOnTop ? .top : .bottom)
}
}

View file

@ -78,7 +78,7 @@ public class EdgeEffectView: UIView {
}
}
public static func generateEdgeGradientData(baseHeight: CGFloat) -> VariableBlurView.Gradient {
public static func generateEdgeGradientData(baseHeight: CGFloat) -> VariableBlurEffect.Gradient {
let gradientColors: [CGFloat] = [
0.8470588235294118,
0.8431372549019608,
@ -267,14 +267,14 @@ public class EdgeEffectView: UIView {
1.0,
]
return VariableBlurView.Gradient(
return VariableBlurEffect.Gradient(
height: baseHeight,
alpha: gradientColors.map { $0 / gradientColorNorm },
positions: gradientLocations
)
}
public static func generateEdgeGradient(baseHeight: CGFloat, isInverted: Bool) -> UIImage {
public static func generateEdgeGradient(baseHeight: CGFloat, isInverted: Bool, extendsInwards: Bool = false) -> UIImage {
let gradientColors: [CGFloat] = [
0.8470588235294118,
0.8431372549019608,
@ -463,12 +463,18 @@ public class EdgeEffectView: UIView {
1.0,
]
var resizingInverted = isInverted
if extendsInwards {
resizingInverted = false
}
let _ = resizingInverted
return generateGradientImage(
size: CGSize(width: 1.0, height: baseHeight),
colors: gradientColors.map { UIColor(white: 0.0, alpha: $0 / gradientColorNorm) },
locations: gradientLocations,
isInverted: isInverted
)!.resizableImage(withCapInsets: UIEdgeInsets(top: isInverted ? baseHeight : 0.0, left: 0.0, bottom: isInverted ? 0.0 : baseHeight, right: 0.0), resizingMode: .stretch)
)!.resizableImage(withCapInsets: UIEdgeInsets(top: resizingInverted ? baseHeight : 0.0, left: 0.0, bottom: resizingInverted ? 0.0 : baseHeight, right: 0.0), resizingMode: .stretch)
}
}
@ -535,17 +541,17 @@ public final class EdgeEffectComponent: Component {
}
}
public final class VariableBlurView: UIView {
public final class VariableBlurEffect {
private struct Params: Equatable {
let size: CGSize
let constantHeight: CGFloat
let isInverted: Bool
let placement: Placement
let gradient: Gradient
init(size: CGSize, constantHeight: CGFloat, isInverted: Bool, gradient: Gradient) {
init(size: CGSize, constantHeight: CGFloat, placement: Placement, gradient: Gradient) {
self.size = size
self.constantHeight = constantHeight
self.isInverted = isInverted
self.placement = placement
self.gradient = gradient
}
}
@ -578,156 +584,61 @@ public final class VariableBlurView: UIView {
}
}
public let maxBlurRadius: CGFloat
public struct Placement: Equatable {
public enum Position {
case top
case bottom
}
public let position: Position
public let extendsInwards: Bool
public init(position: Position, extendsInwards: Bool) {
self.position = position
self.extendsInwards = extendsInwards
}
}
private let effectLayerDelegate: SimpleLayerDelegate
private var mainEffectLayer: CALayer?
private var additionalEffectLayer: CALayer?
private let layer: CALayer
private let isTransparent: Bool
private let maxBlurRadius: CGFloat
private var params: Params?
private var gradientImage: UIImage?
private var isAnimating: Bool = false
private let imageSubview: UIImageView
private let imageSubview: UIImageView?
public init(maxBlurRadius: CGFloat = 20.0) {
public init(layer: CALayer, isTransparent: Bool = false, maxBlurRadius: CGFloat = 20.0) {
self.layer = layer
self.isTransparent = isTransparent
self.maxBlurRadius = maxBlurRadius
self.effectLayerDelegate = SimpleLayerDelegate()
self.mainEffectLayer = createBackdropLayer()
if #available(iOS 26.0, *) {
if let mainEffectLayer = self.mainEffectLayer, let variableBlur = CALayer.variableBlur() {
let imageSubview = UIImageView()
self.imageSubview = imageSubview
imageSubview.layer.name = "mask_source"
if let variableBlur = CALayer.variableBlur() {
variableBlur.setValue(self.maxBlurRadius, forKey: "inputRadius")
variableBlur.setValue("mask_source", forKey: "inputSourceSublayerName")
variableBlur.setValue(true, forKey: "inputNormalizeEdges")
mainEffectLayer.filters = [variableBlur]
/*#if DEBUG
if let classValue = NSClassFromString("CAFilter") as AnyObject as? NSObjectProtocol {
let makeSelector = NSSelectorFromString("filterWithName:")
let filter = classValue.perform(makeSelector, with: "colorMatrix").takeUnretainedValue() as? NSObject
if let filter {
/// Builds a 4x5 (RGBA) color matrix (20 floats, row-major).
/// Applies: Desaturate(s) then TintToward(t, k) in linear RGB.
/// - s: saturation in [0,1] (1 = unchanged, 0 = grayscale)
/// - k: tint strength in [0,1] (0 = none, 1 = fully "brightness->tint" mapping)
/// - t: tint color (linear RGB). Doesn't have to be normalized, but should be non-negative.
/// - bias: optional constant "cast" added in the bias column, scaled by tint color.
let makeDesatTintMatrix: (Float32, Float32, (Float32, Float32, Float32), Float32) -> [Float32] = { s, k, t, bias in
// Rec.709 luma weights (good default for linear RGB)
let wr: Float32 = 0.2126
let wg: Float32 = 0.7152
let wb: Float32 = 0.0722
let (tr, tg, tb) = t
// --- D = desaturation matrix ---
let a = 1 - s
let D00 = wr * a + s, D01 = wg * a, D02 = wb * a
let D10 = wr * a, D11 = wg * a + s, D12 = wb * a
let D20 = wr * a, D21 = wg * a, D22 = wb * a + s
// --- T = outer(t, w): maps luma to tinted RGB (preserves luma per weights) ---
let T00 = tr * wr, T01 = tr * wg, T02 = tr * wb
let T10 = tg * wr, T11 = tg * wg, T12 = tg * wb
let T20 = tb * wr, T21 = tb * wg, T22 = tb * wb
// --- A = (1-k)I + kT ---
let ik = 1 - k
let A00 = ik + k * T00, A01 = k * T01, A02 = k * T02
let A10 = k * T10, A11 = ik + k * T11, A12 = k * T12
let A20 = k * T20, A21 = k * T21, A22 = ik + k * T22
// --- M = A * D (3x3 multiply) ---
let M00 = A00 * D00 + A01 * D10 + A02 * D20
let M01 = A00 * D01 + A01 * D11 + A02 * D21
let M02 = A00 * D02 + A01 * D12 + A02 * D22
let M10 = A10 * D00 + A11 * D10 + A12 * D20
let M11 = A10 * D01 + A11 * D11 + A12 * D21
let M12 = A10 * D02 + A11 * D12 + A12 * D22
let M20 = A20 * D00 + A21 * D10 + A22 * D20
let M21 = A20 * D01 + A21 * D11 + A22 * D21
let M22 = A20 * D02 + A21 * D12 + A22 * D22
// Optional constant cast in bias column
let bR = bias * tr
let bG = bias * tg
let bB = bias * tb
// 4x5 row-major, alpha passthrough:
return [
M00, M01, M02, 0, bR,
M10, M11, M12, 0, bG,
M20, M21, M22, 0, bB,
0, 0, 0, 1, 0
]
}
var matrix: [Float32] = makeDesatTintMatrix(
0.1, // more desaturated
0.1, // moderate tinting
(0.0, 1.0, 0.0), // warm tint target
0.0 // set e.g. 0.03 for a gentle warm cast
)
filter.setValue(NSValue(bytes: &matrix, objCType: "{CAColorMatrix=ffffffffffffffffffff}"), forKey: "inputColorMatrix")
mainEffectLayer.filters = [variableBlur, filter]
}
if isTransparent {
variableBlur.setValue(true, forKey: "inputNormalizeEdgesTransparent")
} else {
variableBlur.setValue(true, forKey: "inputNormalizeEdges")
}
#endif*/
self.layer.filters = [variableBlur]
}
self.layer.addSublayer(imageSubview.layer)
} else {
self.additionalEffectLayer = createBackdropLayer()
self.imageSubview = nil
}
self.imageSubview = UIImageView()
self.imageSubview.layer.name = "mask_source"
super.init(frame: CGRect())
let groupName = "group-\(UInt32.random(in: 0 ... UInt32.max))"
if let mainEffectLayer = self.mainEffectLayer {
mainEffectLayer.delegate = self.effectLayerDelegate
mainEffectLayer.setValue(0.5, forKey: "scale")
mainEffectLayer.setValue(groupName, forKey: "groupName")
self.layer.addSublayer(mainEffectLayer)
if #available(iOS 26.0, *) {
mainEffectLayer.addSublayer(self.imageSubview.layer)
}
}
if let additionalEffectLayer = self.additionalEffectLayer {
self.layer.addSublayer(additionalEffectLayer)
additionalEffectLayer.delegate = self.effectLayerDelegate
additionalEffectLayer.setValue(0.5, forKey: "scale")
if let blurFilter = CALayer.blur() {
blurFilter.setValue(true, forKey: "inputNormalizeEdges")
blurFilter.setValue((maxBlurRadius * 2.0) as NSNumber, forKey: "inputRadius")
additionalEffectLayer.filters = [blurFilter]
}
}
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateLegacyEffect() {
guard let params = self.params else {
return
}
guard let mainEffectLayer = self.mainEffectLayer else {
return
}
guard let variableBlur = CALayer.variableBlur() else {
return
}
@ -735,7 +646,12 @@ public final class VariableBlurView: UIView {
return
}
variableBlur.setValue(self.maxBlurRadius, forKey: "inputRadius")
variableBlur.setValue(true, forKey: "inputNormalizeEdges")
if self.isTransparent {
variableBlur.setValue(true, forKey: "inputNormalizeEdgesTransparent")
} else {
variableBlur.setValue(true, forKey: "inputNormalizeEdges")
}
let image: UIImage? = generateImage(CGSize(width: 1.0, height: min(800.0, params.size.height)), rotatedContext: { size, context in
UIGraphicsPushContext(context)
@ -748,7 +664,10 @@ public final class VariableBlurView: UIView {
let mainEffectFrame: CGRect
let additionalEffectFrame: CGRect
if params.isInverted {
if params.placement.extendsInwards {
mainEffectFrame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))
additionalEffectFrame = CGRect()
} else if params.placement.position == .bottom {
mainEffectFrame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: params.constantHeight))
additionalEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: params.constantHeight), size: CGSize(width: size.width, height: max(0.0, size.height - params.constantHeight)))
} else {
@ -766,11 +685,11 @@ public final class VariableBlurView: UIView {
variableBlur.setValue(cgImage, forKey: "inputMaskImage")
}
mainEffectLayer.filters = [variableBlur]
self.layer.filters = [variableBlur]
}
public func update(size: CGSize, constantHeight: CGFloat, isInverted: Bool, gradient: Gradient, transition: ContainedViewLayoutTransition) {
let params = Params(size: size, constantHeight: constantHeight, isInverted: isInverted, gradient: gradient)
public func update(size: CGSize, constantHeight: CGFloat, placement: Placement, gradient: Gradient, transition: ContainedViewLayoutTransition) {
let params = Params(size: size, constantHeight: constantHeight, placement: placement, gradient: gradient)
if params == self.params {
return
}
@ -779,30 +698,83 @@ public final class VariableBlurView: UIView {
let isHeightUpdated = gradient.height != self.params?.gradient.height || size.height != self.params?.size.height
if isGradientUpdated {
self.gradientImage = EdgeEffectView.generateEdgeGradient(baseHeight: max(1.0, params.gradient.height), isInverted: params.isInverted)
if params.placement.extendsInwards {
let baseHeight = max(1.0, params.gradient.height)
let resizingInverted = params.placement.position != .bottom
self.gradientImage = generateImage(CGSize(width: 1.0, height: baseHeight), opaque: false, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let gradientColors = [UIColor.white.withAlphaComponent(1.0).cgColor, UIColor.white.withAlphaComponent(0.0).cgColor] as CFArray
var locations: [CGFloat] = [0.0, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
if params.placement.position == .bottom {
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: size.height), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions())
} else {
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
}
})?.resizableImage(withCapInsets: UIEdgeInsets(top: resizingInverted ? baseHeight : 0.0, left: 0.0, bottom: resizingInverted ? 0.0 : baseHeight, right: 0.0), resizingMode: .stretch)
} else {
self.gradientImage = EdgeEffectView.generateEdgeGradient(baseHeight: max(1.0, params.gradient.height), isInverted: params.placement.position == .bottom, extendsInwards: params.placement.extendsInwards)
}
}
self.params = params
guard let mainEffectLayer = self.mainEffectLayer else {
return
}
let transition = ComponentTransition(transition)
if #available(iOS 26.0, *) {
if let imageSubview = self.imageSubview {
if isGradientUpdated {
self.imageSubview.image = self.gradientImage
imageSubview.image = self.gradientImage
}
transition.setFrame(layer: mainEffectLayer, frame: CGRect(origin: CGPoint(), size: size))
transition.setFrame(view: self.imageSubview, frame: CGRect(origin: CGPoint(), size: size))
transition.setFrame(layer: self.layer, frame: CGRect(origin: CGPoint(), size: size))
transition.setFrame(view: imageSubview, frame: CGRect(origin: CGPoint(), size: size))
} else {
if isHeightUpdated || isGradientUpdated {
self.updateLegacyEffect()
transition.setFrame(layer: mainEffectLayer, frame: CGRect(origin: CGPoint(), size: size))
transition.setFrame(layer: self.layer, frame: CGRect(origin: CGPoint(), size: size))
} else {
transition.setFrame(layer: mainEffectLayer, frame: CGRect(origin: CGPoint(), size: size))
transition.setFrame(layer: self.layer, frame: CGRect(origin: CGPoint(), size: size))
}
}
}
}
public final class VariableBlurView: UIView {
public let maxBlurRadius: CGFloat
private var effect: VariableBlurEffect?
private let effectLayerDelegate: SimpleLayerDelegate
private var mainEffectLayer: CALayer?
public init(maxBlurRadius: CGFloat = 20.0) {
self.maxBlurRadius = maxBlurRadius
self.effectLayerDelegate = SimpleLayerDelegate()
self.mainEffectLayer = createBackdropLayer()
if let mainEffectLayer = self.mainEffectLayer {
self.effect = VariableBlurEffect(layer: mainEffectLayer, maxBlurRadius: maxBlurRadius)
}
super.init(frame: CGRect())
if let mainEffectLayer = self.mainEffectLayer {
mainEffectLayer.delegate = self.effectLayerDelegate
mainEffectLayer.setValue(0.5, forKey: "scale")
self.layer.addSublayer(mainEffectLayer)
}
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(size: CGSize, constantHeight: CGFloat, isInverted: Bool, gradient: VariableBlurEffect.Gradient, transition: ContainedViewLayoutTransition) {
self.effect?.update(size: size, constantHeight: constantHeight, placement: VariableBlurEffect.Placement(position: isInverted ? .bottom : .top, extendsInwards: false), gradient: gradient, transition: transition)
}
}

View file

@ -653,6 +653,7 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar {
if case .glass = presentationData.theme.style {
let edgeEffectView = EdgeEffectView()
edgeEffectView.isUserInteractionEnabled = false
self.edgeEffectView = edgeEffectView
self.view.addSubview(edgeEffectView)

View file

@ -1638,6 +1638,10 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
dismissedFloatingTopicsPanel = floatingTopicsPanel
}
if floatingTopicsPanelInsets.top != 0.0 {
floatingTopicsPanelInsets.top += 8.0
}
var isSidebarOpen = false
if let floatingTopicsPanel = self.floatingTopicsPanel {
isSidebarOpen = floatingTopicsPanel.component.location == .side
@ -2330,7 +2334,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
overlayNavigationBar.updateLayout(size: barFrame.size, transition: transition)
}
var listInsets = UIEdgeInsets(top: containerInsets.bottom + contentBottomInset, left: containerInsets.right, bottom: containerInsets.top + 6.0, right: containerInsets.left)
var listInsets = UIEdgeInsets(top: containerInsets.bottom + contentBottomInset, left: containerInsets.right, bottom: containerInsets.top, right: containerInsets.left)
let listScrollIndicatorInsets = UIEdgeInsets(top: containerInsets.bottom + inputPanelsHeight, left: containerInsets.right, bottom: containerInsets.top, right: containerInsets.left)
var childContentInsets: UIEdgeInsets = containerInsets

View file

@ -3560,6 +3560,15 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus
if self.item.action == nil {
return false
}
if self.item.isEdit {
return false
}
if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser {
if let stats = self.currentStats, stats.peers.isEmpty {
} else {
return false
}
}
var reactionCount = 0
for reaction in mergedMessageReactionsAndPeers(accountPeerId: self.item.context.account.peerId, accountPeer: nil, message: self.item.message).reactions {
reactionCount += Int(reaction.count)