mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Custom channel reactions
This commit is contained in:
parent
bc3bdcd623
commit
8051e43e4c
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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": [],
|
||||
|
@ -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]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user