[WIP] Release changes

This commit is contained in:
Isaac
2024-01-26 15:33:01 +01:00
parent ce83d7510f
commit 953e1598f7
93 changed files with 4096 additions and 556 deletions

View File

@@ -19,6 +19,9 @@ swift_library(
"//submodules/AppBundle",
"//submodules/ChatPresentationInterfaceState",
"//submodules/TelegramUI/Components/Chat/ChatInputPanelNode",
"//submodules/TelegramUI/Components/EntityKeyboard",
"//submodules/TelegramUI/Components/Chat/TopMessageReactions",
"//submodules/ReactionSelectionNode",
],
visibility = [
"//visibility:public",

View File

@@ -10,14 +10,68 @@ import AccountContext
import AppBundle
import ChatPresentationInterfaceState
import ChatInputPanelNode
import ReactionSelectionNode
import EntityKeyboard
import TopMessageReactions
private final class ChatMessageSelectionInputPanelNodeViewForOverlayContent: UIView, ChatInputPanelViewForOverlayContent {
var reactionContextNode: ReactionContextNode?
var anchorRect: CGRect?
override init(frame: CGRect) {
super.init(frame: frame)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.backgroundTapGesture(_:))))
}
required init(coder: NSCoder) {
preconditionFailure()
}
@objc private func backgroundTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.dismissReactionSelection()
}
}
func dismissReactionSelection() {
if let reactionContextNode = self.reactionContextNode {
self.reactionContextNode = nil
reactionContextNode.animateOut(to: self.anchorRect, animatingOutToReaction: false)
ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut).updateAlpha(node: reactionContextNode, alpha: 0.0, completion: { [weak reactionContextNode] _ in
reactionContextNode?.removeFromSupernode()
})
}
}
func maybeDismissContent(point: CGPoint) {
if self.hitTest(point, with: nil) == self {
self.dismissReactionSelection()
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let reactionContextNode = self.reactionContextNode {
if let result = reactionContextNode.view.hitTest(self.convert(point, to: reactionContextNode.view), with: event) {
return result
}
return self
}
return nil
}
}
public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
private let deleteButton: HighlightableButtonNode
private let reportButton: HighlightableButtonNode
private let forwardButton: HighlightableButtonNode
private let shareButton: HighlightableButtonNode
private let tagButton: HighlightableButtonNode
private let tagEditButton: HighlightableButtonNode
private let separatorNode: ASDisplayNode
private let reactionOverlayContainer: ChatMessageSelectionInputPanelNodeViewForOverlayContent
private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, metrics: LayoutMetrics, isSecondary: Bool, isMediaInputExpanded: Bool)?
private var presentationInterfaceState: ChatPresentationInterfaceState?
private var actions: ChatAvailableMessageActions?
@@ -30,25 +84,7 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
public var selectedMessages = Set<MessageId>() {
didSet {
if oldValue != self.selectedMessages {
self.forwardButton.isEnabled = self.selectedMessages.count != 0
if self.selectedMessages.isEmpty {
self.actions = nil
if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = self.validLayout, let interfaceState = self.presentationInterfaceState {
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
}
self.canDeleteMessagesDisposable.set(nil)
} else if let context = self.context {
self.canDeleteMessagesDisposable.set((context.sharedContext.chatAvailableMessageActions(engine: context.engine, accountPeerId: context.account.peerId, messageIds: self.selectedMessages)
|> deliverOnMainQueue).startStrict(next: { [weak self] actions in
if let strongSelf = self {
strongSelf.actions = actions
if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = strongSelf.validLayout, let interfaceState = strongSelf.presentationInterfaceState {
let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
}
}
}))
}
self.updateActions()
}
}
}
@@ -75,6 +111,16 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
self.shareButton.isAccessibilityElement = true
self.shareButton.accessibilityLabel = strings.VoiceOver_MessageContextShare
self.tagButton = HighlightableButtonNode(pointerStyle: .rectangle(CGSize(width: 56.0, height: 40.0)))
self.tagButton.isAccessibilityElement = true
//TODO:localize
self.tagButton.accessibilityLabel = "Tag"
self.tagEditButton = HighlightableButtonNode(pointerStyle: .rectangle(CGSize(width: 56.0, height: 40.0)))
self.tagEditButton.isAccessibilityElement = true
//TODO:localize
self.tagEditButton.accessibilityLabel = "Edit Tag"
self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
self.reportButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionReport"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
@@ -83,18 +129,26 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
self.tagButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TagIcon"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.tagEditButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TagEditIcon"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor
self.reactionOverlayContainer = ChatMessageSelectionInputPanelNodeViewForOverlayContent()
super.init()
self.addSubnode(self.deleteButton)
self.addSubnode(self.reportButton)
self.addSubnode(self.forwardButton)
self.addSubnode(self.shareButton)
self.addSubnode(self.tagButton)
self.addSubnode(self.tagEditButton)
self.addSubnode(self.separatorNode)
self.viewForOverlayContent = self.reactionOverlayContainer
self.forwardButton.isImplicitlyDisabled = true
self.shareButton.isImplicitlyDisabled = true
@@ -102,12 +156,36 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
self.reportButton.addTarget(self, action: #selector(self.reportButtonPressed), forControlEvents: .touchUpInside)
self.forwardButton.addTarget(self, action: #selector(self.forwardButtonPressed), forControlEvents: .touchUpInside)
self.shareButton.addTarget(self, action: #selector(self.shareButtonPressed), forControlEvents: .touchUpInside)
self.tagButton.addTarget(self, action: #selector(self.tagButtonPressed), forControlEvents: .touchUpInside)
self.tagEditButton.addTarget(self, action: #selector(self.tagButtonPressed), forControlEvents: .touchUpInside)
}
deinit {
self.canDeleteMessagesDisposable.dispose()
}
private func updateActions() {
self.forwardButton.isEnabled = self.selectedMessages.count != 0
if self.selectedMessages.isEmpty {
self.actions = nil
if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = self.validLayout, let interfaceState = self.presentationInterfaceState {
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
}
self.canDeleteMessagesDisposable.set(nil)
} else if let context = self.context {
self.canDeleteMessagesDisposable.set((context.sharedContext.chatAvailableMessageActions(engine: context.engine, accountPeerId: context.account.peerId, messageIds: self.selectedMessages, keepUpdated: true)
|> deliverOnMainQueue).startStrict(next: { [weak self] actions in
if let strongSelf = self {
strongSelf.actions = actions
if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = strongSelf.validLayout, let interfaceState = strongSelf.presentationInterfaceState {
let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
}
}
}))
}
}
public func updateTheme(theme: PresentationTheme) {
if self.theme !== theme {
self.theme = theme
@@ -120,6 +198,8 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
self.tagButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/WebpageIcon"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.tagEditButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/LinkSettingsIcon"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.separatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor
}
@@ -155,6 +235,120 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
}
}
@objc private func tagButtonPressed() {
guard let context = self.context else {
return
}
if self.reactionOverlayContainer.reactionContextNode != nil {
return
}
let reactionItems: Signal<[ReactionItem], NoError> = tagMessageReactions(context: context)
let _ = (reactionItems
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] reactionItems in
guard let self, let actions = self.actions, let context = self.context else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
let reactionContextNode = ReactionContextNode(
context: context,
animationCache: context.animationCache,
presentationData: presentationData,
items: reactionItems.map(ReactionContextItem.reaction),
selectedItems: actions.editTags,
title: actions.editTags.isEmpty ? "Tag a message with emojis for quick search" : "Edit tags of selected messages",
reactionsLocked: false,
alwaysAllowPremiumReactions: false,
allPresetReactionsAreAvailable: true,
getEmojiContent: { animationCache, animationRenderer in
let mappedReactionItems: [EmojiComponentReactionItem] = reactionItems.map { reaction -> EmojiComponentReactionItem in
return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation)
}
return EmojiPagerContentComponent.emojiInputData(
context: context,
animationCache: animationCache,
animationRenderer: animationRenderer,
isStandalone: false,
subject: .messageTag,
hasTrending: false,
topReactionItems: mappedReactionItems,
areUnicodeEmojiEnabled: false,
areCustomEmojiEnabled: true,
chatPeerId: context.account.peerId,
selectedItems: Set(),
premiumIfSavedMessages: false
)
},
isExpandedUpdated: { [weak self] transition in
guard let self else {
return
}
self.update(transition: transition)
},
requestLayout: { [weak self] transition in
guard let self else {
return
}
self.update(transition: transition)
},
requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in
guard let self else {
return
}
self.update(transition: transition)
}
)
reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in
guard let self, let context = self.context, let presentationInterfaceState = self.presentationInterfaceState, let actions = self.actions else {
return
}
var reactions = actions.editTags
if reactions.contains(updateReaction.reaction) {
reactions.remove(updateReaction.reaction)
} else {
reactions.insert(updateReaction.reaction)
}
let mappedUpdatedReactions = reactions.map { reaction -> UpdateMessageReaction in
switch reaction {
case let .builtin(value):
return .builtin(value)
case let .custom(fileId):
return .custom(fileId: fileId, file: nil)
}
}
if let selectionState = presentationInterfaceState.interfaceState.selectionState {
for id in selectionState.selectedIds {
context.engine.messages.setMessageReactions(id: id, reactions: mappedUpdatedReactions)
}
}
self.reactionOverlayContainer.dismissReactionSelection()
}
reactionContextNode.displayTail = true
reactionContextNode.forceTailToRight = true
reactionContextNode.forceDark = false
self.reactionOverlayContainer.reactionContextNode = reactionContextNode
self.reactionOverlayContainer.addSubnode(reactionContextNode)
self.update(transition: .immediate)
})
}
private func update(transition: ContainedViewLayoutTransition) {
if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = self.validLayout, let interfaceState = self.presentationInterfaceState {
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
}
}
override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded)
@@ -182,6 +376,19 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
self.deleteButton.isHidden = false
}
self.reportButton.isHidden = !self.reportButton.isEnabled
if actions.setTag {
if !actions.editTags.isEmpty {
self.tagButton.isHidden = true
self.tagEditButton.isHidden = false
} else {
self.tagButton.isHidden = false
self.tagEditButton.isHidden = true
}
} else {
self.tagButton.isHidden = true
self.tagEditButton.isHidden = true
}
} else {
self.deleteButton.isEnabled = false
self.deleteButton.isHidden = self.peerMedia
@@ -189,6 +396,10 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
self.reportButton.isHidden = true
self.forwardButton.isImplicitlyDisabled = true
self.shareButton.isImplicitlyDisabled = true
self.tagButton.isHidden = true
self.tagEditButton.isHidden = true
self.tagButton.isHidden = true
self.tagEditButton.isHidden = true
}
if self.reportButton.isHidden || (self.peerMedia && self.deleteButton.isHidden && self.reportButton.isHidden) {
@@ -204,41 +415,96 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
width -= additionalSideInsets.right
}
var tagButton: HighlightableButtonNode?
if !self.tagButton.isHidden {
tagButton = self.tagButton
} else if !self.tagEditButton.isHidden {
tagButton = self.tagEditButton
}
let buttons: [HighlightableButtonNode]
if self.reportButton.isHidden {
self.deleteButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 57.0, height: panelHeight))
self.forwardButton.frame = CGRect(origin: CGPoint(x: width - rightInset - 57.0, y: 0.0), size: CGSize(width: 57.0, height: panelHeight))
self.shareButton.frame = CGRect(origin: CGPoint(x: floor((width - rightInset - 57.0) / 2.0), y: 0.0), size: CGSize(width: 57.0, height: panelHeight))
if let tagButton {
buttons = [
self.deleteButton,
self.forwardButton,
tagButton,
self.shareButton
]
} else {
buttons = [
self.deleteButton,
self.forwardButton,
self.shareButton
]
}
} else if !self.deleteButton.isHidden {
let buttons: [HighlightableButtonNode] = [
self.deleteButton,
self.reportButton,
self.shareButton,
self.forwardButton
]
let buttonSize = CGSize(width: 57.0, height: panelHeight)
let availableWidth = width - leftInset - rightInset
let spacing: CGFloat = floor((availableWidth - buttonSize.width * CGFloat(buttons.count)) / CGFloat(buttons.count - 1))
var offset: CGFloat = leftInset
for i in 0 ..< buttons.count {
let button = buttons[i]
if i == buttons.count - 1 {
button.frame = CGRect(origin: CGPoint(x: width - rightInset - buttonSize.width, y: 0.0), size: buttonSize)
} else {
button.frame = CGRect(origin: CGPoint(x: offset, y: 0.0), size: buttonSize)
}
offset += buttonSize.width + spacing
if let tagButton {
buttons = [
self.deleteButton,
self.reportButton,
tagButton,
self.shareButton,
self.forwardButton
]
} else {
buttons = [
self.deleteButton,
self.reportButton,
self.shareButton,
self.forwardButton
]
}
} else {
self.deleteButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 53.0, height: panelHeight))
self.forwardButton.frame = CGRect(origin: CGPoint(x: width - rightInset - 57.0, y: 0.0), size: CGSize(width: 57.0, height: panelHeight))
self.reportButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 53.0, height: 47.0))
self.shareButton.frame = CGRect(origin: CGPoint(x: floor((width - rightInset - 57.0) / 2.0), y: 0.0), size: CGSize(width: 57.0, height: panelHeight))
if let tagButton {
buttons = [
self.deleteButton,
self.forwardButton,
self.reportButton,
tagButton,
self.shareButton
]
} else {
buttons = [
self.deleteButton,
self.forwardButton,
self.reportButton,
self.shareButton
]
}
}
let buttonSize = CGSize(width: 57.0, height: panelHeight)
let availableWidth = width - leftInset - rightInset
let spacing: CGFloat = floor((availableWidth - buttonSize.width * CGFloat(buttons.count)) / CGFloat(buttons.count - 1))
var offset: CGFloat = leftInset
for i in 0 ..< buttons.count {
let button = buttons[i]
if i == buttons.count - 1 {
button.frame = CGRect(origin: CGPoint(x: width - rightInset - buttonSize.width, y: 0.0), size: buttonSize)
} else {
button.frame = CGRect(origin: CGPoint(x: offset, y: 0.0), size: buttonSize)
}
offset += buttonSize.width + spacing
}
transition.updateAlpha(node: self.separatorNode, alpha: isSecondary ? 1.0 : 0.0)
self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: panelHeight), size: CGSize(width: width, height: UIScreenPixel))
if let reactionContextNode = self.reactionOverlayContainer.reactionContextNode, let tagButton {
let isFirstTime = reactionContextNode.bounds.isEmpty
let size = CGSize(width: width, height: maxHeight)
let reactionsAnchorRect = tagButton.frame.offsetBy(dx: -54.0, dy: -(panelHeight - size.height) + 14.0)
transition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - size.height), size: size))
reactionContextNode.updateLayout(size: size, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: transition)
reactionContextNode.updateIsIntersectingContent(isIntersectingContent: true, transition: .immediate)
if isFirstTime {
reactionContextNode.animateIn(from: reactionsAnchorRect)
}
}
return panelHeight
}