mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Improved sticker pack preview
This commit is contained in:
parent
4f571258d0
commit
12c99de957
@ -448,7 +448,7 @@ public protocol SharedAccountContext: class {
|
||||
func openExternalUrl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void)
|
||||
func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messageIds: Set<MessageId>) -> Signal<ChatAvailableMessageActions, NoError>
|
||||
func resolveUrl(account: Account, url: String) -> Signal<ResolvedUrl, NoError>
|
||||
func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void)
|
||||
func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?)
|
||||
func openAddContact(context: AccountContext, firstName: String, lastName: String, phoneNumber: String, label: String, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void)
|
||||
func openAddPersonContact(context: AccountContext, peerId: PeerId, pushController: @escaping (ViewController) -> Void, present: @escaping (ViewController, Any?) -> Void)
|
||||
func presentContactsWarningSuppression(context: AccountContext, present: (ViewController, Any?) -> Void)
|
||||
|
@ -225,6 +225,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
|
||||
public var presentationLayoutUpdated: ((GridNodeCurrentPresentationLayout, ContainedViewLayoutTransition) -> Void)?
|
||||
public var scrollingInitiated: (() -> Void)?
|
||||
public var scrollingCompleted: (() -> Void)?
|
||||
public var interactiveScrollingEnded: (() -> Void)?
|
||||
public var visibleContentOffsetChanged: (GridNodeVisibleContentOffset) -> Void = { _ in }
|
||||
|
||||
public final var floatingSections = false
|
||||
@ -374,6 +375,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
self.interactiveScrollingEnded?()
|
||||
if !decelerate {
|
||||
self.updateItemNodeVisibilititesAndScrolling()
|
||||
self.updateVisibleContentOffset()
|
||||
@ -478,7 +480,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
|
||||
} else if let fillWidth = fillWidth, fillWidth {
|
||||
let nextItemOriginX = nextItemOrigin.x + itemSize.width + itemSpacing
|
||||
let remainingWidth = remainingWidth - CGFloat(itemsInRow - 1) * itemSpacing
|
||||
if nextItemOriginX + itemSize.width > self.gridLayout.size.width && remainingWidth > 0.0 {
|
||||
if nextItemOriginX + itemSize.width > self.gridLayout.size.width - itemInsets.right && remainingWidth > 0.0 {
|
||||
itemSize.width += remainingWidth
|
||||
}
|
||||
}
|
||||
@ -492,7 +494,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
|
||||
index += 1
|
||||
|
||||
nextItemOrigin.x += itemSize.width + itemSpacing
|
||||
if nextItemOrigin.x + itemSize.width > gridLayout.size.width {
|
||||
if nextItemOrigin.x + itemSize.width > gridLayout.size.width - itemInsets.right {
|
||||
nextItemOrigin.x = initialSpacing + itemInsets.left
|
||||
nextItemOrigin.y += itemSize.height + lineSpacing
|
||||
incrementedCurrentRow = false
|
||||
|
@ -827,7 +827,7 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
if topModalIsFlat {
|
||||
maxScale = 1.0
|
||||
maxOffset = 0.0
|
||||
} else if visibleModalCount == 1 {
|
||||
} else if visibleModalCount <= 1 {
|
||||
maxScale = (layout.size.width - 16.0 * 2.0) / layout.size.width
|
||||
maxOffset = (topInset - (layout.size.height - layout.size.height * maxScale) / 2.0)
|
||||
} else {
|
||||
@ -837,7 +837,7 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
|
||||
let scale = 1.0 * visibleRootModalDismissProgress + (1.0 - visibleRootModalDismissProgress) * maxScale
|
||||
let offset = (1.0 - visibleRootModalDismissProgress) * maxOffset
|
||||
transition.updateSublayerTransformScaleAndOffset(node: rootContainerNode, scale: scale, offset: CGPoint(x: 0.0, y: offset))
|
||||
transition.updateSublayerTransformScaleAndOffset(node: rootContainerNode, scale: scale, offset: CGPoint(x: 0.0, y: offset), beginWithCurrentState: true)
|
||||
}
|
||||
} else {
|
||||
if let rootModalFrame = self.rootModalFrame {
|
||||
|
@ -102,7 +102,7 @@ final class NavigationModalFrame: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
private func updateShades(layout: ContainerViewLayout, progress: CGFloat, additionalProgress: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
private func updateShades(layout: ContainerViewLayout, progress: CGFloat, additionalProgress: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
let sideInset: CGFloat = 16.0
|
||||
var topInset: CGFloat = 0.0
|
||||
if let statusBarHeight = layout.statusBarHeight {
|
||||
@ -131,10 +131,10 @@ final class NavigationModalFrame: ASDisplayNode {
|
||||
let cornerSideOffset: CGFloat = progress * sideInset + additionalProgress * sideInset
|
||||
let cornerTopOffset: CGFloat = progress * topInset + additionalProgress * additionalTopInset
|
||||
let cornerBottomOffset: CGFloat = progress * bottomInset
|
||||
transition.updateFrame(node: self.topLeftCorner, frame: CGRect(origin: CGPoint(x: cornerSideOffset, y: cornerTopOffset), size: CGSize(width: cornerSize, height: cornerSize)))
|
||||
transition.updateFrame(node: self.topRightCorner, frame: CGRect(origin: CGPoint(x: layout.size.width - cornerSideOffset - cornerSize, y: cornerTopOffset), size: CGSize(width: cornerSize, height: cornerSize)))
|
||||
transition.updateFrame(node: self.bottomLeftCorner, frame: CGRect(origin: CGPoint(x: cornerSideOffset, y: layout.size.height - cornerBottomOffset - cornerSize), size: CGSize(width: cornerSize, height: cornerSize)))
|
||||
transition.updateFrame(node: self.bottomRightCorner, frame: CGRect(origin: CGPoint(x: layout.size.width - cornerSideOffset - cornerSize, y: layout.size.height - cornerBottomOffset - cornerSize), size: CGSize(width: cornerSize, height: cornerSize)))
|
||||
transition.updateFrame(node: self.topLeftCorner, frame: CGRect(origin: CGPoint(x: cornerSideOffset, y: cornerTopOffset), size: CGSize(width: cornerSize, height: cornerSize)), beginWithCurrentState: true)
|
||||
transition.updateFrame(node: self.topRightCorner, frame: CGRect(origin: CGPoint(x: layout.size.width - cornerSideOffset - cornerSize, y: cornerTopOffset), size: CGSize(width: cornerSize, height: cornerSize)), beginWithCurrentState: true)
|
||||
transition.updateFrame(node: self.bottomLeftCorner, frame: CGRect(origin: CGPoint(x: cornerSideOffset, y: layout.size.height - cornerBottomOffset - cornerSize), size: CGSize(width: cornerSize, height: cornerSize)), beginWithCurrentState: true)
|
||||
transition.updateFrame(node: self.bottomRightCorner, frame: CGRect(origin: CGPoint(x: layout.size.width - cornerSideOffset - cornerSize, y: layout.size.height - cornerBottomOffset - cornerSize), size: CGSize(width: cornerSize, height: cornerSize)), beginWithCurrentState: true)
|
||||
|
||||
let topShadeOffset: CGFloat = progress * topInset + additionalProgress * additionalTopInset
|
||||
let bottomShadeOffset: CGFloat = progress * bottomInset
|
||||
@ -142,10 +142,10 @@ final class NavigationModalFrame: ASDisplayNode {
|
||||
let rightShadeWidth: CGFloat = progress * sideInset + additionalProgress * sideInset
|
||||
let rightShadeOffset: CGFloat = layout.size.width - rightShadeWidth
|
||||
|
||||
transition.updateFrame(node: self.topShade, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: topShadeOffset)))
|
||||
transition.updateFrame(node: self.topShade, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: topShadeOffset)), beginWithCurrentState: true)
|
||||
transition.updateFrame(node: self.bottomShade, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomShadeOffset), size: CGSize(width: layout.size.width, height: bottomShadeOffset)))
|
||||
transition.updateFrame(node: self.leftShade, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: leftShadeOffset, height: layout.size.height)))
|
||||
transition.updateFrame(node: self.rightShade, frame: CGRect(origin: CGPoint(x: rightShadeOffset, y: 0.0), size: CGSize(width: rightShadeWidth, height: layout.size.height)), completion: { _ in
|
||||
transition.updateFrame(node: self.leftShade, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: leftShadeOffset, height: layout.size.height)), beginWithCurrentState: true)
|
||||
transition.updateFrame(node: self.rightShade, frame: CGRect(origin: CGPoint(x: rightShadeOffset, y: 0.0), size: CGSize(width: rightShadeWidth, height: layout.size.height)), beginWithCurrentState: true, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
@ -37,6 +37,8 @@ public final class PeekController: ViewController {
|
||||
self.sourceNode = sourceNode
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
|
@ -1191,7 +1191,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self?.present(c, a)
|
||||
}, dismissInput: {
|
||||
self?.view.endEditing(true)
|
||||
})
|
||||
}, contentContext: nil)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
@ -1450,7 +1450,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe
|
||||
presentControllerImpl?(c, a)
|
||||
}, dismissInput: {
|
||||
dismissInputImpl?()
|
||||
})
|
||||
}, contentContext: nil)
|
||||
}
|
||||
shareBotImpl = { [weak controller] in
|
||||
let _ = (getUserPeer(postbox: context.account.postbox, peerId: peerId)
|
||||
|
@ -186,7 +186,7 @@ func logoutOptionsController(context: AccountContext, navigationController: Navi
|
||||
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, openPeer: { peer, navigation in
|
||||
}, sendFile: nil, sendSticker: nil, present: { controller, arguments in
|
||||
pushControllerImpl?(controller)
|
||||
}, dismissInput: {})
|
||||
}, dismissInput: {}, contentContext: nil)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -887,7 +887,7 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList
|
||||
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, openPeer: { peer, navigation in
|
||||
}, sendFile: nil, sendSticker: nil, present: { controller, arguments in
|
||||
present(.push, controller)
|
||||
}, dismissInput: {})
|
||||
}, dismissInput: {}, contentContext: nil)
|
||||
})
|
||||
})
|
||||
allItems.append(faq)
|
||||
|
@ -824,7 +824,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
|
||||
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: getNavigationControllerImpl?(), openPeer: { peer, navigation in
|
||||
}, sendFile: nil, sendSticker: nil, present: { controller, arguments in
|
||||
pushControllerImpl?(controller)
|
||||
}, dismissInput: {})
|
||||
}, dismissInput: {}, contentContext: nil)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -7,4 +7,772 @@ import TelegramCore
|
||||
import SyncCore
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import MergeLists
|
||||
|
||||
private struct StickerPackPreviewGridEntry: Comparable, Identifiable {
|
||||
let index: Int
|
||||
let stickerItem: StickerPackItem
|
||||
|
||||
var stableId: MediaId {
|
||||
return self.stickerItem.file.fileId
|
||||
}
|
||||
|
||||
static func <(lhs: StickerPackPreviewGridEntry, rhs: StickerPackPreviewGridEntry) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
func item(account: Account, interaction: StickerPackPreviewInteraction) -> StickerPackPreviewGridItem {
|
||||
return StickerPackPreviewGridItem(account: account, stickerItem: self.stickerItem, interaction: interaction)
|
||||
}
|
||||
}
|
||||
|
||||
private struct StickerPackPreviewGridTransaction {
|
||||
let deletions: [Int]
|
||||
let insertions: [GridNodeInsertItem]
|
||||
let updates: [GridNodeUpdateItem]
|
||||
|
||||
init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], account: Account, interaction: StickerPackPreviewInteraction) {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list)
|
||||
|
||||
self.deletions = deleteIndices
|
||||
self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction), previousIndex: $0.2) }
|
||||
self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction)) }
|
||||
}
|
||||
}
|
||||
|
||||
private enum StickerPackAction {
|
||||
case add
|
||||
case remove
|
||||
}
|
||||
|
||||
private enum StickerPackNextAction {
|
||||
case navigatedNext
|
||||
case dismiss
|
||||
}
|
||||
|
||||
private final class StickerPackContainer: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
private let stickerPack: StickerPackReference
|
||||
private let decideNextAction: (StickerPackContainer, StickerPackAction) -> StickerPackNextAction
|
||||
private let requestDismiss: () -> Void
|
||||
private let presentInGlobalOverlay: (ViewController, Any?) -> Void
|
||||
private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?
|
||||
private let backgroundNode: ASImageNode
|
||||
private let gridNode: GridNode
|
||||
private let actionAreaBackgroundNode: ASDisplayNode
|
||||
private let actionAreaSeparatorNode: ASDisplayNode
|
||||
private let buttonNode: HighlightableButtonNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let titleContainer: ASDisplayNode
|
||||
private let titleSeparatorNode: ASDisplayNode
|
||||
|
||||
private(set) var validLayout: (ContainerViewLayout, CGRect, CGFloat, UIEdgeInsets)?
|
||||
|
||||
private var currentEntries: [StickerPackPreviewGridEntry] = []
|
||||
private var enqueuedTransactions: [StickerPackPreviewGridTransaction] = []
|
||||
|
||||
private var itemsDisposable: Disposable?
|
||||
private(set) var currentStickerPack: (StickerPackCollectionInfo, [ItemCollectionItem], Bool)?
|
||||
|
||||
private let isReadyValue = Promise<Bool>()
|
||||
private var didSetReady = false
|
||||
var isReady: Signal<Bool, NoError> {
|
||||
return self.isReadyValue.get()
|
||||
}
|
||||
|
||||
var expandProgress: CGFloat = 0.0
|
||||
var modalProgress: CGFloat = 0.0
|
||||
let expandProgressUpdated: (StickerPackContainer, ContainedViewLayoutTransition) -> Void
|
||||
|
||||
private let interaction: StickerPackPreviewInteraction
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, stickerPack: StickerPackReference, decideNextAction: @escaping (StickerPackContainer, StickerPackAction) -> StickerPackNextAction, requestDismiss: @escaping () -> Void, expandProgressUpdated: @escaping (StickerPackContainer, ContainedViewLayoutTransition) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?) {
|
||||
self.context = context
|
||||
self.presentationData = presentationData
|
||||
self.stickerPack = stickerPack
|
||||
self.decideNextAction = decideNextAction
|
||||
self.requestDismiss = requestDismiss
|
||||
self.presentInGlobalOverlay = presentInGlobalOverlay
|
||||
self.expandProgressUpdated = expandProgressUpdated
|
||||
self.sendSticker = sendSticker
|
||||
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.displaysAsynchronously = true
|
||||
self.backgroundNode.displayWithoutProcessing = true
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor)
|
||||
|
||||
self.gridNode = GridNode()
|
||||
self.gridNode.scrollView.alwaysBounceVertical = true
|
||||
self.gridNode.scrollView.showsVerticalScrollIndicator = false
|
||||
|
||||
self.actionAreaBackgroundNode = ASDisplayNode()
|
||||
self.actionAreaBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor
|
||||
|
||||
self.actionAreaSeparatorNode = ASDisplayNode()
|
||||
self.actionAreaSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor
|
||||
|
||||
self.buttonNode = HighlightableButtonNode()
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.titleContainer = ASDisplayNode()
|
||||
self.titleSeparatorNode = ASDisplayNode()
|
||||
self.titleSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor
|
||||
|
||||
self.interaction = StickerPackPreviewInteraction(playAnimatedStickers: true)
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.gridNode)
|
||||
self.addSubnode(self.actionAreaBackgroundNode)
|
||||
self.addSubnode(self.actionAreaSeparatorNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
|
||||
self.titleContainer.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.titleContainer)
|
||||
self.addSubnode(self.titleSeparatorNode)
|
||||
|
||||
self.gridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in
|
||||
self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition)
|
||||
}
|
||||
|
||||
self.gridNode.interactiveScrollingEnded = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let contentOffset = strongSelf.gridNode.scrollView.contentOffset
|
||||
let insets = strongSelf.gridNode.scrollView.contentInset
|
||||
|
||||
if contentOffset.y <= -insets.top - 30.0 {
|
||||
DispatchQueue.main.async {
|
||||
self?.requestDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.itemsDisposable = (loadedStickerPack(postbox: context.account.postbox, network: context.account.network, reference: stickerPack, forceActualized: false)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] contents in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.updateStickerPackContents(contents)
|
||||
})
|
||||
|
||||
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.buttonNode.alpha = 0.8
|
||||
} else {
|
||||
strongSelf.buttonNode.alpha = 1.0
|
||||
strongSelf.buttonNode.layer.animateAlpha(from: 0.8, to: 1.0, duration: 0.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.itemsDisposable?.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.gridNode.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>? in
|
||||
if let strongSelf = self {
|
||||
if let itemNode = strongSelf.gridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode, let item = itemNode.stickerPackItem {
|
||||
return strongSelf.context.account.postbox.transaction { transaction -> Bool in
|
||||
return getIsStickerSaved(transaction: transaction, fileId: item.file.fileId)
|
||||
}
|
||||
|> deliverOnMainQueue
|
||||
|> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in
|
||||
if let strongSelf = self {
|
||||
var menuItems: [PeekControllerMenuItem] = []
|
||||
if let (info, _, _) = strongSelf.currentStickerPack, info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
|
||||
if strongSelf.sendSticker != nil {
|
||||
menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, font: .bold, action: { node, rect in
|
||||
if let strongSelf = self {
|
||||
return strongSelf.sendSticker?(.standalone(media: item.file), node, rect) ?? false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}))
|
||||
}
|
||||
menuItems.append(PeekControllerMenuItem(title: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in
|
||||
if let strongSelf = self {
|
||||
if isStarred {
|
||||
let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start()
|
||||
} else {
|
||||
let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}))
|
||||
menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true }))
|
||||
}
|
||||
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, present: { [weak self] content, sourceNode in
|
||||
if let strongSelf = self {
|
||||
let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.presentationData.theme), content: content, sourceNode: {
|
||||
return sourceNode
|
||||
})
|
||||
strongSelf.presentInGlobalOverlay(controller, nil)
|
||||
return controller
|
||||
}
|
||||
return nil
|
||||
}, updateContent: { [weak self] content in
|
||||
if let strongSelf = self {
|
||||
var item: StickerPreviewPeekItem?
|
||||
if let content = content as? StickerPreviewPeekContent {
|
||||
item = content.item
|
||||
}
|
||||
strongSelf.updatePreviewingItem(item: item, animated: true)
|
||||
}
|
||||
}, activateBySingleTap: true))
|
||||
}
|
||||
|
||||
@objc func buttonPressed() {
|
||||
guard let (info, items, installed) = currentStickerPack else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings])
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] sharedData in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
var stickerSettings = StickerSettings.defaultSettings
|
||||
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.stickerSettings] as? StickerSettings {
|
||||
stickerSettings = value
|
||||
}
|
||||
|
||||
if installed {
|
||||
let _ = removeStickerPackInteractively(postbox: strongSelf.context.account.postbox, id: info.id, option: .delete).start()
|
||||
} else {
|
||||
let _ = addStickerPackInteractively(postbox: strongSelf.context.account.postbox, info: info, items: items).start()
|
||||
}
|
||||
|
||||
switch strongSelf.decideNextAction(strongSelf, installed ? .remove : .add) {
|
||||
case .dismiss:
|
||||
strongSelf.requestDismiss()
|
||||
case .navigatedNext:
|
||||
strongSelf.updateStickerPackContents(.result(info: info, items: items, installed: !installed))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func updateStickerPackContents(_ contents: LoadedStickerPack) {
|
||||
var entries: [StickerPackPreviewGridEntry] = []
|
||||
|
||||
var updateLayout = false
|
||||
|
||||
switch contents {
|
||||
case .fetching:
|
||||
entries = []
|
||||
case .none:
|
||||
entries = []
|
||||
case let .result(info, items, installed):
|
||||
self.currentStickerPack = (info, items, installed)
|
||||
|
||||
if installed {
|
||||
let text: String
|
||||
if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
|
||||
text = self.presentationData.strings.StickerPack_RemoveStickerCount(info.count)
|
||||
} else {
|
||||
text = self.presentationData.strings.StickerPack_RemoveMaskCount(info.count)
|
||||
}
|
||||
self.buttonNode.setTitle(text.uppercased(), with: Font.semibold(17.0), with: self.presentationData.theme.list.itemDestructiveColor, for: .normal)
|
||||
self.buttonNode.setBackgroundImage(nil, for: [])
|
||||
} else {
|
||||
let text: String
|
||||
if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
|
||||
text = self.presentationData.strings.StickerPack_AddStickerCount(info.count)
|
||||
} else {
|
||||
text = self.presentationData.strings.StickerPack_AddMaskCount(info.count)
|
||||
}
|
||||
self.buttonNode.setTitle(text.uppercased(), with: Font.semibold(17.0), with: self.presentationData.theme.list.itemCheckColors.foregroundColor, for: .normal)
|
||||
let roundedAccentBackground = generateImage(CGSize(width: 50.0, height: 50.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(self.presentationData.theme.list.itemCheckColors.fillColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
|
||||
})?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 25)
|
||||
self.buttonNode.setBackgroundImage(roundedAccentBackground, for: [])
|
||||
}
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: info.title, font: Font.semibold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
|
||||
updateLayout = true
|
||||
|
||||
for item in items {
|
||||
guard let item = item as? StickerPackItem else {
|
||||
continue
|
||||
}
|
||||
entries.append(StickerPackPreviewGridEntry(index: entries.count, stickerItem: item))
|
||||
}
|
||||
}
|
||||
let previousEntries = self.currentEntries
|
||||
self.currentEntries = entries
|
||||
|
||||
if updateLayout, let (layout, _, _, _) = self.validLayout {
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - 12.0 * 2.0, height: .greatestFiniteMagnitude))
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((-titleSize.width) / 2.0), y: floor((-titleSize.height) / 2.0)), size: titleSize)
|
||||
}
|
||||
|
||||
let transaction = StickerPackPreviewGridTransaction(previousList: previousEntries, list: entries, account: self.context.account, interaction: self.interaction)
|
||||
self.enqueueTransaction(transaction)
|
||||
}
|
||||
|
||||
var topContentInset: CGFloat {
|
||||
guard let (_, gridFrame, titleAreaInset, gridInsets) = self.validLayout else {
|
||||
return 0.0
|
||||
}
|
||||
return gridFrame.minY + gridInsets.top - titleAreaInset
|
||||
}
|
||||
|
||||
func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
var insets = layout.insets(options: [.statusBar])
|
||||
insets.top += 10.0
|
||||
|
||||
let buttonHeight: CGFloat = 50.0
|
||||
let actionAreaTopInset: CGFloat = 12.0
|
||||
let buttonSideInset: CGFloat = 10.0
|
||||
let titleAreaInset: CGFloat = 50.0
|
||||
|
||||
var actionAreaHeight: CGFloat = 0.0
|
||||
actionAreaHeight += insets.bottom
|
||||
|
||||
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: layout.size.height - actionAreaHeight - buttonHeight), size: CGSize(width: layout.size.width - buttonSideInset * 2.0, height: buttonHeight)))
|
||||
actionAreaHeight += buttonHeight
|
||||
|
||||
actionAreaHeight += actionAreaTopInset
|
||||
|
||||
transition.updateFrame(node: self.actionAreaBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width, height: actionAreaHeight)))
|
||||
transition.updateFrame(node: self.actionAreaSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
|
||||
|
||||
let gridFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top + titleAreaInset), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - titleAreaInset))
|
||||
|
||||
let itemsPerRow = 4
|
||||
let fillingWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0)
|
||||
let itemWidth = floor(fillingWidth / CGFloat(itemsPerRow))
|
||||
let gridLeftInset = floor((layout.size.width - fillingWidth) / 2.0)
|
||||
|
||||
let initialRevealedRowCount: CGFloat = 4.5
|
||||
|
||||
let topInset = max(0.0, layout.size.height - floor(initialRevealedRowCount * itemWidth) - insets.top - actionAreaHeight - titleAreaInset)
|
||||
|
||||
let gridInsets = UIEdgeInsets(top: insets.top + topInset, left: gridLeftInset, bottom: actionAreaHeight, right: layout.size.width - fillingWidth - gridLeftInset)
|
||||
|
||||
let firstTime = self.validLayout == nil
|
||||
self.validLayout = (layout, gridFrame, titleAreaInset, gridInsets)
|
||||
|
||||
transition.updateFrame(node: self.gridNode, frame: gridFrame)
|
||||
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridFrame.size, insets: gridInsets, scrollIndicatorInsets: nil, preloadSize: 200.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !strongSelf.didSetReady {
|
||||
strongSelf.didSetReady = true
|
||||
strongSelf.isReadyValue.set(.single(true))
|
||||
}
|
||||
})
|
||||
|
||||
if firstTime {
|
||||
while !self.enqueuedTransactions.isEmpty {
|
||||
self.dequeueTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func gridPresentationLayoutUpdated(_ presentationLayout: GridNodeCurrentPresentationLayout, transition: ContainedViewLayoutTransition) {
|
||||
guard let (layout, gridFrame, titleAreaInset, gridInsets) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let minBackgroundY = gridFrame.minY - titleAreaInset
|
||||
let unclippedBackgroundY = gridFrame.minY - presentationLayout.contentOffset.y - titleAreaInset
|
||||
|
||||
let offsetFromInitialPosition = presentationLayout.contentOffset.y + gridInsets.top
|
||||
let expandHeight: CGFloat = 100.0
|
||||
let expandProgress = max(0.0, min(1.0, offsetFromInitialPosition / expandHeight))
|
||||
|
||||
var expandProgressTransition = transition
|
||||
var expandUpdated = false
|
||||
|
||||
let modalProgress: CGFloat = unclippedBackgroundY < minBackgroundY ? 1.0 : 0.0
|
||||
if abs(self.modalProgress - modalProgress) > CGFloat.ulpOfOne {
|
||||
self.modalProgress = modalProgress
|
||||
expandUpdated = true
|
||||
expandProgressTransition = .animated(duration: 0.3, curve: .easeInOut)
|
||||
}
|
||||
|
||||
if abs(self.expandProgress - expandProgress) > CGFloat.ulpOfOne {
|
||||
self.expandProgress = expandProgress
|
||||
expandUpdated = true
|
||||
}
|
||||
|
||||
if expandUpdated {
|
||||
self.expandProgressUpdated(self, expandProgressTransition)
|
||||
}
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: max(minBackgroundY, unclippedBackgroundY)), size: CGSize(width: layout.size.width, height: layout.size.height))
|
||||
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
|
||||
transition.updateFrame(node: self.titleContainer, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: backgroundFrame.minY + floor((50.0) / 2.0)), size: CGSize()))
|
||||
transition.updateFrame(node: self.titleSeparatorNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY + 50.0 - UIScreenPixel), size: CGSize(width: backgroundFrame.width, height: UIScreenPixel)))
|
||||
self.titleSeparatorNode.alpha = unclippedBackgroundY < minBackgroundY ? 1.0 : 0.0
|
||||
}
|
||||
|
||||
private func enqueueTransaction(_ transaction: StickerPackPreviewGridTransaction) {
|
||||
self.enqueuedTransactions.append(transaction)
|
||||
|
||||
if let _ = self.validLayout {
|
||||
self.dequeueTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
private func dequeueTransaction() {
|
||||
if self.enqueuedTransactions.isEmpty {
|
||||
return
|
||||
}
|
||||
let transaction = self.enqueuedTransactions.removeFirst()
|
||||
|
||||
self.gridNode.transaction(GridNodeTransaction(deleteItems: transaction.deletions, insertItems: transaction.insertions, updateItems: transaction.updates, scrollToItem: nil, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.bounds.contains(point) {
|
||||
if !self.backgroundNode.bounds.contains(self.convert(point, to: self.backgroundNode)) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
let result = super.hitTest(point, with: event)
|
||||
return result
|
||||
}
|
||||
|
||||
private func updatePreviewingItem(item: StickerPreviewPeekItem?, animated: Bool) {
|
||||
if self.interaction.previewedItem != item {
|
||||
self.interaction.previewedItem = item
|
||||
|
||||
self.gridNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? StickerPackPreviewGridItemNode {
|
||||
itemNode.updatePreviewing(animated: animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class StickerPackScreenNode: ViewControllerTracingNode {
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
private let stickerPacks: [StickerPackReference]
|
||||
private let modalProgressUpdated: (CGFloat, ContainedViewLayoutTransition) -> Void
|
||||
private let dismissed: () -> Void
|
||||
|
||||
private let dimNode: ASDisplayNode
|
||||
private let containerContainingNode: ASDisplayNode
|
||||
|
||||
private var containers: [StickerPackContainer] = []
|
||||
private var selectedStickerPackIndex: Int
|
||||
private var relativeToSelectedStickerPackTransition: CGFloat = 0.0
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
private var isDismissed: Bool = false
|
||||
|
||||
private let _ready = Promise<Bool>()
|
||||
var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
init(context: AccountContext, stickerPacks: [StickerPackReference], initialSelectedStickerPackIndex: Int, modalProgressUpdated: @escaping (CGFloat, ContainedViewLayoutTransition) -> Void, dismissed: @escaping () -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?) {
|
||||
self.context = context
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.stickerPacks = stickerPacks
|
||||
self.selectedStickerPackIndex = initialSelectedStickerPackIndex
|
||||
self.modalProgressUpdated = modalProgressUpdated
|
||||
self.dismissed = dismissed
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
self.dimNode.alpha = 0.0
|
||||
|
||||
self.containerContainingNode = ASDisplayNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.containers = self.stickerPacks.map { stickerPack in
|
||||
return StickerPackContainer(context: context, presentationData: self.presentationData, stickerPack: stickerPack, decideNextAction: { [weak self] container, action in
|
||||
guard let strongSelf = self, let layout = strongSelf.validLayout, let index = strongSelf.containers.index(where: { $0 === container }) else {
|
||||
return .dismiss
|
||||
}
|
||||
if index == strongSelf.containers.count - 1 {
|
||||
return .dismiss
|
||||
} else {
|
||||
switch action {
|
||||
case .add:
|
||||
var allAdded = true
|
||||
for i in index + 1 ..< strongSelf.containers.count {
|
||||
if let (_, _, installed) = strongSelf.containers[i].currentStickerPack {
|
||||
if !installed {
|
||||
allAdded = false
|
||||
}
|
||||
} else {
|
||||
allAdded = false
|
||||
}
|
||||
}
|
||||
if allAdded {
|
||||
return .dismiss
|
||||
}
|
||||
case .remove:
|
||||
if strongSelf.containers.count == 1 {
|
||||
return .dismiss
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.selectedStickerPackIndex = strongSelf.selectedStickerPackIndex + 1
|
||||
strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.3, curve: .spring))
|
||||
return .navigatedNext
|
||||
}, requestDismiss: { [weak self] in
|
||||
self?.dismiss()
|
||||
}, expandProgressUpdated: { [weak self] container, transition in
|
||||
guard let strongSelf = self, let layout = strongSelf.validLayout, let index = strongSelf.containers.index(where: { $0 === container }) else {
|
||||
return
|
||||
}
|
||||
if index == strongSelf.selectedStickerPackIndex {
|
||||
strongSelf.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
}, presentInGlobalOverlay: presentInGlobalOverlay,
|
||||
sendSticker: sendSticker)
|
||||
}
|
||||
|
||||
for container in self.containers {
|
||||
self.containerContainingNode.addSubnode(container)
|
||||
}
|
||||
|
||||
self.addSubnode(self.dimNode)
|
||||
|
||||
self.addSubnode(self.containerContainingNode)
|
||||
|
||||
self._ready.set(combineLatest(self.containers.map { $0.isReady })
|
||||
|> map { values -> Bool in
|
||||
for value in values {
|
||||
if !value {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTapGesture(_:))))
|
||||
self.containerContainingNode.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
let firstTime = self.validLayout == nil
|
||||
|
||||
self.validLayout = layout
|
||||
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
transition.updateFrame(node: self.containerContainingNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
let expandProgress: CGFloat
|
||||
if self.containers.count == 1 {
|
||||
expandProgress = 1.0
|
||||
} else {
|
||||
expandProgress = self.containers[self.selectedStickerPackIndex].expandProgress
|
||||
}
|
||||
let scaledInset: CGFloat = 16.0
|
||||
let scaledDistance: CGFloat = 4.0
|
||||
let minScale = (layout.size.width - scaledInset * 2.0) / layout.size.width
|
||||
let containerScale = expandProgress * 1.0 + (1.0 - expandProgress) * minScale
|
||||
|
||||
let containerVerticalOffset: CGFloat = (1.0 - expandProgress) * scaledInset * 2.0
|
||||
|
||||
for i in 0 ..< self.containers.count {
|
||||
let container = self.containers[i]
|
||||
|
||||
let indexOffset = i - self.selectedStickerPackIndex
|
||||
var scaledOffset: CGFloat = 0.0
|
||||
scaledOffset = -CGFloat(indexOffset) * (1.0 - expandProgress) * (scaledInset * 2.0) + CGFloat(indexOffset) * scaledDistance
|
||||
|
||||
transition.updateFrame(node: container, frame: CGRect(origin: CGPoint(x: CGFloat(indexOffset) * layout.size.width + self.relativeToSelectedStickerPackTransition + scaledOffset, y: containerVerticalOffset), size: layout.size))
|
||||
transition.updateSublayerTransformScale(node: container, scale: containerScale)
|
||||
if container.validLayout?.0 != layout {
|
||||
container.updateLayout(layout: layout, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
let modalProgress = self.containers[self.selectedStickerPackIndex].modalProgress
|
||||
self.modalProgressUpdated(modalProgress, transition)
|
||||
}
|
||||
|
||||
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
break
|
||||
case .changed:
|
||||
let translation = recognizer.translation(in: self.view)
|
||||
self.relativeToSelectedStickerPackTransition = translation.x
|
||||
if self.selectedStickerPackIndex == 0 {
|
||||
self.relativeToSelectedStickerPackTransition = min(0.0, self.relativeToSelectedStickerPackTransition)
|
||||
}
|
||||
if self.selectedStickerPackIndex == self.containers.count - 1 {
|
||||
self.relativeToSelectedStickerPackTransition = max(0.0, self.relativeToSelectedStickerPackTransition)
|
||||
}
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, transition: .immediate)
|
||||
}
|
||||
case .ended, .cancelled:
|
||||
let translation = recognizer.translation(in: self.view)
|
||||
if abs(translation.x) > 30.0 {
|
||||
let deltaIndex = translation.x > 0 ? -1 : 1
|
||||
self.selectedStickerPackIndex = max(0, min(self.containers.count - 1, Int(self.selectedStickerPackIndex + deltaIndex)))
|
||||
}
|
||||
self.relativeToSelectedStickerPackTransition = 0.0
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.dimNode.alpha = 1.0
|
||||
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
|
||||
let minInset: CGFloat = (self.containers.map { container -> CGFloat in container.topContentInset }).max() ?? 0.0
|
||||
self.containerContainingNode.layer.animatePosition(from: CGPoint(x: 0.0, y: self.containerContainingNode.bounds.height - minInset), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
self.dimNode.alpha = 0.0
|
||||
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
|
||||
let minInset: CGFloat = (self.containers.map { container -> CGFloat in container.topContentInset }).max() ?? 0.0
|
||||
self.containerContainingNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: self.containerContainingNode.bounds.height - minInset), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
if self.isDismissed {
|
||||
return
|
||||
}
|
||||
self.isDismissed = true
|
||||
self.animateOut(completion: { [weak self] in
|
||||
self?.dismissed()
|
||||
})
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !self.bounds.contains(point) {
|
||||
return nil
|
||||
}
|
||||
|
||||
let selectedContainer = self.containers[self.selectedStickerPackIndex]
|
||||
if selectedContainer.hitTest(self.view.convert(point, to: selectedContainer.view), with: event) == nil {
|
||||
return self.dimNode.view
|
||||
}
|
||||
|
||||
let result = super.hitTest(point, with: event)
|
||||
return result
|
||||
}
|
||||
|
||||
@objc private func dimNodeTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class StickerPackScreen: ViewController {
|
||||
private let context: AccountContext
|
||||
private let stickerPacks: [StickerPackReference]
|
||||
private let initialSelectedStickerPackIndex: Int
|
||||
private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?
|
||||
|
||||
private var controllerNode: StickerPackScreenNode {
|
||||
return self.displayNode as! StickerPackScreenNode
|
||||
}
|
||||
|
||||
private let _ready = Promise<Bool>()
|
||||
override public var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
private var alreadyDidAppear: Bool = false
|
||||
|
||||
public init(context: AccountContext, stickerPacks: [StickerPackReference], selectedStickerPackIndex: Int = 0, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?) {
|
||||
self.context = context
|
||||
self.stickerPacks = stickerPacks
|
||||
self.initialSelectedStickerPackIndex = selectedStickerPackIndex
|
||||
self.sendSticker = sendSticker
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = StickerPackScreenNode(context: self.context, stickerPacks: self.stickerPacks, initialSelectedStickerPackIndex: self.initialSelectedStickerPackIndex, modalProgressUpdated: { [weak self] value, transition in
|
||||
DispatchQueue.main.async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.updateModalStyleOverlayTransitionFactor(value, transition: transition)
|
||||
}
|
||||
}, dismissed: { [weak self] in
|
||||
self?.dismiss()
|
||||
}, presentInGlobalOverlay: { [weak self] c, a in
|
||||
self?.presentInGlobalOverlay(c, with: a)
|
||||
}, sendSticker: self.sendSticker.flatMap { [weak self] sendSticker in
|
||||
return { file, sourceNode, sourceRect in
|
||||
if sendSticker(file, sourceNode, sourceRect) {
|
||||
self?.dismiss()
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self._ready.set(self.controllerNode.ready.get())
|
||||
|
||||
super.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !self.alreadyDidAppear {
|
||||
self.alreadyDidAppear = true
|
||||
self.controllerNode.animateIn()
|
||||
}
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
}
|
||||
|
@ -300,7 +300,7 @@ final class ChatBotInfoItemNode: ListViewItemNode {
|
||||
case .none, .ignore:
|
||||
break
|
||||
case let .url(url, concealed):
|
||||
self.item?.controllerInteraction.openUrl(url, concealed, nil)
|
||||
self.item?.controllerInteraction.openUrl(url, concealed, nil, nil)
|
||||
case let .peerMention(peerId, _):
|
||||
self.item?.controllerInteraction.openPeer(peerId, .chat(textInputState: nil, subject: nil), nil)
|
||||
case let .textMention(name):
|
||||
|
@ -166,7 +166,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode {
|
||||
case .text:
|
||||
self.controllerInteraction.sendMessage(markupButton.title)
|
||||
case let .url(url):
|
||||
self.controllerInteraction.openUrl(url, true, nil)
|
||||
self.controllerInteraction.openUrl(url, true, nil, nil)
|
||||
case .requestMap:
|
||||
self.controllerInteraction.shareCurrentLocation()
|
||||
case .requestPhone:
|
||||
|
@ -455,7 +455,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
strongSelf.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.chatDisplayNode.historyNode.view)
|
||||
}, openUrl: { url in
|
||||
self?.openUrl(url, concealed: false)
|
||||
self?.openUrl(url, concealed: false, message: nil)
|
||||
}, openPeer: { peer, navigation in
|
||||
self?.openPeer(peerId: peer.id, navigation: navigation, fromMessage: nil)
|
||||
}, callPeer: { peerId in
|
||||
@ -506,7 +506,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
}, actionInteraction: GalleryControllerActionInteraction(openUrl: { [weak self] url, concealed in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerInteraction?.openUrl(url, concealed, nil)
|
||||
strongSelf.controllerInteraction?.openUrl(url, concealed, nil, nil)
|
||||
}
|
||||
}, openUrlIn: { [weak self] url in
|
||||
if let strongSelf = self {
|
||||
@ -928,9 +928,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
} else {
|
||||
strongSelf.openPeer(peerId: peerId, navigation: .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: inputString)), subject: nil), fromMessage: nil)
|
||||
}
|
||||
}, openUrl: { [weak self] url, concealed, _ in
|
||||
}, openUrl: { [weak self] url, concealed, _, message in
|
||||
if let strongSelf = self {
|
||||
strongSelf.openUrl(url, concealed: concealed)
|
||||
strongSelf.openUrl(url, concealed: concealed, message: message)
|
||||
}
|
||||
}, shareCurrentLocation: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
@ -7220,7 +7220,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}))
|
||||
}
|
||||
|
||||
private func openResolved(_ result: ResolvedUrl) {
|
||||
private func openResolved(_ result: ResolvedUrl, message: Message? = nil) {
|
||||
self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: .chat, navigationController: self.effectiveNavigationController, openPeer: { [weak self] peerId, navigation in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -7262,10 +7262,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
self?.present(c, in: .window(.root), with: a)
|
||||
}, dismissInput: { [weak self] in
|
||||
self?.chatDisplayNode.dismissInput()
|
||||
})
|
||||
}, contentContext: message)
|
||||
}
|
||||
|
||||
private func openUrl(_ url: String, concealed: Bool) {
|
||||
private func openUrl(_ url: String, concealed: Bool, message: Message? = nil) {
|
||||
self.commitPurposefulAction()
|
||||
|
||||
let openImpl: () -> Void = { [weak self] in
|
||||
@ -7308,7 +7308,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
if let strongSelf = self {
|
||||
strongSelf.openResolved(result)
|
||||
strongSelf.openResolved(result, message: message)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ public final class ChatControllerInteraction {
|
||||
let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool) -> Void
|
||||
let requestMessageActionUrlAuth: (String, MessageId, Int32) -> Void
|
||||
let activateSwitchInline: (PeerId?, String) -> Void
|
||||
let openUrl: (String, Bool, Bool?) -> Void
|
||||
let openUrl: (String, Bool, Bool?, Message?) -> Void
|
||||
let shareCurrentLocation: () -> Void
|
||||
let shareAccountContact: () -> Void
|
||||
let sendBotCommand: (MessageId?, String) -> Void
|
||||
@ -115,7 +115,7 @@ public final class ChatControllerInteraction {
|
||||
var searchTextHighightState: (String, [MessageIndex])?
|
||||
var seenOneTimeAnimatedMedia = Set<MessageId>()
|
||||
|
||||
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String?) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
|
||||
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String?) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
|
||||
self.openMessage = openMessage
|
||||
self.openPeer = openPeer
|
||||
self.openPeerMention = openPeerMention
|
||||
@ -180,7 +180,7 @@ public final class ChatControllerInteraction {
|
||||
|
||||
static var `default`: ChatControllerInteraction {
|
||||
return ChatControllerInteraction(openMessage: { _, _ in
|
||||
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
|
||||
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
|
||||
}, presentController: { _, _ in }, navigationController: {
|
||||
return nil
|
||||
}, chatControllerNode: {
|
||||
|
@ -208,7 +208,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
||||
return false
|
||||
}
|
||||
if let singleUrl = accessibilityData.singleUrl {
|
||||
strongSelf.item?.controllerInteraction.openUrl(singleUrl, false, false)
|
||||
strongSelf.item?.controllerInteraction.openUrl(singleUrl, false, false, strongSelf.item?.content.firstMessage)
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -2304,7 +2304,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
||||
break
|
||||
case let .url(url, concealed):
|
||||
foundTapAction = true
|
||||
self.item?.controllerInteraction.openUrl(url, concealed, nil)
|
||||
self.item?.controllerInteraction.openUrl(url, concealed, nil, self.item?.content.firstMessage)
|
||||
break loop
|
||||
case let .peerMention(peerId, _):
|
||||
foundTapAction = true
|
||||
|
@ -763,7 +763,7 @@ public class ChatMessageItemView: ListViewItemNode {
|
||||
case .text:
|
||||
item.controllerInteraction.sendMessage(button.title)
|
||||
case let .url(url):
|
||||
item.controllerInteraction.openUrl(url, true, nil)
|
||||
item.controllerInteraction.openUrl(url, true, nil, nil)
|
||||
case .requestMap:
|
||||
item.controllerInteraction.shareCurrentLocation()
|
||||
case .requestPhone:
|
||||
|
@ -64,7 +64,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.addSubnode(self.textAccessibilityOverlayNode)
|
||||
|
||||
self.textAccessibilityOverlayNode.openUrl = { [weak self] url in
|
||||
self?.item?.controllerInteraction.openUrl(url, false, false)
|
||||
self?.item?.controllerInteraction.openUrl(url, false, false, nil)
|
||||
}
|
||||
|
||||
self.statusNode.openReactions = { [weak self] in
|
||||
|
@ -149,7 +149,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
if let webpage = webPageContent {
|
||||
item.controllerInteraction.openUrl(webpage.url, false, nil)
|
||||
item.controllerInteraction.openUrl(webpage.url, false, nil, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -195,7 +195,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, openMessageContextMenu: { [weak self] message, selectAll, node, frame, _ in
|
||||
self?.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame)
|
||||
}, openMessageContextActions: { _, _, _, _ in
|
||||
}, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _ in
|
||||
}, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _, _ in
|
||||
self?.openUrl(url)
|
||||
}, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in
|
||||
if let strongSelf = self, let navigationController = strongSelf.getNavigationController() {
|
||||
@ -800,7 +800,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
self?.presentController(c, a)
|
||||
}, dismissInput: {
|
||||
self?.view.endEditing(true)
|
||||
})
|
||||
}, contentContext: nil)
|
||||
case .wallpaper:
|
||||
break
|
||||
case .theme:
|
||||
|
@ -501,12 +501,12 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
}
|
||||
} else {
|
||||
if isTelegramMeLink(content.url) || !item.controllerInteraction.openMessage(item.message, .link) {
|
||||
item.controllerInteraction.openUrl(currentPrimaryUrl, false, false)
|
||||
item.controllerInteraction.openUrl(currentPrimaryUrl, false, false, nil)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !item.controllerInteraction.openMessage(item.message, .default) {
|
||||
item.controllerInteraction.openUrl(currentPrimaryUrl, false, false)
|
||||
item.controllerInteraction.openUrl(currentPrimaryUrl, false, false, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -556,10 +556,10 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
item.controllerInteraction.longTap(ChatControllerInteractionLongTapAction.url(url), item.message)
|
||||
} else if url == self.currentPrimaryUrl {
|
||||
if !item.controllerInteraction.openMessage(item.message, .default) {
|
||||
item.controllerInteraction.openUrl(url, false, false)
|
||||
item.controllerInteraction.openUrl(url, false, false, nil)
|
||||
}
|
||||
} else {
|
||||
item.controllerInteraction.openUrl(url, false, true)
|
||||
item.controllerInteraction.openUrl(url, false, true, nil)
|
||||
}
|
||||
}
|
||||
case .hold, .doubleTap:
|
||||
|
@ -295,6 +295,12 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
|
||||
params.navigationController?.pushViewController(controller)
|
||||
return true
|
||||
case let .stickerPack(reference):
|
||||
if true {
|
||||
let controller = StickerPackScreen(context: params.context, stickerPacks: [reference], selectedStickerPackIndex: 0, sendSticker: params.sendSticker)
|
||||
params.dismissInput()
|
||||
params.present(controller, nil)
|
||||
return true
|
||||
}
|
||||
let controller = StickerPackPreviewController(context: params.context, stickerPack: reference, parentNavigationController: params.navigationController)
|
||||
controller.sendSticker = params.sendSticker
|
||||
params.dismissInput()
|
||||
|
@ -17,6 +17,7 @@ import StickerPackPreviewUI
|
||||
import JoinLinkPreviewUI
|
||||
import LanguageLinkPreviewUI
|
||||
import SettingsUI
|
||||
import UrlHandling
|
||||
|
||||
private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer {
|
||||
if case .default = navigation {
|
||||
@ -34,7 +35,7 @@ private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatContr
|
||||
}
|
||||
}
|
||||
|
||||
func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void) {
|
||||
func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
switch resolvedUrl {
|
||||
case let .externalUrl(url):
|
||||
@ -88,9 +89,47 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
||||
openPeer(peerId, .chat(textInputState: nil, subject: .message(messageId)))
|
||||
case let .stickerPack(name):
|
||||
dismissInput()
|
||||
let controller = StickerPackPreviewController(context: context, stickerPack: .name(name), parentNavigationController: navigationController)
|
||||
controller.sendSticker = sendSticker
|
||||
present(controller, nil)
|
||||
if true {
|
||||
var stickerPacks: [StickerPackReference] = []
|
||||
var initialIndex: Int = 0
|
||||
if let message = contentContext as? Message {
|
||||
let dataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.link]).rawValue)
|
||||
var foundMain = false
|
||||
if let matches = dataDetector?.matches(in: message.text, options: [], range: NSRange(message.text.startIndex ..< message.text.endIndex, in: message.text)) {
|
||||
for match in matches {
|
||||
guard let stringRange = Range(match.range, in: message.text) else {
|
||||
continue
|
||||
}
|
||||
let urlText = String(message.text[stringRange])
|
||||
if let resultName = parseStickerPackUrl(urlText) {
|
||||
stickerPacks.append(.name(resultName))
|
||||
if resultName == name {
|
||||
foundMain = true
|
||||
initialIndex = stickerPacks.count - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundMain {
|
||||
stickerPacks.insert(.name(name), at: 0)
|
||||
initialIndex = 0
|
||||
}
|
||||
} else {
|
||||
stickerPacks = [.name(name)]
|
||||
initialIndex = 0
|
||||
}
|
||||
} else {
|
||||
stickerPacks = [.name(name)]
|
||||
initialIndex = 0
|
||||
}
|
||||
if !stickerPacks.isEmpty {
|
||||
let controller = StickerPackScreen(context: context, stickerPacks: stickerPacks, selectedStickerPackIndex: initialIndex, sendSticker: sendSticker)
|
||||
present(controller, nil)
|
||||
}
|
||||
} else {
|
||||
let controller = StickerPackPreviewController(context: context, stickerPack: .name(name), parentNavigationController: navigationController)
|
||||
controller.sendSticker = sendSticker
|
||||
present(controller, nil)
|
||||
}
|
||||
case let .instantView(webpage, anchor):
|
||||
navigationController?.pushViewController(InstantPageController(context: context, webPage: webpage, sourcePeerType: .channel, anchor: anchor))
|
||||
case let .join(link):
|
||||
|
@ -237,7 +237,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
|
||||
context.sharedContext.applicationBindings.getWindowHost()?.present(c, on: .root, blockInteraction: false, completion: {})
|
||||
}, dismissInput: {
|
||||
dismissInput()
|
||||
})
|
||||
}, contentContext: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,7 +77,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
||||
}, requestMessageActionCallback: { _, _, _ in
|
||||
}, requestMessageActionUrlAuth: { _, _, _ in
|
||||
}, activateSwitchInline: { _, _ in
|
||||
}, openUrl: { _, _, _ in
|
||||
}, openUrl: { _, _, _, _ in
|
||||
}, shareCurrentLocation: {
|
||||
}, shareAccountContact: {
|
||||
}, sendBotCommand: { _, _ in
|
||||
|
@ -328,7 +328,7 @@ public class PeerMediaCollectionController: TelegramBaseController {
|
||||
}, requestMessageActionCallback: { _, _, _ in
|
||||
}, requestMessageActionUrlAuth: { _, _, _ in
|
||||
}, activateSwitchInline: { _, _ in
|
||||
}, openUrl: { [weak self] url, _, external in
|
||||
}, openUrl: { [weak self] url, _, external, _ in
|
||||
self?.openUrl(url, external: external ?? false)
|
||||
}, shareCurrentLocation: {
|
||||
}, shareAccountContact: {
|
||||
@ -774,7 +774,7 @@ public class PeerMediaCollectionController: TelegramBaseController {
|
||||
self?.present(c, in: .window(.root), with: a)
|
||||
}, dismissInput: {
|
||||
self?.view.endEditing(true)
|
||||
})
|
||||
}, contentContext: nil)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
@ -983,8 +983,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
return resolveUrlImpl(account: account, url: url)
|
||||
}
|
||||
|
||||
public func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void) {
|
||||
openResolvedUrlImpl(resolvedUrl, context: context, urlContext: urlContext, navigationController: navigationController, openPeer: openPeer, sendFile: sendFile, sendSticker: sendSticker, present: present, dismissInput: dismissInput)
|
||||
public func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) {
|
||||
openResolvedUrlImpl(resolvedUrl, context: context, urlContext: urlContext, navigationController: navigationController, openPeer: openPeer, sendFile: sendFile, sendSticker: sendSticker, present: present, dismissInput: dismissInput, contentContext: contentContext)
|
||||
}
|
||||
|
||||
public func makeDeviceContactInfoController(context: AccountContext, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController {
|
||||
|
@ -42,7 +42,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate
|
||||
}
|
||||
}, sendFile: nil,
|
||||
sendSticker: nil,
|
||||
present: presentImpl, dismissInput: {})
|
||||
present: presentImpl, dismissInput: {}, contentContext: nil)
|
||||
}
|
||||
|
||||
let openLinkImpl: (String) -> Void = { [weak controller] url in
|
||||
|
@ -351,6 +351,28 @@ public func parseProxyUrl(_ url: String) -> (host: String, port: Int32, username
|
||||
return nil
|
||||
}
|
||||
|
||||
public func parseStickerPackUrl(_ url: String) -> String? {
|
||||
let schemes = ["http://", "https://", ""]
|
||||
let baseTelegramMePaths = ["telegram.me", "t.me"]
|
||||
for basePath in baseTelegramMePaths {
|
||||
for scheme in schemes {
|
||||
let basePrefix = scheme + basePath + "/"
|
||||
if url.lowercased().hasPrefix(basePrefix) {
|
||||
if let internalUrl = parseInternalUrl(query: String(url[basePrefix.endIndex...])), case let .stickerPack(name) = internalUrl {
|
||||
return name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let parsedUrl = URL(string: url), parsedUrl.scheme == "tg", let host = parsedUrl.host, let query = parsedUrl.query {
|
||||
if let internalUrl = parseInternalUrl(query: host + "?" + query), case let .stickerPack(name) = internalUrl {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public func parseWallpaperUrl(_ url: String) -> WallpaperUrlParameter? {
|
||||
let schemes = ["http://", "https://", ""]
|
||||
let baseTelegramMePaths = ["telegram.me", "t.me"]
|
||||
|
Loading…
x
Reference in New Issue
Block a user