[WIP] Custom channel reactions

This commit is contained in:
Ali 2023-11-04 00:37:02 +04:00
parent bc3bdcd623
commit 8051e43e4c
12 changed files with 1596 additions and 13 deletions

View File

@ -1188,6 +1188,10 @@ private final class LimitSheetContent: CombinedComponent {
case .nameColors:
titleText = strings.ChannelBoost_EnableColors
string = strings.ChannelBoost_EnableColorsText(valueString).string
case .channelReactions:
//TODO:localize
titleText = "Custom Reactions"
string = "Your channel needs \(valueString) to add custom emoji as reactions.\n\nAsk your **Premium** subscribers to boost your channel with this link:"
}
} else {
let storiesString = strings.ChannelBoost_StoriesPerDay(level)
@ -1769,11 +1773,12 @@ public class PremiumLimitScreen: ViewControllerComponentContainer {
case storiesWeekly
case storiesMonthly
public enum BoostSubject {
case stories
case nameColors
case channelReactions
}
case storiesChannelBoost(peer: EnginePeer, boostSubject: BoostSubject, isCurrent: Bool, level: Int32, currentLevelBoosts: Int32, nextLevelBoosts: Int32?, link: String?, myBoostCount: Int32, canBoostAgain: Bool)
}

View File

@ -597,6 +597,7 @@ public final class EngineMessageReactionListContext {
public enum UpdatePeerAllowedReactionsError {
case generic
case boostRequired
}
func _internal_updatePeerAllowedReactions(account: Account, peerId: PeerId, allowedReactions: PeerAllowedReactions) -> Signal<Never, UpdatePeerAllowedReactionsError> {
@ -620,11 +621,20 @@ func _internal_updatePeerAllowedReactions(account: Account, peerId: PeerId, allo
}
return account.network.request(Api.functions.messages.setChatAvailableReactions(peer: inputPeer, availableReactions: mappedReactions))
|> mapError { _ -> UpdatePeerAllowedReactionsError in
return .generic
|> map(Optional.init)
|> `catch` { error -> Signal<Api.Updates?, UpdatePeerAllowedReactionsError> in
if error.errorDescription == "CHAT_NOT_MODIFIED" {
return .single(nil)
} else if error.errorDescription == "BOOSTS_REQUIRED" {
return .fail(.boostRequired)
} else {
return .fail(.generic)
}
}
|> mapToSignal { result -> Signal<Never, UpdatePeerAllowedReactionsError> in
if let result = result {
account.stateManager.addUpdates(result)
}
return account.postbox.transaction { transaction -> Void in
transaction.updatePeerCachedData(peerIds: [peerId], update: { _, current in

View File

@ -409,6 +409,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatLoadingNode",
"//submodules/TelegramUI/Components/Settings/PeerNameColorScreen",
"//submodules/TelegramUI/Components/ContextMenuScreen",
"//submodules/TelegramUI/Components/PeerAllowedReactionsScreen",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],

View File

@ -164,7 +164,7 @@ private final class WarpView: UIView {
}
}
public struct EmojiComponentReactionItem {
public struct EmojiComponentReactionItem: Equatable {
public var reaction: MessageReaction.Reaction
public var file: TelegramMediaFile
@ -2767,6 +2767,32 @@ public final class EmojiPagerContentComponent: Component {
)
}
public func withSelectedItems(_ selectedItems: Set<MediaId>) -> EmojiPagerContentComponent {
return EmojiPagerContentComponent(
id: self.id,
context: self.context,
avatarPeer: self.avatarPeer,
animationCache: self.animationCache,
animationRenderer: self.animationRenderer,
inputInteractionHolder: self.inputInteractionHolder,
panelItemGroups: panelItemGroups,
contentItemGroups: contentItemGroups,
itemLayoutType: self.itemLayoutType,
itemContentUniqueId: itemContentUniqueId,
searchState: searchState,
warpContentsOnEdges: self.warpContentsOnEdges,
hideBackground: self.hideBackground,
displaySearchWithPlaceholder: self.displaySearchWithPlaceholder,
searchCategories: self.searchCategories,
searchInitiallyHidden: self.searchInitiallyHidden,
searchAlwaysActive: self.searchAlwaysActive,
searchIsPlaceholderOnly: self.searchIsPlaceholderOnly,
emptySearchResults: emptySearchResults,
enableLongPress: self.enableLongPress,
selectedItems: selectedItems
)
}
public static func ==(lhs: EmojiPagerContentComponent, rhs: EmojiPagerContentComponent) -> Bool {
if lhs === rhs {
return true
@ -7124,6 +7150,7 @@ public final class EmojiPagerContentComponent: Component {
case profilePhoto
case groupPhoto
case backgroundIcon
case reactionList
}
public static func emojiInputData(
@ -7195,14 +7222,14 @@ public final class EmojiPagerContentComponent: Component {
}
let availableReactions: Signal<AvailableReactions?, NoError>
if [.reaction, .quickReaction].contains(subject) {
if [.reaction, .quickReaction, .reactionList].contains(subject) {
availableReactions = context.engine.stickers.availableReactions()
} else {
availableReactions = .single(nil)
}
let searchCategories: Signal<EmojiSearchCategories?, NoError>
if [.emoji, .reaction].contains(subject) {
if [.emoji, .reaction, .reactionList].contains(subject) {
searchCategories = context.engine.stickers.emojiSearchCategories(kind: .emoji)
} else if case .status = subject {
searchCategories = context.engine.stickers.emojiSearchCategories(kind: .status)
@ -7587,6 +7614,51 @@ public final class EmojiPagerContentComponent: Component {
}
}
}
} else if subject == .reactionList {
var existingIds = Set<MessageReaction.Reaction>()
if let availableReactions = availableReactions {
for reactionItem in availableReactions.reactions {
if !reactionItem.isEnabled {
continue
}
if existingIds.contains(reactionItem.value) {
continue
}
existingIds.insert(reactionItem.value)
let icon: EmojiPagerContentComponent.Item.Icon
if !hasPremium, case .custom = reactionItem.value {
icon = .locked
} else {
icon = .none
}
var tintMode: Item.TintMode = .none
if reactionItem.selectAnimation.isCustomTemplateEmoji {
tintMode = .primary
}
let animationFile = reactionItem.selectAnimation
let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true)
let resultItem = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: animationFile,
subgroupId: nil,
icon: icon,
tintMode: tintMode
)
let groupId = "liked"
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem]))
}
}
}
} else if [.reaction, .quickReaction].contains(subject) {
var existingIds = Set<MessageReaction.Reaction>()
@ -8159,7 +8231,12 @@ public final class EmojiPagerContentComponent: Component {
)
}
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: featuredEmojiPack.info.title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: true, collapsedLineCount: 3, isClearable: false, headerItem: headerItem, items: [resultItem]))
var isFeatured = true
if case .reactionList = subject {
isFeatured = false
}
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: featuredEmojiPack.info.title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: isFeatured, collapsedLineCount: 3, isClearable: false, headerItem: headerItem, items: [resultItem]))
}
}
}

View File

@ -331,12 +331,13 @@ public final class EntityKeyboardComponent: Component {
let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [
"saved": .saved,
"recent": .recent,
"premium": .premium
"premium": .premium,
"liked": .liked
]
let titleMapping: [String: String] = [
"saved": component.strings.Stickers_Favorites,
"recent": component.strings.Stickers_Recent,
"premium": component.strings.EmojiInput_PanelTitlePremium
"premium": component.strings.EmojiInput_PanelTitlePremium,
]
if let icon = iconMapping[id], let title = titleMapping[id] {
topMaskItems.append(EntityKeyboardTopPanelComponent.Item(
@ -468,6 +469,7 @@ public final class EntityKeyboardComponent: Component {
let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [
"saved": .saved,
"recent": .recent,
"liked": .liked,
"premium": .premium
]
let titleMapping: [String: String] = [
@ -568,12 +570,14 @@ public final class EntityKeyboardComponent: Component {
for itemGroup in emojiContent.panelItemGroups {
if !itemGroup.items.isEmpty {
if let id = itemGroup.groupId.base as? String {
if id == "recent" {
if id == "recent" || id == "liked" {
let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [
"recent": .recent,
"liked": .liked,
]
let titleMapping: [String: String] = [
"recent": component.strings.Stickers_Recent,
"liked": "",
]
if let icon = iconMapping[id], let title = titleMapping[id] {
topEmojiItems.append(EntityKeyboardTopPanelComponent.Item(

View File

@ -276,6 +276,7 @@ final class EntityKeyboardIconTopPanelComponent: Component {
case recent
case saved
case premium
case liked
}
let icon: Icon
@ -360,6 +361,8 @@ final class EntityKeyboardIconTopPanelComponent: Component {
image = UIImage(bundleImageName: "Chat/Input/Media/PanelRecentIcon")
case .saved:
image = UIImage(bundleImageName: "Chat/Input/Media/PanelSavedIcon")
case .liked:
image = UIImage(bundleImageName: "Chat/Input/Media/PanelTrendingIcon")
case .premium:
image = generateImage(CGSize(width: 44.0, height: 44.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
@ -2078,7 +2081,7 @@ public final class EntityKeyboardTopPanelComponent: Component {
}
let isRound: Bool
if let string = activeContentItemId.base as? String, (string == "featuredTop" || string == "recent" || string == "static" || string == "trending") {
if let string = activeContentItemId.base as? String, (string == "featuredTop" || string == "recent" || string == "static" || string == "trending" || string == "liked") {
isRound = true
} else {
isRound = false

View File

@ -0,0 +1,35 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "PeerAllowedReactionsScreen",
module_name = "PeerAllowedReactionsScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/AppBundle",
"//submodules/Components/ViewControllerComponent",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramCore",
"//submodules/Postbox",
"//submodules/AccountContext",
"//submodules/TelegramUI/Components/EntityKeyboard",
"//submodules/TelegramUI/Components/SwitchComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/Markdown",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/Components/PagerComponent",
"//submodules/PremiumUI",
"//submodules/UndoUI",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,240 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import SwitchComponent
import EntityKeyboard
import AccountContext
final class EmojiListInputComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let placeholder: String
let reactionItems: [EmojiComponentReactionItem]
let isInputActive: Bool
let activateInput: () -> Void
init(
context: AccountContext,
theme: PresentationTheme,
placeholder: String,
reactionItems: [EmojiComponentReactionItem],
isInputActive: Bool,
activateInput: @escaping () -> Void
) {
self.context = context
self.theme = theme
self.placeholder = placeholder
self.reactionItems = reactionItems
self.isInputActive = isInputActive
self.activateInput = activateInput
}
static func ==(lhs: EmojiListInputComponent, rhs: EmojiListInputComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.placeholder != rhs.placeholder {
return false
}
if lhs.reactionItems != rhs.reactionItems {
return false
}
if lhs.isInputActive != rhs.isInputActive {
return false
}
return true
}
final class View: UIView {
private var component: EmojiListInputComponent?
private weak var state: EmptyComponentState?
private var itemLayers: [Int64: EmojiPagerContentComponent.View.ItemLayer] = [:]
private let trailingPlaceholder = ComponentView<Empty>()
private let caretIndicator: UIImageView
override init(frame: CGRect) {
self.caretIndicator = UIImageView()
self.caretIndicator.image = generateImage(CGSize(width: 2.0, height: 4.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: size.width * 0.5).cgPath)
context.fillPath()
})?.stretchableImage(withLeftCapWidth: 1, topCapHeight: 2).withRenderingMode(.alwaysTemplate)
super.init(frame: frame)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
required init(coder: NSCoder) {
preconditionFailure()
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
let point = recognizer.location(in: self)
var tapOnItem = false
for (_, itemLayer) in self.itemLayers {
if itemLayer.frame.insetBy(dx: -6.0, dy: -6.0).contains(point) {
tapOnItem = true
break
}
}
if !tapOnItem {
self.component?.activateInput()
}
}
}
func update(component: EmojiListInputComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let verticalInset: CGFloat = 12.0
let placeholderSpacing: CGFloat = 6.0
let minItemSize: CGFloat = 24.0
let itemSpacingFactor: CGFloat = 0.15
let minSideInset: CGFloat = 12.0
self.backgroundColor = component.theme.list.itemBlocksBackgroundColor
self.layer.cornerRadius = 12.0
let maxItemsWidth = availableSize.width - minSideInset * 2.0
let itemsPerRow = Int(floor((maxItemsWidth + minItemSize * itemSpacingFactor) / (minItemSize + minItemSize * itemSpacingFactor)))
let itemSizePlusSpacing = maxItemsWidth / CGFloat(itemsPerRow)
let itemSize = floor(itemSizePlusSpacing * (1.0 - itemSpacingFactor))
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
self.component = component
self.state = state
let trailingPlaceholderSize = self.trailingPlaceholder.update(
transition: .immediate,
component: AnyComponent(Text(text: component.placeholder, font: Font.regular(17.0), color: component.theme.list.itemPlaceholderTextColor)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
)
var lastRowItemCount = component.reactionItems.count % itemsPerRow
if lastRowItemCount == 0 {
lastRowItemCount = itemsPerRow
}
let trailingLineWidth = sideInset + CGFloat(lastRowItemCount) * (itemSize + itemSpacing) + placeholderSpacing
var contentHeight: CGFloat = verticalInset * 2.0 + CGFloat(rowCount) * itemSize + CGFloat(max(0, rowCount - 1)) * itemSpacing
let trailingPlaceholderFrame: CGRect
if availableSize.width - sideInset - trailingLineWidth < trailingPlaceholderSize.width {
contentHeight += itemSize + itemSpacing
trailingPlaceholderFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset + CGFloat(rowCount) * (itemSize + itemSpacing) + floor((itemSize - trailingPlaceholderSize.height) * 0.5)), size: trailingPlaceholderSize)
} else {
trailingPlaceholderFrame = CGRect(origin: CGPoint(x: trailingLineWidth, y: verticalInset + CGFloat(rowCount - 1) * (itemSize + itemSpacing) + floor((itemSize - trailingPlaceholderSize.height) * 0.5)), size: trailingPlaceholderSize)
}
if let trailingPlaceholderView = self.trailingPlaceholder.view {
if trailingPlaceholderView.superview == nil {
trailingPlaceholderView.layer.anchorPoint = CGPoint()
self.addSubview(trailingPlaceholderView)
self.addSubview(self.caretIndicator)
}
transition.setPosition(view: trailingPlaceholderView, position: trailingPlaceholderFrame.origin)
trailingPlaceholderView.bounds = CGRect(origin: CGPoint(), size: trailingPlaceholderFrame.size)
self.caretIndicator.tintColor = component.theme.list.itemAccentColor
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] = []
for i in 0 ..< component.reactionItems.count {
let item = component.reactionItems[i]
let itemKey = item.file.fileId.id
validIds.append(itemKey)
let itemFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i % itemsPerRow) * (itemSize + itemSpacing), y: verticalInset + CGFloat(i / itemsPerRow) * (itemSize + itemSpacing)), size: CGSize(width: itemSize, height: itemSize))
var itemTransition = transition
var animateIn = false
let itemLayer: EmojiPagerContentComponent.View.ItemLayer
if let current = self.itemLayers[itemKey] {
itemLayer = current
} else {
itemTransition = .immediate
animateIn = true
let animationData = EntityKeyboardAnimationData(
file: item.file
)
itemLayer = EmojiPagerContentComponent.View.ItemLayer(
item: EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: item.file,
subgroupId: nil,
icon: .none,
tintMode: .none
),
context: component.context,
attemptSynchronousLoad: false,
content: EmojiPagerContentComponent.ItemContent.animation(animationData),
cache: component.context.animationCache,
renderer: component.context.animationRenderer,
placeholderColor: component.theme.list.mediaPlaceholderColor,
blurredBadgeColor: .clear,
accentIconColor: component.theme.list.itemAccentColor,
pointSize: CGSize(width: 32.0, height: 32.0),
onUpdateDisplayPlaceholder: { _, _ in
}
)
self.itemLayers[itemKey] = itemLayer
self.layer.addSublayer(itemLayer)
}
itemLayer.isVisibleForAnimations = true
itemTransition.setFrame(layer: itemLayer, frame: itemFrame)
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)
}
}
var removedIds: [Int64] = []
for (key, itemLayer) in self.itemLayers {
if !validIds.contains(key) {
removedIds.append(key)
if !transition.animation.isImmediate {
itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemLayer] _ in
itemLayer?.removeFromSuperlayer()
})
itemLayer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false)
} else {
itemLayer.removeFromSuperlayer()
}
}
}
for key in removedIds {
self.itemLayers.removeValue(forKey: key)
}
return CGSize(width: availableSize.width, height: contentHeight)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -0,0 +1,224 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import EntityKeyboard
import AccountContext
import PagerComponent
public final class EmojiSelectionComponent: Component {
public typealias EnvironmentType = Empty
public let theme: PresentationTheme
public let strings: PresentationStrings
public let sideInset: CGFloat
public let bottomInset: CGFloat
public let deviceMetrics: DeviceMetrics
public let emojiContent: EmojiPagerContentComponent
public let backgroundIconColor: UIColor?
public let backgroundColor: UIColor
public let separatorColor: UIColor
public init(
theme: PresentationTheme,
strings: PresentationStrings,
sideInset: CGFloat,
bottomInset: CGFloat,
deviceMetrics: DeviceMetrics,
emojiContent: EmojiPagerContentComponent,
backgroundIconColor: UIColor?,
backgroundColor: UIColor,
separatorColor: UIColor
) {
self.theme = theme
self.strings = strings
self.sideInset = sideInset
self.bottomInset = bottomInset
self.deviceMetrics = deviceMetrics
self.emojiContent = emojiContent
self.backgroundIconColor = backgroundIconColor
self.backgroundColor = backgroundColor
self.separatorColor = separatorColor
}
public static func ==(lhs: EmojiSelectionComponent, rhs: EmojiSelectionComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings != rhs.strings {
return false
}
if lhs.sideInset != rhs.sideInset {
return false
}
if lhs.bottomInset != rhs.bottomInset {
return false
}
if lhs.deviceMetrics != rhs.deviceMetrics {
return false
}
if lhs.emojiContent != rhs.emojiContent {
return false
}
if lhs.backgroundIconColor != rhs.backgroundIconColor {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.separatorColor != rhs.separatorColor {
return false
}
return true
}
public final class View: UIView {
private let keyboardView: ComponentView<Empty>
private let keyboardClippingView: UIView
private let panelHostView: PagerExternalTopPanelContainer
private let panelBackgroundView: BlurredBackgroundView
private let panelSeparatorView: UIView
private let shadowView: UIImageView
private let cornersView: UIImageView
private var component: EmojiSelectionComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.keyboardView = ComponentView<Empty>()
self.keyboardClippingView = UIView()
self.panelHostView = PagerExternalTopPanelContainer()
self.panelBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
self.panelSeparatorView = UIView()
self.shadowView = UIImageView()
self.cornersView = UIImageView()
super.init(frame: frame)
self.addSubview(self.keyboardClippingView)
self.addSubview(self.panelBackgroundView)
self.addSubview(self.panelSeparatorView)
self.addSubview(self.panelHostView)
self.addSubview(self.cornersView)
self.addSubview(self.shadowView)
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)
context.setFillColor(UIColor.black.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 8.0), size: size))
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 8.0), size: size).insetBy(dx: -0.5, dy: -0.5))
})?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 16)
self.cornersView.image = generateImage(CGSize(width: 16.0 + 1.0, height: 16.0), rotatedContext: { size, context in
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: 0.0, y: 8.0), size: size), cornerRadius: 8.0).cgPath)
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)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
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
self.component = component
self.state = state
self.cornersView.tintColor = component.theme.list.blocksBackgroundColor
transition.setFrame(view: self.cornersView, frame: CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: CGSize(width: availableSize.width, height: 16.0)))
transition.setFrame(view: self.shadowView, frame: CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: CGSize(width: availableSize.width, height: 16.0)))
let topPanelHeight: CGFloat = 42.0
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),
topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0),
emojiContent: component.emojiContent,
stickerContent: nil,
maskContent: nil,
gifContent: nil,
hasRecentGifs: false,
availableGifSearchEmojies: [],
defaultToEmojiTab: true,
externalTopPanelContainer: self.panelHostView,
externalBottomPanelContainer: nil,
displayTopPanelBackground: .blur,
topPanelExtensionUpdated: { _, _ in },
topPanelScrollingOffset: { _, _ in },
hideInputUpdated: { _, _, _ in },
hideTopPanelUpdated: { _, _ in },
switchToTextInput: {},
switchToGifSubject: { _ in },
reorderItems: { _, _ in },
makeSearchContainerNode: { _ in return nil },
contentIdUpdated: { _ in },
deviceMetrics: component.deviceMetrics,
hiddenInputHeight: 0.0,
inputHeight: 0.0,
displayBottomPanel: false,
isExpanded: true,
clipContentToTopPanel: false,
useExternalSearchContainer: false,
customTintColor: component.backgroundIconColor
)),
environment: {},
containerSize: availableSize
)
if let keyboardComponentView = self.keyboardView.view {
if keyboardComponentView.superview == nil {
self.keyboardClippingView.addSubview(keyboardComponentView)
}
if panelBackgroundColor.alpha < 0.01 {
self.keyboardClippingView.clipsToBounds = true
} else {
self.keyboardClippingView.clipsToBounds = false
}
transition.setFrame(view: self.keyboardClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelHeight)))
transition.setFrame(view: keyboardComponentView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topPanelHeight), size: keyboardSize))
transition.setFrame(view: self.panelHostView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight - 34.0), size: CGSize(width: keyboardSize.width, height: 0.0)))
transition.setFrame(view: self.panelBackgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: keyboardSize.width, height: topPanelHeight)))
self.panelBackgroundView.update(size: self.panelBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition)
transition.setFrame(view: self.panelSeparatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: keyboardSize.width, height: UIScreenPixel)))
transition.setAlpha(view: self.panelSeparatorView, alpha: 1.0)
}
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -0,0 +1,116 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import SwitchComponent
final class ListSwitchItemComponent: Component {
let theme: PresentationTheme
let title: String
let value: Bool
let valueUpdated: (Bool) -> Void
init(
theme: PresentationTheme,
title: String,
value: Bool,
valueUpdated: @escaping (Bool) -> Void
) {
self.theme = theme
self.title = title
self.value = value
self.valueUpdated = valueUpdated
}
static func ==(lhs: ListSwitchItemComponent, rhs: ListSwitchItemComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.value != rhs.value {
return false
}
return true
}
final class View: UIView {
private let title = ComponentView<Empty>()
private let switchView = ComponentView<Empty>()
private var component: ListSwitchItemComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(component: ListSwitchItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
self.backgroundColor = component.theme.list.itemBlocksBackgroundColor
self.layer.cornerRadius = 12.0
let size = CGSize(width: availableSize.width, height: 44.0)
let rightInset: CGFloat = 16.0
let leftInset: CGFloat = 16.0
let spacing: CGFloat = 8.0
let switchSize = self.switchView.update(
transition: transition,
component: AnyComponent(SwitchComponent(
value: component.value,
valueUpdated: { [weak self] value in
guard let self else {
return
}
self.component?.valueUpdated(value)
}
)),
environment: {},
containerSize: size
)
let switchFrame = CGRect(origin: CGPoint(x: size.width - rightInset - switchSize.width, y: floor((size.height - switchSize.height) * 0.5)), size: switchSize)
if let switchComponentView = self.switchView.view {
if switchComponentView.superview == nil {
self.addSubview(switchComponentView)
}
transition.setFrame(view: switchComponentView, frame: switchFrame)
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(Text(text: component.title, font: Font.regular(17.0), color: component.theme.list.itemPrimaryTextColor)),
environment: {},
containerSize: CGSize(width: max(1.0, switchFrame.minX - spacing - leftInset), height: .greatestFiniteMagnitude)
)
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint()
self.addSubview(titleView)
}
transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -0,0 +1,856 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import AppBundle
import ViewControllerComponent
import AccountContext
import TelegramCore
import Postbox
import SwiftSignalKit
import EntityKeyboard
import MultilineTextComponent
import Markdown
import ButtonComponent
import PremiumUI
import UndoUI
final class PeerAllowedReactionsScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let peerId: EnginePeer.Id
let initialContent: PeerAllowedReactionsScreen.Content
init(
context: AccountContext,
peerId: EnginePeer.Id,
initialContent: PeerAllowedReactionsScreen.Content
) {
self.context = context
self.peerId = peerId
self.initialContent = initialContent
}
static func ==(lhs: PeerAllowedReactionsScreenComponent, rhs: PeerAllowedReactionsScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peerId != rhs.peerId {
return false
}
return true
}
final class View: UIView, UIScrollViewDelegate {
private let scrollView: UIScrollView
private let switchItem = ComponentView<Empty>()
private let switchInfoText = ComponentView<Empty>()
private var reactionsTitleText: ComponentView<Empty>?
private var reactionsInfoText: ComponentView<Empty>?
private var reactionInput: ComponentView<Empty>?
private let actionButton = ComponentView<Empty>()
private var reactionSelectionControl: ComponentView<Empty>?
private var isUpdating: Bool = false
private var component: PeerAllowedReactionsScreenComponent?
private(set) weak var state: EmptyComponentState?
private var environment: EnvironmentType?
private var isEnabled: Bool = false
private var availableReactions: AvailableReactions?
private var enabledReactions: [EmojiComponentReactionItem]?
private var emojiContent: EmojiPagerContentComponent?
private var emojiContentDisposable: Disposable?
private var displayInput: Bool = false
private var isApplyingSettings: Bool = false
private var applyDisposable: Disposable?
override init(frame: CGRect) {
self.scrollView = UIScrollView()
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.scrollsToTop = false
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.scrollView.contentInsetAdjustmentBehavior = .never
self.scrollView.alwaysBounceVertical = true
super.init(frame: frame)
self.scrollView.delegate = self
self.addSubview(self.scrollView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.emojiContentDisposable?.dispose()
self.applyDisposable?.dispose()
}
func scrollToTop() {
self.scrollView.setContentOffset(CGPoint(), animated: true)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrolling(transition: .immediate)
}
private func updateScrolling(transition: Transition) {
let navigationAlphaDistance: CGFloat = 16.0
let navigationAlpha: CGFloat = max(0.0, min(1.0, self.scrollView.contentOffset.y / navigationAlphaDistance))
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha)
transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha)
}
}
private func applySettings() {
guard let component = self.component else {
return
}
if self.isApplyingSettings {
return
}
guard let enabledReactions = self.enabledReactions else {
return
}
guard let availableReactions = self.availableReactions else {
return
}
self.isApplyingSettings = true
self.state?.updated(transition: .immediate)
self.applyDisposable?.dispose()
let allowedReactions: PeerAllowedReactions
if self.isEnabled {
if Set(availableReactions.reactions.map(\.value)) == Set(enabledReactions.map(\.reaction)) {
allowedReactions = .all
} else {
allowedReactions = .limited(enabledReactions.map(\.reaction))
}
} else {
allowedReactions = .empty
}
self.applyDisposable = (component.context.engine.peers.updatePeerAllowedReactions(peerId: component.peerId, allowedReactions: allowedReactions)
|> deliverOnMainQueue).start(error: { [weak self] error in
guard let self, let component = self.component else {
return
}
self.isApplyingSettings = false
self.state?.updated(transition: .immediate)
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)
})
case .generic:
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: "An error occurred", actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}
}, completed: { [weak self] in
guard let self else {
return
}
self.environment?.controller()?.dismiss()
})
}
func update(component: PeerAllowedReactionsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[EnvironmentType.self].value
let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment
self.component = component
self.state = state
let topInset: CGFloat = 24.0
let bottomInset: CGFloat = 8.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let textSideInset: CGFloat = 16.0
let enabledReactions: [EmojiComponentReactionItem]
if let current = self.enabledReactions {
enabledReactions = current
} else {
enabledReactions = component.initialContent.enabledReactions
self.enabledReactions = enabledReactions
self.availableReactions = component.initialContent.availableReactions
self.isEnabled = component.initialContent.isEnabled
}
if self.emojiContentDisposable == nil {
let emojiContent = EmojiPagerContentComponent.emojiInputData(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
isStandalone: false,
subject: .reactionList,
hasTrending: false,
topReactionItems: [],
areUnicodeEmojiEnabled: false,
areCustomEmojiEnabled: true,
chatPeerId: nil,
selectedItems: Set(),
backgroundIconColor: nil,
hasSearch: false,
forceHasPremium: true
)
self.emojiContentDisposable = (emojiContent
|> deliverOnMainQueue).start(next: { [weak self] emojiContent in
guard let self else {
return
}
self.emojiContent = emojiContent
emojiContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak self] _, item, _, _, _, _ in
guard let self, var enabledReactions = self.enabledReactions else {
return
}
if self.isApplyingSettings {
return
}
guard let itemFile = item.itemFile else {
return
}
if let index = enabledReactions.firstIndex(where: { $0.file.fileId.id == itemFile.fileId.id }) {
enabledReactions.remove(at: index)
} 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)
}
enabledReactions.append(EmojiComponentReactionItem(reaction: reaction, file: itemFile))
}
self.enabledReactions = enabledReactions
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
},
deleteBackwards: {
},
openStickerSettings: {
},
openFeatured: {
},
openSearch: {
},
addGroupAction: { _, _, _ in
},
clearGroup: { _ in
},
pushController: { c in
},
presentController: { c in
},
presentGlobalOverlayController: { c in
},
navigationController: {
return nil
},
requestUpdate: { _ in
},
updateSearchQuery: { _ in
},
updateScrollingToItemGroup: {
},
onScroll: {},
chatPeerId: nil,
peekBehavior: nil,
customLayout: nil,
externalBackground: nil,
externalExpansionView: nil,
customContentView: nil,
useOpaqueTheme: true,
hideBackground: false,
stateContext: nil,
addImage: nil
)
if !self.isUpdating {
self.state?.updated(transition: .immediate)
}
})
}
if themeUpdated {
self.backgroundColor = environment.theme.list.blocksBackgroundColor
}
var contentHeight: CGFloat = 0.0
contentHeight += environment.navigationHeight
contentHeight += topInset
let switchSize = self.switchItem.update(
transition: transition,
component: AnyComponent(ListSwitchItemComponent(
theme: environment.theme,
title: environment.strings.PeerInfo_AllowedReactions_AllowAllText,
value: true,
valueUpdated: { [weak self] value in
guard let self else {
return
}
if self.isEnabled != value {
self.isEnabled = value
if self.isEnabled {
if var enabledReactions = self.enabledReactions, enabledReactions.isEmpty {
if let availableReactions = self.availableReactions {
for reactionItem in availableReactions.reactions {
enabledReactions.append(EmojiComponentReactionItem(reaction: reactionItem.value, file: reactionItem.selectAnimation))
}
}
self.enabledReactions = enabledReactions
}
} else {
self.displayInput = false
}
self.state?.updated(transition: .easeInOut(duration: 0.25))
}
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude)
)
let switchFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: switchSize)
if let switchView = self.switchItem.view {
if switchView.superview == nil {
self.scrollView.addSubview(switchView)
}
transition.setFrame(view: switchView, frame: switchFrame)
}
contentHeight += switchSize.height
contentHeight += 7.0
//TODO:localize
let switchInfoTextSize = self.switchInfoText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "You can add emoji from any emoji pack as a reaction.",
font: Font.regular(13.0),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - textSideInset * 2.0, height: .greatestFiniteMagnitude)
)
let switchInfoTextFrame = CGRect(origin: CGPoint(x: sideInset + textSideInset, y: contentHeight), size: switchInfoTextSize)
if let switchInfoTextView = self.switchInfoText.view {
if switchInfoTextView.superview == nil {
switchInfoTextView.layer.anchorPoint = CGPoint()
self.scrollView.addSubview(switchInfoTextView)
}
transition.setPosition(view: switchInfoTextView, position: switchInfoTextFrame.origin)
switchInfoTextView.bounds = CGRect(origin: CGPoint(), size: switchInfoTextFrame.size)
}
contentHeight += switchInfoTextSize.height
contentHeight += 37.0
if self.isEnabled {
var animateIn = false
let reactionsTitleText: ComponentView<Empty>
if let current = self.reactionsTitleText {
reactionsTitleText = current
} else {
reactionsTitleText = ComponentView()
self.reactionsTitleText = reactionsTitleText
animateIn = true
}
//TODO:localize
let reactionsTitleTextSize = reactionsTitleText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "AVAILABLE REACTIONS",
font: Font.regular(13.0),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - textSideInset * 2.0, height: .greatestFiniteMagnitude)
)
let reactionsTitleTextFrame = CGRect(origin: CGPoint(x: sideInset + textSideInset, y: contentHeight), size: reactionsTitleTextSize)
if let reactionsTitleTextView = reactionsTitleText.view {
if reactionsTitleTextView.superview == nil {
reactionsTitleTextView.layer.anchorPoint = CGPoint()
self.scrollView.addSubview(reactionsTitleTextView)
}
if animateIn {
reactionsTitleTextView.frame = reactionsTitleTextFrame
if !transition.animation.isImmediate {
reactionsTitleTextView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
} else {
transition.setPosition(view: reactionsTitleTextView, position: reactionsTitleTextFrame.origin)
reactionsTitleTextView.bounds = CGRect(origin: CGPoint(), size: reactionsTitleTextFrame.size)
}
}
contentHeight += reactionsTitleTextSize.height
contentHeight += 6.0
let reactionInput: ComponentView<Empty>
if let current = self.reactionInput {
reactionInput = current
} else {
reactionInput = ComponentView()
self.reactionInput = reactionInput
}
//TOOD:localize
let reactionInputSize = reactionInput.update(
transition: animateIn ? .immediate : transition,
component: AnyComponent(EmojiListInputComponent(
context: component.context,
theme: environment.theme,
placeholder: "Add Reactions...",
reactionItems: enabledReactions,
isInputActive: self.displayInput,
activateInput: { [weak self] in
guard let self else {
return
}
if self.emojiContent != nil && !self.displayInput {
self.displayInput = true
self.state?.updated(transition: .spring(duration: 0.5))
}
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude)
)
let reactionInputFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: reactionInputSize)
if let reactionInputView = reactionInput.view {
if reactionInputView.superview == nil {
self.scrollView.addSubview(reactionInputView)
}
if animateIn {
reactionInputView.frame = reactionInputFrame
if !transition.animation.isImmediate {
reactionInputView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
} else {
transition.setFrame(view: reactionInputView, frame: reactionInputFrame)
}
}
contentHeight += reactionInputSize.height
contentHeight += 7.0
let reactionsInfoText: ComponentView<Empty>
if let current = self.reactionsInfoText {
reactionsInfoText = current
} else {
reactionsInfoText = ComponentView()
self.reactionsInfoText = reactionsInfoText
}
//TODO:localize
let body = MarkdownAttributeSet(font: UIFont.systemFont(ofSize: 13.0), textColor: environment.theme.list.freeTextColor)
let link = MarkdownAttributeSet(font: UIFont.systemFont(ofSize: 13.0), textColor: environment.theme.list.itemAccentColor, additionalAttributes: ["URL": true as NSNumber])
let attributes = MarkdownAttributes(body: body, bold: body, link: link, linkAttribute: { _ in
return nil
})
let reactionsInfoTextSize = reactionsInfoText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .markdown(text: "You can also [create your own]() emoji packs and use them.", attributes: attributes),
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - textSideInset * 2.0, height: .greatestFiniteMagnitude)
)
let reactionsInfoTextFrame = CGRect(origin: CGPoint(x: sideInset + textSideInset, y: contentHeight), size: reactionsInfoTextSize)
if let reactionsInfoTextView = reactionsInfoText.view {
if reactionsInfoTextView.superview == nil {
reactionsInfoTextView.layer.anchorPoint = CGPoint()
self.scrollView.addSubview(reactionsInfoTextView)
}
if animateIn {
reactionsInfoTextView.frame = reactionsInfoTextFrame
if !transition.animation.isImmediate {
reactionsInfoTextView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
} else {
transition.setPosition(view: reactionsInfoTextView, position: reactionsInfoTextFrame.origin)
reactionsInfoTextView.bounds = CGRect(origin: CGPoint(), size: reactionsInfoTextFrame.size)
}
}
contentHeight += reactionsInfoTextSize.height
contentHeight += 6.0
} else {
if let reactionsTitleText = self.reactionsTitleText {
self.reactionsTitleText = nil
if let reactionsTitleTextView = reactionsTitleText.view {
if !transition.animation.isImmediate {
reactionsTitleTextView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionsTitleTextView] _ in
reactionsTitleTextView?.removeFromSuperview()
})
} else {
reactionsTitleTextView.removeFromSuperview()
}
}
}
if let reactionInput = self.reactionInput {
self.reactionInput = nil
if let reactionInputView = reactionInput.view {
if !transition.animation.isImmediate {
reactionInputView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionInputView] _ in
reactionInputView?.removeFromSuperview()
})
} else {
reactionInputView.removeFromSuperview()
}
}
}
if let reactionsInfoText = self.reactionsInfoText {
self.reactionsInfoText = nil
if let reactionsInfoTextView = reactionsInfoText.view {
if !transition.animation.isImmediate {
reactionsInfoTextView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionsInfoTextView] _ in
reactionsInfoTextView?.removeFromSuperview()
})
} else {
reactionsInfoTextView.removeFromSuperview()
}
}
}
}
//TODO:localize
var buttonContents: [AnyComponentWithIdentity<Empty>] = []
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 buttonSize = self.actionButton.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
color: environment.theme.list.itemCheckColors.fillColor,
foreground: environment.theme.list.itemCheckColors.foregroundColor,
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
),
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(
HStack(buttonContents, spacing: 5.0)
)),
isEnabled: true,
tintWhenDisabled: false,
displaysProgress: self.isApplyingSettings,
action: { [weak self] in
guard let self else {
return
}
self.applySettings()
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
)
contentHeight += buttonSize.height
var inputHeight: CGFloat = 0.0
if self.displayInput, let emojiContent = self.emojiContent {
let reactionSelectionControl: ComponentView<Empty>
var animateIn = false
if let current = self.reactionSelectionControl {
reactionSelectionControl = current
} else {
animateIn = true
reactionSelectionControl = ComponentView()
self.reactionSelectionControl = reactionSelectionControl
}
let reactionSelectionControlSize = reactionSelectionControl.update(
transition: animateIn ? .immediate : transition,
component: AnyComponent(EmojiSelectionComponent(
theme: environment.theme,
strings: environment.strings,
sideInset: environment.safeInsets.left,
bottomInset: environment.safeInsets.bottom,
deviceMetrics: environment.deviceMetrics,
emojiContent: emojiContent.withSelectedItems(Set(enabledReactions.map(\.file.fileId))),
backgroundIconColor: nil,
backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
separatorColor: environment.theme.list.itemBlocksSeparatorColor)
),
environment: {},
containerSize: CGSize(width: availableSize.width, height: min(340.0, max(50.0, availableSize.height - 200.0)))
)
let reactionSelectionControlFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - reactionSelectionControlSize.height), size: reactionSelectionControlSize)
if let reactionSelectionControlView = reactionSelectionControl.view {
if reactionSelectionControlView.superview == nil {
self.addSubview(reactionSelectionControlView)
}
if animateIn {
reactionSelectionControlView.frame = reactionSelectionControlFrame
transition.animatePosition(view: reactionSelectionControlView, from: CGPoint(x: 0.0, y: reactionSelectionControlFrame.height), to: CGPoint(), additive: true)
} else {
transition.setFrame(view: reactionSelectionControlView, frame: reactionSelectionControlFrame)
}
}
inputHeight = reactionSelectionControlSize.height
} else if let reactionSelectionControl = self.reactionSelectionControl {
self.reactionSelectionControl = nil
if let reactionSelectionControlView = reactionSelectionControl.view {
transition.setPosition(view: reactionSelectionControlView, position: CGPoint(x: reactionSelectionControlView.center.x, y: availableSize.height + reactionSelectionControlView.bounds.height * 0.5), completion: { [weak reactionSelectionControlView] _ in
reactionSelectionControlView?.removeFromSuperview()
})
}
}
let buttonY: CGFloat
if self.displayInput {
contentHeight += bottomInset + 8.0
contentHeight += inputHeight
buttonY = availableSize.height - bottomInset - 8.0 - inputHeight - buttonSize.height
} else {
contentHeight += bottomInset
contentHeight += environment.safeInsets.bottom
buttonY = availableSize.height - bottomInset - environment.safeInsets.bottom - buttonSize.height
}
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: buttonY), size: buttonSize)
if let buttonView = self.actionButton.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
transition.setFrame(view: buttonView, frame: buttonFrame)
}
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
}
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: environment.safeInsets.bottom, right: 0.0)
if self.scrollView.scrollIndicatorInsets != scrollInsets {
self.scrollView.scrollIndicatorInsets = scrollInsets
}
self.updateScrolling(transition: transition)
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public class PeerAllowedReactionsScreen: ViewControllerComponentContainer {
public final class Content: Equatable {
public let isEnabled: Bool
public let enabledReactions: [EmojiComponentReactionItem]
public let availableReactions: AvailableReactions?
init(
isEnabled: Bool,
enabledReactions: [EmojiComponentReactionItem],
availableReactions: AvailableReactions?
) {
self.isEnabled = isEnabled
self.enabledReactions = enabledReactions
self.availableReactions = availableReactions
}
public static func ==(lhs: Content, rhs: Content) -> Bool {
if lhs === rhs {
return true
}
if lhs.isEnabled != rhs.isEnabled {
return false
}
if lhs.enabledReactions != rhs.enabledReactions {
return false
}
if lhs.availableReactions != rhs.availableReactions {
return false
}
return true
}
}
private let context: AccountContext
private var isDismissed: Bool = false
public init(
context: AccountContext,
peerId: EnginePeer.Id,
initialContent: Content
) {
self.context = context
super.init(context: context, component: PeerAllowedReactionsScreenComponent(
context: context,
peerId: peerId,
initialContent: initialContent
), navigationBarAppearance: .default, theme: .default)
self.scrollToTop = { [weak self] in
guard let self, let componentView = self.node.hostView.componentView as? PeerAllowedReactionsScreenComponent.View else {
return
}
componentView.scrollToTop()
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.title = presentationData.strings.PeerInfo_AllowedReactions_Title
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)), animated: false)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
@objc private func cancelPressed() {
self.dismiss()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
public static func content(context: AccountContext, peerId: EnginePeer.Id) -> Signal<Content, NoError> {
return combineLatest(
context.engine.stickers.availableReactions(),
context.account.postbox.combinedView(keys: [.cachedPeerData(peerId: peerId)])
)
|> mapToSignal { availableReactions, combinedView -> Signal<Content, NoError> in
guard let cachedDataView = combinedView.views[.cachedPeerData(peerId: peerId)] as? CachedPeerDataView, let cachedData = cachedDataView.cachedPeerData as? CachedChannelData else {
return .complete()
}
var reactions: [MessageReaction.Reaction] = []
var isEnabled = false
if let allowedReactions = cachedData.allowedReactions.knownValue {
switch allowedReactions {
case .all:
isEnabled = true
if let availableReactions {
reactions = availableReactions.reactions.map(\.value)
}
case let .limited(list):
isEnabled = true
reactions.append(contentsOf: list)
case .empty:
isEnabled = false
}
}
var missingReactionFiles: [Int64] = []
for reaction in reactions {
if let availableReactions, let _ = availableReactions.reactions.first(where: { $0.value == reaction }) {
} else {
if case let .custom(fileId) = reaction {
if !missingReactionFiles.contains(fileId) {
missingReactionFiles.append(fileId)
}
}
}
}
return context.engine.stickers.resolveInlineStickers(fileIds: missingReactionFiles)
|> map { files -> Content in
var result: [EmojiComponentReactionItem] = []
for reaction in reactions {
if let availableReactions, let item = availableReactions.reactions.first(where: { $0.value == reaction }) {
result.append(EmojiComponentReactionItem(reaction: reaction, file: item.selectAnimation))
} else {
if case let .custom(fileId) = reaction {
if let file = files[fileId] {
result.append(EmojiComponentReactionItem(reaction: reaction, file: file))
}
}
}
}
return Content(isEnabled: isEnabled, enabledReactions: result, availableReactions: availableReactions)
}
}
|> distinctUntilChanged
}
}

View File

@ -95,6 +95,7 @@ import WebUI
import ShareWithPeersScreen
import ItemListPeerItem
import PeerNameColorScreen
import PeerAllowedReactionsScreen
enum PeerInfoAvatarEditingMode {
case generic
@ -7122,8 +7123,19 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
guard let data = self.data, let peer = data.peer else {
return
}
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
let subscription = Promise<PeerAllowedReactionsScreen.Content>()
subscription.set(PeerAllowedReactionsScreen.content(context: self.context, peerId: peer.id))
let _ = (subscription.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] content in
guard let self else {
return
}
self.controller?.push(PeerAllowedReactionsScreen(context: self.context, peerId: peer.id, initialContent: content))
})
} else {
self.controller?.push(peerAllowedReactionListController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id))
}
}
private func toggleForumTopics(isEnabled: Bool) {
guard let data = self.data, let peer = data.peer else {