Reaction improvements

This commit is contained in:
Ali
2021-12-07 23:43:57 +04:00
parent 2397f3c5b1
commit c58b8c33c4
34 changed files with 1510 additions and 568 deletions

View File

@@ -9,6 +9,7 @@ import TelegramPresentationData
import AccountContext
import AppBundle
import ReactionButtonListComponent
import WebPBinding
private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) {
if let _ = layer.animation(forKey: "clockFrameAnimation") {
@@ -45,59 +46,91 @@ private let reactionCountFont = Font.semibold(11.0)
private let reactionFont = Font.regular(12.0)
private final class StatusReactionNode: ASDisplayNode {
let selectedImageNode: ASImageNode
let iconView: UIImageView
private let iconImageDisposable = MetaDisposable()
private var theme: PresentationTheme?
private var value: String?
private var isSelected: Bool?
override init() {
self.selectedImageNode = ASImageNode()
self.selectedImageNode.displaysAsynchronously = false
self.iconView = UIImageView()
super.init()
self.addSubnode(self.selectedImageNode)
self.view.addSubview(self.iconView)
}
func update(type: ChatMessageDateAndStatusType, value: String, isSelected: Bool, count: Int, theme: PresentationTheme, wallpaper: TelegramWallpaper, animated: Bool) {
deinit {
self.iconImageDisposable.dispose()
}
func update(context: AccountContext, type: ChatMessageDateAndStatusType, value: String, file: TelegramMediaFile?, isSelected: Bool, count: Int, theme: PresentationTheme, wallpaper: TelegramWallpaper, animated: Bool) {
if self.value != value {
self.value = value
let selectedImage: UIImage? = generateImage(CGSize(width: 14.0, height: 14.0), rotatedContext: { size, context in
UIGraphicsPushContext(context)
context.clear(CGRect(origin: CGPoint(), size: size))
context.scaleBy(x: size.width / 20.0, y: size.width / 20.0)
let string = NSAttributedString(string: value, font: reactionFont, textColor: .black)
string.draw(at: CGPoint(x: 1.0, y: 2.0))
UIGraphicsPopContext()
})
if let selectedImage = selectedImage {
self.selectedImageNode.image = selectedImage
self.selectedImageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: selectedImage.size)
let defaultImageSize = CGSize(width: 19.0, height: 19.0)
let imageSize: CGSize
if let file = file {
self.iconImageDisposable.set((context.account.postbox.mediaBox.resourceData(file.resource)
|> deliverOnMainQueue).start(next: { [weak self] data in
guard let strongSelf = self else {
return
}
if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
if let image = WebP.convert(fromWebP: dataValue) {
strongSelf.iconView.image = image
}
}
}))
imageSize = file.dimensions?.cgSize.aspectFitted(defaultImageSize) ?? defaultImageSize
} else {
imageSize = defaultImageSize
}
self.iconView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((defaultImageSize.width - imageSize.width) / 2.0), y: floorToScreenPixels((defaultImageSize.height - imageSize.height) / 2.0)), size: imageSize)
}
}
}
class ChatMessageDateAndStatusNode: ASDisplayNode {
struct ReactionSettings {
struct TrailingReactionSettings {
var displayInline: Bool
var preferAdditionalInset: Bool
init(preferAdditionalInset: Bool) {
init(displayInline: Bool, preferAdditionalInset: Bool) {
self.displayInline = displayInline
self.preferAdditionalInset = preferAdditionalInset
}
}
struct StandaloneReactionSettings {
init() {
}
}
enum LayoutInput {
case trailingContent(contentWidth: CGFloat, reactionSettings: ReactionSettings?)
case standalone
case trailingContent(contentWidth: CGFloat, reactionSettings: TrailingReactionSettings?)
case standalone(reactionSettings: StandaloneReactionSettings?)
var displayInlineReactions: Bool {
switch self {
case let .trailingContent(_, reactionSettings):
if let reactionSettings = reactionSettings {
return reactionSettings.displayInline
} else {
return false
}
case let .standalone(reactionSettings):
if let _ = reactionSettings {
return true
} else {
return false
}
}
}
}
struct Arguments {
@@ -154,7 +187,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
private var clockMinNode: ASImageNode?
private let dateNode: TextNode
private var impressionIcon: ASImageNode?
private var reactionNodes: [StatusReactionNode] = []
private var reactionNodes: [String: StatusReactionNode] = [:]
private let reactionButtonsContainer = ReactionButtonsLayoutContainer()
private var reactionCountNode: TextNode?
private var reactionButtonNode: HighlightTrackingButtonNode?
@@ -247,22 +280,24 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
switch arguments.type {
case .BubbleIncoming, .ImageIncoming, .FreeIncoming:
reactionColors = ReactionButtonComponent.Colors(
background: arguments.presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb,
foreground: arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor.argb,
stroke: arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor.argb
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
)
case .BubbleOutgoing, .ImageOutgoing, .FreeOutgoing:
reactionColors = ReactionButtonComponent.Colors(
background: arguments.presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(0.1).argb,
foreground: arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb,
stroke: arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb
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
)
}
switch arguments.type {
case .BubbleIncoming:
dateColor = arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor
leftInset = 10.0
leftInset = 5.0
loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(arguments.presentationData.theme.theme, size: checkSize)
loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(arguments.presentationData.theme.theme, size: checkSize)
clockFrameImage = graphics.clockBubbleIncomingFrameImage
@@ -278,7 +313,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
case let .BubbleOutgoing(status):
dateColor = arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
outgoingStatus = status
leftInset = 10.0
leftInset = 5.0
loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(arguments.presentationData.theme.theme, size: checkSize)
loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(arguments.presentationData.theme.theme, size: checkSize)
clockFrameImage = graphics.clockBubbleOutgoingFrameImage
@@ -524,13 +559,13 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
var replyCountLayoutAndApply: (TextNodeLayout, () -> TextNode)?
let reactionSize: CGFloat = 14.0
let reactionSize: CGFloat = 19.0
var reactionCountLayoutAndApply: (TextNodeLayout, () -> TextNode)?
let reactionSpacing: CGFloat = -4.0
let reactionTrailingSpacing: CGFloat = 4.0
let reactionSpacing: CGFloat = 2.0
let reactionTrailingSpacing: CGFloat = 6.0
var reactionInset: CGFloat = 0.0
if !"".isEmpty && !arguments.reactions.isEmpty {
if arguments.layoutInput.displayInlineReactions, !arguments.reactions.isEmpty {
reactionInset = -1.0 + CGFloat(arguments.reactions.count) * reactionSize + CGFloat(arguments.reactions.count - 1) * reactionSpacing + reactionTrailingSpacing
var count = 0
@@ -547,9 +582,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
countString = "\(count)"
}
let layoutAndApply = makeReactionCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
reactionInset += max(10.0, layoutAndApply.0.size.width) + 2.0
reactionCountLayoutAndApply = layoutAndApply
if count > arguments.reactions.count {
let layoutAndApply = makeReactionCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
reactionInset += layoutAndApply.0.size.width + 4.0
reactionCountLayoutAndApply = layoutAndApply
}
}
if arguments.replyCount > 0 {
@@ -599,7 +636,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
transition: .immediate
)
case let .trailingContent(contentWidth, reactionSettings):
if let _ = reactionSettings {
if let reactionSettings = reactionSettings, !reactionSettings.displayInline {
reactionButtons = reactionButtonsContainer.update(
context: arguments.context,
action: { value in
@@ -686,11 +723,13 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
resultingHeight = 0.0
}
} else {
var additionalVerticalInset: CGFloat = 0.0
if let reactionSettings = reactionSettings {
if reactionSettings.preferAdditionalInset {
verticalReactionsInset = 5.0
verticalReactionsInset = 8.0
additionalVerticalInset += 1.0
} else {
verticalReactionsInset = 2.0
verticalReactionsInset = 3.0
}
} else {
verticalReactionsInset = 0.0
@@ -698,12 +737,12 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
if currentRowWidth + layoutSize.width > arguments.constrainedSize.width {
resultingWidth = max(layoutSize.width, reactionButtonsSize.width)
resultingHeight = verticalReactionsInset + reactionButtonsSize.height + layoutSize.height
verticalInset = verticalReactionsInset + reactionButtonsSize.height
resultingHeight = verticalReactionsInset + reactionButtonsSize.height + 1.0 + layoutSize.height
verticalInset = verticalReactionsInset + reactionButtonsSize.height + 3.0
} else {
resultingWidth = max(layoutSize.width + currentRowWidth, reactionButtonsSize.width)
verticalInset = verticalReactionsInset + reactionButtonsSize.height - layoutSize.height
resultingHeight = verticalReactionsInset + reactionButtonsSize.height
verticalInset = verticalReactionsInset + reactionButtonsSize.height - layoutSize.height + additionalVerticalInset
resultingHeight = verticalReactionsInset + reactionButtonsSize.height + 1.0
}
}
}
@@ -717,7 +756,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
strongSelf.type = arguments.type
strongSelf.layoutSize = layoutSize
var reactionButtonPosition = CGPoint(x: 0.0, y: verticalReactionsInset)
var reactionButtonPosition = CGPoint(x: -1.0, y: verticalReactionsInset)
for item in reactionButtons.items {
if reactionButtonPosition.x + item.size.width > boundingWidth {
reactionButtonPosition.x = 0.0
@@ -773,7 +812,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
if let blurredBackgroundNode = strongSelf.blurredBackgroundNode {
blurredBackgroundNode.updateColor(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1, transition: .immediate)
animation.animator.updateFrame(layer: blurredBackgroundNode.layer, frame: CGRect(origin: CGPoint(), size: layoutSize), completion: nil)
blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, transition: animation.transition)
blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, animator: animation.animator)
} else {
let blurredBackgroundNode = NavigationBackgroundNode(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1)
strongSelf.blurredBackgroundNode = blurredBackgroundNode
@@ -789,6 +828,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
let _ = dateApply()
if let currentImpressionIcon = currentImpressionIcon {
let impressionIconFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left, y: backgroundInsets.top + 1.0 + offset + verticalInset + floor((date.size.height - impressionSize.height) / 2.0)), size: impressionSize)
currentImpressionIcon.displaysAsynchronously = false
if currentImpressionIcon.image !== impressionImage {
currentImpressionIcon.image = impressionImage
@@ -796,8 +836,10 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
if currentImpressionIcon.supernode == nil {
strongSelf.impressionIcon = currentImpressionIcon
strongSelf.addSubnode(currentImpressionIcon)
currentImpressionIcon.frame = impressionIconFrame
} else {
animation.animator.updateFrame(layer: currentImpressionIcon.layer, frame: impressionIconFrame, completion: nil)
}
currentImpressionIcon.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left, y: backgroundInsets.top + 1.0 + offset + verticalInset + floor((date.size.height - impressionSize.height) / 2.0)), size: impressionSize)
} else if let impressionIcon = strongSelf.impressionIcon {
impressionIcon.removeFromSupernode()
strongSelf.impressionIcon = nil
@@ -908,38 +950,49 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
}
var reactionOffset: CGFloat = leftOffset + leftInset - reactionInset + backgroundInsets.left
if !"".isEmpty {
for i in 0 ..< arguments.reactions.count {
if arguments.layoutInput.displayInlineReactions {
var validReactions = Set<String>()
for reaction in arguments.reactions.sorted(by: { lhs, rhs in
if lhs.isSelected != rhs.isSelected {
if lhs.isSelected {
return true
} else {
return false
}
} else {
return lhs.value < rhs.value
}
}) {
let node: StatusReactionNode
var animateNode = true
if strongSelf.reactionNodes.count > i {
node = strongSelf.reactionNodes[i]
if let current = strongSelf.reactionNodes[reaction.value] {
node = current
} else {
animateNode = false
node = StatusReactionNode()
if strongSelf.reactionNodes.count > i {
let previousNode = strongSelf.reactionNodes[i]
if animation.isAnimated {
previousNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousNode] _ in
previousNode?.removeFromSupernode()
})
} else {
previousNode.removeFromSupernode()
strongSelf.reactionNodes[reaction.value] = node
}
validReactions.insert(reaction.value)
var iconFile: TelegramMediaFile?
if let availableReactions = arguments.availableReactions {
for availableReaction in availableReactions.reactions {
if availableReaction.value == reaction.value {
iconFile = availableReaction.staticIcon
break
}
strongSelf.reactionNodes[i] = node
} else {
strongSelf.reactionNodes.append(node)
}
}
node.update(type: arguments.type, value: arguments.reactions[i].value, isSelected: arguments.reactions[i].isSelected, count: Int(arguments.reactions[i].count), theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, animated: false)
node.update(context: arguments.context, type: arguments.type, value: reaction.value, file: iconFile, isSelected: reaction.isSelected, count: Int(reaction.count), theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, animated: false)
if node.supernode == nil {
strongSelf.addSubnode(node)
if animation.isAnimated {
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
let nodeFrame = CGRect(origin: CGPoint(x: reactionOffset, y: backgroundInsets.top + offset + verticalInset + 1.0), size: CGSize(width: reactionSize, height: reactionSize))
let nodeFrame = CGRect(origin: CGPoint(x: reactionOffset, y: backgroundInsets.top + offset + verticalInset - 2.0), size: CGSize(width: reactionSize, height: reactionSize))
if animateNode {
animation.animator.updateFrame(layer: node.layer, frame: nodeFrame, completion: nil)
} else {
@@ -950,18 +1003,24 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
if !arguments.reactions.isEmpty {
reactionOffset += reactionTrailingSpacing
}
for _ in arguments.reactions.count ..< strongSelf.reactionNodes.count {
let node = strongSelf.reactionNodes.removeLast()
if animation.isAnimated {
node.layer.animateScale(from: 1.0, to: 0.1, 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 {
node.removeFromSupernode()
var removeIds: [String] = []
for (id, node) in strongSelf.reactionNodes {
if !validReactions.contains(id) {
removeIds.append(id)
if animation.isAnimated {
node.layer.animateScale(from: 1.0, to: 0.1, 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 {
node.removeFromSupernode()
}
}
}
for id in removeIds {
strongSelf.reactionNodes.removeValue(forKey: id)
}
}
if let (layout, apply) = reactionCountLayoutAndApply {
@@ -974,7 +1033,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
}
let nodeFrame = CGRect(origin: CGPoint(x: reactionOffset + 1.0, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: layout.size)
let nodeFrame = CGRect(origin: CGPoint(x: reactionOffset - 4.0, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: layout.size)
animation.animator.updateFrame(layer: node.layer, frame: nodeFrame, completion: nil)
reactionOffset += 1.0 + layout.size.width + 4.0
} else if let reactionCountNode = strongSelf.reactionCountNode {
@@ -1068,6 +1127,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
}
func reactionView(value: String) -> UIView? {
for (id, node) in self.reactionNodes {
if id == value {
return node.iconView
}
}
for (_, button) in self.reactionButtonsContainer.buttons {
if let result = button.findTaggedView(tag: ReactionButtonComponent.ViewTag(value: value)) as? ReactionButtonComponent.View {
return result.iconView
@@ -1092,3 +1156,10 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
return nil
}
}
func shouldDisplayInlineDateReactions(message: Message) -> Bool {
if message.id.peerId.namespace == Namespaces.Peer.CloudUser || message.id.peerId.namespace == Namespaces.Peer.SecretChat {
return true
}
return false
}