2023-08-05 15:32:12 +02:00

1456 lines
76 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import AnimationCache
import MultiAnimationRenderer
import EntityKeyboard
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import PagerComponent
import TelegramCore
import Lottie
import EmojiTextAttachmentView
import TextFormat
import AppBundle
import GZip
import EmojiStatusComponent
import Postbox
private func randomGenericReactionEffect(context: AccountContext) -> Signal<String?, NoError> {
return context.engine.stickers.loadedStickerPack(reference: .emojiGenericAnimations, forceActualized: false)
|> map { result -> [TelegramMediaFile]? in
switch result {
case let .result(_, items, _):
return items.map(\.file)
default:
return nil
}
}
|> take(1)
|> mapToSignal { items -> Signal<String?, NoError> in
guard let items = items else {
return .single(nil)
}
guard let file = items.randomElement() else {
return .single(nil)
}
return Signal { subscriber in
let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file)).start()
let dataDisposable = (context.account.postbox.mediaBox.resourceData(file.resource)
|> filter(\.complete)
|> take(1)).start(next: { data in
subscriber.putNext(data.path)
subscriber.putCompletion()
})
return ActionDisposable {
fetchDisposable.dispose()
dataDisposable.dispose()
}
}
}
}
public final class EmojiStatusSelectionComponent: Component {
public typealias EnvironmentType = Empty
public let theme: PresentationTheme
public let strings: PresentationStrings
public let deviceMetrics: DeviceMetrics
public let emojiContent: EmojiPagerContentComponent
public let backgroundColor: UIColor
public let separatorColor: UIColor
public let hideTopPanel: Bool
public let hideTopPanelUpdated: (Bool, Transition) -> Void
public init(
theme: PresentationTheme,
strings: PresentationStrings,
deviceMetrics: DeviceMetrics,
emojiContent: EmojiPagerContentComponent,
backgroundColor: UIColor,
separatorColor: UIColor,
hideTopPanel: Bool,
hideTopPanelUpdated: @escaping (Bool, Transition) -> Void
) {
self.theme = theme
self.strings = strings
self.deviceMetrics = deviceMetrics
self.emojiContent = emojiContent
self.backgroundColor = backgroundColor
self.separatorColor = separatorColor
self.hideTopPanel = hideTopPanel
self.hideTopPanelUpdated = hideTopPanelUpdated
}
public static func ==(lhs: EmojiStatusSelectionComponent, rhs: EmojiStatusSelectionComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings != rhs.strings {
return false
}
if lhs.deviceMetrics != rhs.deviceMetrics {
return false
}
if lhs.emojiContent != rhs.emojiContent {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.separatorColor != rhs.separatorColor {
return false
}
if lhs.hideTopPanel != rhs.hideTopPanel {
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 var component: EmojiStatusSelectionComponent?
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()
super.init(frame: frame)
self.addSubview(self.keyboardClippingView)
self.addSubview(self.panelBackgroundView)
self.addSubview(self.panelSeparatorView)
self.addSubview(self.panelHostView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
func update(component: EmojiStatusSelectionComponent, 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
let topPanelHeight: CGFloat = component.hideTopPanel ? 0.0 : 42.0
let keyboardSize = self.keyboardView.update(
transition: transition,//.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)),
component: AnyComponent(EntityKeyboardComponent(
theme: component.theme,
strings: component.strings,
isContentInFocus: true,
containerInsets: UIEdgeInsets(top: topPanelHeight - 34.0, left: 0.0, bottom: 0.0, right: 0.0),
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: { [weak self] hideTopPanel, transition in
guard let strongSelf = self else {
return
}
strongSelf.component?.hideTopPanelUpdated(hideTopPanel, transition)
},
switchToTextInput: {},
switchToGifSubject: { _ in },
reorderItems: { _, _ in },
makeSearchContainerNode: { _ in return nil },
contentIdUpdated: { _ in },
deviceMetrics: component.deviceMetrics,
hiddenInputHeight: 0.0,
inputHeight: 0.0,
displayBottomPanel: false,
isExpanded: false,
clipContentToTopPanel: false,
useExternalSearchContainer: false
)),
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: component.hideTopPanel ? -UIScreenPixel : topPanelHeight), size: CGSize(width: keyboardSize.width, height: UIScreenPixel)))
transition.setAlpha(view: self.panelSeparatorView, alpha: component.hideTopPanel ? 0.0 : 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)
}
}
public final class EmojiStatusSelectionController: ViewController {
private final class Node: ViewControllerTracingNode {
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
}
}
private weak var controller: EmojiStatusSelectionController?
private let context: AccountContext
private weak var sourceView: UIView?
private var globalSourceRect: CGRect?
private let componentHost: ComponentView<Empty>
private let componentShadowLayer: SimpleLayer
private let cloudLayer0: SimpleLayer
private let cloudShadowLayer0: SimpleLayer
private let cloudLayer1: SimpleLayer
private let cloudShadowLayer1: SimpleLayer
private var presentationData: PresentationData
private var validLayout: ContainerViewLayout?
private let currentSelection: Int64?
private var emojiContentDisposable: Disposable?
private var emojiContent: EmojiPagerContentComponent?
private var freezeUpdates: Bool = false
private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation?
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 previewItem: (groupId: AnyHashable, item: EmojiPagerContentComponent.Item)?
private var dismissedPreviewItem: (groupId: AnyHashable, item: EmojiPagerContentComponent.Item)?
private var previewScreenView: ComponentView<Empty>?
private var availableReactions: AvailableReactions?
private var availableReactionsDisposable: Disposable?
private var genericReactionEffectDisposable: Disposable?
private var genericReactionEffect: String?
private var hapticFeedback: HapticFeedback?
private var isAnimatingOut: Bool = false
private var isDismissed: Bool = false
private var isReactionSearchActive: Bool = false
init(controller: EmojiStatusSelectionController, context: AccountContext, sourceView: UIView?, emojiContent: Signal<EmojiPagerContentComponent, NoError>, currentSelection: Int64?) {
self.controller = controller
self.context = context
self.currentSelection = currentSelection
if let sourceView = sourceView {
self.globalSourceRect = sourceView.convert(sourceView.bounds, to: nil)
}
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.componentHost = ComponentView<Empty>()
self.componentShadowLayer = SimpleLayer()
self.componentShadowLayer.shadowOpacity = 0.12
self.componentShadowLayer.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor
self.componentShadowLayer.shadowOffset = CGSize(width: 0.0, height: 2.0)
self.componentShadowLayer.shadowRadius = 16.0
self.cloudLayer0 = SimpleLayer()
self.cloudShadowLayer0 = SimpleLayer()
self.cloudShadowLayer0.shadowOpacity = 0.12
self.cloudShadowLayer0.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor
self.cloudShadowLayer0.shadowOffset = CGSize(width: 0.0, height: 2.0)
self.cloudShadowLayer0.shadowRadius = 16.0
self.cloudLayer1 = SimpleLayer()
self.cloudShadowLayer1 = SimpleLayer()
self.cloudShadowLayer1.shadowOpacity = 0.12
self.cloudShadowLayer1.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor
self.cloudShadowLayer1.shadowOffset = CGSize(width: 0.0, height: 2.0)
self.cloudShadowLayer1.shadowRadius = 16.0
super.init()
self.layer.addSublayer(self.componentShadowLayer)
self.layer.addSublayer(self.cloudShadowLayer0)
self.layer.addSublayer(self.cloudShadowLayer1)
self.layer.addSublayer(self.cloudLayer0)
self.layer.addSublayer(self.cloudLayer1)
self.stableEmptyResultEmojiDisposable.set((self.context.engine.data.get(
TelegramEngine.EngineData.Item.Collections.FeaturedEmojiPacks()
)
|> deliverOnMainQueue).start(next: { [weak self] featuredEmojiPacks in
guard let strongSelf = self else {
return
}
var filteredFiles: [TelegramMediaFile] = []
let filterList: [String] = ["😖", "😫", "🫠", "😨", ""]
for featuredEmojiPack in featuredEmojiPacks {
for item in featuredEmojiPack.topItems {
for attribute in item.file.attributes {
switch attribute {
case let .CustomEmoji(_, _, alt, _):
if filterList.contains(alt) {
filteredFiles.append(item.file)
}
default:
break
}
}
}
}
strongSelf.emptyResultEmojis = filteredFiles
}))
self.emojiContentDisposable = (combineLatest(queue: .mainQueue(),
emojiContent,
self.emojiSearchState.get()
)
|> deliverOnMainQueue).start(next: { [weak self] emojiContent, emojiSearchState in
guard let strongSelf = self else {
return
}
strongSelf.controller?._ready.set(.single(true))
var emojiContent = emojiContent
if let emojiSearchResult = emojiSearchState.result {
var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults?
if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) {
if strongSelf.stableEmptyResultEmoji == nil {
strongSelf.stableEmptyResultEmoji = strongSelf.emptyResultEmojis.randomElement()
}
emptySearchResults = EmojiPagerContentComponent.EmptySearchResults(
text: strongSelf.presentationData.strings.EmojiSearch_SearchStatusesEmptyResult,
iconFile: strongSelf.stableEmptyResultEmoji
)
} else {
strongSelf.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 {
strongSelf.stableEmptyResultEmoji = nil
}
if strongSelf.emojiContent == nil || !strongSelf.freezeUpdates {
strongSelf.emojiContent = emojiContent
}
emojiContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { groupId, item, _, _, _, isPreview in
guard let strongSelf = self else {
return
}
strongSelf.applyItem(groupId: groupId, item: item, isPreview: isPreview)
},
deleteBackwards: {
},
openStickerSettings: {
},
openFeatured: {
},
openSearch: {
},
addGroupAction: { groupId, isPremiumLocked, _ in
guard let strongSelf = self, let collectionId = groupId.base as? ItemCollectionId else {
return
}
let _ = (strongSelf.context.engine.data.get(
TelegramEngine.EngineData.Item.Collections.FeaturedEmojiPacks()
)
|> deliverOnMainQueue).start(next: { featuredEmojiPacks in
guard let strongSelf = self else {
return
}
for featuredEmojiPack in featuredEmojiPacks {
if featuredEmojiPack.info.id == collectionId {
if let strongSelf = self {
strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId, scrollToGroup: true))
}
let _ = strongSelf.context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start()
break
}
}
})
},
clearGroup: { groupId in
},
pushController: { c in
},
presentController: { c in
},
presentGlobalOverlayController: { c in
},
navigationController: {
return nil
},
requestUpdate: { _ in
},
updateSearchQuery: { query in
guard let self = self 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 = self.context
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
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
return combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
context.engine.stickers.availableReactions(),
hasPremium
)
|> take(1)
|> map { view, availableReactions, hasPremium -> [EmojiPagerContentComponent.ItemGroup] in
var result: [(String, TelegramMediaFile?, String)] = []
var allEmoticons: [String: String] = [:]
for keyword in keywords {
for emoticon in keyword.emoticons {
allEmoticons[emoticon] = keyword.keyword
}
}
for entry in view.entries {
guard let item = entry.item as? StickerPackItem else {
continue
}
for attribute in item.file.attributes {
switch attribute {
case let .CustomEmoji(_, _, alt, _):
if !item.file.isPremiumEmoji || hasPremium {
if !alt.isEmpty, let keyword = allEmoticons[alt] {
result.append((alt, item.file, keyword))
} else if alt == query {
result.append((alt, item.file, alt))
}
}
default:
break
}
}
}
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: .none,
tintMode: animationData.isTemplate ? .primary : .none
)
items.append(item)
}
}
return [EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: false,
items: items
)]
}
}
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 = self.context.engine.stickers.searchEmoji(emojiString: value)
|> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
var items: [EmojiPagerContentComponent.Item] = []
var existingIds = Set<EngineMedia.Id>()
for itemFile in files {
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: .none,
tintMode: animationData.isTemplate ? .primary : .none
)
items.append(item)
}
return .single(([EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: 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,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: true,
items: []
)
], id: AnyHashable(value), version: version, isPreset: true), isSearching: false)
return
}
self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), 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
)
strongSelf.refreshLayout(transition: .immediate)
})
self.availableReactionsDisposable = (context.engine.stickers.availableReactions()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] availableReactions in
guard let strongSelf = self else {
return
}
strongSelf.availableReactions = availableReactions
})
self.genericReactionEffectDisposable = (randomGenericReactionEffect(context: context)
|> deliverOnMainQueue).start(next: { [weak self] path in
self?.genericReactionEffect = path
})
}
deinit {
self.emojiContentDisposable?.dispose()
self.availableReactionsDisposable?.dispose()
self.genericReactionEffectDisposable?.dispose()
self.emojiSearchDisposable.dispose()
}
private func refreshLayout(transition: Transition) {
guard let layout = self.validLayout else {
return
}
self.containerLayoutUpdated(layout: layout, transition: transition)
}
func animateOut(completion: @escaping () -> Void, fromBackground: Bool) {
if self.isAnimatingOut {
return
}
self.isAnimatingOut = true
let duration: Double = fromBackground ? 0.1 : 0.25
self.componentShadowLayer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false)
self.componentHost.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { _ in
completion()
})
self.cloudLayer0.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false)
self.cloudShadowLayer0.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false)
self.cloudLayer1.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false)
self.cloudShadowLayer1.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false)
}
func animateOutToStatus(item: EmojiPagerContentComponent.Item, sourceLayer: CALayer, customEffectFile: String?, destinationView: UIView, fromBackground: Bool) {
self.isUserInteractionEnabled = false
destinationView.isHidden = true
let hapticFeedback: HapticFeedback
if let current = self.hapticFeedback {
hapticFeedback = current
} else {
hapticFeedback = HapticFeedback()
self.hapticFeedback = hapticFeedback
}
hapticFeedback.prepareTap()
var itemCompleted = false
var contentCompleted = false
var effectCompleted = false
let completion: () -> Void = { [weak self] in
guard let strongSelf = self, itemCompleted, contentCompleted, effectCompleted else {
return
}
strongSelf.controller?.dismissNow()
}
var effectView: AnimationView?
if let customEffectFile = customEffectFile, let data = try? Data(contentsOf: URL(fileURLWithPath: customEffectFile)), let composition = try? Animation.from(data: TGGUnzipData(data, 2 * 1024 * 1024) ?? data) {
let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
view.animationSpeed = 1.0
view.backgroundColor = nil
view.isOpaque = false
effectView = view
} else if let itemFile = item.itemFile {
var useCleanEffect = false
if itemFile.isCustomTemplateEmoji {
useCleanEffect = true
}
for attribute in itemFile.attributes {
if case let .CustomEmoji(_, _, _, packReference) = attribute {
switch packReference {
case let .id(id, _):
if id == 773947703670341676 || id == 2964141614563343 {
useCleanEffect = true
}
default:
break
}
}
}
var effectData: Data?
if useCleanEffect {
if let url = getAppBundle().url(forResource: "generic_reaction_avatar_effect", withExtension: "json") {
effectData = try? Data(contentsOf: url)
}
} else if let genericReactionEffect = self.genericReactionEffect, let data = try? Data(contentsOf: URL(fileURLWithPath: genericReactionEffect)) {
effectData = TGGUnzipData(data, 5 * 1024 * 1024) ?? data
} else {
if let url = getAppBundle().url(forResource: "generic_reaction_small_effect", withExtension: "json") {
effectData = try? Data(contentsOf: url)
}
}
if let effectData = effectData, let composition = try? Animation.from(data: effectData) {
let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
view.animationSpeed = 1.0
view.backgroundColor = nil
view.isOpaque = false
let animationCache = self.context.animationCache
let animationRenderer = self.context.animationRenderer
for i in 1 ... 7 {
let allLayers = view.allLayers(forKeypath: AnimationKeypath(keypath: "placeholder_\(i)"))
for animationLayer in allLayers {
let baseItemLayer = InlineStickerItemLayer(
context: self.context,
userLocation: .other,
attemptSynchronousLoad: false,
emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: itemFile.fileId.id, file: itemFile),
file: item.itemFile,
cache: animationCache,
renderer: animationRenderer,
placeholderColor: UIColor(white: 0.0, alpha: 0.0),
pointSize: CGSize(width: 32.0, height: 32.0),
dynamicColor: self.presentationData.theme.list.itemAccentColor
)
switch item.tintMode {
case .accent:
baseItemLayer.contentTintColor = self.presentationData.theme.list.itemAccentColor
case .primary:
baseItemLayer.contentTintColor = self.presentationData.theme.list.itemPrimaryTextColor
case .none:
break
}
if let sublayers = animationLayer.sublayers {
for sublayer in sublayers {
sublayer.isHidden = true
}
}
baseItemLayer.isVisibleForAnimations = true
baseItemLayer.frame = CGRect(origin: CGPoint(x: -0.0, y: -0.0), size: CGSize(width: 500.0, height: 500.0))
animationLayer.addSublayer(baseItemLayer)
}
}
effectView = view
}
}
if let sourceCopyLayer = sourceLayer.snapshotContentTree() {
self.layer.addSublayer(sourceCopyLayer)
sourceCopyLayer.frame = sourceLayer.convert(sourceLayer.bounds, to: self.layer)
sourceLayer.isHidden = true
let previousSourceCopyFrame = sourceCopyLayer.frame
let localDestinationFrame = destinationView.convert(destinationView.bounds, to: self.view)
let destinationSize = max(localDestinationFrame.width, localDestinationFrame.height)
let effectFrame = localDestinationFrame.insetBy(dx: -destinationSize * 2.0, dy: -destinationSize * 2.0)
let destinationNormalScale = localDestinationFrame.width / previousSourceCopyFrame.width
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear)
sourceCopyLayer.position = localDestinationFrame.center
var midPointY: CGFloat = localDestinationFrame.center.y - 30.0
if let layout = self.validLayout {
if midPointY < layout.safeInsets.top + 8.0 {
midPointY = max(localDestinationFrame.center.y, layout.safeInsets.top + 20.0)
}
}
transition.animatePositionWithKeyframes(layer: sourceCopyLayer, keyframes: generateParabollicMotionKeyframes(from: previousSourceCopyFrame.center, to: localDestinationFrame.center, midPointY: midPointY), completion: { [weak self, weak sourceCopyLayer, weak destinationView] _ in
guard let strongSelf = self else {
return
}
itemCompleted = true
sourceCopyLayer?.isHidden = true
if let destinationView = destinationView {
destinationView.isHidden = false
destinationView.layer.animateScale(from: 0.3, to: 1.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring)
}
hapticFeedback.tap()
if let effectView = effectView {
effectView.frame = effectFrame
strongSelf.view.addSubview(effectView)
effectView.play(completion: { _ in
effectCompleted = true
completion()
})
} else {
effectCompleted = true
}
completion()
})
let scaleKeyframes: [CGFloat] = [
1.0,
1.4,
1.0,
destinationNormalScale * 0.3
]
sourceCopyLayer.transform = CATransform3DMakeScale(scaleKeyframes[scaleKeyframes.count - 1], scaleKeyframes[scaleKeyframes.count - 1], 1.0)
sourceCopyLayer.animateKeyframes(values: scaleKeyframes.map({ $0 as NSNumber }), duration: 0.2, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.linear.rawValue)
} else {
itemCompleted = true
destinationView.isHidden = false
}
if let previewScreenView = self.previewScreenView {
self.previewItem = nil
self.dismissedPreviewItem = nil
self.previewScreenView = nil
if let previewScreenComponentView = previewScreenView.view as? EmojiStatusPreviewScreenComponent.View {
previewScreenComponentView.animateOut(targetLayer: nil, completion: { [weak previewScreenComponentView] in
previewScreenComponentView?.removeFromSuperview()
})
} else {
previewScreenView.view?.removeFromSuperview()
}
}
self.animateOut(completion: {
contentCompleted = true
completion()
}, fromBackground: fromBackground)
}
func containerLayoutUpdated(layout: ContainerViewLayout, transition: Transition) {
self.validLayout = layout
var transition = transition
guard let emojiContent = self.emojiContent else {
return
}
let listBackgroundColor: UIColor
let separatorColor: UIColor
if self.presentationData.theme.overallDarkAppearance {
listBackgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor
separatorColor = self.presentationData.theme.list.itemBlocksSeparatorColor
self.componentShadowLayer.shadowOpacity = 0.32
self.cloudShadowLayer0.shadowOpacity = 0.32
self.cloudShadowLayer1.shadowOpacity = 0.32
} else {
listBackgroundColor = self.presentationData.theme.list.plainBackgroundColor
separatorColor = self.presentationData.theme.list.itemPlainSeparatorColor.withMultipliedAlpha(0.5)
self.componentShadowLayer.shadowOpacity = 0.12
self.cloudShadowLayer0.shadowOpacity = 0.12
self.cloudShadowLayer1.shadowOpacity = 0.12
}
self.cloudLayer0.backgroundColor = listBackgroundColor.cgColor
self.cloudLayer1.backgroundColor = listBackgroundColor.cgColor
let sideInset: CGFloat = 16.0
if let scheduledEmojiContentAnimationHint = self.scheduledEmojiContentAnimationHint {
self.scheduledEmojiContentAnimationHint = nil
let contentAnimation = scheduledEmojiContentAnimationHint
transition = Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation)
}
var componentWidth = layout.size.width - sideInset * 2.0
let nativeItemSize: CGFloat = 40.0
let minSpacing: CGFloat = 9.0
let itemsPerRow = Int((componentWidth + minSpacing) / (nativeItemSize + minSpacing))
if itemsPerRow > 8 {
componentWidth = min(componentWidth, 480.0)
}
let componentSize = self.componentHost.update(
transition: transition,
component: AnyComponent(EmojiStatusSelectionComponent(
theme: self.presentationData.theme,
strings: self.presentationData.strings,
deviceMetrics: layout.deviceMetrics,
emojiContent: emojiContent,
backgroundColor: listBackgroundColor,
separatorColor: separatorColor,
hideTopPanel: self.isReactionSearchActive,
hideTopPanelUpdated: { [weak self] hideTopPanel, transition in
guard let strongSelf = self else {
return
}
strongSelf.isReactionSearchActive = hideTopPanel
strongSelf.refreshLayout(transition: transition)
}
)),
environment: {},
containerSize: CGSize(width: componentWidth, height: min(308.0, layout.size.height))
)
if let componentView = self.componentHost.view {
var animateIn = false
if componentView.superview == nil {
self.view.addSubview(componentView)
animateIn = true
componentView.clipsToBounds = true
componentView.layer.cornerRadius = 24.0
}
var sourceOrigin: CGPoint
if let sourceView = self.sourceView {
let sourceRect = sourceView.convert(sourceView.bounds, to: self.view)
sourceOrigin = CGPoint(x: sourceRect.midX, y: sourceRect.maxY)
} else if let globalSourceRect = self.globalSourceRect {
let sourceRect = self.view.convert(globalSourceRect, from: nil)
sourceOrigin = CGPoint(x: sourceRect.midX, y: sourceRect.maxY)
} else {
sourceOrigin = CGPoint(x: layout.size.width / 2.0, y: floor(layout.size.height / 2.0 - componentSize.height))
}
if sourceOrigin.y + 5.0 + componentSize.height > layout.size.height - layout.insets(options: []).bottom {
sourceOrigin.y = layout.size.height - layout.insets(options: []).bottom - componentSize.height - 5.0
}
let componentFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - componentSize.width) / 2.0), y: sourceOrigin.y + 5.0), size: componentSize)
if self.componentShadowLayer.bounds.size != componentFrame.size {
let componentShadowPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: componentFrame.size), cornerRadius: 24.0).cgPath
self.componentShadowLayer.shadowPath = componentShadowPath
}
transition.setFrame(layer: self.componentShadowLayer, frame: componentFrame)
let cloudOffset0: CGFloat = 30.0
let cloudSize0: CGFloat = 16.0
var cloudFrame0 = CGRect(origin: CGPoint(x: floor(sourceOrigin.x + cloudOffset0 - cloudSize0 / 2.0), y: componentFrame.minY - cloudSize0 / 2.0), size: CGSize(width: cloudSize0, height: cloudSize0))
var invertX = false
if cloudFrame0.maxX >= layout.size.width - layout.safeInsets.right - 32.0 {
cloudFrame0.origin.x = floor(sourceOrigin.x - cloudSize0 - cloudOffset0 + cloudSize0 / 2.0)
invertX = true
}
transition.setFrame(layer: self.cloudLayer0, frame: cloudFrame0)
if self.cloudShadowLayer0.bounds.size != cloudFrame0.size {
let cloudShadowPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: cloudFrame0.size), cornerRadius: 24.0).cgPath
self.cloudShadowLayer0.shadowPath = cloudShadowPath
}
transition.setFrame(layer: self.cloudShadowLayer0, frame: cloudFrame0)
transition.setCornerRadius(layer: self.cloudLayer0, cornerRadius: cloudFrame0.width / 2.0)
let cloudOffset1 = CGPoint(x: -9.0, y: -14.0)
let cloudSize1: CGFloat = 8.0
var cloudFrame1 = CGRect(origin: CGPoint(x: floor(cloudFrame0.midX + cloudOffset1.x - cloudSize1 / 2.0), y: floor(cloudFrame0.midY + cloudOffset1.y - cloudSize1 / 2.0)), size: CGSize(width: cloudSize1, height: cloudSize1))
if invertX {
cloudFrame1.origin.x = floor(cloudFrame0.midX - cloudSize1 - cloudOffset1.x + cloudSize1 / 2.0)
}
transition.setFrame(layer: self.cloudLayer1, frame: cloudFrame1)
if self.cloudShadowLayer1.bounds.size != cloudFrame1.size {
let cloudShadowPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: cloudFrame1.size), cornerRadius: 24.0).cgPath
self.cloudShadowLayer1.shadowPath = cloudShadowPath
}
transition.setFrame(layer: self.cloudShadowLayer1, frame: cloudFrame1)
transition.setCornerRadius(layer: self.cloudLayer1, cornerRadius: cloudFrame1.width / 2.0)
transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height)))
if animateIn {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak self] _ in
self?.allowsGroupOpacity = false
})
let contentDuration: Double = 0.3
let contentDelay: Double = 0.14
let initialContentFrame = CGRect(origin: CGPoint(x: cloudFrame0.midX - 24.0, y: componentFrame.minY), size: CGSize(width: 24.0 * 2.0, height: 24.0 * 2.0))
if let emojiView = self.componentHost.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View {
emojiView.animateIn(fromLocation: self.view.convert(initialContentFrame.center, to: emojiView))
}
componentView.layer.animatePosition(from: initialContentFrame.center, to: componentFrame.center, duration: contentDuration, delay: contentDelay, timingFunction: kCAMediaTimingFunctionSpring)
componentView.layer.animateBounds(from: CGRect(origin: CGPoint(x: -(componentFrame.minX - initialContentFrame.minX), y: -(componentFrame.minY - initialContentFrame.minY)), size: initialContentFrame.size), to: CGRect(origin: CGPoint(), size: componentFrame.size), duration: contentDuration, delay: contentDelay, timingFunction: kCAMediaTimingFunctionSpring)
self.componentShadowLayer.animateFrame(from: CGRect(origin: CGPoint(x: cloudFrame0.midX - 24.0, y: componentFrame.minY), size: CGSize(width: 24.0 * 2.0, height: 24.0 * 2.0)), to: componentView.frame, duration: contentDuration, delay: contentDelay, timingFunction: kCAMediaTimingFunctionSpring)
componentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.04, delay: contentDelay)
self.componentShadowLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.04, delay: contentDelay)
let initialComponentShadowPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: initialContentFrame.size), cornerRadius: 24.0).cgPath
self.componentShadowLayer.animate(from: initialComponentShadowPath, to: self.componentShadowLayer.shadowPath!, keyPath: "shadowPath", timingFunction: kCAMediaTimingFunctionSpring, duration: contentDuration, delay: contentDelay)
self.cloudLayer0.animateScale(from: 0.01, to: 1.0, duration: 0.4, delay: 0.05, timingFunction: kCAMediaTimingFunctionSpring)
self.cloudShadowLayer0.animateScale(from: 0.01, to: 1.0, duration: 0.4, delay: 0.05, timingFunction: kCAMediaTimingFunctionSpring)
self.cloudLayer1.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
self.cloudShadowLayer1.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
}
}
if let previewItem = self.previewItem, let itemFile = previewItem.item.itemFile {
let previewScreenView: ComponentView<Empty>
var previewScreenTransition = transition
if let current = self.previewScreenView {
previewScreenView = current
} else {
previewScreenTransition = Transition(animation: .none)
if let emojiView = self.componentHost.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View, let sourceLayer = emojiView.layerForItem(groupId: previewItem.groupId, item: previewItem.item) {
previewScreenTransition = previewScreenTransition.withUserData(EmojiStatusPreviewScreenComponent.TransitionAnimation(
transitionType: .animateIn(sourceLayer: sourceLayer)
))
}
previewScreenView = ComponentView<Empty>()
self.previewScreenView = previewScreenView
}
let _ = previewScreenView.update(
transition: previewScreenTransition,
component: AnyComponent(EmojiStatusPreviewScreenComponent(
theme: self.presentationData.theme,
strings: self.presentationData.strings,
bottomInset: layout.insets(options: []).bottom,
item: EmojiStatusComponent(
context: self.context,
animationCache: self.context.animationCache,
animationRenderer: self.context.animationRenderer,
content: .animation(
content: .file(file: itemFile),
size: CGSize(width: 128.0, height: 128.0),
placeholderColor: self.presentationData.theme.list.plainBackgroundColor.withMultipliedAlpha(0.1),
themeColor: self.presentationData.theme.list.itemAccentColor,
loopMode: .forever
),
isVisibleForAnimations: true,
useSharedAnimation: false,
action: nil
),
dismiss: { [weak self] result in
guard let strongSelf = self else {
return
}
if let result = result, let previewItem = strongSelf.previewItem {
var emojiString: String?
if let itemFile = previewItem.item.itemFile {
attributeLoop: for attribute in itemFile.attributes {
switch attribute {
case let .CustomEmoji(_, _, alt, _):
emojiString = alt
break attributeLoop
default:
break
}
}
}
let context = strongSelf.context
let _ = (context.engine.stickers.availableReactions()
|> take(1)
|> mapToSignal { availableReactions -> Signal<String?, NoError> in
guard let emojiString = emojiString, let availableReactions = availableReactions else {
return .single(nil)
}
for reaction in availableReactions.reactions {
if case let .builtin(value) = reaction.value, value == emojiString {
if let aroundAnimation = reaction.aroundAnimation {
return context.account.postbox.mediaBox.resourceData(aroundAnimation.resource)
|> take(1)
|> map { data -> String? in
if data.complete {
return data.path
} else {
return nil
}
}
} else {
return .single(nil)
}
}
}
return .single(nil)
}
|> deliverOnMainQueue).start(next: { filePath in
guard let strongSelf = self, let previewItem = strongSelf.previewItem, let destinationView = strongSelf.controller?.destinationItemView() else {
return
}
let expirationDate: Int32? = result.timestamp
let _ = (strongSelf.context.engine.accountData.setEmojiStatus(file: previewItem.item.itemFile, expirationDate: expirationDate)
|> deliverOnMainQueue).start()
strongSelf.animateOutToStatus(item: previewItem.item, sourceLayer: result.sourceView.layer, customEffectFile: filePath, destinationView: destinationView, fromBackground: true)
})
} else {
strongSelf.dismissedPreviewItem = strongSelf.previewItem
strongSelf.previewItem = nil
strongSelf.refreshLayout(transition: .immediate)
}
}
)),
environment: {},
containerSize: layout.size
)
if let view = previewScreenView.view {
if view.superview == nil {
self.view.addSubview(view)
}
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(), size: layout.size))
}
} else if let previewScreenView = self.previewScreenView {
self.previewScreenView = nil
if let previewScreenComponentView = previewScreenView.view as? EmojiStatusPreviewScreenComponent.View {
var targetLayer: CALayer?
if let previewItem = self.dismissedPreviewItem, let emojiView = self.componentHost.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View, let sourceLayer = emojiView.layerForItem(groupId: previewItem.groupId, item: previewItem.item) {
targetLayer = sourceLayer
}
previewScreenComponentView.animateOut(targetLayer: targetLayer, completion: { [weak previewScreenComponentView] in
previewScreenComponentView?.removeFromSuperview()
})
} else {
previewScreenView.view?.removeFromSuperview()
}
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let result = super.hitTest(point, with: event) {
if self.isDismissed {
return self.view
}
if result === self.view {
self.isDismissed = true
self.controller?.dismiss()
}
return result
}
return nil
}
private func applyItem(groupId: AnyHashable, item: EmojiPagerContentComponent.Item?, isPreview: Bool) {
guard let controller = self.controller else {
return
}
if isPreview {
guard let item = item else {
return
}
self.previewItem = (groupId, item)
self.view.endEditing(true)
self.refreshLayout(transition: .immediate)
} else {
self.freezeUpdates = true
if case .statusSelection = controller.mode, let item = item, let currentSelection = self.currentSelection, item.itemFile?.fileId.id == currentSelection {
let _ = (self.context.engine.accountData.setEmojiStatus(file: nil, expirationDate: nil)
|> deliverOnMainQueue).start()
controller.dismiss()
return
}
if let _ = item, let destinationView = controller.destinationItemView() {
if let snapshotView = destinationView.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = destinationView.frame
destinationView.superview?.insertSubview(snapshotView, belowSubview: destinationView)
snapshotView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
destinationView.isHidden = true
}
switch controller.mode {
case .statusSelection:
let _ = (self.context.engine.accountData.setEmojiStatus(file: item?.itemFile, expirationDate: nil)
|> deliverOnMainQueue).start()
case let .quickReactionSelection(completion):
if let item = item, let itemFile = item.itemFile {
var selectedReaction: MessageReaction.Reaction?
if let availableReactions = self.availableReactions {
for reaction in availableReactions.reactions {
if reaction.selectAnimation.fileId == itemFile.fileId {
selectedReaction = reaction.value
break
}
}
}
if selectedReaction == nil {
selectedReaction = .custom(itemFile.fileId.id)
}
if let selectedReaction = selectedReaction {
let _ = context.engine.stickers.updateQuickReaction(reaction: selectedReaction).start()
}
}
completion()
}
if let item = item, let destinationView = controller.destinationItemView() {
var emojiString: String?
if let itemFile = item.itemFile {
attributeLoop: for attribute in itemFile.attributes {
switch attribute {
case let .CustomEmoji(_, _, alt, _):
emojiString = alt
break attributeLoop
default:
break
}
}
}
let context = self.context
let _ = (context.engine.stickers.availableReactions()
|> take(1)
|> mapToSignal { availableReactions -> Signal<String?, NoError> in
guard let emojiString = emojiString, let availableReactions = availableReactions else {
return .single(nil)
}
for reaction in availableReactions.reactions {
if case let .builtin(value) = reaction.value, value == emojiString {
if let aroundAnimation = reaction.aroundAnimation {
return context.account.postbox.mediaBox.resourceData(aroundAnimation.resource)
|> take(1)
|> map { data -> String? in
if data.complete {
return data.path
} else {
return nil
}
}
} else {
return .single(nil)
}
}
}
return .single(nil)
}
|> deliverOnMainQueue).start(next: { [weak self] filePath in
guard let strongSelf = self else {
return
}
guard let emojiView = strongSelf.componentHost.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View, let sourceLayer = emojiView.layerForItem( groupId: groupId, item: item) else {
strongSelf.controller?.dismiss()
return
}
strongSelf.animateOutToStatus(item: item, sourceLayer: sourceLayer, customEffectFile: filePath, destinationView: destinationView, fromBackground: false)
})
} else {
controller.dismiss()
}
}
}
}
public enum Mode {
case statusSelection
case quickReactionSelection(completion: () -> Void)
}
private let context: AccountContext
private weak var sourceView: UIView?
private let emojiContent: Signal<EmojiPagerContentComponent, NoError>
private let currentSelection: Int64?
private let mode: Mode
private let destinationItemView: () -> UIView?
fileprivate let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
override public var overlayWantsToBeBelowKeyboard: Bool {
return true
}
public init(context: AccountContext, mode: Mode, sourceView: UIView, emojiContent: Signal<EmojiPagerContentComponent, NoError>, currentSelection: Int64?, destinationItemView: @escaping () -> UIView?) {
self.context = context
self.mode = mode
self.sourceView = sourceView
self.emojiContent = emojiContent
self.currentSelection = currentSelection
self.destinationItemView = destinationItemView
super.init(navigationBarPresentationData: nil)
self.lockOrientation = true
self.statusBar.statusBarStyle = .Ignore
}
required public init(coder: NSCoder) {
preconditionFailure()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
private func dismissNow() {
self.presentingViewController?.dismiss(animated: false, completion: nil)
}
override public func dismiss(completion: (() -> Void)? = nil) {
(self.displayNode as! Node).animateOut(completion: { [weak self] in
self?.presentingViewController?.dismiss(animated: false, completion: nil)
completion?()
}, fromBackground: false)
}
override public func loadDisplayNode() {
self.displayNode = Node(controller: self, context: self.context, sourceView: self.sourceView, emojiContent: self.emojiContent, currentSelection: self.currentSelection)
super.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition))
}
}
private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, midPointY: CGFloat) -> [CGPoint] {
let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: midPointY)
let x1 = sourcePoint.x
let y1 = sourcePoint.y
let x2 = midPoint.x
let y2 = midPoint.y
let x3 = targetPosition.x
let y3 = targetPosition.y
var keyframes: [CGPoint] = []
if abs(y1 - y3) < 5.0 && abs(x1 - x3) < 5.0 {
for i in 0 ..< 10 {
let k = CGFloat(i) / CGFloat(10 - 1)
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
let y = sourcePoint.y * (1.0 - k) + targetPosition.y * k
keyframes.append(CGPoint(x: x, y: y))
}
} else {
let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
for i in 0 ..< 10 {
let k = CGFloat(i) / CGFloat(10 - 1)
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
let y = a * x * x + b * x + c
keyframes.append(CGPoint(x: x, y: y))
}
}
return keyframes
}