mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
1866 lines
105 KiB
Swift
1866 lines
105 KiB
Swift
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
|
|
import BundleIconComponent
|
|
import AnimatedTextComponent
|
|
import TextFormat
|
|
import AudioToolbox
|
|
import PremiumLockButtonSubtitleComponent
|
|
import ListSectionComponent
|
|
import ListItemSliderSelectorComponent
|
|
|
|
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
|
|
}
|
|
|
|
private struct EmojiSearchResult {
|
|
var groups: [EmojiPagerContentComponent.ItemGroup]
|
|
var id: AnyHashable
|
|
var version: Int
|
|
var isPreset: Bool
|
|
}
|
|
|
|
private struct EmojiSearchState {
|
|
var result: EmojiSearchResult?
|
|
var isSearching: Bool
|
|
|
|
init(result: EmojiSearchResult?, isSearching: Bool) {
|
|
self.result = result
|
|
self.isSearching = isSearching
|
|
}
|
|
}
|
|
|
|
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 var reactionCountSection: ComponentView<Empty>?
|
|
private var paidReactionsSection: 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 boostStatus: ChannelBoostStatus?
|
|
private var boostStatusDisposable: Disposable?
|
|
|
|
private var isEnabled: Bool = false
|
|
private var availableReactions: AvailableReactions?
|
|
private var enabledReactions: [EmojiComponentReactionItem]?
|
|
private var allowedReactionCount: Int = 11
|
|
private var appliedReactionSettings: PeerReactionSettings?
|
|
|
|
private var areStarsReactionsEnabled: Bool = true
|
|
|
|
private var emojiContent: EmojiPagerContentComponent?
|
|
private var emojiContentDisposable: Disposable?
|
|
|
|
private let emojiSearchDisposable = MetaDisposable()
|
|
private let emojiSearchState = Promise<EmojiSearchState>(EmojiSearchState(result: nil, isSearching: false))
|
|
private var emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) {
|
|
didSet {
|
|
self.emojiSearchState.set(.single(self.emojiSearchStateValue))
|
|
}
|
|
}
|
|
|
|
private var emptyResultEmojis: [TelegramMediaFile] = []
|
|
private var stableEmptyResultEmoji: TelegramMediaFile?
|
|
private let stableEmptyResultEmojiDisposable = MetaDisposable()
|
|
|
|
private var caretPosition: Int?
|
|
|
|
private var displayInput: Bool = false
|
|
private var recenterOnCaret: Bool = false
|
|
|
|
private var isApplyingSettings: Bool = false
|
|
private var applyDisposable: Disposable?
|
|
|
|
private var resolveStickersBotDisposable: Disposable?
|
|
|
|
private weak var currentUndoController: UndoOverlayController?
|
|
|
|
private var cachedChevronImage: (UIImage, PresentationTheme)?
|
|
|
|
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()
|
|
self.boostStatusDisposable?.dispose()
|
|
self.resolveStickersBotDisposable?.dispose()
|
|
}
|
|
|
|
func scrollToTop() {
|
|
self.scrollView.setContentOffset(CGPoint(), animated: true)
|
|
}
|
|
|
|
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
|
|
guard let component = self.component else {
|
|
return true
|
|
}
|
|
if self.isApplyingSettings {
|
|
return true
|
|
}
|
|
guard var enabledReactions = self.enabledReactions else {
|
|
return true
|
|
}
|
|
if !self.isEnabled {
|
|
enabledReactions.removeAll()
|
|
}
|
|
|
|
enabledReactions.removeAll(where: { $0.reaction == .stars })
|
|
|
|
guard let availableReactions = self.availableReactions else {
|
|
return true
|
|
}
|
|
|
|
let allowedReactions: PeerAllowedReactions
|
|
if self.isEnabled {
|
|
if Set(availableReactions.reactions.filter({ $0.isEnabled }).map(\.value)) == Set(enabledReactions.map(\.reaction)) {
|
|
allowedReactions = .all
|
|
} else {
|
|
if enabledReactions.isEmpty {
|
|
allowedReactions = .empty
|
|
} else {
|
|
allowedReactions = .limited(enabledReactions.map(\.reaction))
|
|
}
|
|
}
|
|
} else {
|
|
allowedReactions = .empty
|
|
}
|
|
|
|
let reactionSettings = PeerReactionSettings(allowedReactions: allowedReactions, maxReactionCount: self.allowedReactionCount >= 11 ? nil : Int32(self.allowedReactionCount), starsAllowed: self.isEnabled && self.areStarsReactionsEnabled)
|
|
|
|
if self.appliedReactionSettings != reactionSettings {
|
|
if case .empty = allowedReactions {
|
|
self.applySettings(standalone: true)
|
|
} else {
|
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
|
self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChannelReactions_UnsavedChangesAlertTitle, text: presentationData.strings.ChannelReactions_UnsavedChangesAlertText, actions: [
|
|
TextAlertAction(type: .genericAction, title: presentationData.strings.ChannelReactions_UnsavedChangesAlertDiscard, action: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.environment?.controller()?.dismiss()
|
|
}),
|
|
TextAlertAction(type: .defaultAction, title: presentationData.strings.ChannelReactions_UnsavedChangesAlertApply, action: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.applySettings(standalone: false)
|
|
})
|
|
]), in: .window(.root))
|
|
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
self.updateScrolling(transition: .immediate)
|
|
}
|
|
|
|
private func updateScrolling(transition: ComponentTransition) {
|
|
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(standalone: Bool) {
|
|
guard let component = self.component else {
|
|
return
|
|
}
|
|
if self.isApplyingSettings {
|
|
return
|
|
}
|
|
guard var enabledReactions = self.enabledReactions else {
|
|
return
|
|
}
|
|
enabledReactions.removeAll(where: { $0.reaction == .stars })
|
|
|
|
if !self.isEnabled {
|
|
enabledReactions.removeAll()
|
|
}
|
|
|
|
guard let availableReactions = self.availableReactions else {
|
|
return
|
|
}
|
|
|
|
let customReactions = enabledReactions.filter({ item in
|
|
switch item.reaction {
|
|
case .custom:
|
|
return true
|
|
case .builtin:
|
|
return false
|
|
case .stars:
|
|
return false
|
|
}
|
|
})
|
|
|
|
if let boostStatus = self.boostStatus, !customReactions.isEmpty, customReactions.count > boostStatus.level {
|
|
self.displayPremiumScreen(reactionCount: customReactions.count)
|
|
return
|
|
}
|
|
|
|
self.isApplyingSettings = true
|
|
self.state?.updated(transition: .immediate)
|
|
|
|
self.applyDisposable?.dispose()
|
|
|
|
let allowedReactions: PeerAllowedReactions
|
|
if self.isEnabled {
|
|
if Set(availableReactions.reactions.filter({ $0.isEnabled }).map(\.value)) == Set(enabledReactions.map(\.reaction)) {
|
|
allowedReactions = .all
|
|
} else if enabledReactions.isEmpty {
|
|
allowedReactions = .empty
|
|
} else {
|
|
allowedReactions = .limited(enabledReactions.map(\.reaction))
|
|
}
|
|
} else {
|
|
allowedReactions = .empty
|
|
}
|
|
let reactionSettings = PeerReactionSettings(allowedReactions: allowedReactions, maxReactionCount: self.allowedReactionCount == 11 ? nil : Int32(self.allowedReactionCount), starsAllowed: self.isEnabled && self.areStarsReactionsEnabled)
|
|
|
|
let applyDisposable = (component.context.engine.peers.updatePeerReactionSettings(peerId: component.peerId, reactionSettings: reactionSettings)
|
|
|> deliverOnMainQueue).start(error: { [weak self] error in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
self.isApplyingSettings = false
|
|
self.state?.updated(transition: .immediate)
|
|
|
|
if !standalone {
|
|
switch error {
|
|
case .boostRequired:
|
|
self.displayPremiumScreen(reactionCount: customReactions.count)
|
|
case .generic:
|
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
|
self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
|
}
|
|
}
|
|
}, completed: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.appliedReactionSettings = reactionSettings
|
|
if !standalone {
|
|
self.environment?.controller()?.dismiss()
|
|
}
|
|
})
|
|
|
|
if standalone {
|
|
let _ = applyDisposable
|
|
} else {
|
|
self.applyDisposable = applyDisposable
|
|
}
|
|
}
|
|
|
|
private func displayPremiumScreen(reactionCount: Int) {
|
|
guard let component = self.component else {
|
|
return
|
|
}
|
|
|
|
let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId))
|
|
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
|
|
guard let self, let component = self.component, let peer, let status = self.boostStatus else {
|
|
return
|
|
}
|
|
|
|
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
|
|
|
|
let link = status.url
|
|
let controller = PremiumLimitScreen(context: component.context, subject: .storiesChannelBoost(peer: peer, boostSubject: .channelReactions(reactionCount: reactionCount), 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(title: nil, text: presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current)
|
|
return true
|
|
}, openStats: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.openBoostStats()
|
|
}, openGift: premiumConfiguration.giveawayGiftsPurchaseAvailable ? { [weak self] in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
let controller = createGiveawayController(context: component.context, peerId: component.peerId, subject: .generic)
|
|
self.environment?.controller()?.push(controller)
|
|
} : nil)
|
|
self.environment?.controller()?.push(controller)
|
|
|
|
HapticFeedback().impact(.light)
|
|
})
|
|
}
|
|
|
|
private func openBoostStats() {
|
|
guard let component = self.component, let boostStatus = self.boostStatus else {
|
|
return
|
|
}
|
|
let statsController = component.context.sharedContext.makeChannelStatsController(context: component.context, updatedPresentationData: nil, peerId: component.peerId, boosts: true, boostStatus: boostStatus)
|
|
self.environment?.controller()?.push(statsController)
|
|
}
|
|
|
|
func update(component: PeerAllowedReactionsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> 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 {
|
|
if let value = component.initialContent.reactionSettings?.starsAllowed {
|
|
self.areStarsReactionsEnabled = value
|
|
} else {
|
|
self.areStarsReactionsEnabled = component.initialContent.isStarReactionAvailable
|
|
}
|
|
|
|
var enabledReactionsValue = component.initialContent.enabledReactions
|
|
if self.areStarsReactionsEnabled {
|
|
if let item = component.initialContent.availableReactions?.reactions.first(where: { $0.value == .stars }) {
|
|
enabledReactionsValue.insert(EmojiComponentReactionItem(reaction: item.value, file: item.selectAnimation), at: 0)
|
|
}
|
|
}
|
|
|
|
enabledReactions = enabledReactionsValue
|
|
self.enabledReactions = enabledReactions
|
|
self.availableReactions = component.initialContent.availableReactions
|
|
self.isEnabled = component.initialContent.isEnabled
|
|
self.appliedReactionSettings = component.initialContent.reactionSettings.flatMap { reactionSettings in
|
|
return PeerReactionSettings(
|
|
allowedReactions: reactionSettings.allowedReactions,
|
|
maxReactionCount: reactionSettings.maxReactionCount == 11 ? nil : reactionSettings.maxReactionCount,
|
|
starsAllowed: reactionSettings.starsAllowed
|
|
)
|
|
}
|
|
self.allowedReactionCount = (component.initialContent.reactionSettings?.maxReactionCount).flatMap(Int.init) ?? 11
|
|
}
|
|
var caretPosition = self.caretPosition ?? enabledReactions.count
|
|
caretPosition = max(0, min(enabledReactions.count, caretPosition))
|
|
self.caretPosition = caretPosition
|
|
|
|
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: true,
|
|
forceHasPremium: true
|
|
)
|
|
self.emojiContentDisposable = (combineLatest(queue: .mainQueue(),
|
|
emojiContent,
|
|
self.emojiSearchState.get()
|
|
)
|
|
|> deliverOnMainQueue).start(next: { [weak self] emojiContent, emojiSearchState in
|
|
guard let self else {
|
|
return
|
|
}
|
|
guard let environment = self.environment else {
|
|
return
|
|
}
|
|
|
|
var emojiContent = emojiContent
|
|
if let emojiSearchResult = emojiSearchState.result {
|
|
var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults?
|
|
if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) {
|
|
if self.stableEmptyResultEmoji == nil {
|
|
self.stableEmptyResultEmoji = self.emptyResultEmojis.randomElement()
|
|
}
|
|
emptySearchResults = EmojiPagerContentComponent.EmptySearchResults(
|
|
text: environment.strings.EmojiSearch_SearchReactionsEmptyResult,
|
|
iconFile: self.stableEmptyResultEmoji
|
|
)
|
|
} else {
|
|
self.stableEmptyResultEmoji = nil
|
|
}
|
|
emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : .active)
|
|
} else {
|
|
self.stableEmptyResultEmoji = nil
|
|
|
|
if emojiSearchState.isSearching {
|
|
emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiContent.contentItemGroups, itemContentUniqueId: emojiContent.itemContentUniqueId, emptySearchResults: emojiContent.emptySearchResults, searchState: .searching)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
AudioServicesPlaySystemSound(0x450)
|
|
|
|
if let index = enabledReactions.firstIndex(where: { $0.file.fileId.id == itemFile.fileId.id }) {
|
|
enabledReactions.remove(at: index)
|
|
if let caretPosition = self.caretPosition, caretPosition > index {
|
|
self.caretPosition = max(0, caretPosition - 1)
|
|
}
|
|
} else {
|
|
if enabledReactions.count >= 100 {
|
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
var animateAsReplacement = false
|
|
if let currentUndoController = self.currentUndoController {
|
|
currentUndoController.dismiss()
|
|
animateAsReplacement = true
|
|
}
|
|
|
|
let undoController = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.ChannelReactions_ToastMaxReactionsReached, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: animateAsReplacement, action: { _ in return false })
|
|
self.currentUndoController = undoController
|
|
self.environment?.controller()?.present(undoController, in: .current)
|
|
return
|
|
}
|
|
|
|
let reaction: MessageReaction.Reaction
|
|
if let availableReactions = self.availableReactions, let reactionItem = availableReactions.reactions.filter({ $0.isEnabled }).first(where: { $0.selectAnimation.fileId.id == itemFile.fileId.id }) {
|
|
reaction = reactionItem.value
|
|
} else {
|
|
reaction = .custom(itemFile.fileId.id)
|
|
|
|
if let boostStatus = self.boostStatus {
|
|
let enabledCustomReactions = enabledReactions.filter({ item in
|
|
switch item.reaction {
|
|
case .custom:
|
|
return true
|
|
case .builtin:
|
|
return false
|
|
case .stars:
|
|
return false
|
|
}
|
|
})
|
|
|
|
let nextCustomReactionCount = enabledCustomReactions.count + 1
|
|
if nextCustomReactionCount > boostStatus.level {
|
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
var animateAsReplacement = false
|
|
if let currentUndoController = self.currentUndoController {
|
|
currentUndoController.dismiss()
|
|
animateAsReplacement = true
|
|
}
|
|
|
|
let text = presentationData.strings.ChannelReactions_ToastLevelBoostRequiredTemplate(presentationData.strings.ChannelReactions_ToastLevelBoostRequiredTemplateLevel(Int32(nextCustomReactionCount)), presentationData.strings.ChannelReactions_ToastLevelBoostRequiredTemplateEmojiCount(Int32(nextCustomReactionCount))).string
|
|
let undoController = UndoOverlayController(presentationData: presentationData, content: .customEmoji(context: component.context, file: itemFile._parse(), loop: false, title: nil, text: text, undoText: nil, customAction: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: animateAsReplacement, action: { _ in return false })
|
|
self.currentUndoController = undoController
|
|
self.environment?.controller()?.present(undoController, in: .current)
|
|
}
|
|
}
|
|
}
|
|
let item = EmojiComponentReactionItem(reaction: reaction, file: itemFile)
|
|
|
|
if let caretPosition = self.caretPosition, caretPosition < enabledReactions.count {
|
|
enabledReactions.insert(item, at: caretPosition)
|
|
self.caretPosition = caretPosition + 1
|
|
} else {
|
|
enabledReactions.append(item)
|
|
self.caretPosition = enabledReactions.count
|
|
}
|
|
self.recenterOnCaret = true
|
|
}
|
|
self.enabledReactions = enabledReactions
|
|
if !self.isUpdating {
|
|
self.state?.updated(transition: .spring(duration: 0.25))
|
|
}
|
|
},
|
|
deleteBackwards: {
|
|
},
|
|
openStickerSettings: {
|
|
},
|
|
openFeatured: {
|
|
},
|
|
openSearch: {
|
|
},
|
|
addGroupAction: { _, _, _ in
|
|
},
|
|
clearGroup: { _ in
|
|
},
|
|
editAction: { _ in
|
|
},
|
|
pushController: { c in
|
|
},
|
|
presentController: { c in
|
|
},
|
|
presentGlobalOverlayController: { c in
|
|
},
|
|
navigationController: {
|
|
return nil
|
|
},
|
|
requestUpdate: { _ in
|
|
},
|
|
updateSearchQuery: { [weak self] query in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
|
|
switch query {
|
|
case .none:
|
|
self.emojiSearchDisposable.set(nil)
|
|
self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
|
|
case let .text(rawQuery, languageCode):
|
|
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
if query.isEmpty {
|
|
self.emojiSearchDisposable.set(nil)
|
|
self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
|
|
} else {
|
|
let context = component.context
|
|
let isEmojiOnly = !"".isEmpty
|
|
|
|
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false)
|
|
if !languageCode.lowercased().hasPrefix("en") {
|
|
signal = signal
|
|
|> mapToSignal { keywords in
|
|
return .single(keywords)
|
|
|> then(
|
|
context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3)
|
|
|> map { englishKeywords in
|
|
return keywords + englishKeywords
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
|
|> map { peer -> Bool in
|
|
guard case let .user(user) = peer else {
|
|
return false
|
|
}
|
|
return user.isPremium
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
let resultSignal: Signal<[EmojiPagerContentComponent.ItemGroup], NoError>
|
|
do {
|
|
let remotePacksSignal: Signal<(sets: FoundStickerSets, isFinalResult: Bool), NoError> = .single((FoundStickerSets(), false))
|
|
|> then(
|
|
context.engine.stickers.searchEmojiSetsRemotely(query: query) |> map {
|
|
($0, true)
|
|
}
|
|
)
|
|
let localPacksSignal: Signal<FoundStickerSets, NoError> = context.engine.stickers.searchEmojiSets(query: query)
|
|
|
|
resultSignal = signal
|
|
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
|
var allEmoticons: [String: String] = [:]
|
|
for keyword in keywords {
|
|
for emoticon in keyword.emoticons {
|
|
allEmoticons[emoticon] = keyword.keyword
|
|
}
|
|
}
|
|
if isEmojiOnly {
|
|
var items: [EmojiPagerContentComponent.Item] = []
|
|
for (_, list) in EmojiPagerContentComponent.staticEmojiMapping {
|
|
for emojiString in list {
|
|
if allEmoticons[emojiString] != nil {
|
|
let item = EmojiPagerContentComponent.Item(
|
|
animationData: nil,
|
|
content: .staticEmoji(emojiString),
|
|
itemFile: nil,
|
|
subgroupId: nil,
|
|
icon: .none,
|
|
tintMode: .none
|
|
)
|
|
items.append(item)
|
|
}
|
|
}
|
|
}
|
|
var resultGroups: [EmojiPagerContentComponent.ItemGroup] = []
|
|
resultGroups.append(EmojiPagerContentComponent.ItemGroup(
|
|
supergroupId: "search",
|
|
groupId: "search",
|
|
title: nil,
|
|
subtitle: nil,
|
|
badge: nil,
|
|
actionButtonTitle: nil,
|
|
isFeatured: false,
|
|
isPremiumLocked: false,
|
|
isEmbedded: false,
|
|
hasClear: false,
|
|
hasEdit: false,
|
|
collapsedLineCount: nil,
|
|
displayPremiumBadges: false,
|
|
headerItem: nil,
|
|
fillWithLoadingPlaceholders: false,
|
|
items: items
|
|
))
|
|
return .single(resultGroups)
|
|
} else {
|
|
let remoteSignal = context.engine.stickers.searchEmoji(query: query, emoticon: Array(allEmoticons.keys), inputLanguageCode: languageCode)
|
|
|
|
return combineLatest(
|
|
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1),
|
|
context.engine.stickers.availableReactions() |> take(1),
|
|
hasPremium |> take(1),
|
|
remotePacksSignal,
|
|
remoteSignal,
|
|
localPacksSignal
|
|
)
|
|
|> map { view, availableReactions, hasPremium, foundPacks, foundEmoji, foundLocalPacks -> [EmojiPagerContentComponent.ItemGroup] in
|
|
var result: [(String, TelegramMediaFile.Accessor?, String)] = []
|
|
|
|
var allEmoticons: [String: String] = [:]
|
|
for keyword in keywords {
|
|
for emoticon in keyword.emoticons {
|
|
allEmoticons[emoticon] = keyword.keyword
|
|
}
|
|
}
|
|
|
|
for itemFile in foundEmoji.items {
|
|
for attribute in itemFile.attributes {
|
|
switch attribute {
|
|
case let .CustomEmoji(_, _, alt, _):
|
|
if !alt.isEmpty, let keyword = allEmoticons[alt] {
|
|
result.append((alt, TelegramMediaFile.Accessor(itemFile), keyword))
|
|
} else if alt == query {
|
|
result.append((alt, TelegramMediaFile.Accessor(itemFile), alt))
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
for entry in view.entries {
|
|
guard let item = entry.item as? StickerPackItem else {
|
|
continue
|
|
}
|
|
if !item.file.isPremiumEmoji {
|
|
if let alt = item.file.customEmojiAlt {
|
|
if !alt.isEmpty, let keyword = allEmoticons[alt] {
|
|
result.append((alt, item.file, keyword))
|
|
} else if alt == query {
|
|
result.append((alt, item.file, alt))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var items: [EmojiPagerContentComponent.Item] = []
|
|
|
|
var existingIds = Set<MediaId>()
|
|
for item in result {
|
|
if let itemFile = item.1 {
|
|
if existingIds.contains(itemFile.fileId) {
|
|
continue
|
|
}
|
|
existingIds.insert(itemFile.fileId)
|
|
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
|
let item = EmojiPagerContentComponent.Item(
|
|
animationData: animationData,
|
|
content: .animation(animationData),
|
|
itemFile: itemFile, subgroupId: nil,
|
|
icon: (!hasPremium && itemFile.isPremiumEmoji) ? .locked : .none,
|
|
tintMode: animationData.isTemplate ? .primary : .none
|
|
)
|
|
items.append(item)
|
|
}
|
|
}
|
|
|
|
var resultGroups: [EmojiPagerContentComponent.ItemGroup] = []
|
|
resultGroups.append(EmojiPagerContentComponent.ItemGroup(
|
|
supergroupId: "search",
|
|
groupId: "search",
|
|
title: nil,
|
|
subtitle: nil,
|
|
badge: nil,
|
|
actionButtonTitle: nil,
|
|
isFeatured: false,
|
|
isPremiumLocked: false,
|
|
isEmbedded: false,
|
|
hasClear: false,
|
|
hasEdit: false,
|
|
collapsedLineCount: nil,
|
|
displayPremiumBadges: false,
|
|
headerItem: nil,
|
|
fillWithLoadingPlaceholders: false,
|
|
items: items
|
|
))
|
|
|
|
var combinedSets: FoundStickerSets
|
|
combinedSets = foundLocalPacks
|
|
combinedSets = combinedSets.merge(with: foundPacks.sets)
|
|
|
|
var existingCollectionIds = Set<ItemCollectionId>()
|
|
for (collectionId, info, _, _) in combinedSets.infos {
|
|
if !existingCollectionIds.contains(collectionId) {
|
|
existingCollectionIds.insert(collectionId)
|
|
} else {
|
|
continue
|
|
}
|
|
|
|
if let info = info as? StickerPackCollectionInfo {
|
|
var topItems: [StickerPackItem] = []
|
|
for e in combinedSets.entries {
|
|
if let item = e.item as? StickerPackItem {
|
|
if e.index.collectionId == collectionId {
|
|
topItems.append(item)
|
|
}
|
|
}
|
|
}
|
|
|
|
var groupItems: [EmojiPagerContentComponent.Item] = []
|
|
for item in topItems {
|
|
var tintMode: EmojiPagerContentComponent.Item.TintMode = .none
|
|
if item.file.isCustomTemplateEmoji {
|
|
tintMode = .primary
|
|
}
|
|
|
|
let animationData = EntityKeyboardAnimationData(file: item.file)
|
|
let resultItem = EmojiPagerContentComponent.Item(
|
|
animationData: animationData,
|
|
content: .animation(animationData),
|
|
itemFile: item.file,
|
|
subgroupId: nil,
|
|
icon: (!hasPremium && item.file.isPremiumEmoji) ? .locked : .none,
|
|
tintMode: tintMode
|
|
)
|
|
|
|
groupItems.append(resultItem)
|
|
}
|
|
|
|
resultGroups.append(EmojiPagerContentComponent.ItemGroup(
|
|
supergroupId: AnyHashable(info.id),
|
|
groupId: AnyHashable(info.id),
|
|
title: info.title,
|
|
subtitle: nil,
|
|
badge: nil,
|
|
actionButtonTitle: nil,
|
|
isFeatured: false,
|
|
isPremiumLocked: false,
|
|
isEmbedded: false,
|
|
hasClear: false,
|
|
hasEdit: false,
|
|
collapsedLineCount: 3,
|
|
displayPremiumBadges: false,
|
|
headerItem: nil,
|
|
fillWithLoadingPlaceholders: false,
|
|
items: groupItems
|
|
))
|
|
}
|
|
}
|
|
return resultGroups
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var version = 0
|
|
self.emojiSearchStateValue.isSearching = true
|
|
self.emojiSearchDisposable.set((resultSignal
|
|
|> delay(0.15, queue: .mainQueue())
|
|
|> deliverOnMainQueue).start(next: { [weak self] result in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false)
|
|
version += 1
|
|
}))
|
|
}
|
|
case let .category(value):
|
|
let resultSignal: Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError>
|
|
do {
|
|
resultSignal = component.context.engine.stickers.searchEmoji(category: value)
|
|
|> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
|
|
var items: [EmojiPagerContentComponent.Item] = []
|
|
|
|
var existingIds = Set<MediaId>()
|
|
for itemFile in files {
|
|
if existingIds.contains(itemFile.fileId) {
|
|
continue
|
|
}
|
|
existingIds.insert(itemFile.fileId)
|
|
let animationData = EntityKeyboardAnimationData(file: TelegramMediaFile.Accessor(itemFile))
|
|
let item = EmojiPagerContentComponent.Item(
|
|
animationData: animationData,
|
|
content: .animation(animationData),
|
|
itemFile: TelegramMediaFile.Accessor(itemFile),
|
|
subgroupId: nil,
|
|
icon: .none,
|
|
tintMode: animationData.isTemplate ? .primary : .none
|
|
)
|
|
items.append(item)
|
|
}
|
|
|
|
return .single(([EmojiPagerContentComponent.ItemGroup(
|
|
supergroupId: "search",
|
|
groupId: "search",
|
|
title: nil,
|
|
subtitle: nil,
|
|
badge: nil,
|
|
actionButtonTitle: nil,
|
|
isFeatured: false,
|
|
isPremiumLocked: false,
|
|
isEmbedded: false,
|
|
hasClear: false,
|
|
hasEdit: false,
|
|
collapsedLineCount: nil,
|
|
displayPremiumBadges: false,
|
|
headerItem: nil,
|
|
fillWithLoadingPlaceholders: false,
|
|
items: items
|
|
)], isFinalResult))
|
|
}
|
|
}
|
|
|
|
var version = 0
|
|
self.emojiSearchDisposable.set((resultSignal
|
|
|> deliverOnMainQueue).start(next: { [weak self] result in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
guard let group = result.items.first else {
|
|
return
|
|
}
|
|
if group.items.isEmpty && !result.isFinalResult {
|
|
//self.emojiSearchStateValue.isSearching = true
|
|
self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: [
|
|
EmojiPagerContentComponent.ItemGroup(
|
|
supergroupId: "search",
|
|
groupId: "search",
|
|
title: nil,
|
|
subtitle: nil,
|
|
badge: nil,
|
|
actionButtonTitle: nil,
|
|
isFeatured: false,
|
|
isPremiumLocked: false,
|
|
isEmbedded: false,
|
|
hasClear: false,
|
|
hasEdit: false,
|
|
collapsedLineCount: nil,
|
|
displayPremiumBadges: false,
|
|
headerItem: nil,
|
|
fillWithLoadingPlaceholders: true,
|
|
items: []
|
|
)
|
|
], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false)
|
|
return
|
|
}
|
|
|
|
self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false)
|
|
version += 1
|
|
}))
|
|
}
|
|
},
|
|
updateScrollingToItemGroup: {
|
|
},
|
|
onScroll: {},
|
|
chatPeerId: nil,
|
|
peekBehavior: nil,
|
|
customLayout: nil,
|
|
externalBackground: nil,
|
|
externalExpansionView: nil,
|
|
customContentView: nil,
|
|
useOpaqueTheme: true,
|
|
hideBackground: false,
|
|
stateContext: nil,
|
|
addImage: nil
|
|
)
|
|
|
|
self.emojiContent = emojiContent
|
|
|
|
if !self.isUpdating {
|
|
self.state?.updated(transition: .immediate)
|
|
}
|
|
})
|
|
}
|
|
|
|
if self.boostStatusDisposable == nil {
|
|
self.boostStatusDisposable = (component.context.engine.peers.getChannelBoostStatus(peerId: component.peerId)
|
|
|> deliverOnMainQueue).start(next: { [weak self] boostStatus in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.boostStatus = boostStatus
|
|
if !self.isUpdating {
|
|
self.state?.updated(transition: .immediate)
|
|
}
|
|
})
|
|
}
|
|
|
|
if themeUpdated {
|
|
self.backgroundColor = environment.theme.list.blocksBackgroundColor
|
|
}
|
|
|
|
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: self.isEnabled,
|
|
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.filter({ $0.isEnabled }) {
|
|
enabledReactions.append(EmojiComponentReactionItem(reaction: reactionItem.value, file: reactionItem.selectAnimation))
|
|
}
|
|
}
|
|
if self.areStarsReactionsEnabled {
|
|
if let item = component.initialContent.availableReactions?.reactions.first(where: { $0.value == .stars }) {
|
|
enabledReactions.insert(EmojiComponentReactionItem(reaction: item.value, file: item.selectAnimation), at: 0)
|
|
}
|
|
}
|
|
self.enabledReactions = enabledReactions
|
|
self.caretPosition = enabledReactions.count
|
|
}
|
|
} else {
|
|
self.displayInput = false
|
|
}
|
|
|
|
self.state?.updated(transition: .immediate)
|
|
}
|
|
}
|
|
)),
|
|
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
|
|
|
|
let switchInfoTextSize = self.switchInfoText.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: environment.strings.ChannelReactions_GeneralInfoLabel,
|
|
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
|
|
}
|
|
|
|
let reactionsTitleTextSize = reactionsTitleText.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: environment.strings.ChannelReactions_ReactionsSectionTitle,
|
|
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
|
|
}
|
|
|
|
let reactionInputSize = reactionInput.update(
|
|
transition: animateIn ? .immediate : transition,
|
|
component: AnyComponent(EmojiListInputComponent(
|
|
context: component.context,
|
|
theme: environment.theme,
|
|
placeholder: environment.strings.ChannelReactions_InputPlaceholder,
|
|
reactionItems: enabledReactions,
|
|
isInputActive: self.displayInput,
|
|
caretPosition: caretPosition,
|
|
activateInput: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if self.emojiContent != nil && !self.displayInput {
|
|
self.displayInput = true
|
|
self.recenterOnCaret = true
|
|
self.state?.updated(transition: .spring(duration: 0.5))
|
|
}
|
|
},
|
|
setCaretPosition: { [weak self] value in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if self.caretPosition != value {
|
|
self.caretPosition = value
|
|
self.state?.updated(transition: .immediate)
|
|
}
|
|
}
|
|
)),
|
|
environment: {},
|
|
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
|
|
}
|
|
|
|
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: [:])
|
|
let attributes = MarkdownAttributes(body: body, bold: body, link: link, linkAttribute: { contents in
|
|
return (TelegramTextAttributes.URL, contents)
|
|
})
|
|
let reactionsInfoTextSize = reactionsInfoText.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .markdown(text: environment.strings.ChannelReactions_ReactionsInfoLabel, attributes: attributes),
|
|
maximumNumberOfLines: 0,
|
|
highlightAction: { attributes in
|
|
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
|
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
|
} else {
|
|
return nil
|
|
}
|
|
},
|
|
tapAction: { [weak self] attributes, _ in
|
|
guard let self, let component = self.component, attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] != nil else {
|
|
return
|
|
}
|
|
self.resolveStickersBotDisposable?.dispose()
|
|
self.resolveStickersBotDisposable = (component.context.engine.peers.resolvePeerByName(name: "stickers", referrer: nil)
|
|
|> mapToSignal { result -> Signal<EnginePeer?, NoError> in
|
|
guard case let .result(result) = result else {
|
|
return .complete()
|
|
}
|
|
return .single(result)
|
|
}
|
|
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
|
guard let self, let component = self.component, let peer else {
|
|
return
|
|
}
|
|
guard let navigationController = self.environment?.controller()?.navigationController as? NavigationController else {
|
|
return
|
|
}
|
|
component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
|
|
navigationController: navigationController,
|
|
context: component.context,
|
|
chatLocation: .peer(peer),
|
|
keepStack: .always
|
|
))
|
|
})
|
|
}
|
|
)),
|
|
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
|
|
|
|
contentHeight += 32.0
|
|
|
|
let reactionCountSection: ComponentView<Empty>
|
|
if let current = self.reactionCountSection {
|
|
reactionCountSection = current
|
|
} else {
|
|
reactionCountSection = ComponentView()
|
|
self.reactionCountSection = reactionCountSection
|
|
}
|
|
|
|
let reactionCountValueList = (1 ... 11).map { i -> String in
|
|
return "\(i)"
|
|
}
|
|
let sliderTitle: String = environment.strings.PeerInfo_AllowedReactions_MaxCountValue(Int32(self.allowedReactionCount))
|
|
let reactionCountSectionSize = reactionCountSection.update(
|
|
transition: transition,
|
|
component: AnyComponent(ListSectionComponent(
|
|
theme: environment.theme,
|
|
header: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: environment.strings.PeerInfo_AllowedReactions_MaxCountSectionTitle,
|
|
font: Font.regular(13.0),
|
|
textColor: environment.theme.list.freeTextColor
|
|
)),
|
|
maximumNumberOfLines: 0
|
|
)),
|
|
footer: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: environment.strings.PeerInfo_AllowedReactions_MaxCountSectionFooter,
|
|
font: Font.regular(13.0),
|
|
textColor: environment.theme.list.freeTextColor
|
|
)),
|
|
maximumNumberOfLines: 0
|
|
)),
|
|
items: [
|
|
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemSliderSelectorComponent(
|
|
theme: environment.theme,
|
|
content: .discrete(ListItemSliderSelectorComponent.Discrete(
|
|
values: reactionCountValueList.map { item in
|
|
return item
|
|
},
|
|
markPositions: false,
|
|
selectedIndex: max(0, min(reactionCountValueList.count - 1, self.allowedReactionCount - 1)),
|
|
title: sliderTitle,
|
|
selectedIndexUpdated: { [weak self] index in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let index = max(1, min(reactionCountValueList.count, index + 1))
|
|
self.allowedReactionCount = index
|
|
self.state?.updated(transition: .immediate)
|
|
}
|
|
))
|
|
)))
|
|
],
|
|
displaySeparators: false
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
|
)
|
|
let reactionCountSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: reactionCountSectionSize)
|
|
if let reactionCountSectionView = reactionCountSection.view {
|
|
if reactionCountSectionView.superview == nil {
|
|
self.scrollView.addSubview(reactionCountSectionView)
|
|
}
|
|
if animateIn {
|
|
reactionCountSectionView.frame = reactionCountSectionFrame
|
|
if !transition.animation.isImmediate {
|
|
reactionCountSectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
} else {
|
|
transition.setFrame(view: reactionCountSectionView, frame: reactionCountSectionFrame)
|
|
}
|
|
}
|
|
contentHeight += reactionCountSectionSize.height
|
|
|
|
if component.initialContent.isStarReactionAvailable {
|
|
contentHeight += 32.0
|
|
|
|
let paidReactionsSection: ComponentView<Empty>
|
|
if let current = self.paidReactionsSection {
|
|
paidReactionsSection = current
|
|
} else {
|
|
paidReactionsSection = ComponentView()
|
|
self.paidReactionsSection = paidReactionsSection
|
|
}
|
|
|
|
let parsedString = parseMarkdownIntoAttributedString(environment.strings.PeerInfo_AllowedReactions_StarReactionsFooter, attributes: MarkdownAttributes(
|
|
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor),
|
|
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor),
|
|
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor),
|
|
linkAttribute: { url in
|
|
return ("URL", url)
|
|
}))
|
|
|
|
let paidReactionsFooterText = NSMutableAttributedString(attributedString: parsedString)
|
|
|
|
if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme {
|
|
self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme)
|
|
}
|
|
if let range = paidReactionsFooterText.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 {
|
|
paidReactionsFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: paidReactionsFooterText.string))
|
|
}
|
|
|
|
let paidReactionsSectionSize = paidReactionsSection.update(
|
|
transition: transition,
|
|
component: AnyComponent(ListSectionComponent(
|
|
theme: environment.theme,
|
|
header: nil,
|
|
footer: AnyComponent(MultilineTextComponent(
|
|
text: .plain(paidReactionsFooterText),
|
|
maximumNumberOfLines: 0,
|
|
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1),
|
|
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
|
|
highlightAction: { attributes in
|
|
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
|
|
return NSAttributedString.Key(rawValue: "URL")
|
|
} else {
|
|
return nil
|
|
}
|
|
}, tapAction: { [weak self] attributes, _ in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
if let url = attributes[NSAttributedString.Key(rawValue: "URL")] as? String {
|
|
component.context.sharedContext.applicationBindings.openUrl(url)
|
|
}
|
|
}
|
|
)),
|
|
items: [
|
|
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListSwitchItemComponent(
|
|
theme: environment.theme,
|
|
title: environment.strings.PeerInfo_AllowedReactions_StarReactions,
|
|
value: self.areStarsReactionsEnabled,
|
|
valueUpdated: { [weak self] value in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
self.areStarsReactionsEnabled = value
|
|
|
|
var enabledReactions = self.enabledReactions ?? []
|
|
if self.areStarsReactionsEnabled {
|
|
if let item = component.initialContent.availableReactions?.reactions.first(where: { $0.value == .stars }) {
|
|
enabledReactions.insert(EmojiComponentReactionItem(reaction: item.value, file: item.selectAnimation), at: 0)
|
|
if let caretPosition = self.caretPosition {
|
|
self.caretPosition = min(enabledReactions.count, caretPosition + 1)
|
|
}
|
|
}
|
|
} else {
|
|
if let index = enabledReactions.firstIndex(where: { $0.reaction == .stars }) {
|
|
enabledReactions.remove(at: index)
|
|
if let caretPosition = self.caretPosition, caretPosition > index {
|
|
self.caretPosition = max(0, caretPosition - 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
self.enabledReactions = enabledReactions
|
|
|
|
if !self.isUpdating {
|
|
self.state?.updated(transition: .spring(duration: 0.25))
|
|
}
|
|
}
|
|
)))
|
|
]
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
|
)
|
|
let paidReactionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: paidReactionsSectionSize)
|
|
if let paidReactionsSectionView = paidReactionsSection.view {
|
|
if paidReactionsSectionView.superview == nil {
|
|
self.scrollView.addSubview(paidReactionsSectionView)
|
|
}
|
|
if animateIn {
|
|
paidReactionsSectionView.frame = paidReactionsSectionFrame
|
|
if !transition.animation.isImmediate {
|
|
paidReactionsSectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
} else {
|
|
transition.setFrame(view: paidReactionsSectionView, frame: paidReactionsSectionFrame)
|
|
}
|
|
}
|
|
contentHeight += paidReactionsSectionSize.height
|
|
contentHeight += 12.0
|
|
} else {
|
|
contentHeight += 12.0
|
|
|
|
if let paidReactionsSection = self.paidReactionsSection {
|
|
self.paidReactionsSection = nil
|
|
if let paidReactionsSectionView = paidReactionsSection.view {
|
|
if !transition.animation.isImmediate {
|
|
paidReactionsSectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak paidReactionsSectionView] _ in
|
|
paidReactionsSectionView?.removeFromSuperview()
|
|
})
|
|
} else {
|
|
paidReactionsSectionView.removeFromSuperview()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} 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()
|
|
}
|
|
}
|
|
}
|
|
|
|
if let reactionCountSection = self.reactionCountSection {
|
|
self.reactionCountSection = nil
|
|
if let reactionCountSectionView = reactionCountSection.view {
|
|
if !transition.animation.isImmediate {
|
|
reactionCountSectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionCountSectionView] _ in
|
|
reactionCountSectionView?.removeFromSuperview()
|
|
})
|
|
} else {
|
|
reactionCountSectionView.removeFromSuperview()
|
|
}
|
|
}
|
|
}
|
|
|
|
if let paidReactionsSection = self.paidReactionsSection {
|
|
self.paidReactionsSection = nil
|
|
if let paidReactionsSectionView = paidReactionsSection.view {
|
|
if !transition.animation.isImmediate {
|
|
paidReactionsSectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak paidReactionsSectionView] _ in
|
|
paidReactionsSectionView?.removeFromSuperview()
|
|
})
|
|
} else {
|
|
paidReactionsSectionView.removeFromSuperview()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var buttonContents: [AnyComponentWithIdentity<Empty>] = []
|
|
buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(
|
|
Text(text: environment.strings.ChannelReactions_SaveAction, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor)
|
|
)))
|
|
|
|
let customReactionCount = self.isEnabled ? enabledReactions.filter({ item in
|
|
switch item.reaction {
|
|
case .custom:
|
|
return true
|
|
case .builtin:
|
|
return false
|
|
case .stars:
|
|
return false
|
|
}
|
|
}).count : 0
|
|
|
|
if let boostStatus = self.boostStatus, customReactionCount > boostStatus.level {
|
|
buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(PremiumLockButtonSubtitleComponent(
|
|
count: customReactionCount,
|
|
theme: environment.theme,
|
|
strings: environment.strings
|
|
))))
|
|
}
|
|
|
|
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(
|
|
VStack(buttonContents, spacing: 3.0)
|
|
)),
|
|
isEnabled: true,
|
|
tintWhenDisabled: false,
|
|
displaysProgress: self.isApplyingSettings,
|
|
action: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.applySettings(standalone: false)
|
|
}
|
|
)),
|
|
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
|
|
reactionSelectionControl.parentState = state
|
|
}
|
|
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))),
|
|
stickerContent: nil,
|
|
backgroundIconColor: nil,
|
|
backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
|
|
separatorColor: environment.theme.list.itemBlocksSeparatorColor,
|
|
backspace: enabledReactions.isEmpty ? nil : { [weak self] in
|
|
guard let self, var enabledReactions = self.enabledReactions, !enabledReactions.isEmpty else {
|
|
return
|
|
}
|
|
if let caretPosition = self.caretPosition, caretPosition < enabledReactions.count {
|
|
if caretPosition > 0 {
|
|
enabledReactions.remove(at: caretPosition - 1)
|
|
self.caretPosition = caretPosition - 1
|
|
self.recenterOnCaret = true
|
|
}
|
|
} else {
|
|
enabledReactions.removeLast()
|
|
self.caretPosition = enabledReactions.count
|
|
self.recenterOnCaret = true
|
|
}
|
|
self.enabledReactions = enabledReactions
|
|
|
|
if !enabledReactions.contains(where: { $0.reaction == .stars }) {
|
|
self.areStarsReactionsEnabled = false
|
|
}
|
|
|
|
if !self.isUpdating {
|
|
self.state?.updated(transition: .spring(duration: 0.25))
|
|
}
|
|
}
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
|
|
)
|
|
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)
|
|
transition.setAlpha(view: buttonView, alpha: self.isEnabled ? 1.0 : 0.0)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if self.recenterOnCaret {
|
|
self.recenterOnCaret = false
|
|
|
|
if let reactionInputView = self.reactionInput?.view as? EmojiListInputComponent.View, let localCaretRect = reactionInputView.caretRect() {
|
|
let caretRect = reactionInputView.convert(localCaretRect, to: self.scrollView)
|
|
var scrollViewBounds = self.scrollView.bounds
|
|
let minButtonDistance: CGFloat = 16.0
|
|
if -scrollViewBounds.minY + caretRect.maxY > buttonFrame.minY - minButtonDistance {
|
|
scrollViewBounds.origin.y = -(buttonFrame.minY - minButtonDistance - caretRect.maxY)
|
|
if scrollViewBounds.origin.y < 0.0 {
|
|
scrollViewBounds.origin.y = 0.0
|
|
}
|
|
}
|
|
if self.scrollView.bounds != scrollViewBounds {
|
|
transition.setBounds(view: self.scrollView, bounds: scrollViewBounds)
|
|
}
|
|
}
|
|
}
|
|
|
|
self.updateScrolling(transition: transition)
|
|
|
|
return availableSize
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View()
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> 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?
|
|
public let reactionSettings: PeerReactionSettings?
|
|
public let isStarReactionAvailable: Bool
|
|
|
|
init(
|
|
isEnabled: Bool,
|
|
enabledReactions: [EmojiComponentReactionItem],
|
|
availableReactions: AvailableReactions?,
|
|
reactionSettings: PeerReactionSettings?,
|
|
isStarReactionAvailable: Bool
|
|
) {
|
|
self.isEnabled = isEnabled
|
|
self.enabledReactions = enabledReactions
|
|
self.availableReactions = availableReactions
|
|
self.reactionSettings = reactionSettings
|
|
self.isStarReactionAvailable = isStarReactionAvailable
|
|
}
|
|
|
|
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
|
|
}
|
|
if lhs.reactionSettings != rhs.reactionSettings {
|
|
return false
|
|
}
|
|
if lhs.isStarReactionAvailable != rhs.isStarReactionAvailable {
|
|
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.title = context.sharedContext.currentPresentationData.with({ $0 }).strings.ChannelReactions_Reactions
|
|
|
|
self.scrollToTop = { [weak self] in
|
|
guard let self, let componentView = self.node.hostView.componentView as? PeerAllowedReactionsScreenComponent.View else {
|
|
return
|
|
}
|
|
componentView.scrollToTop()
|
|
}
|
|
|
|
self.attemptNavigation = { [weak self] complete in
|
|
guard let self, let componentView = self.node.hostView.componentView as? PeerAllowedReactionsScreenComponent.View else {
|
|
return true
|
|
}
|
|
|
|
return componentView.attemptNavigation(complete: complete)
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
let reactionSettings = cachedData.reactionSettings.knownValue
|
|
if let reactionSettings {
|
|
switch reactionSettings.allowedReactions {
|
|
case .all:
|
|
isEnabled = true
|
|
if let availableReactions {
|
|
reactions = availableReactions.reactions.filter({ $0.isEnabled }).map(\.value)
|
|
}
|
|
case let .limited(list):
|
|
isEnabled = true
|
|
reactions.append(contentsOf: list)
|
|
case .empty:
|
|
isEnabled = false
|
|
}
|
|
if let starsAllowed = reactionSettings.starsAllowed, starsAllowed {
|
|
isEnabled = true
|
|
}
|
|
}
|
|
|
|
var missingReactionFiles: [Int64] = []
|
|
for reaction in reactions {
|
|
if let availableReactions, let _ = availableReactions.reactions.filter({ $0.isEnabled }).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.filter({ $0.isEnabled }).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: TelegramMediaFile.Accessor(file)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return Content(isEnabled: isEnabled, enabledReactions: result, availableReactions: availableReactions, reactionSettings: reactionSettings, isStarReactionAvailable: cachedData.flags.contains(.paidMediaAllowed))
|
|
}
|
|
}
|
|
|> distinctUntilChanged
|
|
}
|
|
}
|