Channel reactions

This commit is contained in:
Ali 2023-11-07 17:58:36 +04:00
parent 216b731562
commit 4f23fa6e09
13 changed files with 641 additions and 138 deletions

View File

@ -0,0 +1,60 @@
import Foundation
import UIKit
public final class VStack<ChildEnvironment: Equatable>: CombinedComponent {
public typealias EnvironmentType = ChildEnvironment
private let items: [AnyComponentWithIdentity<ChildEnvironment>]
private let spacing: CGFloat
public init(_ items: [AnyComponentWithIdentity<ChildEnvironment>], spacing: CGFloat) {
self.items = items
self.spacing = spacing
}
public static func ==(lhs: VStack<ChildEnvironment>, rhs: VStack<ChildEnvironment>) -> Bool {
if lhs.items != rhs.items {
return false
}
if lhs.spacing != rhs.spacing {
return false
}
return true
}
public static var body: Body {
let children = ChildMap(environment: ChildEnvironment.self, keyedBy: AnyHashable.self)
return { context in
let updatedChildren = context.component.items.map { item in
return children[item.id].update(
component: item.component, environment: {
context.environment[ChildEnvironment.self]
},
availableSize: context.availableSize,
transition: context.transition
)
}
var size = CGSize(width: 0.0, height: 0.0)
for child in updatedChildren {
size.height += child.size.height
size.width = max(size.width, child.size.width)
}
size.height += context.component.spacing * CGFloat(updatedChildren.count - 1)
var nextY = 0.0
for child in updatedChildren {
context.add(child
.position(child.size.centered(in: CGRect(origin: CGPoint(x: floor((size.width - child.size.width) * 0.5), y: nextY), size: child.size)).center)
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
nextY += child.size.height
nextY += context.component.spacing
}
return size
}
}
}

View File

@ -40,6 +40,9 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode", "//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode",
"//submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView", "//submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/Components/BundleIconComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -29,6 +29,8 @@ import ChatMessageInteractiveMediaNode
import WallpaperPreviewMedia import WallpaperPreviewMedia
import ChatMessageAttachedContentButtonNode import ChatMessageAttachedContentButtonNode
import MessageInlineBlockBackgroundView import MessageInlineBlockBackgroundView
import ComponentFlow
import PlainButtonComponent
public enum ChatMessageAttachedContentActionIcon { public enum ChatMessageAttachedContentActionIcon {
case instant case instant
@ -67,6 +69,9 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
private var actionButtonSeparator: SimpleLayer? private var actionButtonSeparator: SimpleLayer?
public var statusNode: ChatMessageDateAndStatusNode? public var statusNode: ChatMessageDateAndStatusNode?
private var closeButton: ComponentView<Empty>?
private var closeButtonImage: UIImage?
private var inlineMediaValue: Media? private var inlineMediaValue: Media?
//private var additionalImageBadgeNode: ChatMessageInteractiveMediaBadge? //private var additionalImageBadgeNode: ChatMessageInteractiveMediaBadge?
@ -88,6 +93,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
public var defaultContentAction: () -> ChatMessageBubbleContentTapAction = { return ChatMessageBubbleContentTapAction(content: .none) } public var defaultContentAction: () -> ChatMessageBubbleContentTapAction = { return ChatMessageBubbleContentTapAction(content: .none) }
private var tapRecognizer: UITapGestureRecognizer?
public var visibility: ListViewItemNodeVisibility = .none { public var visibility: ListViewItemNodeVisibility = .none {
didSet { didSet {
if oldValue != self.visibility { if oldValue != self.visibility {
@ -152,7 +159,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
let isPreview = presentationData.isPreview let isPreview = presentationData.isPreview
let fontSize: CGFloat let fontSize: CGFloat
if message.adAttribute != nil { if message.adAttribute != nil {
fontSize = floor(presentationData.fontSize.baseDisplaySize) fontSize = floor(presentationData.fontSize.baseDisplaySize * 15.0 / 17.0)
} else { } else {
fontSize = floor(presentationData.fontSize.baseDisplaySize * 14.0 / 17.0) fontSize = floor(presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)
} }
@ -540,7 +547,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
actionTitle, actionTitle,
mainColor, mainColor,
false, false,
message.adAttribute != nil false
) )
actionButtonMinWidthAndFinalizeLayout = (buttonWidth, continueLayout) actionButtonMinWidthAndFinalizeLayout = (buttonWidth, continueLayout)
actualWidth = max(actualWidth, buttonWidth) actualWidth = max(actualWidth, buttonWidth)
@ -606,47 +613,41 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
let maxStatusContentWidth: CGFloat = constrainedSize.width - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right let maxStatusContentWidth: CGFloat = constrainedSize.width - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right
var trailingContentWidth: CGFloat? var trailingContentWidth: CGFloat?
if let _ = message.adAttribute, let (textLayout, _) = textLayoutAndApply { if !displayLine, let (actionButtonMinWidth, _) = actionButtonMinWidthAndFinalizeLayout {
if textLayout.hasRTL { trailingContentWidth = actionButtonMinWidth
trailingContentWidth = 10000.0
} else {
trailingContentWidth = textLayout.trailingLineWidth
}
} else {
if !displayLine, let (actionButtonMinWidth, _) = actionButtonMinWidthAndFinalizeLayout {
trailingContentWidth = actionButtonMinWidth
}
} }
var statusLayoutAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))? var statusLayoutAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))?
if case let .linear(_, bottom) = position { if case let .linear(_, bottom) = position {
switch bottom { switch bottom {
case .None, .Neighbour(_, .footer, _): case .None, .Neighbour(_, .footer, _):
let statusLayoutAndContinueValue = makeStatusLayout(ChatMessageDateAndStatusNode.Arguments( if message.adAttribute == nil {
context: context, let statusLayoutAndContinueValue = makeStatusLayout(ChatMessageDateAndStatusNode.Arguments(
presentationData: presentationData, context: context,
edited: edited, presentationData: presentationData,
impressionCount: viewCount, edited: edited,
dateText: dateText, impressionCount: viewCount,
type: statusType, dateText: dateText,
layoutInput: .trailingContent( type: statusType,
contentWidth: trailingContentWidth, layoutInput: .trailingContent(
reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium, forceInline: associatedData.forceInlineReactions), preferAdditionalInset: false) contentWidth: trailingContentWidth,
), reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium, forceInline: associatedData.forceInlineReactions), preferAdditionalInset: false)
constrainedSize: CGSize(width: maxStatusContentWidth, height: CGFloat.greatestFiniteMagnitude), ),
availableReactions: associatedData.availableReactions, constrainedSize: CGSize(width: maxStatusContentWidth, height: CGFloat.greatestFiniteMagnitude),
reactions: dateReactionsAndPeers.reactions, availableReactions: associatedData.availableReactions,
reactionPeers: dateReactionsAndPeers.peers, reactions: dateReactionsAndPeers.reactions,
displayAllReactionPeers: message.id.peerId.namespace == Namespaces.Peer.CloudUser, reactionPeers: dateReactionsAndPeers.peers,
replyCount: dateReplies, displayAllReactionPeers: message.id.peerId.namespace == Namespaces.Peer.CloudUser,
isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, replyCount: dateReplies,
hasAutoremove: message.isSelfExpiring, isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread,
canViewReactionList: canViewMessageReactionList(message: message), hasAutoremove: message.isSelfExpiring,
animationCache: controllerInteraction.presentationContext.animationCache, canViewReactionList: canViewMessageReactionList(message: message),
animationRenderer: controllerInteraction.presentationContext.animationRenderer animationCache: controllerInteraction.presentationContext.animationCache,
)) animationRenderer: controllerInteraction.presentationContext.animationRenderer
statusLayoutAndContinue = statusLayoutAndContinueValue ))
actualWidth = max(actualWidth, statusLayoutAndContinueValue.0) statusLayoutAndContinue = statusLayoutAndContinueValue
actualWidth = max(actualWidth, statusLayoutAndContinueValue.0)
}
default: default:
break break
} }
@ -839,22 +840,22 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
} }
} }
if case let .linear(_, bottom) = position, let statusSizeAndApply { if case let .linear(_, bottom) = position {
switch bottom { switch bottom {
case .None, .Neighbour(_, .footer, _): case .None, .Neighbour(_, .footer, _):
let bottomStatusContentHeight = statusBackgroundSpacing + statusSizeAndApply.0.height if let statusSizeAndApply {
actualSize.height += bottomStatusContentHeight let bottomStatusContentHeight = statusBackgroundSpacing + statusSizeAndApply.0.height
backgroundInsets.bottom += bottomStatusContentHeight actualSize.height += bottomStatusContentHeight
backgroundInsets.bottom += bottomStatusContentHeight
} else {
actualSize.height += 11.0
backgroundInsets.bottom += 11.0
}
default: default:
break break
} }
} }
if let _ = message.adAttribute {
actualSize.height += 2.0
backgroundInsets.bottom += 2.0
}
return (actualSize, { animation, synchronousLoads, applyInfo in return (actualSize, { animation, synchronousLoads, applyInfo in
guard let self else { guard let self else {
return return
@ -867,9 +868,9 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
animation.animator.updateFrame(layer: self.transformContainer.layer, frame: CGRect(origin: CGPoint(), size: actualSize), completion: nil) animation.animator.updateFrame(layer: self.transformContainer.layer, frame: CGRect(origin: CGPoint(), size: actualSize), completion: nil)
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: actualSize.width - backgroundInsets.left - backgroundInsets.right, height: actualSize.height - backgroundInsets.top - backgroundInsets.bottom))
if displayLine { if displayLine {
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: actualSize.width - backgroundInsets.left - backgroundInsets.right, height: actualSize.height - backgroundInsets.top - backgroundInsets.bottom))
let backgroundView: MessageInlineBlockBackgroundView let backgroundView: MessageInlineBlockBackgroundView
if let current = self.backgroundView { if let current = self.backgroundView {
backgroundView = current backgroundView = current
@ -982,11 +983,82 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
title.textNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) title.textNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
animation.animator.updatePosition(layer: title.textNode.layer, position: titleFrame.origin, completion: nil) animation.animator.updatePosition(layer: title.textNode.layer, position: titleFrame.origin, completion: nil)
} }
if message.adAttribute != nil {
let closeButtonImage: UIImage
if let current = self.closeButtonImage {
closeButtonImage = current
} else {
closeButtonImage = generateImage(CGSize(width: 12.0, height: 12.0), rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
let color = UIColor.white
context.setAlpha(color.alpha)
context.setBlendMode(.copy)
context.setStrokeColor(UIColor.white.cgColor)
context.setLineWidth(1.0 + UIScreenPixel)
context.setLineCap(.round)
let bounds = CGRect(origin: .zero, size: size).insetBy(dx: 1.0 + UIScreenPixel, dy: 1.0 + UIScreenPixel)
context.move(to: CGPoint(x: bounds.minX, y: bounds.minY))
context.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
context.strokePath()
context.move(to: CGPoint(x: bounds.maxX, y: bounds.minY))
context.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
context.strokePath()
})!.withRenderingMode(.alwaysTemplate)
self.closeButtonImage = closeButtonImage
}
let closeButton: ComponentView<Empty>
if let current = self.closeButton {
closeButton = current
} else {
closeButton = ComponentView()
self.closeButton = closeButton
}
let closeButtonSize = closeButton.update(
transition: .immediate,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(Image(image: closeButtonImage, tintColor: mainColor)),
effectAlignment: .center,
action: { [weak controllerInteraction] in
guard let controllerInteraction else {
return
}
controllerInteraction.openNoAdsDemo()
}
)),
environment: {},
containerSize: CGSize(width: 12.0, height: 12.0)
)
let closeButtonFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - 8.0 - closeButtonSize.width, y: backgroundInsets.top + 8.0), size: closeButtonSize)
if let closeButtonView = closeButton.view {
if closeButtonView.superview == nil {
self.transformContainer.view.addSubview(closeButtonView)
}
animation.animator.updateFrame(layer: closeButtonView.layer, frame: closeButtonFrame, completion: nil)
}
} else {
if let closeButton = self.closeButton {
self.closeButton = nil
closeButton.view?.removeFromSuperview()
}
}
} else { } else {
if let title = self.title { if let title = self.title {
self.title = nil self.title = nil
title.textNode.removeFromSupernode() title.textNode.removeFromSupernode()
} }
if let closeButton = self.closeButton {
self.closeButton = nil
closeButton.view?.removeFromSuperview()
}
} }
if let item = contentDisplayOrder.first(where: { $0.item == .subtitle }), let (subtitleLayout, subtitleApply) = subtitleLayoutAndApply { if let item = contentDisplayOrder.first(where: { $0.item == .subtitle }), let (subtitleLayout, subtitleApply) = subtitleLayoutAndApply {
@ -1128,10 +1200,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
} }
if let item = contentDisplayOrder.first(where: { $0.item == .actionButton }), let (actionButtonSize, actionButtonApply) = actionButtonSizeAndApply { if let item = contentDisplayOrder.first(where: { $0.item == .actionButton }), let (actionButtonSize, actionButtonApply) = actionButtonSizeAndApply {
var actionButtonFrame = CGRect(origin: CGPoint(x: insets.left, y: item.offsetY), size: actionButtonSize) let actionButtonFrame = CGRect(origin: CGPoint(x: insets.left, y: item.offsetY), size: actionButtonSize)
if let _ = message.adAttribute, let statusSizeAndApply {
actionButtonFrame.origin.y += statusSizeAndApply.0.height
}
let actionButton = actionButtonApply(animation) let actionButton = actionButtonApply(animation)
@ -1150,25 +1219,21 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
} else { } else {
animation.animator.updateFrame(layer: actionButton.layer, frame: actionButtonFrame, completion: nil) animation.animator.updateFrame(layer: actionButton.layer, frame: actionButtonFrame, completion: nil)
} }
let separatorFrame = CGRect(origin: CGPoint(x: actionButtonFrame.minX, y: actionButtonFrame.minY - 1.0), size: CGSize(width: actionButtonFrame.width, height: UIScreenPixel))
if let _ = message.adAttribute { let actionButtonSeparator: SimpleLayer
if let current = self.actionButtonSeparator {
actionButtonSeparator = current
animation.animator.updateFrame(layer: actionButtonSeparator, frame: separatorFrame, completion: nil)
} else { } else {
let separatorFrame = CGRect(origin: CGPoint(x: actionButtonFrame.minX, y: actionButtonFrame.minY - 1.0), size: CGSize(width: actionButtonFrame.width, height: UIScreenPixel)) actionButtonSeparator = SimpleLayer()
self.actionButtonSeparator = actionButtonSeparator
let actionButtonSeparator: SimpleLayer self.layer.addSublayer(actionButtonSeparator)
if let current = self.actionButtonSeparator { actionButtonSeparator.frame = separatorFrame
actionButtonSeparator = current
animation.animator.updateFrame(layer: actionButtonSeparator, frame: separatorFrame, completion: nil)
} else {
actionButtonSeparator = SimpleLayer()
self.actionButtonSeparator = actionButtonSeparator
self.layer.addSublayer(actionButtonSeparator)
actionButtonSeparator.frame = separatorFrame
}
actionButtonSeparator.backgroundColor = mainColor.withMultipliedAlpha(0.2).cgColor
} }
actionButtonSeparator.backgroundColor = mainColor.withMultipliedAlpha(0.2).cgColor
} else { } else {
if let actionButton = self.actionButton { if let actionButton = self.actionButton {
self.actionButton = nil self.actionButton = nil
@ -1182,10 +1247,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
} }
if let statusSizeAndApply { if let statusSizeAndApply {
var statusFrame = CGRect(origin: CGPoint(x: actualSize.width - backgroundInsets.right - statusSizeAndApply.0.width, y: actualSize.height - layoutConstants.text.bubbleInsets.bottom - statusSizeAndApply.0.height), size: statusSizeAndApply.0) let statusFrame = CGRect(origin: CGPoint(x: actualSize.width - backgroundInsets.right - statusSizeAndApply.0.width, y: actualSize.height - layoutConstants.text.bubbleInsets.bottom - statusSizeAndApply.0.height), size: statusSizeAndApply.0)
if let _ = message.adAttribute, let (actionButtonSize, _) = actionButtonSizeAndApply {
statusFrame.origin.y -= actionButtonSize.height + statusBackgroundSpacing
}
let statusNode = statusSizeAndApply.1(self.statusNode == nil ? .None : animation) let statusNode = statusSizeAndApply.1(self.statusNode == nil ? .None : animation)
if self.statusNode !== statusNode { if self.statusNode !== statusNode {
@ -1216,12 +1278,36 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
self.statusNode = nil self.statusNode = nil
statusNode.removeFromSupernode() statusNode.removeFromSupernode()
} }
if message.adAttribute != nil {
if self.tapRecognizer == nil {
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
self.tapRecognizer = tapRecognizer
self.view.addGestureRecognizer(tapRecognizer)
}
} else {
if let tapRecognizer = self.tapRecognizer {
self.tapRecognizer = nil
self.view.removeGestureRecognizer(tapRecognizer)
}
}
}) })
}) })
}) })
} }
} }
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
guard let message = self.message else {
return
}
if case .ended = recognizer.state {
if message.adAttribute != nil {
self.activateAction?()
}
}
}
public func updateHiddenMedia(_ media: [Media]?) -> Bool { public func updateHiddenMedia(_ media: [Media]?) -> Bool {
if let currentMedia = self.media { if let currentMedia = self.media {
if let media = media { if let media = media {
@ -1296,9 +1382,13 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
} }
if let backgroundView = self.backgroundView, backgroundView.frame.contains(point) { if let backgroundView = self.backgroundView, backgroundView.frame.contains(point) {
return self.defaultContentAction() if let message = self.message, message.adAttribute != nil {
return ChatMessageBubbleContentTapAction(content: .none)
} else {
return self.defaultContentAction()
}
} else { } else {
return .init(content: .none) return ChatMessageBubbleContentTapAction(content: .none)
} }
} }

View File

@ -1877,7 +1877,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
var currentCredibilityIcon: EmojiStatusComponent.Content? var currentCredibilityIcon: EmojiStatusComponent.Content?
var initialDisplayHeader = true var initialDisplayHeader = true
if hidesHeaders { if hidesHeaders || item.message.adAttribute != nil {
initialDisplayHeader = false initialDisplayHeader = false
} else if let backgroundHiding, case .always = backgroundHiding { } else if let backgroundHiding, case .always = backgroundHiding {
initialDisplayHeader = false initialDisplayHeader = false

View File

@ -479,8 +479,12 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
} }
} }
} else if let adAttribute = item.message.adAttribute { } else if let adAttribute = item.message.adAttribute {
title = nil //TODO:localize
subtitle = nil //Recommended?
title = "Sponsored"
subtitle = item.message.author.flatMap {
NSAttributedString(string: EnginePeer($0).compactDisplayTitle, font: titleFont)
}
text = item.message.text text = item.message.text
for attribute in item.message.attributes { for attribute in item.message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute { if let attribute = attribute as? TextEntitiesMessageAttribute {
@ -514,7 +518,7 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
actionTitle = item.presentationData.strings.Conversation_ViewChannel actionTitle = item.presentationData.strings.Conversation_ViewChannel
} }
} }
displayLine = false displayLine = true
} }
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, subtitle, text, entities, mediaAndFlags, badge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer) let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, subtitle, text, entities, mediaAndFlags, badge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer)

View File

@ -25,6 +25,8 @@ swift_library(
"//submodules/Components/MultilineTextComponent", "//submodules/Components/MultilineTextComponent",
"//submodules/Markdown", "//submodules/Markdown",
"//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/PagerComponent", "//submodules/Components/PagerComponent",
"//submodules/PremiumUI", "//submodules/PremiumUI",
"//submodules/UndoUI", "//submodules/UndoUI",

View File

@ -14,7 +14,9 @@ final class EmojiListInputComponent: Component {
let placeholder: String let placeholder: String
let reactionItems: [EmojiComponentReactionItem] let reactionItems: [EmojiComponentReactionItem]
let isInputActive: Bool let isInputActive: Bool
let caretPosition: Int
let activateInput: () -> Void let activateInput: () -> Void
let setCaretPosition: (Int) -> Void
init( init(
context: AccountContext, context: AccountContext,
@ -22,14 +24,18 @@ final class EmojiListInputComponent: Component {
placeholder: String, placeholder: String,
reactionItems: [EmojiComponentReactionItem], reactionItems: [EmojiComponentReactionItem],
isInputActive: Bool, isInputActive: Bool,
activateInput: @escaping () -> Void caretPosition: Int,
activateInput: @escaping () -> Void,
setCaretPosition: @escaping (Int) -> Void
) { ) {
self.context = context self.context = context
self.theme = theme self.theme = theme
self.placeholder = placeholder self.placeholder = placeholder
self.reactionItems = reactionItems self.reactionItems = reactionItems
self.isInputActive = isInputActive self.isInputActive = isInputActive
self.caretPosition = caretPosition
self.activateInput = activateInput self.activateInput = activateInput
self.setCaretPosition = setCaretPosition
} }
static func ==(lhs: EmojiListInputComponent, rhs: EmojiListInputComponent) -> Bool { static func ==(lhs: EmojiListInputComponent, rhs: EmojiListInputComponent) -> Bool {
@ -48,6 +54,9 @@ final class EmojiListInputComponent: Component {
if lhs.isInputActive != rhs.isInputActive { if lhs.isInputActive != rhs.isInputActive {
return false return false
} }
if lhs.caretPosition != rhs.caretPosition {
return false
}
return true return true
} }
@ -78,19 +87,29 @@ final class EmojiListInputComponent: Component {
} }
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
guard let component = self.component else {
return
}
if case .ended = recognizer.state { if case .ended = recognizer.state {
let point = recognizer.location(in: self) let point = recognizer.location(in: self)
var tapOnItem = false var tapOnItem = false
for (_, itemLayer) in self.itemLayers { for (itemId, itemLayer) in self.itemLayers {
if itemLayer.frame.insetBy(dx: -6.0, dy: -6.0).contains(point) { if itemLayer.frame.insetBy(dx: -6.0, dy: -6.0).contains(point) {
if let itemIndex = component.reactionItems.firstIndex(where: { $0.file.fileId.id == itemId }) {
var caretPosition = point.x >= itemLayer.frame.midX ? (itemIndex + 1) : itemIndex
caretPosition = max(0, min(component.reactionItems.count, caretPosition))
component.setCaretPosition(caretPosition)
}
tapOnItem = true tapOnItem = true
break break
} }
} }
if !tapOnItem { if !tapOnItem {
self.component?.activateInput() component.setCaretPosition(component.reactionItems.count)
component.activateInput()
} }
} }
} }
@ -113,7 +132,8 @@ final class EmojiListInputComponent: Component {
let itemSpacing = floor(itemSizePlusSpacing * itemSpacingFactor) let itemSpacing = floor(itemSizePlusSpacing * itemSpacingFactor)
let sideInset = floor((availableSize.width - (itemSize * CGFloat(itemsPerRow) + itemSpacing * CGFloat(itemsPerRow - 1))) * 0.5) let sideInset = floor((availableSize.width - (itemSize * CGFloat(itemsPerRow) + itemSpacing * CGFloat(itemsPerRow - 1))) * 0.5)
let rowCount = (component.reactionItems.count + (itemsPerRow - 1)) / itemsPerRow var rowCount = (component.reactionItems.count + (itemsPerRow - 1)) / itemsPerRow
rowCount = max(1, rowCount)
self.component = component self.component = component
self.state = state self.state = state
@ -126,7 +146,7 @@ final class EmojiListInputComponent: Component {
) )
var lastRowItemCount = component.reactionItems.count % itemsPerRow var lastRowItemCount = component.reactionItems.count % itemsPerRow
if lastRowItemCount == 0 { if lastRowItemCount == 0 && !component.reactionItems.isEmpty {
lastRowItemCount = itemsPerRow lastRowItemCount = itemsPerRow
} }
let trailingLineWidth = sideInset + CGFloat(lastRowItemCount) * (itemSize + itemSpacing) + placeholderSpacing let trailingLineWidth = sideInset + CGFloat(lastRowItemCount) * (itemSize + itemSpacing) + placeholderSpacing
@ -148,10 +168,13 @@ final class EmojiListInputComponent: Component {
} }
transition.setPosition(view: trailingPlaceholderView, position: trailingPlaceholderFrame.origin) transition.setPosition(view: trailingPlaceholderView, position: trailingPlaceholderFrame.origin)
trailingPlaceholderView.bounds = CGRect(origin: CGPoint(), size: trailingPlaceholderFrame.size) trailingPlaceholderView.bounds = CGRect(origin: CGPoint(), size: trailingPlaceholderFrame.size)
}
self.caretIndicator.tintColor = component.theme.list.itemAccentColor
self.caretIndicator.tintColor = component.theme.list.itemAccentColor
self.caretIndicator.isHidden = !component.isInputActive
if component.caretPosition >= component.reactionItems.count {
transition.setFrame(view: self.caretIndicator, frame: CGRect(origin: CGPoint(x: trailingPlaceholderFrame.minX, y: trailingPlaceholderFrame.minY + floorToScreenPixels((trailingPlaceholderFrame.height - 22.0) * 0.5)), size: CGSize(width: 2.0, height: 22.0))) transition.setFrame(view: self.caretIndicator, frame: CGRect(origin: CGPoint(x: trailingPlaceholderFrame.minX, y: trailingPlaceholderFrame.minY + floorToScreenPixels((trailingPlaceholderFrame.height - 22.0) * 0.5)), size: CGSize(width: 2.0, height: 22.0)))
self.caretIndicator.isHidden = !component.isInputActive
} }
var validIds: [Int64] = [] var validIds: [Int64] = []
@ -201,6 +224,11 @@ final class EmojiListInputComponent: Component {
itemLayer.isVisibleForAnimations = true itemLayer.isVisibleForAnimations = true
itemTransition.setFrame(layer: itemLayer, frame: itemFrame) itemTransition.setFrame(layer: itemLayer, frame: itemFrame)
if component.caretPosition == i {
transition.setFrame(view: self.caretIndicator, frame: CGRect(origin: CGPoint(x: itemFrame.minX - 2.0, y: itemFrame.minY + floorToScreenPixels((itemFrame.height - 22.0) * 0.5)), size: CGSize(width: 2.0, height: 22.0)))
}
if animateIn, !transition.animation.isImmediate { if animateIn, !transition.animation.isImmediate {
itemLayer.animateScale(from: 0.001, to: 1.0, duration: 0.2) itemLayer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
itemLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) itemLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)

View File

@ -20,6 +20,7 @@ public final class EmojiSelectionComponent: Component {
public let backgroundIconColor: UIColor? public let backgroundIconColor: UIColor?
public let backgroundColor: UIColor public let backgroundColor: UIColor
public let separatorColor: UIColor public let separatorColor: UIColor
public let backspace: (() -> Void)?
public init( public init(
theme: PresentationTheme, theme: PresentationTheme,
@ -30,7 +31,8 @@ public final class EmojiSelectionComponent: Component {
emojiContent: EmojiPagerContentComponent, emojiContent: EmojiPagerContentComponent,
backgroundIconColor: UIColor?, backgroundIconColor: UIColor?,
backgroundColor: UIColor, backgroundColor: UIColor,
separatorColor: UIColor separatorColor: UIColor,
backspace: (() -> Void)?
) { ) {
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
@ -41,6 +43,7 @@ public final class EmojiSelectionComponent: Component {
self.backgroundIconColor = backgroundIconColor self.backgroundIconColor = backgroundIconColor
self.backgroundColor = backgroundColor self.backgroundColor = backgroundColor
self.separatorColor = separatorColor self.separatorColor = separatorColor
self.backspace = backspace
} }
public static func ==(lhs: EmojiSelectionComponent, rhs: EmojiSelectionComponent) -> Bool { public static func ==(lhs: EmojiSelectionComponent, rhs: EmojiSelectionComponent) -> Bool {
@ -71,6 +74,9 @@ public final class EmojiSelectionComponent: Component {
if lhs.separatorColor != rhs.separatorColor { if lhs.separatorColor != rhs.separatorColor {
return false return false
} }
if (lhs.backspace == nil) != (rhs.backspace == nil) {
return false
}
return true return true
} }
@ -83,6 +89,9 @@ public final class EmojiSelectionComponent: Component {
private let shadowView: UIImageView private let shadowView: UIImageView
private let cornersView: UIImageView private let cornersView: UIImageView
private let backspaceBackgroundView: UIImageView
private let backspaceButtonView: HighlightTrackingButton
private var component: EmojiSelectionComponent? private var component: EmojiSelectionComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
@ -95,6 +104,9 @@ public final class EmojiSelectionComponent: Component {
self.shadowView = UIImageView() self.shadowView = UIImageView()
self.cornersView = UIImageView() self.cornersView = UIImageView()
self.backspaceBackgroundView = UIImageView()
self.backspaceButtonView = HighlightTrackingButton()
super.init(frame: frame) super.init(frame: frame)
self.addSubview(self.keyboardClippingView) self.addSubview(self.keyboardClippingView)
@ -104,6 +116,9 @@ public final class EmojiSelectionComponent: Component {
self.addSubview(self.cornersView) self.addSubview(self.cornersView)
self.addSubview(self.shadowView) self.addSubview(self.shadowView)
self.backspaceButtonView.addSubview(self.backspaceBackgroundView)
self.addSubview(self.backspaceButtonView)
self.shadowView.image = generateImage(CGSize(width: 16.0, height: 16.0), rotatedContext: { size, context in self.shadowView.image = generateImage(CGSize(width: 16.0, height: 16.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(), blur: 40.0, color: UIColor(white: 0.0, alpha: 0.05).cgColor) context.setShadow(offset: CGSize(), blur: 40.0, color: UIColor(white: 0.0, alpha: 0.05).cgColor)
@ -123,6 +138,35 @@ public final class EmojiSelectionComponent: Component {
context.fillPath() context.fillPath()
context.clear(CGRect(origin: CGPoint(x: 8.0, y: 0.0), size: CGSize(width: 1.0, height: size.height))) context.clear(CGRect(origin: CGPoint(x: 8.0, y: 0.0), size: CGSize(width: 1.0, height: size.height)))
})?.withRenderingMode(.alwaysTemplate).stretchableImage(withLeftCapWidth: 8, topCapHeight: 16) })?.withRenderingMode(.alwaysTemplate).stretchableImage(withLeftCapWidth: 8, topCapHeight: 16)
self.backspaceButtonView.highligthedChanged = { [weak self] highlighted in
if let self, self.backspaceButtonView.bounds.width > 0.0 {
let topScale: CGFloat = (self.backspaceButtonView.bounds.width - 8.0) / self.backspaceButtonView.bounds.width
let maxScale: CGFloat = (self.backspaceButtonView.bounds.width + 2.0) / self.backspaceButtonView.bounds.width
if highlighted {
self.backspaceButtonView.layer.removeAnimation(forKey: "sublayerTransform")
self.backspaceButtonView.alpha = 0.7
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
transition.setScale(layer: self.backspaceButtonView.layer, scale: topScale)
} else {
self.backspaceButtonView.alpha = 1.0
self.backspaceButtonView.layer.animateAlpha(from: 7, to: 1.0, duration: 0.2)
let transition = Transition(animation: .none)
transition.setScale(layer: self.backspaceButtonView.layer, scale: 1.0)
self.backspaceButtonView.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
self.backspaceButtonView.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue)
})
}
}
}
self.backspaceButtonView.addTarget(self, action: #selector(self.backspacePressed), for: .touchUpInside)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -132,12 +176,17 @@ public final class EmojiSelectionComponent: Component {
deinit { deinit {
} }
@objc private func backspacePressed() {
self.component?.backspace?()
}
func update(component: EmojiSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize { func update(component: EmojiSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.backgroundColor = component.backgroundColor self.backgroundColor = component.backgroundColor
let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85) let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85)
self.panelBackgroundView.updateColor(color: panelBackgroundColor, transition: .immediate) self.panelBackgroundView.updateColor(color: panelBackgroundColor, transition: .immediate)
self.panelSeparatorView.backgroundColor = component.separatorColor self.panelSeparatorView.backgroundColor = component.separatorColor
let previousComponent = self.component
self.component = component self.component = component
self.state = state self.state = state
@ -148,13 +197,41 @@ public final class EmojiSelectionComponent: Component {
let topPanelHeight: CGFloat = 42.0 let topPanelHeight: CGFloat = 42.0
let backspaceButtonInset = UIEdgeInsets(top: 9.0, left: 0.0, bottom: 36.0, right: 9.0)
let backspaceButtonSize = CGSize(width: 36.0, height: 36.0)
if previousComponent?.theme !== component.theme {
self.backspaceBackgroundView.image = generateImage(CGSize(width: backspaceButtonSize.width + 12.0 * 2.0, height: backspaceButtonSize.height + 12.0 * 2.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(), blur: 40.0, color: UIColor(white: 0.0, alpha: 0.15).cgColor)
context.setFillColor(component.theme.list.plainBackgroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 12.0, y: 12.0), size: backspaceButtonSize))
context.setShadow(offset: CGSize(), blur: 0.0, color: nil)
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputClearIcon"), color: component.theme.chat.inputMediaPanel.panelIconColor) {
let imageSize = image.size
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: 12.0 + floor((backspaceButtonSize.width - imageSize.width) * 0.5) - 1.0, y: 12.0 + floor((backspaceButtonSize.height - imageSize.height) * 0.5)), size: imageSize))
}
})
}
self.backspaceBackgroundView.frame = CGRect(origin: CGPoint(), size: backspaceButtonSize).insetBy(dx: -12.0, dy: -12.0)
let backspaceButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - component.sideInset - backspaceButtonInset.right - backspaceButtonSize.width, y: availableSize.height - component.bottomInset - backspaceButtonInset.bottom), size: backspaceButtonSize)
transition.setPosition(view: self.backspaceButtonView, position: backspaceButtonFrame.center)
transition.setBounds(view: self.backspaceButtonView, bounds: CGRect(origin: CGPoint(), size: backspaceButtonFrame.size))
if component.backspace != nil {
transition.setAlpha(view: self.backspaceButtonView, alpha: 1.0)
transition.setScale(view: self.backspaceButtonView, scale: 1.0)
} else {
transition.setAlpha(view: self.backspaceButtonView, alpha: 0.0)
transition.setScale(view: self.backspaceButtonView, scale: 0.001)
}
let keyboardSize = self.keyboardView.update( let keyboardSize = self.keyboardView.update(
transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)), transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)),
component: AnyComponent(EntityKeyboardComponent( component: AnyComponent(EntityKeyboardComponent(
theme: component.theme, theme: component.theme,
strings: component.strings, strings: component.strings,
isContentInFocus: false, isContentInFocus: false,
containerInsets: UIEdgeInsets(top: topPanelHeight - 34.0, left: component.sideInset, bottom: component.bottomInset, right: component.sideInset), containerInsets: UIEdgeInsets(top: topPanelHeight - 34.0, left: component.sideInset, bottom: component.bottomInset + backspaceButtonInset.bottom + backspaceButtonSize.height + backspaceButtonInset.top, right: component.sideInset),
topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0), topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0),
emojiContent: component.emojiContent, emojiContent: component.emojiContent,
stickerContent: nil, stickerContent: nil,

View File

@ -16,6 +16,66 @@ import Markdown
import ButtonComponent import ButtonComponent
import PremiumUI import PremiumUI
import UndoUI import UndoUI
import BundleIconComponent
import AnimatedTextComponent
private final class ButtonSubtitleComponent: CombinedComponent {
let count: Int
let theme: PresentationTheme
init(count: Int, theme: PresentationTheme) {
self.count = count
self.theme = theme
}
static func ==(lhs: ButtonSubtitleComponent, rhs: ButtonSubtitleComponent) -> Bool {
if lhs.count != rhs.count {
return false
}
if lhs.theme !== rhs.theme {
return false
}
return true
}
static var body: Body {
let icon = Child(BundleIconComponent.self)
let text = Child(AnimatedTextComponent.self)
return { context in
let icon = icon.update(
component: BundleIconComponent(
name: "Chat/Input/Accessory Panels/TextLockIcon",
tintColor: context.component.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.7),
maxSize: CGSize(width: 10.0, height: 10.0)
),
availableSize: CGSize(width: 100.0, height: 100.0),
transition: context.transition
)
//TODO:localize
var textItems: [AnimatedTextComponent.Item] = []
textItems.append(AnimatedTextComponent.Item(id: AnyHashable(0 as Int), content: .text("Level ")))
textItems.append(AnimatedTextComponent.Item(id: AnyHashable(1 as Int), content: .number(context.component.count, minDigits: 1)))
textItems.append(AnimatedTextComponent.Item(id: AnyHashable(2 as Int), content: .text(" Required")))
let text = text.update(
component: AnimatedTextComponent(font: Font.medium(11.0), color: context.component.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.7), items: textItems),
availableSize: CGSize(width: context.availableSize.width - 20.0, height: 100.0),
transition: context.transition
)
let spacing: CGFloat = 3.0
let size = CGSize(width: icon.size.width + spacing + text.size.width, height: text.size.height)
context.add(icon
.position(icon.size.centered(in: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: icon.size.width, height: size.height))).center)
)
context.add(text
.position(text.size.centered(in: CGRect(origin: CGPoint(x: icon.size.width + spacing, y: 0.0), size: text.size)).center)
)
return size
}
}
}
final class PeerAllowedReactionsScreenComponent: Component { final class PeerAllowedReactionsScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -62,18 +122,24 @@ final class PeerAllowedReactionsScreenComponent: Component {
private(set) weak var state: EmptyComponentState? private(set) weak var state: EmptyComponentState?
private var environment: EnvironmentType? private var environment: EnvironmentType?
private var boostStatus: ChannelBoostStatus?
private var boostStatusDisposable: Disposable?
private var isEnabled: Bool = false private var isEnabled: Bool = false
private var availableReactions: AvailableReactions? private var availableReactions: AvailableReactions?
private var enabledReactions: [EmojiComponentReactionItem]? private var enabledReactions: [EmojiComponentReactionItem]?
private var emojiContent: EmojiPagerContentComponent? private var emojiContent: EmojiPagerContentComponent?
private var emojiContentDisposable: Disposable? private var emojiContentDisposable: Disposable?
private var caretPosition: Int?
private var displayInput: Bool = false private var displayInput: Bool = false
private var isApplyingSettings: Bool = false private var isApplyingSettings: Bool = false
private var applyDisposable: Disposable? private var applyDisposable: Disposable?
private weak var currentUndoController: UndoOverlayController?
override init(frame: CGRect) { override init(frame: CGRect) {
self.scrollView = UIScrollView() self.scrollView = UIScrollView()
self.scrollView.showsVerticalScrollIndicator = true self.scrollView.showsVerticalScrollIndicator = true
@ -97,6 +163,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
deinit { deinit {
self.emojiContentDisposable?.dispose() self.emojiContentDisposable?.dispose()
self.applyDisposable?.dispose() self.applyDisposable?.dispose()
self.boostStatusDisposable?.dispose()
} }
func scrollToTop() { func scrollToTop() {
@ -123,13 +190,31 @@ final class PeerAllowedReactionsScreenComponent: Component {
if self.isApplyingSettings { if self.isApplyingSettings {
return return
} }
guard let enabledReactions = self.enabledReactions else { guard var enabledReactions = self.enabledReactions else {
return return
} }
if !self.isEnabled {
enabledReactions.removeAll()
}
guard let availableReactions = self.availableReactions else { guard let availableReactions = self.availableReactions else {
return return
} }
let customReactions = enabledReactions.filter({ item in
switch item.reaction {
case .custom:
return true
case .builtin:
return false
}
})
if let boostStatus = self.boostStatus, !customReactions.isEmpty, customReactions.count > boostStatus.level {
self.displayPremiumScreen()
return
}
self.isApplyingSettings = true self.isApplyingSettings = true
self.state?.updated(transition: .immediate) self.state?.updated(transition: .immediate)
@ -155,38 +240,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
switch error { switch error {
case .boostRequired: case .boostRequired:
let _ = combineLatest( self.displayPremiumScreen()
queue: Queue.mainQueue(),
component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId)),
component.context.engine.peers.getChannelBoostStatus(peerId: component.peerId)
).startStandalone(next: { [weak self] peer, status in
guard let self, let component = self.component, let peer, let status else {
return
}
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
let link = status.url
let controller = PremiumLimitScreen(context: component.context, subject: .storiesChannelBoost(peer: peer, boostSubject: .channelReactions, isCurrent: true, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: link, myBoostCount: 0, canBoostAgain: false), count: Int32(status.boosts), action: { [weak self] in
guard let self, let component = self.component else {
return true
}
UIPasteboard.general.string = link
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
self.environment?.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current)
return true
}, openStats: nil, openGift: premiumConfiguration.giveawayGiftsPurchaseAvailable ? { [weak self] in
guard let self, let component = self.component else {
return
}
let controller = createGiveawayController(context: component.context, peerId: component.peerId, subject: .generic)
self.environment?.controller()?.push(controller)
} : nil)
self.environment?.controller()?.push(controller)
HapticFeedback().impact(.light)
})
case .generic: case .generic:
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize //TODO:localize
@ -200,6 +254,68 @@ final class PeerAllowedReactionsScreenComponent: Component {
}) })
} }
private func displayPremiumScreen() {
guard let component = self.component else {
return
}
let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId))
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
guard let self, let component = self.component, let peer, let status = self.boostStatus else {
return
}
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
let link = status.url
let controller = PremiumLimitScreen(context: component.context, subject: .storiesChannelBoost(peer: peer, boostSubject: .channelReactions, isCurrent: true, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: link, myBoostCount: 0, canBoostAgain: false), count: Int32(status.boosts), action: { [weak self] in
guard let self, let component = self.component else {
return true
}
UIPasteboard.general.string = link
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
self.environment?.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current)
return true
}, openStats: { [weak self] in
guard let self else {
return
}
self.openBoostStats()
}, openGift: premiumConfiguration.giveawayGiftsPurchaseAvailable ? { [weak self] in
guard let self, let component = self.component else {
return
}
let controller = createGiveawayController(context: component.context, peerId: component.peerId, subject: .generic)
self.environment?.controller()?.push(controller)
} : nil)
self.environment?.controller()?.push(controller)
HapticFeedback().impact(.light)
})
}
private func openBoostStats() {
guard let component = self.component else {
return
}
let _ = (component.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.StatsDatacenterId(id: component.peerId)
)
|> deliverOnMainQueue).start(next: { [weak self] statsDatacenterId in
guard let self, let component = self.component, let boostStatus = self.boostStatus else {
return
}
guard let statsDatacenterId else {
return
}
let statsController = component.context.sharedContext.makeChannelStatsController(context: component.context, updatedPresentationData: nil, peerId: component.peerId, boosts: true, boostStatus: boostStatus, statsDatacenterId: statsDatacenterId)
self.environment?.controller()?.push(statsController)
})
}
func update(component: PeerAllowedReactionsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize { func update(component: PeerAllowedReactionsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true self.isUpdating = true
defer { defer {
@ -228,6 +344,9 @@ final class PeerAllowedReactionsScreenComponent: Component {
self.availableReactions = component.initialContent.availableReactions self.availableReactions = component.initialContent.availableReactions
self.isEnabled = component.initialContent.isEnabled self.isEnabled = component.initialContent.isEnabled
} }
var caretPosition = self.caretPosition ?? enabledReactions.count
caretPosition = max(0, min(enabledReactions.count, caretPosition))
self.caretPosition = caretPosition
if self.emojiContentDisposable == nil { if self.emojiContentDisposable == nil {
let emojiContent = EmojiPagerContentComponent.emojiInputData( let emojiContent = EmojiPagerContentComponent.emojiInputData(
@ -267,18 +386,47 @@ final class PeerAllowedReactionsScreenComponent: Component {
if let index = enabledReactions.firstIndex(where: { $0.file.fileId.id == itemFile.fileId.id }) { if let index = enabledReactions.firstIndex(where: { $0.file.fileId.id == itemFile.fileId.id }) {
enabledReactions.remove(at: index) enabledReactions.remove(at: index)
if let caretPosition = self.caretPosition, caretPosition > index {
self.caretPosition = max(0, caretPosition - 1)
}
} else { } else {
let reaction: MessageReaction.Reaction let reaction: MessageReaction.Reaction
if let availableReactions = self.availableReactions, let reactionItem = availableReactions.reactions.first(where: { $0.selectAnimation.fileId.id == itemFile.fileId.id }) { if let availableReactions = self.availableReactions, let reactionItem = availableReactions.reactions.first(where: { $0.selectAnimation.fileId.id == itemFile.fileId.id }) {
reaction = reactionItem.value reaction = reactionItem.value
} else { } else {
reaction = .custom(itemFile.fileId.id) reaction = .custom(itemFile.fileId.id)
if let boostStatus = self.boostStatus {
let enabledCustomReactions = enabledReactions.filter({ item in
switch item.reaction {
case .custom:
return true
case .builtin:
return false
}
})
let nextCustomReactionCount = enabledCustomReactions.count + 1
if nextCustomReactionCount > boostStatus.level {
//TODO:localize
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
self.environment?.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: component.context, file: itemFile, loop: false, title: nil, text: "Your channel needs to reach **Level \(nextCustomReactionCount)** to add **\(nextCustomReactionCount) custom emoji as reactions.**", undoText: nil, customAction: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
}
}
let item = EmojiComponentReactionItem(reaction: reaction, file: itemFile)
if let caretPosition = self.caretPosition, caretPosition < enabledReactions.count {
enabledReactions.insert(item, at: caretPosition)
self.caretPosition = caretPosition + 1
} else {
enabledReactions.append(item)
self.caretPosition = enabledReactions.count
} }
enabledReactions.append(EmojiComponentReactionItem(reaction: reaction, file: itemFile))
} }
self.enabledReactions = enabledReactions self.enabledReactions = enabledReactions
if !self.isUpdating { if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4)) self.state?.updated(transition: .spring(duration: 0.25))
} }
}, },
deleteBackwards: { deleteBackwards: {
@ -327,6 +475,19 @@ final class PeerAllowedReactionsScreenComponent: Component {
}) })
} }
if self.boostStatusDisposable == nil {
self.boostStatusDisposable = (component.context.engine.peers.getChannelBoostStatus(peerId: component.peerId)
|> deliverOnMainQueue).start(next: { [weak self] boostStatus in
guard let self else {
return
}
self.boostStatus = boostStatus
if !self.isUpdating {
self.state?.updated(transition: .immediate)
}
})
}
if themeUpdated { if themeUpdated {
self.backgroundColor = environment.theme.list.blocksBackgroundColor self.backgroundColor = environment.theme.list.blocksBackgroundColor
} }
@ -340,7 +501,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
component: AnyComponent(ListSwitchItemComponent( component: AnyComponent(ListSwitchItemComponent(
theme: environment.theme, theme: environment.theme,
title: environment.strings.PeerInfo_AllowedReactions_AllowAllText, title: environment.strings.PeerInfo_AllowedReactions_AllowAllText,
value: true, value: self.isEnabled,
valueUpdated: { [weak self] value in valueUpdated: { [weak self] value in
guard let self else { guard let self else {
return return
@ -356,6 +517,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
} }
} }
self.enabledReactions = enabledReactions self.enabledReactions = enabledReactions
self.caretPosition = enabledReactions.count
} }
} else { } else {
self.displayInput = false self.displayInput = false
@ -467,6 +629,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
placeholder: "Add Reactions...", placeholder: "Add Reactions...",
reactionItems: enabledReactions, reactionItems: enabledReactions,
isInputActive: self.displayInput, isInputActive: self.displayInput,
caretPosition: caretPosition,
activateInput: { [weak self] in activateInput: { [weak self] in
guard let self else { guard let self else {
return return
@ -475,6 +638,15 @@ final class PeerAllowedReactionsScreenComponent: Component {
self.displayInput = true self.displayInput = true
self.state?.updated(transition: .spring(duration: 0.5)) self.state?.updated(transition: .spring(duration: 0.5))
} }
},
setCaretPosition: { [weak self] value in
guard let self else {
return
}
if self.caretPosition != value {
self.caretPosition = value
self.state?.updated(transition: .immediate)
}
} }
)), )),
environment: {}, environment: {},
@ -584,13 +756,23 @@ final class PeerAllowedReactionsScreenComponent: Component {
buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(
Text(text: "Update Reactions", font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor) Text(text: "Update Reactions", font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor)
))) )))
/*if self.remainingTimer > 0 {
buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent( let customReactionCount = self.isEnabled ? enabledReactions.filter({ item in
AnimatedTextComponent(font: Font.with(size: 17.0, weight: .semibold, traits: .monospacedNumbers), color: environment.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.5), items: [ switch item.reaction {
AnimatedTextComponent.Item(id: AnyHashable(0 as Int), content: .number(self.remainingTimer, minDigits: 0)) case .custom:
]) return true
))) case .builtin:
}*/ return false
}
}).count : 0
if let boostStatus = self.boostStatus, customReactionCount > boostStatus.level {
//TODO:localize
buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(ButtonSubtitleComponent(
count: customReactionCount,
theme: environment.theme
))))
}
let buttonSize = self.actionButton.update( let buttonSize = self.actionButton.update(
transition: transition, transition: transition,
@ -601,7 +783,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
), ),
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(
HStack(buttonContents, spacing: 5.0) VStack(buttonContents, spacing: 3.0)
)), )),
isEnabled: true, isEnabled: true,
tintWhenDisabled: false, tintWhenDisabled: false,
@ -640,8 +822,26 @@ final class PeerAllowedReactionsScreenComponent: Component {
emojiContent: emojiContent.withSelectedItems(Set(enabledReactions.map(\.file.fileId))), emojiContent: emojiContent.withSelectedItems(Set(enabledReactions.map(\.file.fileId))),
backgroundIconColor: nil, backgroundIconColor: nil,
backgroundColor: environment.theme.list.itemBlocksBackgroundColor, backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
separatorColor: environment.theme.list.itemBlocksSeparatorColor) separatorColor: environment.theme.list.itemBlocksSeparatorColor,
), backspace: enabledReactions.isEmpty ? nil : { [weak self] in
guard let self, var enabledReactions = self.enabledReactions, !enabledReactions.isEmpty else {
return
}
if let caretPosition = self.caretPosition, caretPosition < enabledReactions.count {
if caretPosition > 0 {
enabledReactions.remove(at: caretPosition - 1)
self.caretPosition = caretPosition - 1
}
} else {
enabledReactions.removeLast()
self.caretPosition = enabledReactions.count
}
self.enabledReactions = enabledReactions
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.25))
}
}
)),
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width, height: min(340.0, max(50.0, availableSize.height - 200.0))) containerSize: CGSize(width: availableSize.width, height: min(340.0, max(50.0, availableSize.height - 200.0)))
) )

View File

@ -20,6 +20,10 @@ import MediaEditorScreen
import ChatControllerInteraction import ChatControllerInteraction
public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParams) { public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParams) {
if case let .peer(peer) = params.chatLocation {
let _ = params.context.engine.peers.ensurePeerIsLocallyAvailable(peer: peer).startStandalone()
}
var viewForumAsMessages: Signal<Bool, NoError> = .single(false) var viewForumAsMessages: Signal<Bool, NoError> = .single(false)
if case let .peer(peer) = params.chatLocation, case let .channel(channel) = peer, channel.flags.contains(.isForum) { if case let .peer(peer) = params.chatLocation, case let .channel(channel) = peer, channel.flags.contains(.isForum) {
viewForumAsMessages = params.context.account.postbox.combinedView(keys: [.cachedPeerData(peerId: peer.id)]) viewForumAsMessages = params.context.account.postbox.combinedView(keys: [.cachedPeerData(peerId: peer.id)])

View File

@ -32,14 +32,16 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
let id: AnyHashable let id: AnyHashable
let label: Label let label: Label
let additionalBadgeLabel: String?
let text: String let text: String
let icon: UIImage? let icon: UIImage?
let iconSignal: Signal<UIImage?, NoError>? let iconSignal: Signal<UIImage?, NoError>?
let action: (() -> Void)? let action: (() -> Void)?
init(id: AnyHashable, label: Label = .none, text: String, icon: UIImage? = nil, iconSignal: Signal<UIImage?, NoError>? = nil, action: (() -> Void)?) { init(id: AnyHashable, label: Label = .none, additionalBadgeLabel: String? = nil, text: String, icon: UIImage? = nil, iconSignal: Signal<UIImage?, NoError>? = nil, action: (() -> Void)?) {
self.id = id self.id = id
self.label = label self.label = label
self.additionalBadgeLabel = additionalBadgeLabel
self.text = text self.text = text
self.icon = icon self.icon = icon
self.iconSignal = iconSignal self.iconSignal = iconSignal
@ -57,6 +59,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
private let iconNode: ASImageNode private let iconNode: ASImageNode
private let labelBadgeNode: ASImageNode private let labelBadgeNode: ASImageNode
private let labelNode: ImmediateTextNode private let labelNode: ImmediateTextNode
private var additionalLabelNode: ImmediateTextNode?
private let textNode: ImmediateTextNode private let textNode: ImmediateTextNode
private let arrowNode: ASImageNode private let arrowNode: ASImageNode
private let bottomSeparatorNode: ASDisplayNode private let bottomSeparatorNode: ASDisplayNode
@ -228,6 +231,13 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
if self.labelBadgeNode.supernode == nil { if self.labelBadgeNode.supernode == nil {
self.insertSubnode(self.labelBadgeNode, belowSubnode: self.labelNode) self.insertSubnode(self.labelBadgeNode, belowSubnode: self.labelNode)
} }
} else if item.additionalBadgeLabel != nil {
if previousItem?.additionalBadgeLabel == nil {
self.labelBadgeNode.image = generateFilledRoundedRectImage(size: CGSize(width: 16.0, height: 16.0), cornerRadius: 5.0, color: presentationData.theme.list.itemCheckColors.fillColor)?.stretchableImage(withLeftCapWidth: 6, topCapHeight: 6)
}
if self.labelBadgeNode.supernode == nil {
self.insertSubnode(self.labelBadgeNode, belowSubnode: self.labelNode)
}
} else { } else {
self.labelBadgeNode.removeFromSupernode() self.labelBadgeNode.removeFromSupernode()
} }
@ -247,9 +257,30 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
labelFrame = CGRect(origin: CGPoint(x: width - rightInset - labelSize.width, y: 12.0), size: labelSize) labelFrame = CGRect(origin: CGPoint(x: width - rightInset - labelSize.width, y: 12.0), size: labelSize)
} }
if let additionalBadgeLabel = item.additionalBadgeLabel {
let additionalLabelNode: ImmediateTextNode
if let current = self.additionalLabelNode {
additionalLabelNode = current
} else {
additionalLabelNode = ImmediateTextNode()
additionalLabelNode.isUserInteractionEnabled = false
self.additionalLabelNode = additionalLabelNode
self.addSubnode(additionalLabelNode)
}
additionalLabelNode.attributedText = NSAttributedString(string: additionalBadgeLabel, font: Font.medium(11.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor)
let additionalLabelSize = additionalLabelNode.updateLayout(CGSize(width: labelConstrainWidth, height: .greatestFiniteMagnitude))
additionalLabelNode.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 10.0, y: floor((height - additionalLabelSize.height) / 2.0) + 1.0), size: additionalLabelSize)
} else if let additionalLabelNode = self.additionalLabelNode {
self.additionalLabelNode = nil
additionalLabelNode.removeFromSupernode()
}
let labelBadgeNodeFrame: CGRect let labelBadgeNodeFrame: CGRect
if case .titleBadge = item.label { if case .titleBadge = item.label {
labelBadgeNodeFrame = labelFrame.insetBy(dx: -4.0, dy: -2.0 + UIScreenPixel) labelBadgeNodeFrame = labelFrame.insetBy(dx: -4.0, dy: -2.0 + UIScreenPixel)
} else if let additionalLabelNode = self.additionalLabelNode {
labelBadgeNodeFrame = additionalLabelNode.frame.insetBy(dx: -4.0, dy: -2.0 + UIScreenPixel)
} else { } else {
labelBadgeNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth, y: floorToScreenPixels(labelFrame.midY - badgeDiameter / 2.0)), size: CGSize(width: badgeWidth, height: badgeDiameter)) labelBadgeNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth, y: floorToScreenPixels(labelFrame.midY - badgeDiameter / 2.0)), size: CGSize(width: badgeWidth, height: badgeDiameter))
} }

View File

@ -1615,7 +1615,11 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
} else { } else {
label = "" label = ""
} }
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .text(label), text: presentationData.strings.PeerInfo_Reactions, icon: UIImage(bundleImageName: "Settings/Menu/Reactions"), action: { var additionalBadgeLabel: String? = nil
if case .broadcast = channel.info {
additionalBadgeLabel = presentationData.strings.Settings_New
}
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .text(label), additionalBadgeLabel: additionalBadgeLabel, text: presentationData.strings.PeerInfo_Reactions, icon: UIImage(bundleImageName: "Settings/Menu/Reactions"), action: {
interaction.editingOpenReactionsSetup() interaction.editingOpenReactionsSetup()
})) }))
} }

View File

@ -392,7 +392,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural) let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural)
self.textNode.attributedText = attributedText self.textNode.attributedText = attributedText
self.textNode.maximumNumberOfLines = 2 self.textNode.maximumNumberOfLines = 10
displayUndo = false displayUndo = false
self.originalRemainingSeconds = 5 self.originalRemainingSeconds = 5
case let .swipeToReply(title, text): case let .swipeToReply(title, text):
@ -780,7 +780,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
return ("URL", contents) return ("URL", contents)
}), textAlignment: .natural) }), textAlignment: .natural)
self.textNode.attributedText = attributedText self.textNode.attributedText = attributedText
self.textNode.maximumNumberOfLines = 2 self.textNode.maximumNumberOfLines = 5
if text.contains("](") { if text.contains("](") {
isUserInteractionEnabled = true isUserInteractionEnabled = true