mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
2893 lines
142 KiB
Swift
2893 lines
142 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import TelegramPresentationData
|
|
import AccountContext
|
|
import ComponentFlow
|
|
import ViewControllerComponent
|
|
import EntityKeyboard
|
|
import PagerComponent
|
|
import FeaturedStickersScreen
|
|
import TelegramNotices
|
|
import ChatEntityKeyboardInputNode
|
|
import ContextUI
|
|
import ChatPresentationInterfaceState
|
|
import MediaEditor
|
|
import EntityKeyboardGifContent
|
|
import CameraButtonComponent
|
|
import BundleIconComponent
|
|
import LottieComponent
|
|
import LottieComponentResourceContent
|
|
import UndoUI
|
|
import GalleryUI
|
|
import TextLoadingEffect
|
|
import TelegramStringFormatting
|
|
|
|
private final class StickerSelectionComponent: Component {
|
|
typealias EnvironmentType = Empty
|
|
|
|
let context: AccountContext
|
|
let theme: PresentationTheme
|
|
let strings: PresentationStrings
|
|
let deviceMetrics: DeviceMetrics
|
|
let topInset: CGFloat
|
|
let bottomInset: CGFloat
|
|
let content: StickerPickerInputData
|
|
let backgroundColor: UIColor
|
|
let separatorColor: UIColor
|
|
let getController: () -> StickerPickerScreen?
|
|
|
|
init(
|
|
context: AccountContext,
|
|
theme: PresentationTheme,
|
|
strings: PresentationStrings,
|
|
deviceMetrics: DeviceMetrics,
|
|
topInset: CGFloat,
|
|
bottomInset: CGFloat,
|
|
content: StickerPickerInputData,
|
|
backgroundColor: UIColor,
|
|
separatorColor: UIColor,
|
|
getController: @escaping () -> StickerPickerScreen?
|
|
) {
|
|
self.context = context
|
|
self.theme = theme
|
|
self.strings = strings
|
|
self.deviceMetrics = deviceMetrics
|
|
self.topInset = topInset
|
|
self.bottomInset = bottomInset
|
|
self.content = content
|
|
self.backgroundColor = backgroundColor
|
|
self.separatorColor = separatorColor
|
|
self.getController = getController
|
|
}
|
|
|
|
public static func ==(lhs: StickerSelectionComponent, rhs: StickerSelectionComponent) -> Bool {
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.strings != rhs.strings {
|
|
return false
|
|
}
|
|
if lhs.deviceMetrics != rhs.deviceMetrics {
|
|
return false
|
|
}
|
|
if lhs.topInset != rhs.topInset {
|
|
return false
|
|
}
|
|
if lhs.bottomInset != rhs.bottomInset {
|
|
return false
|
|
}
|
|
if lhs.content != rhs.content {
|
|
return false
|
|
}
|
|
if lhs.backgroundColor != rhs.backgroundColor {
|
|
return false
|
|
}
|
|
if lhs.separatorColor != rhs.separatorColor {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class KeyboardClippingView: UIView {
|
|
var hitEdgeInsets: UIEdgeInsets = .zero
|
|
|
|
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
let bounds = self.bounds.inset(by: self.hitEdgeInsets)
|
|
return bounds.contains(point)
|
|
}
|
|
}
|
|
|
|
public final class View: UIView {
|
|
fileprivate let keyboardView: ComponentView<Empty>
|
|
private let keyboardClippingView: KeyboardClippingView
|
|
private let panelHostView: PagerExternalTopPanelContainer
|
|
private let panelBackgroundView: BlurredBackgroundView
|
|
private let panelSeparatorView: UIView
|
|
|
|
private var component: StickerSelectionComponent?
|
|
private weak var state: EmptyComponentState?
|
|
|
|
private var interaction: ChatEntityKeyboardInputNode.Interaction?
|
|
private var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
|
|
|
private var searchVisible = false
|
|
private var forceUpdate = false
|
|
|
|
private var ignoreNextZeroScrollingOffset = false
|
|
private var topPanelScrollingOffset: CGFloat = 0.0
|
|
private var keyboardContentId: AnyHashable?
|
|
|
|
override init(frame: CGRect) {
|
|
self.keyboardView = ComponentView<Empty>()
|
|
self.keyboardClippingView = KeyboardClippingView()
|
|
self.panelHostView = PagerExternalTopPanelContainer()
|
|
self.panelBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
|
self.panelBackgroundView.isUserInteractionEnabled = false
|
|
self.panelSeparatorView = UIView()
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.addSubview(self.keyboardClippingView)
|
|
self.addSubview(self.panelBackgroundView)
|
|
self.addSubview(self.panelSeparatorView)
|
|
self.addSubview(self.panelHostView)
|
|
|
|
self.interaction = ChatEntityKeyboardInputNode.Interaction(
|
|
sendSticker: { [weak self] file, silent, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, _ in
|
|
if let self, let controller = self.component?.getController() {
|
|
controller.forEachController { c in
|
|
if let c = c as? (ViewController & StickerPackScreen) {
|
|
c.dismiss(animated: true)
|
|
}
|
|
return true
|
|
}
|
|
controller.window?.forEachController({ c in
|
|
if let c = c as? (ViewController & StickerPackScreen) {
|
|
c.dismiss(animated: true)
|
|
}
|
|
})
|
|
if controller.completion(.file(file, .sticker)) {
|
|
controller.dismiss(animated: true)
|
|
}
|
|
}
|
|
return false
|
|
},
|
|
sendEmoji: { _, _, _ in
|
|
},
|
|
sendGif: { [weak self] file, _, _, _, _ in
|
|
if let self, let controller = self.component?.getController() {
|
|
if controller.completion(.video(file.media)) {
|
|
controller.dismiss(animated: true)
|
|
}
|
|
}
|
|
return false
|
|
},
|
|
sendBotContextResultAsGif: { [weak self] collection, result, _, _, _, _ in
|
|
if let self, let controller = self.component?.getController() {
|
|
if case let .internalReference(reference) = result {
|
|
if let file = reference.file {
|
|
if controller.completion(.video(file)) {
|
|
controller.dismiss(animated: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
},
|
|
updateChoosingSticker: { _ in },
|
|
switchToTextInput: {},
|
|
dismissTextInput: {},
|
|
insertText: { _ in
|
|
},
|
|
backwardsDeleteText: {},
|
|
openStickerEditor: {},
|
|
presentController: { [weak self] c, a in
|
|
if let self, let controller = self.component?.getController() {
|
|
controller.present(c, in: .window(.root), with: a)
|
|
}
|
|
},
|
|
presentGlobalOverlayController: { [weak self] c, a in
|
|
if let self, let controller = self.component?.getController() {
|
|
controller.presentInGlobalOverlay(c, with: a)
|
|
}
|
|
},
|
|
getNavigationController: {
|
|
return nil
|
|
},
|
|
requestLayout: { transition in
|
|
let _ = transition
|
|
}
|
|
)
|
|
|
|
self.inputNodeInteraction = ChatMediaInputNodeInteraction(
|
|
navigateToCollectionId: { _ in
|
|
},
|
|
navigateBackToStickers: {
|
|
},
|
|
setGifMode: { _ in
|
|
},
|
|
openSettings: {
|
|
},
|
|
openTrending: { _ in
|
|
},
|
|
dismissTrendingPacks: { _ in
|
|
},
|
|
toggleSearch: { _, _, _ in
|
|
},
|
|
openPeerSpecificSettings: {
|
|
},
|
|
dismissPeerSpecificSettings: {
|
|
},
|
|
clearRecentlyUsedStickers: {
|
|
}
|
|
)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
}
|
|
|
|
func scrolledToItemGroup() {
|
|
self.topPanelScrollingOffset = 30.0
|
|
self.ignoreNextZeroScrollingOffset = true
|
|
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
|
}
|
|
|
|
func update(component: StickerSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> 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 = 42.0
|
|
let topInset = component.topInset
|
|
|
|
let controller = component.getController()
|
|
let defaultToEmoji = controller?.defaultToEmoji ?? false
|
|
|
|
let context = component.context
|
|
let stickerPeekBehavior = EmojiContentPeekBehaviorImpl(
|
|
context: context,
|
|
forceTheme: controller?.forceDark == true ? defaultDarkColorPresentationTheme : nil,
|
|
interaction: nil,
|
|
chatPeerId: nil,
|
|
present: { c, a in
|
|
}
|
|
)
|
|
|
|
let isFullscreen = controller?.isFullscreen == true
|
|
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 + topInset, left: 0.0, bottom: component.bottomInset, right: 0.0),
|
|
topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0),
|
|
emojiContent: component.content.emoji,
|
|
stickerContent: component.content.stickers,
|
|
maskContent: nil,
|
|
gifContent: component.content.gifs,
|
|
hasRecentGifs: !isFullscreen,
|
|
availableGifSearchEmojies: [],
|
|
defaultToEmojiTab: defaultToEmoji,
|
|
externalTopPanelContainer: self.panelHostView,
|
|
externalBottomPanelContainer: nil,
|
|
displayTopPanelBackground: .blur,
|
|
topPanelExtensionUpdated: { _, _ in
|
|
},
|
|
topPanelScrollingOffset: { [weak self] offset, transition in
|
|
if let self {
|
|
if self.ignoreNextZeroScrollingOffset && offset == 0.0 {
|
|
} else {
|
|
self.ignoreNextZeroScrollingOffset = false
|
|
self.topPanelScrollingOffset = offset
|
|
}
|
|
}
|
|
},
|
|
hideInputUpdated: { [weak self] _, searchVisible, transition in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.forceUpdate = true
|
|
self.searchVisible = searchVisible
|
|
self.state?.updated(transition: transition)
|
|
},
|
|
hideTopPanelUpdated: { _, _ in
|
|
},
|
|
switchToTextInput: {},
|
|
switchToGifSubject: { _ in },
|
|
reorderItems: { _, _ in },
|
|
makeSearchContainerNode: { [weak self] content in
|
|
guard let self, let interaction = self.interaction, let inputNodeInteraction = self.inputNodeInteraction, let component = self.component, let controller = component.getController() else {
|
|
return nil
|
|
}
|
|
|
|
let mappedMode: ChatMediaInputSearchMode
|
|
switch content {
|
|
case .stickers:
|
|
mappedMode = .sticker
|
|
case .gifs:
|
|
mappedMode = .gif
|
|
}
|
|
|
|
var presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
if controller.forceDark == true {
|
|
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
|
|
}
|
|
let searchContainerNode = PaneSearchContainerNode(
|
|
context: context,
|
|
theme: presentationData.theme,
|
|
strings: presentationData.strings,
|
|
interaction: interaction,
|
|
inputNodeInteraction: inputNodeInteraction,
|
|
mode: mappedMode,
|
|
stickerActionTitle: presentationData.strings.StickerPack_AddSticker,
|
|
trendingGifsPromise: self.component?.getController()?.node.trendingGifsPromise ?? Promise(nil),
|
|
cancel: {
|
|
},
|
|
peekBehavior: stickerPeekBehavior
|
|
)
|
|
searchContainerNode.openGifContextMenu = { [weak self] item, sourceNode, sourceRect, gesture, isSaved in
|
|
guard let self, let node = self.component?.getController()?.node else {
|
|
return
|
|
}
|
|
node.openGifContextMenu(file: item.file, contextResult: item.contextResult, sourceView: sourceNode.view, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved)
|
|
}
|
|
return searchContainerNode
|
|
},
|
|
contentIdUpdated: { [weak self] id in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.keyboardContentId = id
|
|
},
|
|
deviceMetrics: component.deviceMetrics,
|
|
hiddenInputHeight: 0.0,
|
|
inputHeight: 0.0,
|
|
displayBottomPanel: !isFullscreen,
|
|
isExpanded: true,
|
|
clipContentToTopPanel: false,
|
|
useExternalSearchContainer: false
|
|
)),
|
|
environment: {},
|
|
forceUpdate: self.forceUpdate,
|
|
containerSize: availableSize
|
|
)
|
|
self.forceUpdate = false
|
|
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 + topInset), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelHeight - topInset)))
|
|
self.keyboardClippingView.hitEdgeInsets = UIEdgeInsets(top: -topPanelHeight - topInset, left: 0.0, bottom: 0.0, right: 0.0)
|
|
|
|
transition.setFrame(view: keyboardComponentView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topPanelHeight - topInset), size: keyboardSize))
|
|
transition.setFrame(view: self.panelHostView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight + topInset - 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 + topInset)))
|
|
self.panelBackgroundView.update(size: self.panelBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition)
|
|
|
|
let topPanelAlpha: CGFloat
|
|
if self.searchVisible || self.keyboardContentId == AnyHashable("gifs") {
|
|
topPanelAlpha = 0.0
|
|
if isFullscreen, let navigationBar = controller?.navigationBar, navigationBar.alpha > 0.0 {
|
|
transition.setAlpha(view: navigationBar.view, alpha: 0.0)
|
|
}
|
|
} else if isFullscreen {
|
|
topPanelAlpha = 1.0
|
|
if let navigationBar = controller?.navigationBar, navigationBar.alpha < 1.0 {
|
|
transition.setAlpha(view: navigationBar.view, alpha: 1.0)
|
|
}
|
|
} else {
|
|
topPanelAlpha = max(0.0, min(1.0, (self.topPanelScrollingOffset / 20.0)))
|
|
}
|
|
|
|
transition.setAlpha(view: self.panelBackgroundView, alpha: topPanelAlpha)
|
|
transition.setAlpha(view: self.panelSeparatorView, alpha: topPanelAlpha)
|
|
|
|
transition.setFrame(view: self.panelSeparatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight + topInset - UIScreenPixel), size: CGSize(width: keyboardSize.width, height: UIScreenPixel)))
|
|
}
|
|
|
|
return availableSize
|
|
}
|
|
|
|
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
let result = super.hitTest(point, with: event)
|
|
|
|
if self.searchVisible, let keyboardView = self.keyboardView.view, let keyboardResult = keyboardView.hitTest(self.convert(point, to: keyboardView), with: event) {
|
|
return keyboardResult
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public 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 StickerPickerScreen: ViewController {
|
|
final class Node: ViewControllerTracingNode, ASScrollViewDelegate, ASGestureRecognizerDelegate {
|
|
private var presentationData: PresentationData
|
|
private weak var controller: StickerPickerScreen?
|
|
private let theme: PresentationTheme
|
|
|
|
let dim: ASDisplayNode
|
|
let wrappingView: UIView
|
|
let containerView: UIView
|
|
let hostView: ComponentHostView<Empty>
|
|
|
|
fileprivate var content: StickerPickerInputData?
|
|
private let contentDisposable = MetaDisposable()
|
|
private var hasRecentGifsDisposable: Disposable?
|
|
fileprivate let trendingGifsPromise = Promise<ChatMediaInputGifPaneTrendingState?>(nil)
|
|
private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation?
|
|
|
|
private(set) var isExpanded = false
|
|
private var panGestureRecognizer: UIPanGestureRecognizer?
|
|
private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?, listNode: ListView?)?
|
|
|
|
private var currentIsVisible: Bool = false
|
|
private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
|
|
|
|
fileprivate var temporaryDismiss = false
|
|
|
|
private var gifMode: GifPagerContentComponent.Subject? {
|
|
didSet {
|
|
if let gifMode = self.gifMode, gifMode != oldValue {
|
|
self.reloadGifContext()
|
|
}
|
|
}
|
|
}
|
|
|
|
private var gifContext: GifContext? {
|
|
didSet {
|
|
if let gifContext = self.gifContext {
|
|
self.gifComponent.set(gifContext.component)
|
|
}
|
|
}
|
|
}
|
|
private let gifComponent = Promise<EntityKeyboardGifContent>()
|
|
private var gifInputInteraction: GifPagerContentComponent.InputInteraction?
|
|
|
|
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 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 let stickerSearchDisposable = MetaDisposable()
|
|
private let stickerSearchState = Promise<EmojiSearchState>(EmojiSearchState(result: nil, isSearching: false))
|
|
private var stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false) {
|
|
didSet {
|
|
self.stickerSearchState.set(.single(self.stickerSearchStateValue))
|
|
}
|
|
}
|
|
|
|
private var storyStickersContentView: StoryStickersContentView?
|
|
|
|
init(context: AccountContext, controller: StickerPickerScreen, theme: PresentationTheme) {
|
|
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
self.controller = controller
|
|
self.theme = theme
|
|
|
|
self.dim = ASDisplayNode()
|
|
self.dim.alpha = 0.0
|
|
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
|
|
|
self.wrappingView = SparseContainerView()
|
|
self.containerView = SparseContainerView()
|
|
self.hostView = ComponentHostView()
|
|
|
|
super.init()
|
|
|
|
self.containerView.clipsToBounds = true
|
|
self.containerView.backgroundColor = .clear
|
|
|
|
if !controller.isFullscreen {
|
|
self.addSubnode(self.dim)
|
|
}
|
|
|
|
self.view.addSubview(self.wrappingView)
|
|
self.wrappingView.addSubview(self.containerView)
|
|
self.containerView.addSubview(self.hostView)
|
|
|
|
if controller.hasInteractiveStickers {
|
|
self.storyStickersContentView = StoryStickersContentView(
|
|
context: context,
|
|
weather: controller.weather
|
|
)
|
|
self.storyStickersContentView?.locationAction = { [weak self] in
|
|
self?.controller?.presentLocationPicker()
|
|
}
|
|
self.storyStickersContentView?.audioAction = { [weak self] in
|
|
self?.controller?.presentAudioPicker()
|
|
}
|
|
self.storyStickersContentView?.reactionAction = { [weak self] in
|
|
self?.controller?.addReaction()
|
|
}
|
|
self.storyStickersContentView?.linkAction = { [weak self] in
|
|
guard let self, let controller = self.controller else {
|
|
return
|
|
}
|
|
if controller.context.isPremium {
|
|
controller.addLink()
|
|
} else {
|
|
self.presentLinkPremiumSuggestion()
|
|
}
|
|
}
|
|
self.storyStickersContentView?.weatherAction = { [weak self] in
|
|
self?.controller?.addWeather()
|
|
}
|
|
}
|
|
|
|
let gifItems: Signal<EntityKeyboardGifContent?, NoError>
|
|
if controller.hasGifs {
|
|
let hasRecentGifs = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|
|
|> map { savedGifs -> Bool in
|
|
return !savedGifs.isEmpty
|
|
}
|
|
|
|
self.hasRecentGifsDisposable = (hasRecentGifs
|
|
|> deliverOnMainQueue).start(next: { [weak self] hasRecentGifs in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
if let gifMode = strongSelf.gifMode {
|
|
if !hasRecentGifs, case .recent = gifMode {
|
|
strongSelf.gifMode = .trending
|
|
}
|
|
} else {
|
|
strongSelf.gifMode = hasRecentGifs ? .recent : .trending
|
|
}
|
|
}).strict()
|
|
|
|
self.trendingGifsPromise.set(.single(nil))
|
|
self.trendingGifsPromise.set(paneGifSearchForQuery(context: context, query: "", offset: nil, incompleteResults: true, delayRequest: false, updateActivity: nil)
|
|
|> map { items -> ChatMediaInputGifPaneTrendingState? in
|
|
if let items = items {
|
|
return ChatMediaInputGifPaneTrendingState(files: items.files, nextOffset: items.nextOffset)
|
|
} else {
|
|
return nil
|
|
}
|
|
})
|
|
|
|
let gifInputInteraction = GifPagerContentComponent.InputInteraction(
|
|
performItemAction: { [weak self] item, view, rect in
|
|
guard let self, let controller = self.controller else {
|
|
return
|
|
}
|
|
if controller.completion(.video(item.file.media)) {
|
|
controller.dismiss(animated: true)
|
|
}
|
|
},
|
|
openGifContextMenu: { [weak self] item, sourceView, sourceRect, gesture, isSaved in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.openGifContextMenu(file: item.file, contextResult: item.contextResult, sourceView: sourceView, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved)
|
|
},
|
|
loadMore: { [weak self] token in
|
|
guard let strongSelf = self, let gifContext = strongSelf.gifContext else {
|
|
return
|
|
}
|
|
gifContext.loadMore(token: token)
|
|
},
|
|
openSearch: { [weak self] in
|
|
if let self, let componentView = self.hostView.componentView as? StickerSelectionComponent.View {
|
|
if let pagerView = componentView.keyboardView.view as? EntityKeyboardComponent.View {
|
|
pagerView.openSearch()
|
|
}
|
|
self.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
|
|
}
|
|
},
|
|
updateSearchQuery: { [weak self] query in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if let query {
|
|
self.gifMode = .emojiSearch(query)
|
|
} else {
|
|
self.gifMode = .recent
|
|
}
|
|
},
|
|
hideBackground: true,
|
|
hasSearch: true
|
|
)
|
|
self.gifInputInteraction = gifInputInteraction
|
|
|
|
gifItems = .single(EntityKeyboardGifContent(
|
|
hasRecentGifs: true,
|
|
component: GifPagerContentComponent(
|
|
context: context,
|
|
inputInteraction: gifInputInteraction,
|
|
subject: .recent,
|
|
items: [],
|
|
isLoading: false,
|
|
loadMoreToken: nil,
|
|
displaySearchWithPlaceholder: nil,
|
|
searchCategories: nil,
|
|
searchInitiallyHidden: true,
|
|
searchState: .empty(hasResults: false),
|
|
hideBackground: true
|
|
)
|
|
))
|
|
} else {
|
|
gifItems = .single(nil)
|
|
}
|
|
|
|
let data = combineLatest(
|
|
queue: Queue.mainQueue(),
|
|
controller.inputData,
|
|
gifItems |> then(self.gifComponent.get() |> map(Optional.init)),
|
|
self.stickerSearchState.get(),
|
|
self.emojiSearchState.get()
|
|
)
|
|
|
|
self.contentDisposable.set(data.start(next: { [weak self] inputData, gifData, stickerSearchState, emojiSearchState in
|
|
if let strongSelf = self {
|
|
guard var inputData = inputData as? StickerPickerInputData else {
|
|
return
|
|
}
|
|
|
|
let presentationData = strongSelf.presentationData
|
|
inputData.gifs = gifData?.component
|
|
|
|
if let emoji = inputData.emoji {
|
|
if let emojiSearchResult = emojiSearchState.result {
|
|
var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults?
|
|
if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) {
|
|
emptySearchResults = EmojiPagerContentComponent.EmptySearchResults(
|
|
text: presentationData.strings.EmojiSearch_SearchEmojiEmptyResult,
|
|
iconFile: nil
|
|
)
|
|
}
|
|
|
|
let defaultSearchState: EmojiPagerContentComponent.SearchState = emojiSearchResult.isPreset ? .active : .empty(hasResults: true)
|
|
inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : defaultSearchState)
|
|
} else if emojiSearchState.isSearching {
|
|
inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emoji.contentItemGroups, itemContentUniqueId: emoji.itemContentUniqueId, emptySearchResults: emoji.emptySearchResults, searchState: .searching)
|
|
}
|
|
}
|
|
|
|
if let stickers = inputData.stickers {
|
|
if let stickerSearchResult = stickerSearchState.result {
|
|
var stickerSearchResults: EmojiPagerContentComponent.EmptySearchResults?
|
|
if !stickerSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) {
|
|
stickerSearchResults = EmojiPagerContentComponent.EmptySearchResults(
|
|
text: presentationData.strings.EmojiSearch_SearchStickersEmptyResult,
|
|
iconFile: nil
|
|
)
|
|
}
|
|
|
|
let defaultSearchState: EmojiPagerContentComponent.SearchState = stickerSearchResult.isPreset ? .active : .empty(hasResults: true)
|
|
inputData.stickers = stickers.withUpdatedItemGroups(panelItemGroups: stickers.panelItemGroups, contentItemGroups: stickerSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: stickerSearchResult.id, version: stickerSearchResult.version), emptySearchResults: stickerSearchResults, searchState: stickerSearchState.isSearching ? .searching : defaultSearchState)
|
|
} else if stickerSearchState.isSearching {
|
|
inputData.stickers = stickers.withUpdatedItemGroups(panelItemGroups: stickers.panelItemGroups, contentItemGroups: stickers.contentItemGroups, itemContentUniqueId: stickers.itemContentUniqueId, emptySearchResults: stickers.emptySearchResults, searchState: .searching)
|
|
}
|
|
}
|
|
|
|
strongSelf.updateContent(inputData)
|
|
}
|
|
}))
|
|
}
|
|
|
|
deinit {
|
|
self.contentDisposable.dispose()
|
|
self.emojiSearchDisposable.dispose()
|
|
self.stickerSearchDisposable.dispose()
|
|
self.hasRecentGifsDisposable?.dispose()
|
|
}
|
|
|
|
private func reloadGifContext() {
|
|
if let gifInputInteraction = self.gifInputInteraction, let gifMode = self.gifMode, let context = self.controller?.context {
|
|
self.gifContext = GifContext(context: context, subject: gifMode, gifInputInteraction: gifInputInteraction, trendingGifs: self.trendingGifsPromise.get())
|
|
}
|
|
}
|
|
|
|
fileprivate func openGifContextMenu(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) {
|
|
guard let controller = self.controller else {
|
|
return
|
|
}
|
|
let context = controller.context
|
|
|
|
let canSaveGif: Bool
|
|
if file.media.fileId.namespace == Namespaces.Media.CloudFile {
|
|
canSaveGif = true
|
|
} else {
|
|
canSaveGif = false
|
|
}
|
|
|
|
let _ = (context.engine.stickers.isGifSaved(id: file.media.fileId)
|
|
|> deliverOnMainQueue).start(next: { [weak self] isGifSaved in
|
|
var isGifSaved = isGifSaved
|
|
if !canSaveGif {
|
|
isGifSaved = false
|
|
}
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
|
|
|
|
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
|
|
|
|
let gallery = GalleryController(context: context, source: .standaloneMessage(message, nil), streamSingleVideo: true, replaceRootController: { _, _ in
|
|
}, baseNavigationController: nil)
|
|
gallery.setHintWillBePresentedInPreviewingContext(true)
|
|
|
|
var items: [ContextMenuItem] = []
|
|
items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaEditor_AddGif, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.actionSheet.primaryTextColor)
|
|
}, action: { [weak self] _, f in
|
|
f(.default)
|
|
if let self, let controller = self.controller {
|
|
if isSaved {
|
|
if controller.completion(.video(file.media)) {
|
|
self.controller?.dismiss(animated: true)
|
|
}
|
|
} else if let (_, result) = contextResult {
|
|
if case let .internalReference(reference) = result {
|
|
if let file = reference.file {
|
|
if controller.completion(.video(file)) {
|
|
self.controller?.dismiss(animated: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})))
|
|
|
|
if isSaved || isGifSaved {
|
|
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
|
|
}, action: { _, f in
|
|
f(.dismissWithoutContent)
|
|
|
|
let _ = removeSavedGif(postbox: context.account.postbox, mediaId: file.media.fileId).start()
|
|
})))
|
|
} else if canSaveGif && !isGifSaved {
|
|
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Preview_SaveGif, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor)
|
|
}, action: { [weak self] _, f in
|
|
f(.dismissWithoutContent)
|
|
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
let _ = (toggleGifSaved(account: context.account, fileReference: file, saved: true)
|
|
|> deliverOnMainQueue).start(next: { [weak self] result in
|
|
guard let controller = self?.controller else {
|
|
return
|
|
}
|
|
switch result {
|
|
case .generic:
|
|
controller.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
|
case let .limitExceeded(limit, premiumLimit):
|
|
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
|
let text: String
|
|
if limit == premiumLimit || premiumConfiguration.isPremiumDisabled {
|
|
text = presentationData.strings.Premium_MaxSavedGifsFinalText
|
|
} else {
|
|
text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string
|
|
}
|
|
controller.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { [weak controller] action in
|
|
if case .info = action, let controller {
|
|
let premiumController = context.sharedContext.makePremiumIntroController(context: context, source: .savedGifs, forceDark: controller.forceDark, dismissed: nil)
|
|
controller.pushController(premiumController)
|
|
return true
|
|
}
|
|
return false
|
|
}), in: .window(.root))
|
|
}
|
|
})
|
|
})))
|
|
}
|
|
|
|
let contextController = ContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceView: sourceView, sourceRect: sourceRect)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
|
controller.presentInGlobalOverlay(contextController)
|
|
})
|
|
}
|
|
|
|
func updateContent(_ content: StickerPickerInputData) {
|
|
self.content = content
|
|
|
|
guard let controller = self.controller else {
|
|
return
|
|
}
|
|
|
|
content.emoji?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
|
|
performItemAction: { [weak self] groupId, item, _, _, _, _ in
|
|
guard let strongSelf = self, let controller = strongSelf.controller else {
|
|
return
|
|
}
|
|
let context = controller.context
|
|
if groupId == AnyHashable("featuredTop"), let file = item.itemFile {
|
|
let _ = (
|
|
combineLatest(
|
|
ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: controller.context.account.peerId, premiumIfSavedMessages: true),
|
|
ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: controller.context.account.peerId, premiumIfSavedMessages: false)
|
|
)
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { [weak self] hasPremium, hasGlobalPremium in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)
|
|
let _ = (combineLatest(
|
|
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
|
|
context.account.postbox.combinedView(keys: [viewKey])
|
|
)
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { [weak self] emojiPacksView, views in
|
|
guard let view = views.views[viewKey] as? OrderedItemListView else {
|
|
return
|
|
}
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
var installedCollectionIds = Set<ItemCollectionId>()
|
|
for (id, _, _) in emojiPacksView.collectionInfos {
|
|
installedCollectionIds.insert(id)
|
|
}
|
|
|
|
let stickerPacks = view.items.map({ $0.contents.get(FeaturedStickerPackItem.self)! }).filter({
|
|
!installedCollectionIds.contains($0.info.id)
|
|
})
|
|
|
|
for featuredStickerPack in stickerPacks {
|
|
if featuredStickerPack.topItems.contains(where: { $0.file.fileId == file.fileId }) {
|
|
if let componentView = self.hostView.componentView as? StickerSelectionComponent.View {
|
|
if let pagerView = componentView.keyboardView.view as? EntityKeyboardComponent.View, let emojiInputInteraction = self.content?.emoji?.inputInteractionHolder.inputInteraction, let controller = self.controller {
|
|
pagerView.openCustomSearch(content: EmojiSearchContent(
|
|
context: context,
|
|
forceTheme: controller.forceDark ? defaultDarkPresentationTheme : nil,
|
|
items: stickerPacks,
|
|
initialFocusId: featuredStickerPack.info.id,
|
|
hasPremiumForUse: hasPremium,
|
|
hasPremiumForInstallation: hasGlobalPremium,
|
|
parentInputInteraction: emojiInputInteraction
|
|
))
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
})
|
|
})
|
|
} else if let file = item.itemFile {
|
|
if controller.completion(.file(.standalone(media: file), .sticker)) {
|
|
controller.dismiss(animated: true)
|
|
}
|
|
} else if case let .staticEmoji(emoji) = item.content {
|
|
if let image = generateImage(CGSize(width: 256.0, height: 256.0), scale: 1.0, rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: .zero, size: size))
|
|
|
|
let attributedString = NSAttributedString(string: emoji, attributes: [NSAttributedString.Key.font: Font.regular(200), NSAttributedString.Key.foregroundColor: UIColor.white])
|
|
|
|
let line = CTLineCreateWithAttributedString(attributedString)
|
|
let lineBounds = CTLineGetBoundsWithOptions(line, [.useOpticalBounds])
|
|
|
|
let lineOffset = CGPoint(x: 1.0 - UIScreenPixel, y: 0.0)
|
|
let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (size.height - lineBounds.size.height) / 2.0) + lineOffset.y)
|
|
|
|
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
|
context.scaleBy(x: 1.0, y: -1.0)
|
|
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
|
|
|
context.translateBy(x: lineOrigin.x, y: lineOrigin.y)
|
|
CTLineDraw(line, context)
|
|
context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y)
|
|
}) {
|
|
if controller.completion(.image(image, .sticker)) {
|
|
controller.dismiss(animated: true)
|
|
}
|
|
} else {
|
|
controller.dismiss(animated: true)
|
|
}
|
|
}
|
|
},
|
|
deleteBackwards: nil,
|
|
openStickerSettings: nil,
|
|
openFeatured: nil,
|
|
openSearch: {
|
|
},
|
|
addGroupAction: { [weak self] groupId, isPremiumLocked, _ in
|
|
guard let strongSelf = self, let controller = strongSelf.controller, let collectionId = groupId.base as? ItemCollectionId else {
|
|
return
|
|
}
|
|
let context = controller.context
|
|
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)
|
|
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { views in
|
|
guard let view = views.views[viewKey] as? OrderedItemListView else {
|
|
return
|
|
}
|
|
for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
|
|
if featuredStickerPack.info.id == collectionId {
|
|
let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), forceActualized: false)
|
|
|> mapToSignal { result -> Signal<Void, NoError> in
|
|
switch result {
|
|
case let .result(info, items, installed):
|
|
if installed {
|
|
return .complete()
|
|
} else {
|
|
return context.engine.stickers.addStickerPackInteractively(info: info, items: items)
|
|
}
|
|
case .fetching:
|
|
break
|
|
case .none:
|
|
break
|
|
}
|
|
return .complete()
|
|
}
|
|
|> deliverOnMainQueue).start(completed: {
|
|
})
|
|
|
|
break
|
|
}
|
|
}
|
|
})
|
|
},
|
|
clearGroup: { [weak self] groupId in
|
|
guard let strongSelf = self, let controller = strongSelf.controller else {
|
|
return
|
|
}
|
|
var presentationData = controller.context.sharedContext.currentPresentationData.with { $0 }
|
|
if controller.forceDark {
|
|
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
|
|
}
|
|
let context = controller.context
|
|
if groupId == AnyHashable("recent") {
|
|
let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize))
|
|
var items: [ActionSheetItem] = []
|
|
items.append(ActionSheetButtonItem(title: presentationData.strings.Emoji_ClearRecent, color: .destructive, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
let _ = context.engine.stickers.clearRecentlyUsedEmoji().start()
|
|
}))
|
|
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
})
|
|
])])
|
|
context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet)
|
|
} else if groupId == AnyHashable("popular") {
|
|
let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize))
|
|
var items: [ActionSheetItem] = []
|
|
items.append(ActionSheetTextItem(title: presentationData.strings.Chat_ClearReactionsAlertText, parseMarkdown: true))
|
|
items.append(ActionSheetButtonItem(title: presentationData.strings.Chat_ClearReactionsAlertAction, color: .destructive, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupRemoved(id: "popular"))
|
|
let _ = context.engine.stickers.clearRecentlyUsedReactions().start()
|
|
}))
|
|
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
})
|
|
])])
|
|
context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet)
|
|
}
|
|
},
|
|
editAction: { _ in },
|
|
pushController: { c in
|
|
},
|
|
presentController: { c in
|
|
},
|
|
presentGlobalOverlayController: { c in
|
|
},
|
|
navigationController: { [weak self] in
|
|
return self?.controller?.navigationController as? NavigationController
|
|
},
|
|
requestUpdate: { [weak self] transition in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if !transition.animation.isImmediate, let (layout, navigationHeight) = strongSelf.currentLayout {
|
|
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition)
|
|
}
|
|
},
|
|
updateSearchQuery: { [weak self] query in
|
|
guard let self, let controller = self.controller else {
|
|
return
|
|
}
|
|
let context = controller.context
|
|
|
|
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 {
|
|
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: Signal<Bool, NoError> = .single(true)
|
|
let resultSignal = combineLatest(
|
|
signal,
|
|
hasPremium
|
|
)
|
|
|> mapToSignal { keywords, hasPremium -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
|
var allEmoticons: [String: String] = [:]
|
|
for keyword in keywords {
|
|
for emoticon in keyword.emoticons {
|
|
allEmoticons[emoticon] = keyword.keyword
|
|
}
|
|
}
|
|
let remoteSignal: Signal<(items: [TelegramMediaFile], isFinalResult: Bool), NoError>
|
|
if hasPremium {
|
|
remoteSignal = context.engine.stickers.searchEmoji(emojiString: Array(allEmoticons.keys))
|
|
} else {
|
|
remoteSignal = .single(([], true))
|
|
}
|
|
return remoteSignal
|
|
|> mapToSignal { foundEmoji -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
|
if foundEmoji.items.isEmpty && !foundEmoji.isFinalResult {
|
|
return .complete()
|
|
}
|
|
var items: [EmojiPagerContentComponent.Item] = []
|
|
|
|
let appendUnicodeEmoji = {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !hasPremium {
|
|
appendUnicodeEmoji()
|
|
}
|
|
|
|
var existingIds = Set<MediaId>()
|
|
for itemFile in foundEmoji.items {
|
|
if existingIds.contains(itemFile.fileId) {
|
|
continue
|
|
}
|
|
existingIds.insert(itemFile.fileId)
|
|
if itemFile.isPremiumEmoji && !hasPremium {
|
|
continue
|
|
}
|
|
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)
|
|
}
|
|
|
|
if hasPremium {
|
|
appendUnicodeEmoji()
|
|
}
|
|
|
|
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
|
|
)])
|
|
}
|
|
}
|
|
|
|
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 = 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: 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,
|
|
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 = 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: { [weak self] in
|
|
if let self, let componentView = self.hostView.componentView as? StickerSelectionComponent.View {
|
|
componentView.scrolledToItemGroup()
|
|
}
|
|
self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
|
|
},
|
|
onScroll: {},
|
|
chatPeerId: nil,
|
|
peekBehavior: nil,
|
|
customLayout: nil,
|
|
externalBackground: nil,
|
|
externalExpansionView: nil,
|
|
customContentView: nil,
|
|
useOpaqueTheme: false,
|
|
hideBackground: true,
|
|
stateContext: nil,
|
|
addImage: controller.hasGifs ? { [weak self] in
|
|
if let self, let controller = self.controller {
|
|
let _ = controller.completion(nil)
|
|
controller.dismiss(animated: true)
|
|
controller.presentGallery()
|
|
}
|
|
} : nil
|
|
)
|
|
|
|
var stickerPeekBehavior: EmojiContentPeekBehaviorImpl?
|
|
if let controller = self.controller {
|
|
stickerPeekBehavior = EmojiContentPeekBehaviorImpl(
|
|
context: controller.context,
|
|
forceTheme: controller.forceDark ? defaultDarkColorPresentationTheme : nil,
|
|
interaction: nil,
|
|
chatPeerId: nil,
|
|
present: { [weak controller] c, a in
|
|
controller?.presentInGlobalOverlay(c, with: a)
|
|
}
|
|
)
|
|
}
|
|
|
|
content.stickers?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
|
|
performItemAction: { [weak self] groupId, item, _, _, _, _ in
|
|
guard let self, let controller = self.controller, let file = item.itemFile else {
|
|
return
|
|
}
|
|
let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 }
|
|
if groupId == AnyHashable("featuredTop") {
|
|
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
|
|
let _ = (controller.context.account.postbox.combinedView(keys: [viewKey])
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { [weak self] views in
|
|
guard let self, let controller = self.controller, let view = views.views[viewKey] as? OrderedItemListView else {
|
|
return
|
|
}
|
|
for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
|
|
if featuredStickerPack.topItems.contains(where: { $0.file.fileId == file.fileId }) {
|
|
controller.pushController(FeaturedStickersScreen(
|
|
context: controller.context,
|
|
highlightedPackId: featuredStickerPack.info.id,
|
|
forceTheme: defaultDarkColorPresentationTheme,
|
|
stickerActionTitle: presentationData.strings.StickerPack_AddSticker,
|
|
sendSticker: { [weak self] fileReference, _, _ in
|
|
guard let self, let controller = self.controller else {
|
|
return false
|
|
}
|
|
if controller.completion(.file(fileReference, .sticker)) {
|
|
controller.dismiss(animated: true)
|
|
}
|
|
return true
|
|
}
|
|
))
|
|
|
|
break
|
|
}
|
|
}
|
|
})
|
|
} else {
|
|
let reference: FileMediaReference
|
|
if groupId == AnyHashable("saved") {
|
|
reference = .savedSticker(media: file)
|
|
} else if groupId == AnyHashable("recent") {
|
|
reference = .recentSticker(media: file)
|
|
} else {
|
|
reference = .standalone(media: file)
|
|
}
|
|
let _ = controller.completion(.file(reference, .sticker))
|
|
controller.dismiss(animated: true)
|
|
}
|
|
},
|
|
deleteBackwards: nil,
|
|
openStickerSettings: nil,
|
|
openFeatured: nil,
|
|
openSearch: { [weak self] in
|
|
if let self, let componentView = self.hostView.componentView as? StickerSelectionComponent.View {
|
|
if let pagerView = componentView.keyboardView.view as? EntityKeyboardComponent.View {
|
|
pagerView.openSearch()
|
|
}
|
|
self.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
|
|
}
|
|
},
|
|
addGroupAction: { [weak self] groupId, isPremiumLocked, _ in
|
|
guard let strongSelf = self, let controller = strongSelf.controller, let collectionId = groupId.base as? ItemCollectionId else {
|
|
return
|
|
}
|
|
let context = controller.context
|
|
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
|
|
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { views in
|
|
guard let view = views.views[viewKey] as? OrderedItemListView else {
|
|
return
|
|
}
|
|
for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
|
|
if featuredStickerPack.info.id == collectionId {
|
|
let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), forceActualized: false)
|
|
|> mapToSignal { result -> Signal<Void, NoError> in
|
|
switch result {
|
|
case let .result(info, items, installed):
|
|
if installed {
|
|
return .complete()
|
|
} else {
|
|
return context.engine.stickers.addStickerPackInteractively(info: info, items: items)
|
|
}
|
|
case .fetching:
|
|
break
|
|
case .none:
|
|
break
|
|
}
|
|
return .complete()
|
|
}
|
|
|> deliverOnMainQueue).start(completed: {
|
|
})
|
|
|
|
break
|
|
}
|
|
}
|
|
})
|
|
},
|
|
clearGroup: { [weak self] groupId in
|
|
guard let strongSelf = self, let controller = strongSelf.controller else {
|
|
return
|
|
}
|
|
let context = controller.context
|
|
if groupId == AnyHashable("recent") {
|
|
var presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
if controller.forceDark {
|
|
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
|
|
}
|
|
let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize))
|
|
var items: [ActionSheetItem] = []
|
|
items.append(ActionSheetButtonItem(title: presentationData.strings.Stickers_ClearRecent, color: .destructive, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
let _ = context.engine.stickers.clearRecentlyUsedStickers().start()
|
|
}))
|
|
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
})
|
|
])])
|
|
context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet)
|
|
} else if groupId == AnyHashable("featuredTop") {
|
|
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
|
|
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { views in
|
|
guard let view = views.views[viewKey] as? OrderedItemListView else {
|
|
return
|
|
}
|
|
var stickerPackIds: [Int64] = []
|
|
for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
|
|
stickerPackIds.append(featuredStickerPack.info.id.id)
|
|
}
|
|
let _ = ApplicationSpecificNotice.setDismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager, values: stickerPackIds).start()
|
|
})
|
|
} else if groupId == AnyHashable("peerSpecific") {
|
|
}
|
|
},
|
|
editAction: { _ in },
|
|
pushController: { c in
|
|
},
|
|
presentController: { c in
|
|
},
|
|
presentGlobalOverlayController: { c in
|
|
},
|
|
navigationController: { [weak self] in
|
|
return self?.controller?.navigationController as? NavigationController
|
|
},
|
|
requestUpdate: { [weak self] transition in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if !transition.animation.isImmediate, let (layout, navigationHeight) = strongSelf.currentLayout {
|
|
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition)
|
|
}
|
|
},
|
|
updateSearchQuery: { [weak self] query in
|
|
guard let strongSelf = self, let controller = strongSelf.controller else {
|
|
return
|
|
}
|
|
let context = controller.context
|
|
|
|
switch query {
|
|
case .none:
|
|
strongSelf.stickerSearchDisposable.set(nil)
|
|
strongSelf.stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false)
|
|
case .text:
|
|
strongSelf.stickerSearchDisposable.set(nil)
|
|
strongSelf.stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false)
|
|
case let .category(value):
|
|
let resultSignal = context.engine.stickers.searchStickers(category: value, scope: [.installed, .remote])
|
|
|> mapToSignal { files -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
|
|
var items: [EmojiPagerContentComponent.Item] = []
|
|
|
|
var existingIds = Set<MediaId>()
|
|
for item in files.items {
|
|
let itemFile = item.file
|
|
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,
|
|
badge: nil,
|
|
actionButtonTitle: nil,
|
|
isFeatured: false,
|
|
isPremiumLocked: false,
|
|
isEmbedded: false,
|
|
hasClear: false,
|
|
hasEdit: false,
|
|
collapsedLineCount: nil,
|
|
displayPremiumBadges: false,
|
|
headerItem: nil,
|
|
fillWithLoadingPlaceholders: false,
|
|
items: items
|
|
)], files.isFinalResult))
|
|
}
|
|
|
|
var version = 0
|
|
strongSelf.stickerSearchDisposable.set((resultSignal
|
|
|> deliverOnMainQueue).start(next: { [weak self] result in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
guard let group = result.items.first else {
|
|
return
|
|
}
|
|
if group.items.isEmpty && !result.isFinalResult {
|
|
strongSelf.stickerSearchStateValue = 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
|
|
}
|
|
strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false)
|
|
version += 1
|
|
}))
|
|
}
|
|
},
|
|
updateScrollingToItemGroup: { [weak self] in
|
|
if let self, let componentView = self.hostView.componentView as? StickerSelectionComponent.View {
|
|
componentView.scrolledToItemGroup()
|
|
}
|
|
self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
|
|
},
|
|
onScroll: {},
|
|
chatPeerId: nil,
|
|
peekBehavior: stickerPeekBehavior,
|
|
customLayout: nil,
|
|
externalBackground: nil,
|
|
externalExpansionView: nil,
|
|
customContentView: self.storyStickersContentView,
|
|
useOpaqueTheme: false,
|
|
hideBackground: true,
|
|
stateContext: nil,
|
|
addImage: controller.hasGifs ? { [weak self] in
|
|
if let self, let controller = self.controller {
|
|
let _ = controller.completion(nil)
|
|
controller.dismiss(animated: true)
|
|
controller.presentGallery()
|
|
}
|
|
} : nil
|
|
)
|
|
|
|
if let (layout, navigationHeight) = self.currentLayout {
|
|
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate)
|
|
}
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
|
panRecognizer.delegate = self.wrappedGestureRecognizerDelegate
|
|
panRecognizer.delaysTouchesBegan = false
|
|
panRecognizer.cancelsTouchesInView = true
|
|
self.panGestureRecognizer = panRecognizer
|
|
self.wrappingView.addGestureRecognizer(panRecognizer)
|
|
|
|
self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
|
|
|
if let controller = self.controller {
|
|
controller.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
|
|
}
|
|
}
|
|
|
|
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
let _ = self.controller?.completion(nil)
|
|
self.controller?.dismiss(animated: true)
|
|
}
|
|
}
|
|
|
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
guard let controller = self.controller, !controller.isFullscreen else {
|
|
return false
|
|
}
|
|
if let (layout, _) = self.currentLayout {
|
|
if layout.metrics.isTablet {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer {
|
|
if otherGestureRecognizer is PagerPanGestureRecognizer {
|
|
return false
|
|
} else if otherGestureRecognizer is UIPanGestureRecognizer, let scrollView = otherGestureRecognizer.view, scrollView.frame.width > scrollView.frame.height {
|
|
return false
|
|
} else if otherGestureRecognizer is PeekControllerGestureRecognizer {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
private var isDismissing = false
|
|
func animateIn() {
|
|
guard let controller = self.controller, !controller.isFullscreen else {
|
|
return
|
|
}
|
|
ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0)
|
|
|
|
let targetPosition = self.containerView.center
|
|
let startPosition = targetPosition.offsetBy(dx: 0.0, dy: self.bounds.height)
|
|
|
|
self.containerView.center = startPosition
|
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
|
transition.animateView(allowUserInteraction: true, {
|
|
self.containerView.center = targetPosition
|
|
}, completion: { _ in
|
|
})
|
|
}
|
|
|
|
func animateOut(completion: @escaping () -> Void = {}) {
|
|
self.isDismissing = true
|
|
|
|
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
|
positionTransition.updatePosition(layer: self.containerView.layer, position: CGPoint(x: self.containerView.center.x, y: self.bounds.height + self.containerView.bounds.height / 2.0), completion: { [weak self] _ in
|
|
self?.controller?.dismiss(animated: false, completion: completion)
|
|
})
|
|
let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
|
alphaTransition.updateAlpha(node: self.dim, alpha: 0.0)
|
|
|
|
if !self.temporaryDismiss {
|
|
self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition)
|
|
}
|
|
}
|
|
|
|
func presentLinkPremiumSuggestion() {
|
|
guard let controller = self.controller else {
|
|
return
|
|
}
|
|
let tooltipController = UndoOverlayController(
|
|
presentationData: self.presentationData,
|
|
content: .linkCopied(
|
|
text: self.presentationData.strings.Story_Editor_TooltipLinkPremium
|
|
),
|
|
elevatedLayout: true,
|
|
position: .top,
|
|
animateInAsReplacement: false, action: { [weak controller] action in
|
|
if case .info = action, let controller {
|
|
let _ = controller.completion(nil)
|
|
controller.dismiss(animated: true)
|
|
|
|
let premiumController = controller.context.sharedContext.makePremiumIntroController(context: controller.context, source: .storiesLinks, forceDark: controller.forceDark, dismissed: nil)
|
|
controller.pushController(premiumController)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
)
|
|
controller.present(tooltipController, in: .window(.root))
|
|
}
|
|
|
|
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ComponentTransition) {
|
|
guard let controller = self.controller else {
|
|
return
|
|
}
|
|
self.currentLayout = (layout, navigationHeight)
|
|
|
|
self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0))
|
|
|
|
let effectiveExpanded = self.isExpanded || layout.metrics.isTablet
|
|
|
|
let isLandscape = layout.orientation == .landscape
|
|
let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset
|
|
let topInset: CGFloat
|
|
var bottomInset = layout.intrinsicInsets.bottom
|
|
if let (panInitialTopInset, panOffset, _, _) = self.panGestureArguments {
|
|
if effectiveExpanded {
|
|
topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset))
|
|
} else {
|
|
topInset = max(0.0, panInitialTopInset + min(0.0, panOffset))
|
|
}
|
|
} else {
|
|
topInset = effectiveExpanded ? 0.0 : edgeTopInset
|
|
}
|
|
transition.setFrame(view: self.wrappingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size), completion: nil)
|
|
|
|
var modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset)
|
|
if self.isDismissing {
|
|
modalProgress = 0.0
|
|
}
|
|
self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition)
|
|
if self.isDismissing {
|
|
return
|
|
}
|
|
|
|
let clipFrame: CGRect
|
|
let contentFrame: CGRect
|
|
if controller.isFullscreen {
|
|
clipFrame = CGRect(origin: CGPoint(), size: layout.size)
|
|
contentFrame = clipFrame
|
|
} else if layout.metrics.widthClass == .compact {
|
|
self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.25)
|
|
if isLandscape {
|
|
self.containerView.layer.cornerRadius = 0.0
|
|
} else {
|
|
self.containerView.layer.cornerRadius = 10.0
|
|
}
|
|
|
|
if #available(iOS 11.0, *) {
|
|
if layout.safeInsets.bottom.isZero {
|
|
self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
} else {
|
|
self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
|
}
|
|
}
|
|
|
|
if isLandscape {
|
|
clipFrame = CGRect(origin: CGPoint(), size: layout.size)
|
|
contentFrame = clipFrame
|
|
} else {
|
|
let coveredByModalTransition: CGFloat = 0.0
|
|
var containerTopInset: CGFloat = 10.0
|
|
if let statusBarHeight = layout.statusBarHeight {
|
|
containerTopInset += statusBarHeight
|
|
}
|
|
|
|
let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: containerTopInset - coveredByModalTransition * 10.0), size: CGSize(width: layout.size.width, height: layout.size.height - containerTopInset))
|
|
let maxScale: CGFloat = (layout.size.width - 16.0 * 2.0) / layout.size.width
|
|
let containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition
|
|
let maxScaledTopInset: CGFloat = containerTopInset - 10.0
|
|
let scaledTopInset: CGFloat = containerTopInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition
|
|
let containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0))
|
|
|
|
clipFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height)
|
|
contentFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height - topInset)
|
|
}
|
|
} else {
|
|
self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4)
|
|
self.containerView.layer.cornerRadius = 10.0
|
|
|
|
let verticalInset: CGFloat = 44.0
|
|
|
|
let maxSide = max(layout.size.width, layout.size.height)
|
|
let minSide = min(layout.size.width, layout.size.height)
|
|
let containerSize = CGSize(width: floorToScreenPixels(min(layout.size.width - 20.0, floor(maxSide / 2.0)) * 0.66), height: floorToScreenPixels((min(layout.size.height, minSide) - verticalInset * 2.0) * 0.66))
|
|
clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize)
|
|
contentFrame = clipFrame
|
|
|
|
bottomInset = 0.0
|
|
}
|
|
|
|
transition.setFrame(view: self.containerView, frame: clipFrame)
|
|
|
|
if let content = self.content {
|
|
var stickersTransition: ComponentTransition = transition
|
|
if let scheduledEmojiContentAnimationHint = self.scheduledEmojiContentAnimationHint {
|
|
self.scheduledEmojiContentAnimationHint = nil
|
|
let contentAnimation = scheduledEmojiContentAnimationHint
|
|
stickersTransition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation)
|
|
}
|
|
|
|
var contentSize = self.hostView.update(
|
|
transition: stickersTransition,
|
|
component: AnyComponent(
|
|
StickerSelectionComponent(
|
|
context: controller.context,
|
|
theme: self.theme,
|
|
strings: self.presentationData.strings,
|
|
deviceMetrics: layout.deviceMetrics,
|
|
topInset: controller.isFullscreen ? navigationHeight : 0.0,
|
|
bottomInset: bottomInset,
|
|
content: content,
|
|
backgroundColor: self.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.85),
|
|
separatorColor: self.theme.rootController.navigationBar.separatorColor,
|
|
getController: { [weak self] in
|
|
if let self {
|
|
return self.controller
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
)
|
|
),
|
|
environment: {},
|
|
forceUpdate: true,
|
|
containerSize: CGSize(width: contentFrame.size.width, height: contentFrame.height)
|
|
)
|
|
contentSize.height = max(layout.size.height - navigationHeight, contentSize.height)
|
|
transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: contentSize), completion: nil)
|
|
}
|
|
}
|
|
|
|
private var didPlayAppearAnimation = false
|
|
func updateIsVisible(isVisible: Bool) {
|
|
if self.currentIsVisible == isVisible {
|
|
return
|
|
}
|
|
self.currentIsVisible = isVisible
|
|
|
|
guard let currentLayout = self.currentLayout else {
|
|
return
|
|
}
|
|
self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: .immediate)
|
|
|
|
if !self.didPlayAppearAnimation {
|
|
self.didPlayAppearAnimation = true
|
|
self.animateIn()
|
|
}
|
|
}
|
|
|
|
private var defaultTopInset: CGFloat {
|
|
guard let (layout, _) = self.currentLayout else {
|
|
return 210.0
|
|
}
|
|
|
|
if let controller = self.controller, controller.isFullscreen {
|
|
return 0.0
|
|
}
|
|
|
|
if case .compact = layout.metrics.widthClass {
|
|
var factor: CGFloat = 0.2488
|
|
if layout.size.width <= 320.0 {
|
|
factor = 0.15
|
|
}
|
|
return floor(max(layout.size.width, layout.size.height) * factor)
|
|
} else {
|
|
return 210.0
|
|
}
|
|
}
|
|
|
|
private func findScrollView(view: UIView?) -> (UIScrollView, ListView?)? {
|
|
if let view = view {
|
|
if let view = view as? PagerExpandableScrollView {
|
|
return (view, nil)
|
|
}
|
|
if let view = view as? GridNodeScrollerView {
|
|
return (view, nil)
|
|
}
|
|
if let node = view.asyncdisplaykit_node as? ListView {
|
|
return (node.scroller, node)
|
|
}
|
|
return findScrollView(view: view.superview)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
|
guard let (layout, navigationHeight) = self.currentLayout else {
|
|
return
|
|
}
|
|
|
|
guard let controller = self.controller, !controller.isFullscreen else {
|
|
return
|
|
}
|
|
|
|
let isLandscape = layout.orientation == .landscape
|
|
let edgeTopInset = isLandscape ? 0.0 : defaultTopInset
|
|
|
|
switch recognizer.state {
|
|
case .began:
|
|
let point = recognizer.location(in: self.view)
|
|
let currentHitView = self.hitTest(point, with: nil)
|
|
|
|
var scrollViewAndListNode = self.findScrollView(view: currentHitView)
|
|
if scrollViewAndListNode?.0.frame.height == self.frame.width {
|
|
scrollViewAndListNode = nil
|
|
}
|
|
let scrollView = scrollViewAndListNode?.0
|
|
let listNode = scrollViewAndListNode?.1
|
|
|
|
let topInset: CGFloat
|
|
if self.isExpanded {
|
|
topInset = 0.0
|
|
} else {
|
|
topInset = edgeTopInset
|
|
}
|
|
|
|
self.panGestureArguments = (topInset, 0.0, scrollView, listNode)
|
|
case .changed:
|
|
guard let (topInset, panOffset, scrollView, listNode) = self.panGestureArguments else {
|
|
return
|
|
}
|
|
let visibleContentOffset = listNode?.visibleContentOffset()
|
|
let contentOffset = scrollView?.contentOffset.y ?? 0.0
|
|
|
|
var translation = recognizer.translation(in: self.view).y
|
|
|
|
var currentOffset = topInset + translation
|
|
|
|
let epsilon = 1.0
|
|
if case let .known(value) = visibleContentOffset, value <= epsilon {
|
|
if let scrollView = scrollView {
|
|
scrollView.bounces = false
|
|
scrollView.setContentOffset(CGPoint(x: scrollView.contentOffset.x, y: 0.0), animated: false)
|
|
}
|
|
} else if let scrollView = scrollView, contentOffset <= -scrollView.contentInset.top + epsilon {
|
|
scrollView.bounces = false
|
|
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false)
|
|
} else if let scrollView = scrollView {
|
|
translation = panOffset
|
|
currentOffset = topInset + translation
|
|
if self.isExpanded {
|
|
recognizer.setTranslation(CGPoint(), in: self.view)
|
|
} else if currentOffset > 0.0 {
|
|
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false)
|
|
}
|
|
}
|
|
|
|
self.panGestureArguments = (topInset, translation, scrollView, listNode)
|
|
|
|
if !self.isExpanded {
|
|
if currentOffset > 0.0, let scrollView = scrollView {
|
|
scrollView.panGestureRecognizer.setTranslation(CGPoint(), in: scrollView)
|
|
}
|
|
}
|
|
|
|
var bounds = self.bounds
|
|
if self.isExpanded {
|
|
bounds.origin.y = -max(0.0, translation - edgeTopInset)
|
|
} else {
|
|
bounds.origin.y = -translation
|
|
}
|
|
bounds.origin.y = min(0.0, bounds.origin.y)
|
|
self.bounds = bounds
|
|
|
|
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate)
|
|
case .ended:
|
|
guard let (currentTopInset, panOffset, scrollView, listNode) = self.panGestureArguments else {
|
|
return
|
|
}
|
|
self.panGestureArguments = nil
|
|
|
|
let visibleContentOffset = listNode?.visibleContentOffset()
|
|
let contentOffset = scrollView?.contentOffset.y ?? 0.0
|
|
|
|
let translation = recognizer.translation(in: self.view).y
|
|
var velocity = recognizer.velocity(in: self.view)
|
|
|
|
if self.isExpanded {
|
|
if case let .known(value) = visibleContentOffset, value > 0.1 {
|
|
velocity = CGPoint()
|
|
} else if case .unknown = visibleContentOffset {
|
|
velocity = CGPoint()
|
|
} else if contentOffset > 0.1 {
|
|
velocity = CGPoint()
|
|
}
|
|
}
|
|
|
|
var bounds = self.bounds
|
|
if self.isExpanded {
|
|
bounds.origin.y = -max(0.0, translation - edgeTopInset)
|
|
} else {
|
|
bounds.origin.y = -translation
|
|
}
|
|
bounds.origin.y = min(0.0, bounds.origin.y)
|
|
|
|
scrollView?.bounces = true
|
|
|
|
let offset = currentTopInset + panOffset
|
|
let topInset: CGFloat = edgeTopInset
|
|
|
|
var dismissing = false
|
|
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) {
|
|
let _ = self.controller?.completion(nil)
|
|
self.controller?.dismiss(animated: true, completion: nil)
|
|
dismissing = true
|
|
} else if self.isExpanded {
|
|
if velocity.y > 300.0 || offset > topInset / 2.0 {
|
|
self.isExpanded = false
|
|
if let listNode = listNode {
|
|
listNode.scroller.setContentOffset(CGPoint(), animated: false)
|
|
} else if let scrollView = scrollView {
|
|
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false)
|
|
}
|
|
|
|
let distance = topInset - offset
|
|
let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance)
|
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity))
|
|
|
|
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition))
|
|
} else {
|
|
self.isExpanded = true
|
|
|
|
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut)))
|
|
}
|
|
} else if (velocity.y < -300.0 || offset < topInset / 2.0) {
|
|
let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset)
|
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity))
|
|
self.isExpanded = true
|
|
|
|
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition))
|
|
} else {
|
|
if let listNode = listNode {
|
|
listNode.scroller.setContentOffset(CGPoint(), animated: false)
|
|
} else if let scrollView = scrollView {
|
|
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false)
|
|
}
|
|
|
|
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut)))
|
|
}
|
|
|
|
if !dismissing {
|
|
var bounds = self.bounds
|
|
let previousBounds = bounds
|
|
bounds.origin.y = 0.0
|
|
self.bounds = bounds
|
|
self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
|
}
|
|
case .cancelled:
|
|
self.panGestureArguments = nil
|
|
|
|
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut)))
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) {
|
|
guard isExpanded != self.isExpanded else {
|
|
return
|
|
}
|
|
self.isExpanded = isExpanded
|
|
|
|
guard let (layout, navigationHeight) = self.currentLayout else {
|
|
return
|
|
}
|
|
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition))
|
|
}
|
|
}
|
|
|
|
var node: Node {
|
|
return self.displayNode as! Node
|
|
}
|
|
|
|
public enum Weather {
|
|
public struct LoadedWeather {
|
|
public let emoji: String
|
|
public let emojiFile: TelegramMediaFile
|
|
public let temperature: Double
|
|
|
|
public init(emoji: String, emojiFile: TelegramMediaFile, temperature: Double) {
|
|
self.emoji = emoji
|
|
self.emojiFile = emojiFile
|
|
self.temperature = temperature
|
|
}
|
|
}
|
|
|
|
case none
|
|
case notDetermined
|
|
case notAllowed
|
|
case notPreloaded
|
|
case fetching
|
|
case loaded(StickerPickerScreen.Weather.LoadedWeather)
|
|
}
|
|
|
|
private let context: AccountContext
|
|
private let theme: PresentationTheme
|
|
let forceDark: Bool
|
|
private let inputData: Signal<StickerPickerInput, NoError>
|
|
let defaultToEmoji: Bool
|
|
let isFullscreen: Bool
|
|
let hasEmoji: Bool
|
|
let hasGifs: Bool
|
|
let hasInteractiveStickers: Bool
|
|
let weather: Signal<StickerPickerScreen.Weather, NoError>
|
|
|
|
private var currentLayout: ContainerViewLayout?
|
|
|
|
public var pushController: (ViewController) -> Void = { _ in }
|
|
public var presentController: (ViewController) -> Void = { _ in }
|
|
|
|
public var completion: (DrawingStickerEntity.Content?) -> Bool = { _ in return true }
|
|
|
|
public var presentGallery: () -> Void = { }
|
|
public var presentLocationPicker: () -> Void = { }
|
|
public var presentAudioPicker: () -> Void = { }
|
|
public var addReaction: () -> Void = { }
|
|
public var addLink: () -> Void = { }
|
|
public var addWeather: () -> Void = { }
|
|
|
|
public init(context: AccountContext, inputData: Signal<StickerPickerInput, NoError>, forceDark: Bool = false, expanded: Bool = false, defaultToEmoji: Bool = false, hasEmoji: Bool = true, hasGifs: Bool = false, hasInteractiveStickers: Bool = true, weather: Signal<StickerPickerScreen.Weather, NoError> = .single(.none)) {
|
|
self.context = context
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
self.theme = forceDark ? defaultDarkColorPresentationTheme : presentationData.theme
|
|
self.forceDark = forceDark
|
|
self.inputData = inputData
|
|
self.isFullscreen = expanded
|
|
self.defaultToEmoji = defaultToEmoji
|
|
self.hasEmoji = hasEmoji
|
|
self.hasGifs = hasGifs
|
|
self.hasInteractiveStickers = hasInteractiveStickers
|
|
self.weather = weather
|
|
|
|
super.init(navigationBarPresentationData: expanded ? NavigationBarPresentationData(presentationData: presentationData) : nil)
|
|
|
|
self.statusBar.statusBarStyle = .Ignore
|
|
|
|
if expanded {
|
|
self.title = presentationData.strings.Stickers_ChooseSticker_Title
|
|
self.navigationPresentation = .modal
|
|
}
|
|
}
|
|
|
|
required init(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public override func loadDisplayNode() {
|
|
self.displayNode = Node(context: self.context, controller: self, theme: self.theme)
|
|
self.displayNodeDidLoad()
|
|
|
|
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
|
}
|
|
|
|
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
|
self.view.endEditing(true)
|
|
if self.isFullscreen {
|
|
super.dismiss(animated: flag, completion: completion)
|
|
} else {
|
|
if flag {
|
|
self.node.animateOut(completion: {
|
|
super.dismiss(animated: false, completion: {})
|
|
completion?()
|
|
})
|
|
} else {
|
|
super.dismiss(animated: false, completion: {})
|
|
completion?()
|
|
}
|
|
}
|
|
}
|
|
|
|
public override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
self.node.updateIsVisible(isVisible: true)
|
|
}
|
|
|
|
public override func viewDidDisappear(_ animated: Bool) {
|
|
super.viewDidDisappear(animated)
|
|
|
|
self.node.updateIsVisible(isVisible: false)
|
|
}
|
|
|
|
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
|
self.currentLayout = layout
|
|
super.containerLayoutUpdated(layout, transition: transition)
|
|
|
|
let navigationHeight: CGFloat = 56.0
|
|
|
|
self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition))
|
|
}
|
|
}
|
|
|
|
private final class InteractiveStickerButtonContent: Component {
|
|
let context: AccountContext
|
|
let theme: PresentationTheme
|
|
let title: String?
|
|
let iconName: String?
|
|
let iconFile: TelegramMediaFile?
|
|
let useOpaqueTheme: Bool
|
|
weak var tintContainerView: UIView?
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
theme: PresentationTheme,
|
|
title: String?,
|
|
iconName: String?,
|
|
iconFile: TelegramMediaFile? = nil,
|
|
useOpaqueTheme: Bool,
|
|
tintContainerView: UIView
|
|
) {
|
|
self.context = context
|
|
self.theme = theme
|
|
self.title = title
|
|
self.iconName = iconName
|
|
self.iconFile = iconFile
|
|
self.useOpaqueTheme = useOpaqueTheme
|
|
self.tintContainerView = tintContainerView
|
|
}
|
|
|
|
public static func ==(lhs: InteractiveStickerButtonContent, rhs: InteractiveStickerButtonContent) -> Bool {
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.title != rhs.title {
|
|
return false
|
|
}
|
|
if lhs.iconName != rhs.iconName {
|
|
return false
|
|
}
|
|
if lhs.useOpaqueTheme != rhs.useOpaqueTheme {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
override public static var layerClass: AnyClass {
|
|
return PassthroughLayer.self
|
|
}
|
|
|
|
private let backgroundLayer = SimpleLayer()
|
|
let tintBackgroundLayer = SimpleLayer()
|
|
|
|
private var loadingView: TextLoadingEffectView?
|
|
private var icon: ComponentView<Empty>
|
|
private var title: ComponentView<Empty>
|
|
|
|
private var component: InteractiveStickerButtonContent?
|
|
|
|
override init(frame: CGRect) {
|
|
self.icon = ComponentView<Empty>()
|
|
self.title = ComponentView<Empty>()
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.isExclusiveTouch = true
|
|
|
|
self.layer.addSublayer(self.backgroundLayer)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(component: InteractiveStickerButtonContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
self.backgroundLayer.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.11).cgColor
|
|
|
|
let iconSize: CGSize
|
|
let buttonSize: CGSize
|
|
if let title = component.title {
|
|
if let iconFile = component.iconFile {
|
|
iconSize = self.icon.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(
|
|
LottieComponent(
|
|
content: LottieComponent.ResourceContent(context: component.context, file: iconFile, attemptSynchronously: true, providesPlaceholder: true),
|
|
color: nil,
|
|
placeholderColor: UIColor(rgb: 0xffffff, alpha: 0.4),
|
|
loop: !["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"].contains(component.iconName ?? "")
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: 20.0, height: 20.0)
|
|
)
|
|
} else {
|
|
iconSize = self.icon.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(BundleIconComponent(
|
|
name: component.iconName ?? "",
|
|
tintColor: .white,
|
|
maxSize: CGSize(width: 20.0, height: 20.0)
|
|
)),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
}
|
|
let titleSize = self.title.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(Text(
|
|
text: title.uppercased(),
|
|
font: Font.with(size: 23.0, design: .camera),
|
|
color: .white
|
|
)),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
|
|
let padding: CGFloat = 7.0
|
|
let spacing: CGFloat = 4.0
|
|
buttonSize = CGSize(width: padding + iconSize.width + spacing + titleSize.width + padding, height: 34.0)
|
|
|
|
if let view = self.icon.view {
|
|
if view.superview == nil {
|
|
self.addSubview(view)
|
|
}
|
|
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: padding, y: floorToScreenPixels((buttonSize.height - iconSize.height) / 2.0)), size: iconSize))
|
|
}
|
|
if let view = self.title.view {
|
|
if view.superview == nil {
|
|
self.addSubview(view)
|
|
}
|
|
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: padding + iconSize.width + spacing, y: floorToScreenPixels((buttonSize.height - titleSize.height) / 2.0)), size: titleSize))
|
|
}
|
|
|
|
if let loadingView = self.loadingView {
|
|
self.loadingView = nil
|
|
loadingView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
|
loadingView.removeFromSuperview()
|
|
})
|
|
}
|
|
} else {
|
|
buttonSize = CGSize(width: 87.0, height: 34.0)
|
|
|
|
let loadingView: TextLoadingEffectView
|
|
if let current = self.loadingView {
|
|
loadingView = current
|
|
} else {
|
|
loadingView = TextLoadingEffectView()
|
|
self.addSubview(loadingView)
|
|
self.loadingView = loadingView
|
|
}
|
|
}
|
|
|
|
self.backgroundLayer.cornerRadius = 6.0
|
|
self.tintBackgroundLayer.cornerRadius = 6.0
|
|
|
|
var transition = transition
|
|
if self.backgroundLayer.frame.width.isZero {
|
|
transition = .immediate
|
|
}
|
|
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: buttonSize))
|
|
|
|
if self.tintBackgroundLayer.superlayer == nil, let tintContainerView = component.tintContainerView {
|
|
Queue.mainQueue().justDispatch {
|
|
let mappedFrame = self.convert(self.bounds, to: tintContainerView)
|
|
transition.setFrame(layer: self.tintBackgroundLayer, frame: mappedFrame)
|
|
}
|
|
}
|
|
|
|
if let loadingView = self.loadingView {
|
|
let loadingSize = CGSize(width: buttonSize.width - 18.0, height: 16.0)
|
|
loadingView.update(color: UIColor.white, rect: CGRect(origin: .zero, size: loadingSize))
|
|
loadingView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((buttonSize.width - loadingSize.width) / 2.0), y: floorToScreenPixels((buttonSize.width - loadingSize.width) / 2.0)), size: loadingSize)
|
|
}
|
|
|
|
return buttonSize
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
private final class InteractiveReactionButtonContent: Component {
|
|
let theme: PresentationTheme
|
|
|
|
public init(
|
|
theme: PresentationTheme
|
|
) {
|
|
self.theme = theme
|
|
}
|
|
|
|
public static func ==(lhs: InteractiveReactionButtonContent, rhs: InteractiveReactionButtonContent) -> Bool {
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
override public static var layerClass: AnyClass {
|
|
return PassthroughLayer.self
|
|
}
|
|
|
|
private var icon: ComponentView<Empty>
|
|
|
|
private var component: InteractiveReactionButtonContent?
|
|
|
|
override init(frame: CGRect) {
|
|
self.icon = ComponentView<Empty>()
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.isExclusiveTouch = true
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(component: InteractiveReactionButtonContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
let bounds = CGRect(origin: .zero, size: CGSize(width: 54.0, height: 54.0))
|
|
let iconSize = self.icon.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(BundleIconComponent(
|
|
name: "Media Editor/Reaction",
|
|
tintColor: nil,
|
|
maxSize: CGSize(width: 52.0, height: 52.0)
|
|
)),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
|
|
if let view = self.icon.view {
|
|
if view.superview == nil {
|
|
self.addSubview(view)
|
|
}
|
|
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: 2.0, y: 0.0), size: iconSize))
|
|
}
|
|
|
|
return bounds.size
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
private final class RoundVideoButtonContent: Component {
|
|
let theme: PresentationTheme
|
|
|
|
public init(
|
|
theme: PresentationTheme
|
|
) {
|
|
self.theme = theme
|
|
}
|
|
|
|
public static func ==(lhs: RoundVideoButtonContent, rhs: RoundVideoButtonContent) -> Bool {
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
override public static var layerClass: AnyClass {
|
|
return PassthroughLayer.self
|
|
}
|
|
|
|
private let backgroundLayer = SimpleLayer()
|
|
private var icon: ComponentView<Empty>
|
|
|
|
private var component: InteractiveReactionButtonContent?
|
|
|
|
override init(frame: CGRect) {
|
|
self.icon = ComponentView<Empty>()
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.isExclusiveTouch = true
|
|
|
|
self.layer.addSublayer(self.backgroundLayer)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(component: RoundVideoButtonContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
self.backgroundLayer.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.11).cgColor
|
|
|
|
let bounds = CGRect(origin: .zero, size: CGSize(width: 54.0, height: 54.0))
|
|
let backgroundSize = CGSize(width: 50.0, height: 50.0)
|
|
self.backgroundLayer.frame = backgroundSize.centered(in: bounds)
|
|
self.backgroundLayer.cornerRadius = backgroundSize.width / 2.0
|
|
|
|
let iconSize = self.icon.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(BundleIconComponent(
|
|
name: "Chat List/Tabs/IconCamera",
|
|
tintColor: nil,
|
|
maxSize: CGSize(width: 30.0, height: 30.0)
|
|
)),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
|
|
if let view = self.icon.view {
|
|
if view.superview == nil {
|
|
self.addSubview(view)
|
|
}
|
|
transition.setFrame(view: view, frame: iconSize.centered(in: bounds))
|
|
}
|
|
|
|
return bounds.size
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
final class ItemStack<ChildEnvironment: Equatable>: CombinedComponent {
|
|
typealias EnvironmentType = ChildEnvironment
|
|
|
|
private let items: [AnyComponentWithIdentity<ChildEnvironment>]
|
|
private let padding: CGFloat
|
|
private let minSpacing: CGFloat
|
|
private let verticalSpacing: CGFloat
|
|
private let maxHorizontalItems: Int
|
|
|
|
init(_ items: [AnyComponentWithIdentity<ChildEnvironment>], padding: CGFloat, minSpacing: CGFloat, verticalSpacing: CGFloat, maxHorizontalItems: Int) {
|
|
self.items = items
|
|
self.padding = padding
|
|
self.minSpacing = minSpacing
|
|
self.verticalSpacing = verticalSpacing
|
|
self.maxHorizontalItems = maxHorizontalItems
|
|
}
|
|
|
|
static func ==(lhs: ItemStack<ChildEnvironment>, rhs: ItemStack<ChildEnvironment>) -> Bool {
|
|
if lhs.items != rhs.items {
|
|
return false
|
|
}
|
|
if lhs.padding != rhs.padding {
|
|
return false
|
|
}
|
|
if lhs.minSpacing != rhs.minSpacing {
|
|
return false
|
|
}
|
|
if lhs.verticalSpacing != rhs.verticalSpacing {
|
|
return false
|
|
}
|
|
if lhs.maxHorizontalItems != rhs.maxHorizontalItems {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
static var body: Body {
|
|
let children = ChildMap(environment: ChildEnvironment.self, keyedBy: AnyHashable.self)
|
|
|
|
return { context in
|
|
let updatedChildren = context.component.items.map { item in
|
|
return children[item.id].update(
|
|
component: item.component, environment: {
|
|
context.environment[ChildEnvironment.self]
|
|
},
|
|
availableSize: context.availableSize,
|
|
transition: context.transition
|
|
)
|
|
}
|
|
|
|
var groups: [[Int]] = []
|
|
var currentGroup: [Int] = []
|
|
for i in 0 ..< updatedChildren.count {
|
|
var itemsWidth: CGFloat = 0.0
|
|
for j in currentGroup {
|
|
itemsWidth += updatedChildren[j].size.width
|
|
}
|
|
itemsWidth += updatedChildren[i].size.width
|
|
let rowItemsCount = currentGroup.count + 1
|
|
|
|
let remainingWidth = context.availableSize.width - itemsWidth - context.component.padding * 2.0
|
|
let spacing = remainingWidth / CGFloat(rowItemsCount - 1)
|
|
if spacing < context.component.minSpacing || currentGroup.count == context.component.maxHorizontalItems {
|
|
groups.append(currentGroup)
|
|
currentGroup = []
|
|
}
|
|
currentGroup.append(i)
|
|
}
|
|
if !currentGroup.isEmpty {
|
|
groups.append(currentGroup)
|
|
}
|
|
|
|
var size = CGSize(width: context.availableSize.width, height: 0.0)
|
|
for group in groups {
|
|
var groupHeight: CGFloat = 0.0
|
|
var spacing = context.component.minSpacing
|
|
var itemsWidth = 0.0
|
|
for i in group {
|
|
let childSize = updatedChildren[i].size
|
|
groupHeight = max(groupHeight, childSize.height)
|
|
itemsWidth += childSize.width
|
|
}
|
|
let remainingWidth = context.availableSize.width - itemsWidth - context.component.padding * 2.0
|
|
spacing = remainingWidth / CGFloat(group.count - 1)
|
|
|
|
var useCenteredLayout = false
|
|
if spacing > 30.0 || group.count == 1 {
|
|
spacing = 30.0
|
|
useCenteredLayout = true
|
|
}
|
|
|
|
var nextX: CGFloat
|
|
if useCenteredLayout {
|
|
let totalWidth = itemsWidth + spacing * CGFloat(group.count - 1)
|
|
nextX = floorToScreenPixels((size.width - totalWidth) / 2.0)
|
|
} else {
|
|
nextX = context.component.padding
|
|
}
|
|
for i in group {
|
|
let child = updatedChildren[i]
|
|
let frame = CGRect(origin: CGPoint(x: nextX, y: size.height + floorToScreenPixels((groupHeight - child.size.height) / 2.0)), size: child.size)
|
|
|
|
context.add(child
|
|
.position(child.size.centered(in: frame).center)
|
|
)
|
|
nextX += child.size.width + spacing
|
|
}
|
|
size.height += groupHeight + context.component.verticalSpacing
|
|
}
|
|
|
|
return size
|
|
}
|
|
}
|
|
}
|
|
|
|
final class StoryStickersContentView: UIView, EmojiCustomContentView {
|
|
private let context: AccountContext
|
|
|
|
let tintContainerView = UIView()
|
|
private let container = ComponentView<Empty>()
|
|
|
|
private var weatherDisposable: Disposable?
|
|
private var weather: StickerPickerScreen.Weather = .none
|
|
|
|
var locationAction: () -> Void = {}
|
|
var audioAction: () -> Void = {}
|
|
var reactionAction: () -> Void = {}
|
|
var linkAction: () -> Void = {}
|
|
var weatherAction: () -> Void = {}
|
|
|
|
private var params: (theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, availableSize: CGSize)?
|
|
|
|
init(context: AccountContext, weather: Signal<StickerPickerScreen.Weather, NoError>) {
|
|
self.context = context
|
|
|
|
super.init(frame: .zero)
|
|
|
|
self.weatherDisposable = (weather
|
|
|> deliverOnMainQueue).start(next: { [weak self] weather in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.weather = weather
|
|
if let (theme, strings, useOpaqueTheme, availableSize) = self.params {
|
|
let _ = self.update(theme: theme, strings: strings, useOpaqueTheme: useOpaqueTheme, availableSize: availableSize, transition: .easeInOut(duration: 0.25))
|
|
}
|
|
})
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.weatherDisposable?.dispose()
|
|
}
|
|
|
|
func update(theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
|
|
self.params = (theme, strings, useOpaqueTheme, availableSize)
|
|
|
|
let padding: CGFloat = 22.0
|
|
var maxHorizontalItems = 2
|
|
var items: [AnyComponentWithIdentity<Empty>] = []
|
|
items.append(
|
|
AnyComponentWithIdentity(
|
|
id: "link",
|
|
component: AnyComponent(
|
|
CameraButton(
|
|
content: AnyComponentWithIdentity(
|
|
id: "content",
|
|
component: AnyComponent(
|
|
InteractiveStickerButtonContent(
|
|
context: self.context,
|
|
theme: theme,
|
|
title: strings.MediaEditor_AddLink,
|
|
iconName: self.context.isPremium ? "Media Editor/Link" : "Media Editor/LinkLocked",
|
|
useOpaqueTheme: useOpaqueTheme,
|
|
tintContainerView: self.tintContainerView
|
|
)
|
|
)
|
|
),
|
|
action: { [weak self] in
|
|
if let self {
|
|
self.linkAction()
|
|
}
|
|
})
|
|
)
|
|
)
|
|
)
|
|
items.append(
|
|
AnyComponentWithIdentity(
|
|
id: "location",
|
|
component: AnyComponent(
|
|
CameraButton(
|
|
content: AnyComponentWithIdentity(
|
|
id: "content",
|
|
component: AnyComponent(
|
|
InteractiveStickerButtonContent(
|
|
context: self.context,
|
|
theme: theme,
|
|
title: strings.MediaEditor_AddLocationShort,
|
|
iconName: "Chat/Attach Menu/Location",
|
|
useOpaqueTheme: useOpaqueTheme,
|
|
tintContainerView: self.tintContainerView
|
|
)
|
|
)
|
|
),
|
|
action: { [weak self] in
|
|
if let self {
|
|
self.locationAction()
|
|
}
|
|
})
|
|
)
|
|
)
|
|
)
|
|
|
|
if case .none = self.weather {
|
|
|
|
} else {
|
|
maxHorizontalItems = 3
|
|
|
|
let weatherButtonContent: AnyComponent<Empty>
|
|
switch self.weather {
|
|
case .notAllowed, .notDetermined, .notPreloaded:
|
|
weatherButtonContent = AnyComponent(
|
|
InteractiveStickerButtonContent(
|
|
context: self.context,
|
|
theme: theme,
|
|
title: stringForTemperature(24),
|
|
iconName: "☀️",
|
|
iconFile: self.context.animatedEmojiStickersValue["☀️"]?.first?.file,
|
|
useOpaqueTheme: useOpaqueTheme,
|
|
tintContainerView: self.tintContainerView
|
|
)
|
|
)
|
|
case let .loaded(weather):
|
|
weatherButtonContent = AnyComponent(
|
|
InteractiveStickerButtonContent(
|
|
context: self.context,
|
|
theme: theme,
|
|
title: stringForTemperature(weather.temperature),
|
|
iconName: weather.emoji,
|
|
iconFile: weather.emojiFile,
|
|
useOpaqueTheme: useOpaqueTheme,
|
|
tintContainerView: self.tintContainerView
|
|
)
|
|
)
|
|
case .fetching:
|
|
weatherButtonContent = AnyComponent(
|
|
InteractiveStickerButtonContent(
|
|
context: self.context,
|
|
theme: theme,
|
|
title: nil,
|
|
iconName: nil,
|
|
useOpaqueTheme: useOpaqueTheme,
|
|
tintContainerView: self.tintContainerView
|
|
)
|
|
)
|
|
default:
|
|
fatalError()
|
|
}
|
|
items.append(
|
|
AnyComponentWithIdentity(
|
|
id: "weather",
|
|
component: AnyComponent(
|
|
CameraButton(
|
|
content: AnyComponentWithIdentity(
|
|
id: "weather",
|
|
component: weatherButtonContent
|
|
),
|
|
action: { [weak self] in
|
|
if let self {
|
|
self.weatherAction()
|
|
}
|
|
})
|
|
)
|
|
)
|
|
)
|
|
}
|
|
|
|
items.append(
|
|
AnyComponentWithIdentity(
|
|
id: "audio",
|
|
component: AnyComponent(
|
|
CameraButton(
|
|
content: AnyComponentWithIdentity(
|
|
id: "audio",
|
|
component: AnyComponent(
|
|
InteractiveStickerButtonContent(
|
|
context: self.context,
|
|
theme: theme,
|
|
title: strings.MediaEditor_AddAudio,
|
|
iconName: "Media Editor/Audio",
|
|
useOpaqueTheme: useOpaqueTheme,
|
|
tintContainerView: self.tintContainerView
|
|
)
|
|
)
|
|
),
|
|
action: { [weak self] in
|
|
if let self {
|
|
self.audioAction()
|
|
}
|
|
})
|
|
)
|
|
)
|
|
)
|
|
|
|
items.append(
|
|
AnyComponentWithIdentity(
|
|
id: "reaction",
|
|
component: AnyComponent(
|
|
CameraButton(
|
|
content: AnyComponentWithIdentity(
|
|
id: "reaction",
|
|
component: AnyComponent(
|
|
InteractiveReactionButtonContent(theme: theme)
|
|
)
|
|
),
|
|
action: { [weak self] in
|
|
if let self {
|
|
self.reactionAction()
|
|
}
|
|
})
|
|
)
|
|
)
|
|
)
|
|
|
|
let size = self.container.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
ItemStack(
|
|
items,
|
|
padding: 18.0,
|
|
minSpacing: 8.0,
|
|
verticalSpacing: 12.0,
|
|
maxHorizontalItems: maxHorizontalItems
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
if let view = self.container.view {
|
|
if view.superview == nil {
|
|
self.addSubview(view)
|
|
}
|
|
view.frame = CGRect(origin: CGPoint(x: 0.0, y: padding), size: size)
|
|
}
|
|
|
|
return CGSize(width: size.width, height: size.height + padding * 2.0 - 12.0)
|
|
}
|
|
}
|
|
|
|
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
|
|
let controller: ViewController
|
|
weak var sourceView: UIView?
|
|
let sourceRect: CGRect
|
|
|
|
let navigationController: NavigationController? = nil
|
|
|
|
let passthroughTouches: Bool = false
|
|
|
|
init(controller: ViewController, sourceView: UIView?, sourceRect: CGRect) {
|
|
self.controller = controller
|
|
self.sourceView = sourceView
|
|
self.sourceRect = sourceRect
|
|
}
|
|
|
|
func transitionInfo() -> ContextControllerTakeControllerInfo? {
|
|
let sourceView = self.sourceView
|
|
let sourceRect = self.sourceRect
|
|
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceView] in
|
|
if let sourceView = sourceView {
|
|
return (sourceView, sourceRect)
|
|
} else {
|
|
return nil
|
|
}
|
|
})
|
|
}
|
|
|
|
func animatedIn() {
|
|
if let controller = self.controller as? GalleryController {
|
|
controller.viewDidAppear(false)
|
|
}
|
|
}
|
|
}
|