Improved sticker pack preview

This commit is contained in:
Ali 2019-12-07 00:51:16 +04:00
parent 4f571258d0
commit 12c99de957
29 changed files with 892 additions and 53 deletions

View File

@ -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)

View File

@ -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

View File

@ -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 {

View File

@ -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()
})
}

View File

@ -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) {

View File

@ -1191,7 +1191,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
self?.present(c, a)
}, dismissInput: {
self?.view.endEditing(true)
})
}, contentContext: nil)
}
}
}))

View File

@ -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)

View File

@ -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)
})
}

View File

@ -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)

View File

@ -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)
})
})
}

View File

@ -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)
}
}

View File

@ -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):

View File

@ -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:

View File

@ -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)
}
}))
}

View File

@ -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: {

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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:

View File

@ -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:

View File

@ -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()

View File

@ -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):

View File

@ -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)
}
}

View File

@ -77,7 +77,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, requestMessageActionCallback: { _, _, _ in
}, requestMessageActionUrlAuth: { _, _, _ in
}, activateSwitchInline: { _, _ in
}, openUrl: { _, _, _ in
}, openUrl: { _, _, _, _ in
}, shareCurrentLocation: {
}, shareAccountContact: {
}, sendBotCommand: { _, _ in

View File

@ -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)
}
}))
}

View File

@ -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 {

View File

@ -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

View File

@ -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"]