mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Channel reactions
This commit is contained in:
parent
216b731562
commit
4f23fa6e09
60
submodules/ComponentFlow/Source/Components/VStack.swift
Normal file
60
submodules/ComponentFlow/Source/Components/VStack.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -40,6 +40,9 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode",
|
||||
"//submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -29,6 +29,8 @@ import ChatMessageInteractiveMediaNode
|
||||
import WallpaperPreviewMedia
|
||||
import ChatMessageAttachedContentButtonNode
|
||||
import MessageInlineBlockBackgroundView
|
||||
import ComponentFlow
|
||||
import PlainButtonComponent
|
||||
|
||||
public enum ChatMessageAttachedContentActionIcon {
|
||||
case instant
|
||||
@ -67,6 +69,9 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
private var actionButtonSeparator: SimpleLayer?
|
||||
public var statusNode: ChatMessageDateAndStatusNode?
|
||||
|
||||
private var closeButton: ComponentView<Empty>?
|
||||
private var closeButtonImage: UIImage?
|
||||
|
||||
private var inlineMediaValue: Media?
|
||||
|
||||
//private var additionalImageBadgeNode: ChatMessageInteractiveMediaBadge?
|
||||
@ -88,6 +93,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
|
||||
public var defaultContentAction: () -> ChatMessageBubbleContentTapAction = { return ChatMessageBubbleContentTapAction(content: .none) }
|
||||
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
|
||||
public var visibility: ListViewItemNodeVisibility = .none {
|
||||
didSet {
|
||||
if oldValue != self.visibility {
|
||||
@ -152,7 +159,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
let isPreview = presentationData.isPreview
|
||||
let fontSize: CGFloat
|
||||
if message.adAttribute != nil {
|
||||
fontSize = floor(presentationData.fontSize.baseDisplaySize)
|
||||
fontSize = floor(presentationData.fontSize.baseDisplaySize * 15.0 / 17.0)
|
||||
} else {
|
||||
fontSize = floor(presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)
|
||||
}
|
||||
@ -540,7 +547,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
actionTitle,
|
||||
mainColor,
|
||||
false,
|
||||
message.adAttribute != nil
|
||||
false
|
||||
)
|
||||
actionButtonMinWidthAndFinalizeLayout = (buttonWidth, continueLayout)
|
||||
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
|
||||
|
||||
var trailingContentWidth: CGFloat?
|
||||
if let _ = message.adAttribute, let (textLayout, _) = textLayoutAndApply {
|
||||
if textLayout.hasRTL {
|
||||
trailingContentWidth = 10000.0
|
||||
} else {
|
||||
trailingContentWidth = textLayout.trailingLineWidth
|
||||
}
|
||||
} else {
|
||||
if !displayLine, let (actionButtonMinWidth, _) = actionButtonMinWidthAndFinalizeLayout {
|
||||
trailingContentWidth = actionButtonMinWidth
|
||||
}
|
||||
if !displayLine, let (actionButtonMinWidth, _) = actionButtonMinWidthAndFinalizeLayout {
|
||||
trailingContentWidth = actionButtonMinWidth
|
||||
}
|
||||
|
||||
var statusLayoutAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))?
|
||||
if case let .linear(_, bottom) = position {
|
||||
switch bottom {
|
||||
case .None, .Neighbour(_, .footer, _):
|
||||
let statusLayoutAndContinueValue = makeStatusLayout(ChatMessageDateAndStatusNode.Arguments(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
edited: edited,
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
layoutInput: .trailingContent(
|
||||
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,
|
||||
reactions: dateReactionsAndPeers.reactions,
|
||||
reactionPeers: dateReactionsAndPeers.peers,
|
||||
displayAllReactionPeers: message.id.peerId.namespace == Namespaces.Peer.CloudUser,
|
||||
replyCount: dateReplies,
|
||||
isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread,
|
||||
hasAutoremove: message.isSelfExpiring,
|
||||
canViewReactionList: canViewMessageReactionList(message: message),
|
||||
animationCache: controllerInteraction.presentationContext.animationCache,
|
||||
animationRenderer: controllerInteraction.presentationContext.animationRenderer
|
||||
))
|
||||
statusLayoutAndContinue = statusLayoutAndContinueValue
|
||||
actualWidth = max(actualWidth, statusLayoutAndContinueValue.0)
|
||||
if message.adAttribute == nil {
|
||||
let statusLayoutAndContinueValue = makeStatusLayout(ChatMessageDateAndStatusNode.Arguments(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
edited: edited,
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
layoutInput: .trailingContent(
|
||||
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,
|
||||
reactions: dateReactionsAndPeers.reactions,
|
||||
reactionPeers: dateReactionsAndPeers.peers,
|
||||
displayAllReactionPeers: message.id.peerId.namespace == Namespaces.Peer.CloudUser,
|
||||
replyCount: dateReplies,
|
||||
isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread,
|
||||
hasAutoremove: message.isSelfExpiring,
|
||||
canViewReactionList: canViewMessageReactionList(message: message),
|
||||
animationCache: controllerInteraction.presentationContext.animationCache,
|
||||
animationRenderer: controllerInteraction.presentationContext.animationRenderer
|
||||
))
|
||||
statusLayoutAndContinue = statusLayoutAndContinueValue
|
||||
actualWidth = max(actualWidth, statusLayoutAndContinueValue.0)
|
||||
}
|
||||
default:
|
||||
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 {
|
||||
case .None, .Neighbour(_, .footer, _):
|
||||
let bottomStatusContentHeight = statusBackgroundSpacing + statusSizeAndApply.0.height
|
||||
actualSize.height += bottomStatusContentHeight
|
||||
backgroundInsets.bottom += bottomStatusContentHeight
|
||||
if let statusSizeAndApply {
|
||||
let bottomStatusContentHeight = statusBackgroundSpacing + statusSizeAndApply.0.height
|
||||
actualSize.height += bottomStatusContentHeight
|
||||
backgroundInsets.bottom += bottomStatusContentHeight
|
||||
} else {
|
||||
actualSize.height += 11.0
|
||||
backgroundInsets.bottom += 11.0
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let _ = message.adAttribute {
|
||||
actualSize.height += 2.0
|
||||
backgroundInsets.bottom += 2.0
|
||||
}
|
||||
|
||||
return (actualSize, { animation, synchronousLoads, applyInfo in
|
||||
guard let self else {
|
||||
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)
|
||||
|
||||
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 {
|
||||
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
|
||||
if let current = self.backgroundView {
|
||||
backgroundView = current
|
||||
@ -982,11 +983,82 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
title.textNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||
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 {
|
||||
if let title = self.title {
|
||||
self.title = nil
|
||||
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 {
|
||||
@ -1128,10 +1200,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
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)
|
||||
if let _ = message.adAttribute, let statusSizeAndApply {
|
||||
actionButtonFrame.origin.y += statusSizeAndApply.0.height
|
||||
}
|
||||
let actionButtonFrame = CGRect(origin: CGPoint(x: insets.left, y: item.offsetY), size: actionButtonSize)
|
||||
|
||||
let actionButton = actionButtonApply(animation)
|
||||
|
||||
@ -1150,25 +1219,21 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
} else {
|
||||
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 {
|
||||
let separatorFrame = CGRect(origin: CGPoint(x: actionButtonFrame.minX, y: actionButtonFrame.minY - 1.0), size: CGSize(width: actionButtonFrame.width, height: UIScreenPixel))
|
||||
|
||||
let actionButtonSeparator: SimpleLayer
|
||||
if let current = self.actionButtonSeparator {
|
||||
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 = SimpleLayer()
|
||||
self.actionButtonSeparator = actionButtonSeparator
|
||||
self.layer.addSublayer(actionButtonSeparator)
|
||||
actionButtonSeparator.frame = separatorFrame
|
||||
}
|
||||
|
||||
actionButtonSeparator.backgroundColor = mainColor.withMultipliedAlpha(0.2).cgColor
|
||||
} else {
|
||||
if let actionButton = self.actionButton {
|
||||
self.actionButton = nil
|
||||
@ -1182,10 +1247,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
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)
|
||||
if let _ = message.adAttribute, let (actionButtonSize, _) = actionButtonSizeAndApply {
|
||||
statusFrame.origin.y -= actionButtonSize.height + statusBackgroundSpacing
|
||||
}
|
||||
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)
|
||||
|
||||
let statusNode = statusSizeAndApply.1(self.statusNode == nil ? .None : animation)
|
||||
if self.statusNode !== statusNode {
|
||||
@ -1216,12 +1278,36 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
self.statusNode = nil
|
||||
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 {
|
||||
if let currentMedia = self.media {
|
||||
if let media = media {
|
||||
@ -1296,9 +1382,13 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
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 {
|
||||
return .init(content: .none)
|
||||
return ChatMessageBubbleContentTapAction(content: .none)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1877,7 +1877,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
var currentCredibilityIcon: EmojiStatusComponent.Content?
|
||||
|
||||
var initialDisplayHeader = true
|
||||
if hidesHeaders {
|
||||
if hidesHeaders || item.message.adAttribute != nil {
|
||||
initialDisplayHeader = false
|
||||
} else if let backgroundHiding, case .always = backgroundHiding {
|
||||
initialDisplayHeader = false
|
||||
|
@ -479,8 +479,12 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
|
||||
}
|
||||
}
|
||||
} else if let adAttribute = item.message.adAttribute {
|
||||
title = nil
|
||||
subtitle = nil
|
||||
//TODO:localize
|
||||
//Recommended?
|
||||
title = "Sponsored"
|
||||
subtitle = item.message.author.flatMap {
|
||||
NSAttributedString(string: EnginePeer($0).compactDisplayTitle, font: titleFont)
|
||||
}
|
||||
text = item.message.text
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
||||
@ -514,7 +518,7 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
|
||||
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)
|
||||
|
@ -25,6 +25,8 @@ swift_library(
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Markdown",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/TelegramUI/Components/AnimatedTextComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/Components/PagerComponent",
|
||||
"//submodules/PremiumUI",
|
||||
"//submodules/UndoUI",
|
||||
|
@ -14,7 +14,9 @@ final class EmojiListInputComponent: Component {
|
||||
let placeholder: String
|
||||
let reactionItems: [EmojiComponentReactionItem]
|
||||
let isInputActive: Bool
|
||||
let caretPosition: Int
|
||||
let activateInput: () -> Void
|
||||
let setCaretPosition: (Int) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
@ -22,14 +24,18 @@ final class EmojiListInputComponent: Component {
|
||||
placeholder: String,
|
||||
reactionItems: [EmojiComponentReactionItem],
|
||||
isInputActive: Bool,
|
||||
activateInput: @escaping () -> Void
|
||||
caretPosition: Int,
|
||||
activateInput: @escaping () -> Void,
|
||||
setCaretPosition: @escaping (Int) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.placeholder = placeholder
|
||||
self.reactionItems = reactionItems
|
||||
self.isInputActive = isInputActive
|
||||
self.caretPosition = caretPosition
|
||||
self.activateInput = activateInput
|
||||
self.setCaretPosition = setCaretPosition
|
||||
}
|
||||
|
||||
static func ==(lhs: EmojiListInputComponent, rhs: EmojiListInputComponent) -> Bool {
|
||||
@ -48,6 +54,9 @@ final class EmojiListInputComponent: Component {
|
||||
if lhs.isInputActive != rhs.isInputActive {
|
||||
return false
|
||||
}
|
||||
if lhs.caretPosition != rhs.caretPosition {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -78,19 +87,29 @@ final class EmojiListInputComponent: Component {
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
if case .ended = recognizer.state {
|
||||
let point = recognizer.location(in: self)
|
||||
|
||||
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 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
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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 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.state = state
|
||||
@ -126,7 +146,7 @@ final class EmojiListInputComponent: Component {
|
||||
)
|
||||
|
||||
var lastRowItemCount = component.reactionItems.count % itemsPerRow
|
||||
if lastRowItemCount == 0 {
|
||||
if lastRowItemCount == 0 && !component.reactionItems.isEmpty {
|
||||
lastRowItemCount = itemsPerRow
|
||||
}
|
||||
let trailingLineWidth = sideInset + CGFloat(lastRowItemCount) * (itemSize + itemSpacing) + placeholderSpacing
|
||||
@ -148,10 +168,13 @@ final class EmojiListInputComponent: Component {
|
||||
}
|
||||
transition.setPosition(view: trailingPlaceholderView, position: trailingPlaceholderFrame.origin)
|
||||
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)))
|
||||
self.caretIndicator.isHidden = !component.isInputActive
|
||||
}
|
||||
|
||||
var validIds: [Int64] = []
|
||||
@ -201,6 +224,11 @@ final class EmojiListInputComponent: Component {
|
||||
itemLayer.isVisibleForAnimations = true
|
||||
|
||||
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 {
|
||||
itemLayer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
|
||||
itemLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
|
@ -20,6 +20,7 @@ public final class EmojiSelectionComponent: Component {
|
||||
public let backgroundIconColor: UIColor?
|
||||
public let backgroundColor: UIColor
|
||||
public let separatorColor: UIColor
|
||||
public let backspace: (() -> Void)?
|
||||
|
||||
public init(
|
||||
theme: PresentationTheme,
|
||||
@ -30,7 +31,8 @@ public final class EmojiSelectionComponent: Component {
|
||||
emojiContent: EmojiPagerContentComponent,
|
||||
backgroundIconColor: UIColor?,
|
||||
backgroundColor: UIColor,
|
||||
separatorColor: UIColor
|
||||
separatorColor: UIColor,
|
||||
backspace: (() -> Void)?
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
@ -41,6 +43,7 @@ public final class EmojiSelectionComponent: Component {
|
||||
self.backgroundIconColor = backgroundIconColor
|
||||
self.backgroundColor = backgroundColor
|
||||
self.separatorColor = separatorColor
|
||||
self.backspace = backspace
|
||||
}
|
||||
|
||||
public static func ==(lhs: EmojiSelectionComponent, rhs: EmojiSelectionComponent) -> Bool {
|
||||
@ -71,6 +74,9 @@ public final class EmojiSelectionComponent: Component {
|
||||
if lhs.separatorColor != rhs.separatorColor {
|
||||
return false
|
||||
}
|
||||
if (lhs.backspace == nil) != (rhs.backspace == nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -83,6 +89,9 @@ public final class EmojiSelectionComponent: Component {
|
||||
private let shadowView: UIImageView
|
||||
private let cornersView: UIImageView
|
||||
|
||||
private let backspaceBackgroundView: UIImageView
|
||||
private let backspaceButtonView: HighlightTrackingButton
|
||||
|
||||
private var component: EmojiSelectionComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
@ -95,6 +104,9 @@ public final class EmojiSelectionComponent: Component {
|
||||
self.shadowView = UIImageView()
|
||||
self.cornersView = UIImageView()
|
||||
|
||||
self.backspaceBackgroundView = UIImageView()
|
||||
self.backspaceButtonView = HighlightTrackingButton()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.keyboardClippingView)
|
||||
@ -104,6 +116,9 @@ public final class EmojiSelectionComponent: Component {
|
||||
self.addSubview(self.cornersView)
|
||||
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
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
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.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)
|
||||
|
||||
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) {
|
||||
@ -132,12 +176,17 @@ public final class EmojiSelectionComponent: Component {
|
||||
deinit {
|
||||
}
|
||||
|
||||
@objc private func backspacePressed() {
|
||||
self.component?.backspace?()
|
||||
}
|
||||
|
||||
func update(component: EmojiSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
self.backgroundColor = component.backgroundColor
|
||||
let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85)
|
||||
self.panelBackgroundView.updateColor(color: panelBackgroundColor, transition: .immediate)
|
||||
self.panelSeparatorView.backgroundColor = component.separatorColor
|
||||
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
@ -148,13 +197,41 @@ public final class EmojiSelectionComponent: Component {
|
||||
|
||||
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(
|
||||
transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)),
|
||||
component: AnyComponent(EntityKeyboardComponent(
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
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),
|
||||
emojiContent: component.emojiContent,
|
||||
stickerContent: nil,
|
||||
|
@ -16,6 +16,66 @@ import Markdown
|
||||
import ButtonComponent
|
||||
import PremiumUI
|
||||
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 {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -62,18 +122,24 @@ final class PeerAllowedReactionsScreenComponent: Component {
|
||||
private(set) weak var state: EmptyComponentState?
|
||||
private var environment: EnvironmentType?
|
||||
|
||||
private var boostStatus: ChannelBoostStatus?
|
||||
private var boostStatusDisposable: Disposable?
|
||||
|
||||
private var isEnabled: Bool = false
|
||||
private var availableReactions: AvailableReactions?
|
||||
private var enabledReactions: [EmojiComponentReactionItem]?
|
||||
|
||||
private var emojiContent: EmojiPagerContentComponent?
|
||||
private var emojiContentDisposable: Disposable?
|
||||
private var caretPosition: Int?
|
||||
|
||||
private var displayInput: Bool = false
|
||||
|
||||
private var isApplyingSettings: Bool = false
|
||||
private var applyDisposable: Disposable?
|
||||
|
||||
private weak var currentUndoController: UndoOverlayController?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = UIScrollView()
|
||||
self.scrollView.showsVerticalScrollIndicator = true
|
||||
@ -97,6 +163,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
|
||||
deinit {
|
||||
self.emojiContentDisposable?.dispose()
|
||||
self.applyDisposable?.dispose()
|
||||
self.boostStatusDisposable?.dispose()
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
@ -123,13 +190,31 @@ final class PeerAllowedReactionsScreenComponent: Component {
|
||||
if self.isApplyingSettings {
|
||||
return
|
||||
}
|
||||
guard let enabledReactions = self.enabledReactions else {
|
||||
guard var enabledReactions = self.enabledReactions else {
|
||||
return
|
||||
}
|
||||
if !self.isEnabled {
|
||||
enabledReactions.removeAll()
|
||||
}
|
||||
|
||||
guard let availableReactions = self.availableReactions else {
|
||||
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.state?.updated(transition: .immediate)
|
||||
|
||||
@ -155,38 +240,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
|
||||
|
||||
switch error {
|
||||
case .boostRequired:
|
||||
let _ = combineLatest(
|
||||
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)
|
||||
})
|
||||
self.displayPremiumScreen()
|
||||
case .generic:
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
//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 {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
@ -228,6 +344,9 @@ final class PeerAllowedReactionsScreenComponent: Component {
|
||||
self.availableReactions = component.initialContent.availableReactions
|
||||
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 {
|
||||
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 }) {
|
||||
enabledReactions.remove(at: index)
|
||||
if let caretPosition = self.caretPosition, caretPosition > index {
|
||||
self.caretPosition = max(0, caretPosition - 1)
|
||||
}
|
||||
} else {
|
||||
let reaction: MessageReaction.Reaction
|
||||
if let availableReactions = self.availableReactions, let reactionItem = availableReactions.reactions.first(where: { $0.selectAnimation.fileId.id == itemFile.fileId.id }) {
|
||||
reaction = reactionItem.value
|
||||
} else {
|
||||
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
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
self.state?.updated(transition: .spring(duration: 0.25))
|
||||
}
|
||||
},
|
||||
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 {
|
||||
self.backgroundColor = environment.theme.list.blocksBackgroundColor
|
||||
}
|
||||
@ -340,7 +501,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
|
||||
component: AnyComponent(ListSwitchItemComponent(
|
||||
theme: environment.theme,
|
||||
title: environment.strings.PeerInfo_AllowedReactions_AllowAllText,
|
||||
value: true,
|
||||
value: self.isEnabled,
|
||||
valueUpdated: { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
@ -356,6 +517,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
self.enabledReactions = enabledReactions
|
||||
self.caretPosition = enabledReactions.count
|
||||
}
|
||||
} else {
|
||||
self.displayInput = false
|
||||
@ -467,6 +629,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
|
||||
placeholder: "Add Reactions...",
|
||||
reactionItems: enabledReactions,
|
||||
isInputActive: self.displayInput,
|
||||
caretPosition: caretPosition,
|
||||
activateInput: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
@ -475,6 +638,15 @@ final class PeerAllowedReactionsScreenComponent: Component {
|
||||
self.displayInput = true
|
||||
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: {},
|
||||
@ -584,13 +756,23 @@ final class PeerAllowedReactionsScreenComponent: Component {
|
||||
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)
|
||||
)))
|
||||
/*if self.remainingTimer > 0 {
|
||||
buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(
|
||||
AnimatedTextComponent(font: Font.with(size: 17.0, weight: .semibold, traits: .monospacedNumbers), color: environment.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.5), items: [
|
||||
AnimatedTextComponent.Item(id: AnyHashable(0 as Int), content: .number(self.remainingTimer, minDigits: 0))
|
||||
])
|
||||
)))
|
||||
}*/
|
||||
|
||||
let customReactionCount = self.isEnabled ? enabledReactions.filter({ item in
|
||||
switch item.reaction {
|
||||
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(
|
||||
transition: transition,
|
||||
@ -601,7 +783,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
|
||||
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
|
||||
),
|
||||
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(
|
||||
HStack(buttonContents, spacing: 5.0)
|
||||
VStack(buttonContents, spacing: 3.0)
|
||||
)),
|
||||
isEnabled: true,
|
||||
tintWhenDisabled: false,
|
||||
@ -640,8 +822,26 @@ final class PeerAllowedReactionsScreenComponent: Component {
|
||||
emojiContent: emojiContent.withSelectedItems(Set(enabledReactions.map(\.file.fileId))),
|
||||
backgroundIconColor: nil,
|
||||
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: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: min(340.0, max(50.0, availableSize.height - 200.0)))
|
||||
)
|
||||
|
@ -20,6 +20,10 @@ import MediaEditorScreen
|
||||
import ChatControllerInteraction
|
||||
|
||||
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)
|
||||
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)])
|
||||
|
@ -32,14 +32,16 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
|
||||
|
||||
let id: AnyHashable
|
||||
let label: Label
|
||||
let additionalBadgeLabel: String?
|
||||
let text: String
|
||||
let icon: UIImage?
|
||||
let iconSignal: Signal<UIImage?, NoError>?
|
||||
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.label = label
|
||||
self.additionalBadgeLabel = additionalBadgeLabel
|
||||
self.text = text
|
||||
self.icon = icon
|
||||
self.iconSignal = iconSignal
|
||||
@ -57,6 +59,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
|
||||
private let iconNode: ASImageNode
|
||||
private let labelBadgeNode: ASImageNode
|
||||
private let labelNode: ImmediateTextNode
|
||||
private var additionalLabelNode: ImmediateTextNode?
|
||||
private let textNode: ImmediateTextNode
|
||||
private let arrowNode: ASImageNode
|
||||
private let bottomSeparatorNode: ASDisplayNode
|
||||
@ -228,6 +231,13 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
|
||||
if self.labelBadgeNode.supernode == nil {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
if case .titleBadge = item.label {
|
||||
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 {
|
||||
labelBadgeNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth, y: floorToScreenPixels(labelFrame.midY - badgeDiameter / 2.0)), size: CGSize(width: badgeWidth, height: badgeDiameter))
|
||||
}
|
||||
|
@ -1615,7 +1615,11 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
|
||||
} else {
|
||||
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()
|
||||
}))
|
||||
}
|
||||
|
@ -392,7 +392,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
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)
|
||||
self.textNode.attributedText = attributedText
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
self.textNode.maximumNumberOfLines = 10
|
||||
displayUndo = false
|
||||
self.originalRemainingSeconds = 5
|
||||
case let .swipeToReply(title, text):
|
||||
@ -780,7 +780,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
return ("URL", contents)
|
||||
}), textAlignment: .natural)
|
||||
self.textNode.attributedText = attributedText
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
self.textNode.maximumNumberOfLines = 5
|
||||
|
||||
if text.contains("](") {
|
||||
isUserInteractionEnabled = true
|
||||
|
Loading…
x
Reference in New Issue
Block a user