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/ChatMessagePollBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/Components/BundleIconComponent",
],
visibility = [
"//visibility:public",

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)

View File

@ -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",

View File

@ -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)

View File

@ -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,

View File

@ -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)))
)

View File

@ -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)])

View File

@ -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))
}

View File

@ -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()
}))
}

View File

@ -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