Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2022-08-02 19:15:15 +03:00
commit b87e3e0b1a
43 changed files with 1312 additions and 601 deletions

View File

@ -7940,6 +7940,9 @@ Sorry for the inconvenience.";
"EmojiInput.PremiumEmojiToast.Text" = "Subscribe to Telegram Premium to unlock premium emoji.";
"EmojiInput.PremiumEmojiToast.Action" = "More";
"EmojiInput.PremiumEmojiToast.TryText" = "Try sending these emojis in **Saved Messages** for free to test.";
"EmojiInput.PremiumEmojiToast.TryAction" = "Open";
"StickerPacks.DeleteEmojiPacksConfirmation_1" = "Delete 1 Emoji Pack";
"StickerPacks.DeleteEmojiPacksConfirmation_any" = "Delete %@ Emoji Packs";

View File

@ -369,6 +369,11 @@ public enum ChatLocation: Equatable {
case feed(id: Int32)
}
public enum ChatControllerActivateInput {
case text
case entityInput
}
public final class NavigateToChatControllerParams {
public let navigationController: NavigationController
public let chatController: ChatController?
@ -379,7 +384,7 @@ public final class NavigateToChatControllerParams {
public let botStart: ChatControllerInitialBotStart?
public let attachBotStart: ChatControllerInitialAttachBotStart?
public let updateTextInputState: ChatTextInputState?
public let activateInput: Bool
public let activateInput: ChatControllerActivateInput?
public let keepStack: NavigateToChatKeepStack
public let useExisting: Bool
public let useBackAnimation: Bool
@ -398,7 +403,7 @@ public final class NavigateToChatControllerParams {
public let setupController: (ChatController) -> Void
public let completion: (ChatController) -> Void
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, useBackAnimation: Bool = false, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [PeerId] = [], changeColors: Bool = false, setupController: @escaping (ChatController) -> Void = { _ in }, completion: @escaping (ChatController) -> Void = { _ in }) {
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: ChatControllerActivateInput? = nil, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, useBackAnimation: Bool = false, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [PeerId] = [], changeColors: Bool = false, setupController: @escaping (ChatController) -> Void = { _ in }, completion: @escaping (ChatController) -> Void = { _ in }) {
self.navigationController = navigationController
self.chatController = chatController
self.chatLocationContextHolder = chatLocationContextHolder

View File

@ -240,6 +240,10 @@
return [super textInputMode];
}
- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated {
[super scrollRectToVisible:rect animated:false];
}
#endif
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
@ -756,7 +760,16 @@
NSRange range = [self selectedRange];
range.location = range.location + range.length - 1;
range.length = 1;
[self.textView scrollRangeToVisible:range];
UITextPosition *caretPosition = [self.textView positionFromPosition:self.textView.beginningOfDocument offset:range.location];
if (caretPosition) {
CGRect caretRect = [self.textView caretRectForPosition:caretPosition];
caretRect.origin.y -= self.textView.contentInset.top;
caretRect.size.height += self.textView.contentInset.top + self.textView.contentInset.bottom + 4.0f;
[self.textView scrollRectToVisible:caretRect animated:false];
}
//[self.textView scrollRangeToVisible:range];
}
#pragma mark - Keyboard

View File

@ -917,7 +917,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
scrollToEndIfExists = true
}
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(id: peer.id), activateInput: activateInput && !peer.isDeleted, scrollToEndIfExists: scrollToEndIfExists, animated: !scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [], parentGroupId: strongSelf.groupId, chatListFilter: strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.id, completion: { [weak self] controller in
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(id: peer.id), activateInput: (activateInput && !peer.isDeleted) ? .text : nil, scrollToEndIfExists: scrollToEndIfExists, animated: !scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [], parentGroupId: strongSelf.groupId, chatListFilter: strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.id, completion: { [weak self] controller in
self?.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true)
if let promoInfo = promoInfo {
switch promoInfo {

View File

@ -167,8 +167,8 @@ public final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
sizeNode.bounds = CGRect(origin: CGPoint(), size: sizeFrame.size)
let previousFrame = sizeNode.frame
if previousFrame.center.y != sizeFrame.center.y {
textTransition.updatePosition(node: sizeNode, position: sizeFrame.center)
if previousFrame.midY != sizeFrame.midY {
textTransition.updatePosition(node: sizeNode, position: CGPoint(x: sizeFrame.midX, y: sizeFrame.midY))
} else {
sizeNode.layer.removeAllAnimations()
sizeNode.frame = sizeFrame
@ -178,7 +178,7 @@ public final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
let sizeSize = sizeNode.frame.size
let sizeFrame = CGRect(x: active ? 42.0 : 7.0, y: active ? 19.0 : 2.0, width: sizeSize.width, height: sizeSize.height)
sizeNode.bounds = CGRect(origin: CGPoint(), size: sizeFrame.size)
textTransition.updatePosition(node: sizeNode, position: sizeFrame.center)
textTransition.updatePosition(node: sizeNode, position: CGPoint(x: sizeFrame.midX, y: sizeFrame.midY))
transition.updateAlpha(node: sizeNode, alpha: 0.0)
}
@ -190,7 +190,7 @@ public final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
let durationFrame = CGRect(x: active ? 42.0 : 7.0, y: active ? 6.0 : 2.0 + UIScreenPixel, width: durationSize.width, height: durationSize.height)
self.durationNode.bounds = CGRect(origin: CGPoint(), size: durationFrame.size)
textTransition.updatePosition(node: self.durationNode, position: durationFrame.center)
textTransition.updatePosition(node: self.durationNode, position: CGPoint(x: durationFrame.midX, y: durationFrame.midY))
let iconNode: ASImageNode
if let current = self.iconNode {

View File

@ -744,7 +744,7 @@ public final class DatePickerNode: ASDisplayNode {
self.monthTextNode.frame = monthTextFrame
let monthArrowFrame = CGRect(x: monthTextFrame.maxX + 10.0, y: monthTextFrame.minY + 4.0, width: 7.0, height: 12.0)
self.monthArrowNode.position = monthArrowFrame.center
self.monthArrowNode.position = CGPoint(x: monthArrowFrame.midX, y: monthArrowFrame.midY)
self.monthArrowNode.bounds = CGRect(origin: CGPoint(), size: monthArrowFrame.size)
transition.updateTransformRotation(node: self.monthArrowNode, angle: self.state.displayingMonthSelection ? CGFloat.pi / 2.0 : 0.0)

View File

@ -3,6 +3,12 @@ import UIKit
import AsyncDisplayKit
import ObjCRuntimeUtils
extension CGRect {
var center: CGPoint {
return CGPoint(x: self.midX, y: self.midY)
}
}
public enum ContainedViewLayoutTransitionCurve: Equatable, Hashable {
case linear
case easeInOut

View File

@ -591,10 +591,6 @@ public extension CGRect {
var bottomRight: CGPoint {
return CGPoint(x: self.maxX, y: self.maxY)
}
var center: CGPoint {
return CGPoint(x: self.midX, y: self.midY)
}
}
public extension CGPoint {

View File

@ -838,10 +838,10 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((bounds.size.height - node.lineHeight) / 2.0)), size: CGSize(width: bounds.size.width, height: node.lineHeight))
let foregroundContentFrame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height))
node.backgroundNode.position = backgroundFrame.center
node.backgroundNode.position = CGPoint(x: backgroundFrame.midX, y: backgroundFrame.midY)
node.backgroundNode.bounds = CGRect(origin: CGPoint(), size: backgroundFrame.size)
node.foregroundContentNode.position = foregroundContentFrame.center
node.foregroundContentNode.position = CGPoint(x: foregroundContentFrame.midX, y: foregroundContentFrame.midY)
node.foregroundContentNode.bounds = CGRect(origin: CGPoint(), size: foregroundContentFrame.size)
node.bufferingNode.frame = backgroundFrame

View File

@ -13,6 +13,12 @@ import PasscodeInputFieldNode
import MonotonicTime
import GradientBackground
private extension CGRect {
var center: CGPoint {
return CGPoint(x: self.midX, y: self.midY)
}
}
private let titleFont = Font.regular(20.0)
private let subtitleFont = Font.regular(15.0)
private let buttonFont = Font.regular(17.0)

View File

@ -122,7 +122,7 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
if self.sparks {
let lineWidth: CGFloat = 1.75
let center = bounds.center
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius: CGFloat = (bounds.size.width - lineWidth - 2.5 * 2.0) * 0.5
let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * self.progress
@ -211,7 +211,7 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
context.setLineJoin(.miter)
context.setMiterLimit(10.0)
let center = bounds.center
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius: CGFloat = (bounds.size.width - lineWidth - 2.5 * 2.0) * 0.5
let startAngle: CGFloat = -CGFloat.pi / 2.0

View File

@ -880,7 +880,7 @@ public func groupStatsController(context: AccountContext, updatedPresentationDat
let _ = (context.account.postbox.loadedPeerWithId(participantPeerId)
|> take(1)
|> deliverOnMainQueue).start(next: { peer in
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, updateTextInputState: nil, activateInput: false, keepStack: .always, useExisting: false, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: (.member(peer), ""), animated: true))
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, updateTextInputState: nil, keepStack: .always, useExisting: false, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: (.member(peer), ""), animated: true))
})
}
}

View File

@ -7,6 +7,12 @@ import UIKitRuntimeUtils
import AnimatedStickerNode
import TelegramAnimatedStickerNode
private extension CGRect {
var center: CGPoint {
return CGPoint(x: self.midX, y: self.midY)
}
}
private let separatorHeight: CGFloat = 1.0 / UIScreen.main.scale
private func tabBarItemImage(_ image: UIImage?, title: String, backgroundColor: UIColor, tintColor: UIColor, horizontal: Bool, imageMode: Bool, centered: Bool = false) -> (UIImage, CGFloat) {
let font = horizontal ? Font.regular(13.0) : Font.medium(10.0)

View File

@ -1173,6 +1173,7 @@ public class Account {
}
self.managedOperationsDisposable.add(managedGreetingStickers(postbox: self.postbox, network: self.network).start())
self.managedOperationsDisposable.add(managedPremiumStickers(postbox: self.postbox, network: self.network).start())
self.managedOperationsDisposable.add(managedAllPremiumStickers(postbox: self.postbox, network: self.network).start())
if !supplementary {
let mediaBox = postbox.mediaBox

View File

@ -178,3 +178,27 @@ func managedPremiumStickers(postbox: Postbox, network: Network) -> Signal<Void,
})
return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func managedAllPremiumStickers(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudAllPremiumStickers, reverseHashOrder: false, forceFetch: false, fetch: { hash in
return network.request(Api.functions.messages.getStickers(emoticon: "📂⭐️", hash: 0))
|> retryRequest
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
switch result {
case .stickersNotModified:
return .single(nil)
case let .stickers(_, stickers):
var items: [OrderedItemListEntry] = []
for sticker in stickers {
if let file = telegramMediaFileFromApiDocument(sticker), let id = file.id {
if let entry = CodableEntry(RecentMediaItem(file)) {
items.append(OrderedItemListEntry(id: RecentMediaItemId(id).rawValue, contents: entry))
}
}
}
return .single(items)
}
}
})
return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}

View File

@ -67,6 +67,7 @@ public struct Namespaces {
public static let CloudPremiumStickers: Int32 = 13
public static let LocalRecentEmoji: Int32 = 14
public static let CloudFeaturedEmojiPacks: Int32 = 15
public static let CloudAllPremiumStickers: Int32 = 16
}
public struct CachedItemCollection {

View File

@ -289,6 +289,7 @@ swift_library(
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramUI/Components/ChatInputPanelContainer:ChatInputPanelContainer",
"//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities",
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent:EmojiSuggestionsComponent",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
"//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC",
"//submodules/Media/LocalAudioTranscription:LocalAudioTranscription",

View File

@ -100,6 +100,12 @@ private final class ExpansionPanRecognizer: UIGestureRecognizer, UIGestureRecogn
if let scrollView = traceScrollView(view: view, point: point).0 ?? hitView.flatMap(traceScrollViewUp) {
if scrollView is ListViewScroller || scrollView is GridNodeScrollerView || scrollView.asyncdisplaykit_node is ASScrollNode {
found = false
} else if let textView = scrollView as? UITextView {
if textView.contentSize.height <= textView.bounds.height {
found = true
} else {
found = false
}
} else {
found = true
}
@ -206,7 +212,7 @@ private final class ExpansionPanRecognizer: UIGestureRecognizer, UIGestureRecogn
}
}
public final class ChatInputPanelContainer: SparseNode, UIScrollViewDelegate {
public final class ChatInputPanelContainer: SparseNode {
public var expansionUpdated: ((ContainedViewLayoutTransition) -> Void)?
private var expansionRecognizer: ExpansionPanRecognizer?

View File

@ -0,0 +1,28 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "EmojiSuggestionsComponent",
module_name = "EmojiSuggestionsComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/AccountContext:AccountContext",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TextFormat:TextFormat",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,370 @@
import Foundation
import UIKit
import SwiftSignalKit
import Display
import AnimationCache
import MultiAnimationRenderer
import ComponentFlow
import AccountContext
import TelegramCore
import Postbox
import TelegramPresentationData
import EmojiTextAttachmentView
import TextFormat
public final class EmojiSuggestionsComponent: Component {
public typealias EnvironmentType = Empty
public static func suggestionData(context: AccountContext, isSavedMessages: Bool, query: String) -> Signal<[TelegramMediaFile], NoError> {
let hasPremium: Signal<Bool, NoError>
if isSavedMessages {
hasPremium = .single(true)
} else {
hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> Bool in
guard case let .user(user) = peer else {
return false
}
return user.isPremium
}
|> distinctUntilChanged
}
return combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
context.account.viewTracker.featuredEmojiPacks(),
hasPremium
)
|> take(1)
|> map { view, featuredEmojiPacks, hasPremium -> [TelegramMediaFile] in
var result: [TelegramMediaFile] = []
let normalizedQuery = query.basicEmoji.0
var existingIds = Set<MediaId>()
for entry in view.entries {
guard let item = entry.item as? StickerPackItem else {
continue
}
for attribute in item.file.attributes {
switch attribute {
case let .CustomEmoji(_, alt, _):
if alt == query || (!normalizedQuery.isEmpty && alt == normalizedQuery) {
if !item.file.isPremiumEmoji || hasPremium {
if !existingIds.contains(item.file.fileId) {
existingIds.insert(item.file.fileId)
result.append(item.file)
}
}
}
default:
break
}
}
}
for featuredPack in featuredEmojiPacks {
for item in featuredPack.topItems {
for attribute in item.file.attributes {
switch attribute {
case let .CustomEmoji(_, alt, _):
if alt == query || (!normalizedQuery.isEmpty && alt == normalizedQuery) {
if !item.file.isPremiumEmoji || hasPremium {
if !existingIds.contains(item.file.fileId) {
existingIds.insert(item.file.fileId)
result.append(item.file)
}
}
}
default:
break
}
}
}
}
return result
}
}
public let context: AccountContext
public let theme: PresentationTheme
public let animationCache: AnimationCache
public let animationRenderer: MultiAnimationRenderer
public let files: [TelegramMediaFile]
public let action: (TelegramMediaFile) -> Void
public init(
context: AccountContext,
theme: PresentationTheme,
animationCache: AnimationCache,
animationRenderer: MultiAnimationRenderer,
files: [TelegramMediaFile],
action: @escaping (TelegramMediaFile) -> Void
) {
self.context = context
self.theme = theme
self.animationCache = animationCache
self.animationRenderer = animationRenderer
self.files = files
self.action = action
}
public static func ==(lhs: EmojiSuggestionsComponent, rhs: EmojiSuggestionsComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.animationCache !== rhs.animationCache {
return false
}
if lhs.animationRenderer !== rhs.animationRenderer {
return false
}
if lhs.files != rhs.files {
return false
}
return true
}
public final class View: UIView, UIScrollViewDelegate {
private struct ItemLayout: Equatable {
let spacing: CGFloat
let itemSize: CGFloat
let verticalInset: CGFloat
let itemCount: Int
let contentSize: CGSize
let sideInset: CGFloat
init(itemCount: Int) {
#if DEBUG
//var itemCount = itemCount
//itemCount = 100
#endif
self.spacing = 9.0
self.itemSize = 38.0
self.verticalInset = 5.0
self.sideInset = 5.0
self.itemCount = itemCount
self.contentSize = CGSize(width: self.sideInset * 2.0 + CGFloat(self.itemCount - 1) * self.spacing + CGFloat(self.itemCount) * self.itemSize, height: self.itemSize + self.verticalInset * 2.0)
}
func frame(at index: Int) -> CGRect {
return CGRect(origin: CGPoint(x: self.sideInset + CGFloat(index) * (self.spacing + self.itemSize), y: self.verticalInset), size: CGSize(width: self.itemSize, height: self.itemSize))
}
}
private let backgroundLayer: SimpleShapeLayer
private let scrollView: UIScrollView
private var component: EmojiSuggestionsComponent?
private var itemLayout: ItemLayout?
private var ignoreScrolling: Bool = false
private var visibleLayers: [MediaId: InlineStickerItemLayer] = [:]
override init(frame: CGRect) {
self.backgroundLayer = SimpleShapeLayer()
self.backgroundLayer.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor
self.backgroundLayer.shadowOffset = CGSize(width: 0.0, height: 2.0)
self.backgroundLayer.shadowRadius = 15.0
self.backgroundLayer.shadowOpacity = 0.15
self.scrollView = UIScrollView()
super.init(frame: frame)
self.disablesInteractiveTransitionGestureRecognizer = true
self.disablesInteractiveKeyboardGestureRecognizer = true
self.scrollView.layer.anchorPoint = CGPoint()
self.scrollView.delaysContentTouches = false
self.scrollView.clipsToBounds = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
self.layer.addSublayer(self.backgroundLayer)
self.addSubview(self.scrollView)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
let location = recognizer.location(in: self.scrollView)
if self.scrollView.bounds.contains(location) {
var closestFile: (file: TelegramMediaFile, distance: CGFloat)?
for (_, itemLayer) in self.visibleLayers {
guard let file = itemLayer.file else {
continue
}
let distance = abs(location.x - itemLayer.position.x)
if let (_, currentDistance) = closestFile {
if distance < currentDistance {
closestFile = (file, distance)
}
} else {
closestFile = (file, distance)
}
}
if let (file, _) = closestFile {
self.component?.action(file)
}
}
}
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateVisibleItems(synchronousLoad: false)
}
}
private func updateVisibleItems(synchronousLoad: Bool) {
guard let component = self.component, let itemLayout = self.itemLayout else {
return
}
let visibleBounds = self.scrollView.bounds
var visibleIds = Set<MediaId>()
for i in 0 ..< component.files.count {
let itemFrame = itemLayout.frame(at: i)
if visibleBounds.intersects(itemFrame) {
let item = component.files[i]
visibleIds.insert(item.fileId)
let itemLayer: InlineStickerItemLayer
if let current = self.visibleLayers[item.fileId] {
itemLayer = current
} else {
itemLayer = InlineStickerItemLayer(
context: component.context,
attemptSynchronousLoad: synchronousLoad,
emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: nil, fileId: item.fileId.id, file: item),
file: item,
cache: component.animationCache,
renderer: component.animationRenderer,
placeholderColor: component.theme.list.mediaPlaceholderColor,
pointSize: itemFrame.size
)
self.visibleLayers[item.fileId] = itemLayer
self.scrollView.layer.addSublayer(itemLayer)
}
itemLayer.frame = itemFrame
itemLayer.isVisibleForAnimations = true
}
}
var removedIds: [MediaId] = []
for (id, itemLayer) in self.visibleLayers {
if !visibleIds.contains(id) {
itemLayer.removeFromSuperlayer()
removedIds.append(id)
}
}
for id in removedIds {
self.visibleLayers.removeValue(forKey: id)
}
}
public func adjustBackground(relativePositionX: CGFloat) {
let size = self.bounds.size
if size.width.isZero {
return
}
let radius: CGFloat = 10.0
let notchSize = CGSize(width: 19.0, height: 7.5)
let path = CGMutablePath()
path.move(to: CGPoint(x: radius, y: 0.0))
path.addArc(tangent1End: CGPoint(x: 0.0, y: 0.0), tangent2End: CGPoint(x: 0.0, y: radius), radius: radius)
path.addLine(to: CGPoint(x: 0.0, y: size.height - notchSize.height - radius))
path.addArc(tangent1End: CGPoint(x: 0.0, y: size.height - notchSize.height), tangent2End: CGPoint(x: radius, y: size.height - notchSize.height), radius: radius)
let notchBase = CGPoint(x: min(size.width - radius - notchSize.width, max(radius, floor(relativePositionX - notchSize.width / 2.0))), y: size.height - notchSize.height)
path.addLine(to: notchBase)
path.addCurve(to: CGPoint(x: notchBase.x + 7.49968, y: notchBase.y + 5.32576), control1: CGPoint(x: notchBase.x + 2.10085, y: notchBase.y + 0.0), control2: CGPoint(x: notchBase.x + 5.41005, y: notchBase.y + 3.11103))
path.addCurve(to: CGPoint(x: notchBase.x + 8.95665, y: notchBase.y + 6.61485), control1: CGPoint(x: notchBase.x + 8.2352, y: notchBase.y + 6.10531), control2: CGPoint(x: notchBase.x + 8.60297, y: notchBase.y + 6.49509))
path.addCurve(to: CGPoint(x: notchBase.x + 9.91544, y: notchBase.y + 6.61599), control1: CGPoint(x: notchBase.x + 9.29432, y: notchBase.y + 6.72919), control2: CGPoint(x: notchBase.x + 9.5775, y: notchBase.y + 6.72953))
path.addCurve(to: CGPoint(x: notchBase.x + 11.3772, y: notchBase.y + 5.32853), control1: CGPoint(x: notchBase.x + 10.2694, y: notchBase.y + 6.49707), control2: CGPoint(x: notchBase.x + 10.6387, y: notchBase.y + 6.10756))
path.addCurve(to: CGPoint(x: notchBase.x + 19.0, y: notchBase.y + 0.0), control1: CGPoint(x: notchBase.x + 13.477, y: notchBase.y + 3.11363), control2: CGPoint(x: notchBase.x + 16.817, y: notchBase.y + 0.0))
path.addLine(to: CGPoint(x: size.width - radius, y: size.height - notchSize.height))
path.addArc(tangent1End: CGPoint(x: size.width, y: size.height - notchSize.height), tangent2End: CGPoint(x: size.width, y: size.height - notchSize.height - radius), radius: radius)
path.addLine(to: CGPoint(x: size.width, y: radius))
path.addArc(tangent1End: CGPoint(x: size.width, y: 0.0), tangent2End: CGPoint(x: size.width - radius, y: 0.0), radius: radius)
path.addLine(to: CGPoint(x: radius, y: 0.0))
self.backgroundLayer.frame = CGRect(origin: CGPoint(), size: size)
self.backgroundLayer.path = path
self.backgroundLayer.shadowPath = path
}
func update(component: EmojiSuggestionsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
let height: CGFloat = 54.0
if self.component?.theme !== component.theme {
self.backgroundLayer.fillColor = component.theme.list.plainBackgroundColor.cgColor
}
var resetScrollingPosition = false
if self.component?.files != component.files {
resetScrollingPosition = true
}
self.component = component
let itemLayout = ItemLayout(itemCount: component.files.count)
self.itemLayout = itemLayout
let size = CGSize(width: min(availableSize.width, itemLayout.contentSize.width), height: height)
let scrollFrame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: itemLayout.contentSize.height))
self.ignoreScrolling = true
if self.scrollView.frame != scrollFrame {
self.scrollView.frame = scrollFrame
}
if self.scrollView.contentSize != itemLayout.contentSize {
self.scrollView.contentSize = itemLayout.contentSize
}
if resetScrollingPosition {
self.scrollView.contentOffset = CGPoint()
}
self.ignoreScrolling = false
self.updateVisibleItems(synchronousLoad: resetScrollingPosition)
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -90,7 +90,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
private var isDisplayingPlaceholder: Bool = false
private var file: TelegramMediaFile?
public private(set) var file: TelegramMediaFile?
private var infoDisposable: Disposable?
private var disposable: Disposable?
private var fetchDisposable: Disposable?
@ -186,10 +186,6 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
let placeholderColor = self.placeholderColor
self.loadDisposable = self.renderer.loadFirstFrame(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: animationCacheFetchFile(context: self.context, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true), completion: { [weak self] result, isFinal in
if !result {
if !isFinal {
return
}
MultiAnimationRendererImpl.firstFrameQueue.async {
let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor)
@ -201,7 +197,10 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
strongSelf.contents = image.cgImage
strongSelf.isDisplayingPlaceholder = true
}
strongSelf.loadAnimation()
if isFinal {
strongSelf.loadAnimation()
}
}
}
} else {

View File

@ -28,7 +28,6 @@ swift_library(
"//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache",
"//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramUI/Components/MultiVideoRenderer:MultiVideoRenderer",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/SoftwareVideo:SoftwareVideo",
"//submodules/ShimmerEffect:ShimmerEffect",
@ -42,7 +41,7 @@ swift_library(
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent",
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
"//submodules/MurMurHash32:MurMurHash32",
"//submodules/LocalizedPeerData:LocalizedPeerData",
],
visibility = [
"//visibility:public",

View File

@ -1524,6 +1524,7 @@ public final class EmojiPagerContentComponent: Component {
public let id: AnyHashable
public let context: AccountContext
public let avatarPeer: EnginePeer?
public let animationCache: AnimationCache
public let animationRenderer: MultiAnimationRenderer
public let inputInteractionHolder: InputInteractionHolder
@ -1533,6 +1534,7 @@ public final class EmojiPagerContentComponent: Component {
public init(
id: AnyHashable,
context: AccountContext,
avatarPeer: EnginePeer?,
animationCache: AnimationCache,
animationRenderer: MultiAnimationRenderer,
inputInteractionHolder: InputInteractionHolder,
@ -1541,6 +1543,7 @@ public final class EmojiPagerContentComponent: Component {
) {
self.id = id
self.context = context
self.avatarPeer = avatarPeer
self.animationCache = animationCache
self.animationRenderer = animationRenderer
self.inputInteractionHolder = inputInteractionHolder
@ -1558,6 +1561,9 @@ public final class EmojiPagerContentComponent: Component {
if lhs.context !== rhs.context {
return false
}
if lhs.avatarPeer != rhs.avatarPeer {
return false
}
if lhs.animationCache !== rhs.animationCache {
return false
}

View File

@ -10,6 +10,7 @@ import BlurredBackgroundComponent
import BundleIconComponent
import AudioToolbox
import SwiftSignalKit
import LocalizedPeerData
public final class EntityKeyboardChildEnvironment: Equatable {
public let theme: PresentationTheme
@ -333,29 +334,47 @@ public final class EntityKeyboardComponent: Component {
for itemGroup in stickerContent.itemGroups {
if let id = itemGroup.supergroupId.base as? String {
let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [
"saved": .saved,
"recent": .recent,
"premium": .premium
]
let titleMapping: [String: String] = [
"saved": component.strings.Stickers_Favorites,
"recent": component.strings.Stickers_Recent,
"premium": component.strings.EmojiInput_PanelTitlePremium
]
if let icon = iconMapping[id], let title = titleMapping[id] {
topStickerItems.append(EntityKeyboardTopPanelComponent.Item(
id: itemGroup.supergroupId,
isReorderable: false,
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
icon: icon,
theme: component.theme,
title: title,
pressed: { [weak self] in
self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil)
}
if id == "peerSpecific" {
if let avatarPeer = stickerContent.avatarPeer {
topStickerItems.append(EntityKeyboardTopPanelComponent.Item(
id: itemGroup.supergroupId,
isReorderable: false,
content: AnyComponent(EntityKeyboardAvatarTopPanelComponent(
context: stickerContent.context,
peer: avatarPeer,
theme: component.theme,
title: avatarPeer.compactDisplayTitle,
pressed: { [weak self] in
self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil)
}
))
))
))
}
} else {
let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [
"saved": .saved,
"recent": .recent,
"premium": .premium
]
let titleMapping: [String: String] = [
"saved": component.strings.Stickers_Favorites,
"recent": component.strings.Stickers_Recent,
"premium": component.strings.EmojiInput_PanelTitlePremium
]
if let icon = iconMapping[id], let title = titleMapping[id] {
topStickerItems.append(EntityKeyboardTopPanelComponent.Item(
id: itemGroup.supergroupId,
isReorderable: false,
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
icon: icon,
theme: component.theme,
title: title,
pressed: { [weak self] in
self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil)
}
))
))
}
}
} else {
if !itemGroup.items.isEmpty {

View File

@ -12,7 +12,7 @@ import MultiAnimationRenderer
import AccountContext
import MultilineTextComponent
import LottieAnimationComponent
import MurMurHash32
import AvatarNode
final class EntityKeyboardAnimationTopPanelComponent: Component {
typealias EnvironmentType = EntityKeyboardTopPanelItemEnvironment
@ -424,6 +424,137 @@ final class EntityKeyboardIconTopPanelComponent: Component {
}
}
final class EntityKeyboardAvatarTopPanelComponent: Component {
typealias EnvironmentType = EntityKeyboardTopPanelItemEnvironment
let context: AccountContext
let peer: EnginePeer
let theme: PresentationTheme
let title: String
let pressed: () -> Void
init(
context: AccountContext,
peer: EnginePeer,
theme: PresentationTheme,
title: String,
pressed: @escaping () -> Void
) {
self.context = context
self.peer = peer
self.theme = theme
self.title = title
self.pressed = pressed
}
static func ==(lhs: EntityKeyboardAvatarTopPanelComponent, rhs: EntityKeyboardAvatarTopPanelComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.title != rhs.title {
return false
}
return true
}
final class View: UIView {
let avatarNode: AvatarNode
var component: EntityKeyboardAvatarTopPanelComponent?
var titleView: ComponentView<Empty>?
override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0))
super.init(frame: frame)
self.addSubnode(self.avatarNode)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.component?.pressed()
}
}
func update(component: EntityKeyboardAvatarTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
let itemEnvironment = environment[EntityKeyboardTopPanelItemEnvironment.self].value
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer)
self.component = component
let nativeIconSize: CGSize = itemEnvironment.isExpanded ? CGSize(width: 44.0, height: 44.0) : CGSize(width: 24.0, height: 24.0)
let boundingIconSize: CGSize = itemEnvironment.isExpanded ? CGSize(width: 38.0, height: 38.0) : CGSize(width: 24.0, height: 24.0)
let iconSize = boundingIconSize
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) / 2.0), y: floor((nativeIconSize.height - iconSize.height) / 2.0)), size: iconSize)
transition.containedViewLayoutTransition.updateFrame(node: self.avatarNode, frame: iconFrame)
if itemEnvironment.isExpanded {
let titleView: ComponentView<Empty>
if let current = self.titleView {
titleView = current
} else {
titleView = ComponentView<Empty>()
self.titleView = titleView
}
let titleSize = titleView.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.regular(10.0), textColor: component.theme.chat.inputPanel.primaryTextColor)),
insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)
)),
environment: {},
containerSize: CGSize(width: 62.0, height: 100.0)
)
if let view = titleView.view {
if view.superview == nil {
view.alpha = 0.0
self.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: availableSize.height - titleSize.height - 1.0), size: titleSize)
transition.setAlpha(view: view, alpha: 1.0)
}
} else if let titleView = self.titleView {
self.titleView = nil
if let view = titleView.view {
if !transition.animation.isImmediate {
view.alpha = 0.0
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.08, completion: { [weak view] _ in
view?.removeFromSuperview()
})
} else {
view.removeFromSuperview()
}
}
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class EntityKeyboardStaticStickersPanelComponent: Component {
typealias EnvironmentType = EntityKeyboardTopPanelItemEnvironment

View File

@ -218,7 +218,8 @@ private final class ItemAnimationContext {
}
}
static let queue = Queue(name: "ItemAnimationContext", qos: .default)
static let queue0 = Queue(name: "ItemAnimationContext-0", qos: .default)
static let queue1 = Queue(name: "ItemAnimationContext-1", qos: .default)
private let cache: AnimationCache
private let stateUpdated: () -> Void
@ -667,17 +668,60 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
}
if !tasks.isEmpty {
ItemAnimationContext.queue.async {
var completions: [() -> Void] = []
for task in tasks {
let complete = task.task()
completions.append(complete)
if tasks.count > 2 {
let tasks0 = Array(tasks.prefix(tasks.count / 2))
let tasks1 = Array(tasks.suffix(tasks.count - tasks0.count))
var tasks0Completions: [() -> Void]?
var tasks1Completions: [() -> Void]?
let complete: (Int, [() -> Void]) -> Void = { index, completions in
Queue.mainQueue().async {
if index == 0 {
tasks0Completions = completions
} else if index == 1 {
tasks1Completions = completions
}
if let tasks0Completions = tasks0Completions, let tasks1Completions = tasks1Completions {
for completion in tasks0Completions {
completion()
}
for completion in tasks1Completions {
completion()
}
}
}
}
if !completions.isEmpty {
Queue.mainQueue().async {
for completion in completions {
completion()
ItemAnimationContext.queue0.async {
var completions: [() -> Void] = []
for task in tasks0 {
let complete = task.task()
completions.append(complete)
}
complete(0, completions)
}
ItemAnimationContext.queue1.async {
var completions: [() -> Void] = []
for task in tasks1 {
let complete = task.task()
completions.append(complete)
}
complete(1, completions)
}
} else {
ItemAnimationContext.queue0.async {
var completions: [() -> Void] = []
for task in tasks {
let complete = task.task()
completions.append(complete)
}
if !completions.isEmpty {
Queue.mainQueue().async {
for completion in completions {
completion()
}
}
}
}

View File

@ -1,20 +0,0 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "MultiVideoRenderer",
module_name = "MultiVideoRenderer",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Display:Display",
"//submodules/SoftwareVideo:SoftwareVideo",
],
visibility = [
"//visibility:public",
],
)

View File

@ -1,399 +0,0 @@
import Foundation
import UIKit
import SwiftSignalKit
import Display
import SoftwareVideo
/*public protocol MultiVideoRenderer: AnyObject {
func add(groupId: String, target: MultiVideoRenderTarget, itemId: String, size: CGSize, source: @escaping (@escaping (String) -> Void) -> Disposable) -> Disposable
}
open class MultiVideoRenderTarget: SimpleLayer {
fileprivate let deinitCallbacks = Bag<() -> Void>()
fileprivate let updateStateCallbacks = Bag<() -> Void>()
public final var shouldBeAnimating: Bool = false {
didSet {
if self.shouldBeAnimating != oldValue {
for f in self.updateStateCallbacks.copyItems() {
f()
}
}
}
}
deinit {
for f in self.deinitCallbacks.copyItems() {
f()
}
}
open func updateDisplayPlaceholder(displayPlaceholder: Bool) {
}
}
private final class ItemVideoContext {
static let queue = Queue(name: "ItemVideoContext", qos: .default)
private let stateUpdated: () -> Void
private var disposable: Disposable?
private var displayLink: ConstantDisplayLinkAnimator?
private var frameManager: SoftwareVideoLayerFrameManager?
private(set) var isPlaying: Bool = false {
didSet {
if self.isPlaying != oldValue {
self.stateUpdated()
}
}
}
let targets = Bag<Weak<MultiVideoRenderTarget>>()
init(itemId: String, source: @escaping (@escaping (String) -> Void) -> Disposable, stateUpdated: @escaping () -> Void) {
self.stateUpdated = stateUpdated
self.disposable = source({ [weak self] in
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
//strongSelf.frameManager = SoftwareVideoLayerFrameManager(account: <#T##Account#>, fileReference: <#T##FileMediaReference#>, layerHolder: <#T##SampleBufferLayer#>)
strongSelf.updateIsPlaying()
if result.item == nil {
for target in strongSelf.targets.copyItems() {
if let target = target.value {
target.updateDisplayPlaceholder(displayPlaceholder: true)
}
}
}
}
})
}
deinit {
self.disposable?.dispose()
self.displayLink?.invalidate()
}
func updateAddedTarget(target: MultiAnimationRenderTarget) {
if let item = self.item, let currentFrameGroup = self.currentFrameGroup {
let currentFrame = self.frameIndex % item.numFrames
if let contentsRect = currentFrameGroup.contentsRect(index: currentFrame) {
target.updateDisplayPlaceholder(displayPlaceholder: false)
target.contents = currentFrameGroup.image.cgImage
target.contentsRect = contentsRect
}
}
self.updateIsPlaying()
}
func updateIsPlaying() {
var isPlaying = true
if self.item == nil {
isPlaying = false
}
var shouldBeAnimating = false
for target in self.targets.copyItems() {
if let target = target.value {
if target.shouldBeAnimating {
shouldBeAnimating = true
break
}
}
}
if !shouldBeAnimating {
isPlaying = false
}
self.isPlaying = isPlaying
}
func animationTick() -> LoadFrameGroupTask? {
return self.update(advanceFrame: true)
}
private func update(advanceFrame: Bool) -> LoadFrameGroupTask? {
guard let item = self.item else {
return nil
}
let currentFrame = self.frameIndex % item.numFrames
if let currentFrameGroup = self.currentFrameGroup, currentFrameGroup.frameRange.contains(currentFrame) {
} else if !self.isLoadingFrameGroup {
self.currentFrameGroup = nil
self.isLoadingFrameGroup = true
let frameSkip = self.frameSkip
return LoadFrameGroupTask(task: { [weak self] in
let possibleCounts: [Int] = [10, 12, 14, 16, 18, 20]
let countIndex = Int.random(in: 0 ..< possibleCounts.count)
let currentFrameGroup = FrameGroup(item: item, baseFrameIndex: currentFrame, count: possibleCounts[countIndex], skip: frameSkip)
return {
guard let strongSelf = self else {
return
}
strongSelf.isLoadingFrameGroup = false
if let currentFrameGroup = currentFrameGroup {
strongSelf.currentFrameGroup = currentFrameGroup
for target in strongSelf.targets.copyItems() {
target.value?.contents = currentFrameGroup.image.cgImage
}
let _ = strongSelf.update(advanceFrame: false)
}
}
})
}
if advanceFrame {
self.frameIndex += self.frameSkip
}
if let currentFrameGroup = self.currentFrameGroup, let contentsRect = currentFrameGroup.contentsRect(index: currentFrame) {
for target in self.targets.copyItems() {
if let target = target.value {
target.updateDisplayPlaceholder(displayPlaceholder: false)
target.contentsRect = contentsRect
}
}
}
return nil
}
}
public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
private final class GroupContext {
private var frameSkip: Int
private let stateUpdated: () -> Void
private var itemContexts: [String: ItemAnimationContext] = [:]
private(set) var isPlaying: Bool = false {
didSet {
if self.isPlaying != oldValue {
self.stateUpdated()
}
}
}
init(frameSkip: Int, stateUpdated: @escaping () -> Void) {
self.frameSkip = frameSkip
self.stateUpdated = stateUpdated
}
func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable {
let itemContext: ItemAnimationContext
if let current = self.itemContexts[itemId] {
itemContext = current
} else {
itemContext = ItemAnimationContext(cache: cache, itemId: itemId, size: size, frameSkip: self.frameSkip, fetch: fetch, stateUpdated: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateIsPlaying()
})
self.itemContexts[itemId] = itemContext
}
let index = itemContext.targets.add(Weak(target))
itemContext.updateAddedTarget(target: target)
let deinitIndex = target.deinitCallbacks.add { [weak self, weak itemContext] in
Queue.mainQueue().async {
guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemId] === itemContext else {
return
}
itemContext.targets.remove(index)
if itemContext.targets.isEmpty {
strongSelf.itemContexts.removeValue(forKey: itemId)
}
}
}
let updateStateIndex = target.updateStateCallbacks.add { [weak itemContext] in
guard let itemContext = itemContext else {
return
}
itemContext.updateIsPlaying()
}
return ActionDisposable { [weak self, weak itemContext, weak target] in
guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemId] === itemContext else {
return
}
if let target = target {
target.deinitCallbacks.remove(deinitIndex)
target.updateStateCallbacks.remove(updateStateIndex)
}
itemContext.targets.remove(index)
if itemContext.targets.isEmpty {
strongSelf.itemContexts.removeValue(forKey: itemId)
}
}
}
func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool {
if let item = cache.getSynchronously(sourceId: itemId, size: size) {
guard let frameGroup = FrameGroup(item: item, baseFrameIndex: 0, count: 1, skip: 1) else {
return false
}
target.contents = frameGroup.image.cgImage
return true
} else {
return false
}
}
private func updateIsPlaying() {
var isPlaying = false
for (_, itemContext) in self.itemContexts {
if itemContext.isPlaying {
isPlaying = true
break
}
}
self.isPlaying = isPlaying
}
func animationTick() -> [LoadFrameGroupTask] {
var tasks: [LoadFrameGroupTask] = []
for (_, itemContext) in self.itemContexts {
if itemContext.isPlaying {
if let task = itemContext.animationTick() {
tasks.append(task)
}
}
}
return tasks
}
}
private var groupContexts: [String: GroupContext] = [:]
private var frameSkip: Int
private var displayLink: ConstantDisplayLinkAnimator?
private(set) var isPlaying: Bool = false {
didSet {
if self.isPlaying != oldValue {
if self.isPlaying {
if self.displayLink == nil {
self.displayLink = ConstantDisplayLinkAnimator { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.animationTick()
}
self.displayLink?.frameInterval = self.frameSkip
self.displayLink?.isPaused = false
}
} else {
if let displayLink = self.displayLink {
self.displayLink = nil
displayLink.invalidate()
}
}
}
}
}
public init() {
if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.activeProcessorCount > 2 {
self.frameSkip = 1
} else {
self.frameSkip = 2
}
}
public func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable {
let groupContext: GroupContext
if let current = self.groupContexts[groupId] {
groupContext = current
} else {
groupContext = GroupContext(frameSkip: self.frameSkip, stateUpdated: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateIsPlaying()
})
self.groupContexts[groupId] = groupContext
}
let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch)
return ActionDisposable {
disposable.dispose()
}
}
public func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool {
let groupContext: GroupContext
if let current = self.groupContexts[groupId] {
groupContext = current
} else {
groupContext = GroupContext(frameSkip: self.frameSkip, stateUpdated: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateIsPlaying()
})
self.groupContexts[groupId] = groupContext
}
return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId, size: size)
}
private func updateIsPlaying() {
var isPlaying = false
for (_, groupContext) in self.groupContexts {
if groupContext.isPlaying {
isPlaying = true
break
}
}
self.isPlaying = isPlaying
}
private func animationTick() {
var tasks: [LoadFrameGroupTask] = []
for (_, groupContext) in self.groupContexts {
if groupContext.isPlaying {
tasks.append(contentsOf: groupContext.animationTick())
}
}
if !tasks.isEmpty {
ItemAnimationContext.queue.async {
var completions: [() -> Void] = []
for task in tasks {
let complete = task.task()
completions.append(complete)
}
if !completions.isEmpty {
Queue.mainQueue().async {
for completion in completions {
completion()
}
}
}
}
}
}
}
*/

View File

@ -9,6 +9,12 @@ import AnimationCache
import MultiAnimationRenderer
import TelegramCore
private extension CGRect {
var center: CGPoint {
return CGPoint(x: self.midX, y: self.midY)
}
}
private final class InlineStickerItem: Hashable {
let emoji: ChatTextInputTextCustomEmojiAttribute
let file: TelegramMediaFile?

View File

@ -849,7 +849,7 @@ final class AuthorizedApplicationContext {
if visiblePeerId != peerId || messageId != nil {
if self.rootController.rootTabController != nil {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: .peer(id: peerId), subject: messageId.flatMap { .message(id: .id($0), highlight: true, timecode: nil) }, activateInput: activateInput))
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: .peer(id: peerId), subject: messageId.flatMap { .message(id: .id($0), highlight: true, timecode: nil) }, activateInput: activateInput ? .text : nil))
} else {
self.scheduledOpenChatWithPeerId = (peerId, messageId, activateInput)
}

View File

@ -401,7 +401,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private var willAppear = false
private var didAppear = false
private var scheduledActivateInput = false
private var scheduledActivateInput: ChatControllerActivateInput?
private var raiseToListen: RaiseToListenManager?
private var voicePlaylistDidEndTimestamp: Double = 0.0
@ -7168,7 +7168,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self else {
return
}
strongSelf.chatDisplayNode.openStickers()
strongSelf.chatDisplayNode.openStickers(beginWithEmoji: false)
strongSelf.mediaRecordingModeTooltipController?.dismissImmediately()
}, editMessage: { [weak self] in
if let strongSelf = self, let editMessage = strongSelf.presentationInterfaceState.interfaceState.editMessage {
@ -7192,7 +7192,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let value = value as? ChatTextInputTextCustomEmojiAttribute {
if let file = value.file {
inlineStickers[file.fileId] = file
if file.isPremiumEmoji && !strongSelf.presentationInterfaceState.isPremium {
if file.isPremiumEmoji && !strongSelf.presentationInterfaceState.isPremium && strongSelf.chatLocation.peerId != strongSelf.context.account.peerId {
if firstLockedPremiumEmoji == nil {
firstLockedPremiumEmoji = file
}
@ -9252,11 +9252,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
if self.scheduledActivateInput {
self.scheduledActivateInput = false
if let scheduledActivateInput = scheduledActivateInput, case .text = scheduledActivateInput {
self.scheduledActivateInput = nil
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
return state.updatedInputMode({ _ in .text })
return state.updatedInputMode({ _ in
switch scheduledActivateInput {
case .text:
return .text
case .entityInput:
return .media(mode: .other, expanded: nil, focused: false)
}
})
})
}
@ -9691,12 +9698,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}))
}
if self.scheduledActivateInput {
self.scheduledActivateInput = false
if let scheduledActivateInput = self.scheduledActivateInput {
self.scheduledActivateInput = nil
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
return state.updatedInputMode({ _ in .text })
})
switch scheduledActivateInput {
case .text:
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
return state.updatedInputMode({ _ in
return .text
})
})
case .entityInput:
self.chatDisplayNode.openStickers(beginWithEmoji: true)
}
}
if let snapshotState = self.storedAnimateFromSnapshotState {
@ -14118,7 +14132,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
subject = nil
}
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: chatLocation, chatLocationContextHolder: result.contextHolder, subject: subject, activateInput: result.isEmpty, keepStack: .always))
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: chatLocation, chatLocationContextHolder: result.contextHolder, subject: subject, activateInput: result.isEmpty ? .text : nil, keepStack: .always))
subscriber.putCompletion()
}, error: { _ in
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
@ -16098,13 +16112,25 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
func activateInput() {
func activateInput(type: ChatControllerActivateInput) {
if self.didAppear {
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
return state.updatedInputMode({ _ in .text })
})
switch type {
case .text:
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
return state.updatedInputMode({ _ in
switch type {
case .text:
return .text
case .entityInput:
return .media(mode: .other, expanded: nil, focused: false)
}
})
})
case .entityInput:
self.chatDisplayNode.openStickers(beginWithEmoji: true)
}
} else {
self.scheduledActivateInput = true
self.scheduledActivateInput = type
}
}

View File

@ -105,6 +105,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
private var navigationModalFrame: NavigationModalFrame?
let inputPanelContainerNode: ChatInputPanelContainer
private let inputPanelOverlayNode: SparseNode
private let inputPanelClippingNode: SparseNode
private let inputPanelBackgroundNode: NavigationBackgroundNode
private var intrinsicInputPanelBackgroundNodeSize: CGSize?
@ -121,7 +122,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
private var inputPanelNode: ChatInputPanelNode?
private(set) var inputPanelOverscrollNode: ChatInputPanelOverscrollNode?
private weak var currentDismissedInputPanelNode: ASDisplayNode?
private weak var currentDismissedInputPanelNode: ChatInputPanelNode?
private var secondaryInputPanelNode: ChatInputPanelNode?
private(set) var accessoryPanelNode: AccessoryPanelNode?
private var inputContextPanelNode: ChatInputContextPanelNode?
@ -245,6 +246,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
private var lastSendTimestamp = 0.0
private var openStickersBeginWithEmoji: Bool = false
private var openStickersDisposable: Disposable?
private var displayVideoUnmuteTipDisposable: Disposable?
@ -388,6 +390,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.loadingNode = ChatLoadingNode(theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper, bubbleCorners: self.chatPresentationInterfaceState.bubbleCorners)
self.inputPanelContainerNode = ChatInputPanelContainer()
self.inputPanelOverlayNode = SparseNode()
self.inputPanelClippingNode = SparseNode()
if case let .color(color) = self.chatPresentationInterfaceState.chatWallpaper, UIColor(rgb: color).isEqual(self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper) {
@ -547,6 +550,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.addSubnode(self.inputContextOverTextPanelContainer)
self.inputPanelContainerNode.addSubnode(self.inputPanelClippingNode)
self.inputPanelContainerNode.addSubnode(self.inputPanelOverlayNode)
self.inputPanelClippingNode.addSubnode(self.inputPanelBackgroundNode)
self.inputPanelClippingNode.addSubnode(self.inputPanelBackgroundSeparatorNode)
self.inputPanelBackgroundNode.addSubnode(self.inputPanelBottomBackgroundSeparatorNode)
@ -1091,7 +1095,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
var immediatelyLayoutSecondaryInputPanelAndAnimateAppearance = false
var inputPanelNodeHandlesTransition = false
var dismissedInputPanelNode: ASDisplayNode?
var dismissedInputPanelNode: ChatInputPanelNode?
var dismissedSecondaryInputPanelNode: ASDisplayNode?
var dismissedAccessoryPanelNode: AccessoryPanelNode?
var dismissedInputContextPanelNode: ChatInputContextPanelNode?
@ -1123,6 +1127,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
if inputPanelNode.supernode !== self {
immediatelyLayoutInputPanelAndAnimateAppearance = true
self.inputPanelClippingNode.insertSubnode(inputPanelNode, aboveSubnode: self.inputPanelBackgroundNode)
if let viewForOverlayContent = inputPanelNode.viewForOverlayContent {
self.inputPanelOverlayNode.view.addSubview(viewForOverlayContent)
}
}
} else {
let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset - 120.0, isSecondary: false, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: self.inputPanelContainerNode.expansionFraction == 1.0)
@ -1650,6 +1658,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.inputPanelContainerNode.update(size: layout.size, scrollableDistance: max(0.0, maximumInputNodeHeight - layout.standardInputHeight), isExpansionEnabled: isInputExpansionEnabled, transition: transition)
transition.updatePosition(node: self.inputPanelClippingNode, position: CGRect(origin: apparentInputBackgroundFrame.origin, size: layout.size).center, beginWithCurrentState: true)
transition.updateBounds(node: self.inputPanelClippingNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: apparentInputBackgroundFrame.origin.y), size: layout.size), beginWithCurrentState: true)
transition.updatePosition(node: self.inputPanelOverlayNode, position: CGRect(origin: apparentInputBackgroundFrame.origin, size: layout.size).center, beginWithCurrentState: true)
transition.updateBounds(node: self.inputPanelOverlayNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: apparentInputBackgroundFrame.origin.y), size: layout.size), beginWithCurrentState: true)
transition.updateFrame(node: self.inputPanelBackgroundNode, frame: apparentInputBackgroundFrame, beginWithCurrentState: true)
transition.updateFrame(node: self.contentDimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: apparentInputBackgroundFrame.origin.y)))
@ -1802,6 +1812,14 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
transition.updateFrame(node: inputPanelNode, frame: apparentInputPanelFrame)
transition.updateAlpha(node: inputPanelNode, alpha: 1.0)
}
if let viewForOverlayContent = inputPanelNode.viewForOverlayContent {
if inputPanelNodeHandlesTransition {
viewForOverlayContent.frame = apparentInputPanelFrame
} else {
transition.updateFrame(view: viewForOverlayContent, frame: apparentInputPanelFrame)
}
}
}
if let dismissedInputPanelNode = dismissedInputPanelNode, dismissedInputPanelNode !== self.secondaryInputPanelNode {
@ -1832,6 +1850,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
alphaCompleted = true
completed()
})
dismissedInputPanelNode.viewForOverlayContent?.removeFromSuperview()
}
if let dismissedSecondaryInputPanelNode = dismissedSecondaryInputPanelNode, dismissedSecondaryInputPanelNode !== self.inputPanelNode {
@ -2407,11 +2427,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
context: self.context,
currentInputData: inputMediaNodeData,
updatedInputData: self.inputMediaNodeDataPromise.get(),
defaultToEmojiTab: !self.chatPresentationInterfaceState.interfaceState.effectiveInputState.inputText.string.isEmpty,
defaultToEmojiTab: !self.chatPresentationInterfaceState.interfaceState.effectiveInputState.inputText.string.isEmpty || self.openStickersBeginWithEmoji,
controllerInteraction: self.controllerInteraction,
interfaceInteraction: self.interfaceInteraction,
chatPeerId: peerId
)
self.openStickersBeginWithEmoji = false
return inputNode
}
@ -2700,6 +2721,28 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
break
}
var maybeDismissOverlayContent = true
if let inputNode = self.inputNode, inputNode.bounds.contains(self.view.convert(point, to: inputNode.view)) {
if let externalTopPanelContainer = inputNode.externalTopPanelContainer {
if externalTopPanelContainer.hitTest(self.view.convert(point, to: externalTopPanelContainer), with: nil) != nil {
maybeDismissOverlayContent = true
} else {
maybeDismissOverlayContent = false
}
} else {
maybeDismissOverlayContent = false
}
}
if let inputPanelNode = self.inputPanelNode, let viewForOverlayContent = inputPanelNode.viewForOverlayContent {
if let result = viewForOverlayContent.hitTest(self.view.convert(point, to: viewForOverlayContent), with: event) {
return result
}
if maybeDismissOverlayContent {
viewForOverlayContent.maybeDismissContent(point: self.view.convert(point, to: viewForOverlayContent))
}
}
return nil
}
@ -2851,7 +2894,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
}
}
func openStickers() {
func openStickers(beginWithEmoji: Bool) {
self.openStickersBeginWithEmoji = beginWithEmoji
if let inputMediaNode = self.inputMediaNode {
if self.openStickersDisposable == nil {
self.openStickersDisposable = (inputMediaNode.ready
@ -2916,7 +2961,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
if let value = value as? ChatTextInputTextCustomEmojiAttribute {
if let file = value.file {
inlineStickers[file.fileId] = file
if file.isPremiumEmoji && !self.chatPresentationInterfaceState.isPremium {
if file.isPremiumEmoji && !self.chatPresentationInterfaceState.isPremium && self.chatPresentationInterfaceState.chatLocation.peerId != self.context.account.peerId {
if firstLockedPremiumEmoji == nil {
firstLockedPremiumEmoji = file
}

View File

@ -86,16 +86,24 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
}
static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, areCustomEmojiEnabled: Bool) -> Signal<EmojiPagerContentComponent, NoError> {
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> Bool in
guard case let .user(user) = peer else {
return false
static func hasPremium(context: AccountContext, chatPeerId: EnginePeer.Id?, premiumIfSavedMessages: Bool) -> Signal<Bool, NoError> {
let hasPremium: Signal<Bool, NoError>
if premiumIfSavedMessages, let chatPeerId = chatPeerId, chatPeerId == context.account.peerId {
hasPremium = .single(true)
} else {
hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> Bool in
guard case let .user(user) = peer else {
return false
}
return user.isPremium
}
return user.isPremium
|> distinctUntilChanged
}
|> distinctUntilChanged
return hasPremium
}
static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, areCustomEmojiEnabled: Bool, chatPeerId: EnginePeer.Id?) -> Signal<EmojiPagerContentComponent, NoError> {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
@ -103,7 +111,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
let emojiItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.LocalRecentEmoji], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
hasPremium,
ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true),
context.account.viewTracker.featuredEmojiPacks()
)
|> map { view, hasPremium, featuredEmojiPacks -> EmojiPagerContentComponent in
@ -305,6 +313,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
return EmojiPagerContentComponent(
id: "emoji",
context: context,
avatarPeer: nil,
animationCache: animationCache,
animationRenderer: animationRenderer,
inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(),
@ -340,15 +349,6 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> Bool in
guard case let .user(user) = peer else {
return false
}
return user.isPremium
}
|> distinctUntilChanged
let animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
@ -359,21 +359,64 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
animationRenderer = MultiAnimationRendererImpl()
//}
let emojiItems = emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, areCustomEmojiEnabled: areCustomEmojiEnabled)
let emojiItems = emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId)
let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks]
let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudPremiumStickers]
let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers]
struct PeerSpecificPackData: Equatable {
var info: StickerPackCollectionInfo
var items: [StickerPackItem]
var peer: EnginePeer
static func ==(lhs: PeerSpecificPackData, rhs: PeerSpecificPackData) -> Bool {
if lhs.info.id != rhs.info.id {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.peer != rhs.peer {
return false
}
return true
}
}
let peerSpecificPack: Signal<PeerSpecificPackData?, NoError>
if let chatPeerId = chatPeerId {
peerSpecificPack = combineLatest(
context.engine.peers.peerSpecificStickerPack(peerId: chatPeerId),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: chatPeerId))
)
|> map { packData, peer -> PeerSpecificPackData? in
guard let peer = peer else {
return nil
}
guard let (info, items) = packData.packInfo else {
return nil
}
return PeerSpecificPackData(info: info, items: items.compactMap { $0 as? StickerPackItem }, peer: peer)
}
|> distinctUntilChanged
} else {
peerSpecificPack = .single(nil)
}
let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings
let stickerItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: stickerOrderedItemListCollectionIds, namespaces: stickerNamespaces, aroundIndex: nil, count: 10000000),
hasPremium,
ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false),
context.account.viewTracker.featuredStickerPacks(),
context.engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: Namespaces.CachedItemCollection.featuredStickersConfiguration, id: ValueBoxKey(length: 0))),
ApplicationSpecificNotice.dismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager)
ApplicationSpecificNotice.dismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager),
peerSpecificPack
)
|> map { view, hasPremium, featuredStickerPacks, featuredStickersConfiguration, dismissedTrendingStickerPacks -> EmojiPagerContentComponent in
|> map { view, hasPremium, featuredStickerPacks, featuredStickersConfiguration, dismissedTrendingStickerPacks, peerSpecificPack -> EmojiPagerContentComponent in
struct ItemGroup {
var supergroupId: AnyHashable
var id: AnyHashable
@ -397,7 +440,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
recentStickers = orderedView
} else if orderedView.collectionId == Namespaces.OrderedItemList.CloudSavedStickers {
savedStickers = orderedView
} else if orderedView.collectionId == Namespaces.OrderedItemList.CloudPremiumStickers {
} else if orderedView.collectionId == Namespaces.OrderedItemList.CloudAllPremiumStickers {
cloudPremiumStickers = orderedView
}
}
@ -548,6 +591,37 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
}
var avatarPeer: EnginePeer?
if let peerSpecificPack = peerSpecificPack {
avatarPeer = peerSpecificPack.peer
var processedIds = Set<MediaId>()
for item in peerSpecificPack.items {
if isPremiumDisabled && item.file.isPremiumSticker {
continue
}
if processedIds.contains(item.file.fileId) {
continue
}
processedIds.insert(item.file.fileId)
let resultItem = EmojiPagerContentComponent.Item(
animationData: EntityKeyboardAnimationData(file: item.file),
itemFile: item.file,
staticEmoji: nil,
subgroupId: nil
)
let groupId = "peerSpecific"
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: peerSpecificPack.peer.compactDisplayTitle, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem]))
}
}
}
for entry in view.entries {
guard let item = entry.item as? StickerPackItem else {
continue
@ -651,6 +725,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
return EmojiPagerContentComponent(
id: "stickers",
context: context,
avatarPeer: avatarPeer,
animationCache: animationCache,
animationRenderer: animationRenderer,
inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(),
@ -1075,18 +1150,10 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
self.externalTopPanelContainerImpl = PagerExternalTopPanelContainer()
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> Bool in
guard case let .user(user) = peer else {
return false
}
return user.isPremium
}
|> distinctUntilChanged
var premiumToastCounter = 0
self.emojiInputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak self, weak interfaceInteraction, weak controllerInteraction] _, item, _, _, _ in
let _ = (hasPremium |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in
let _ = (ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true) |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in
guard let strongSelf = self, let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else {
return
}
@ -1114,23 +1181,48 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: presentationData.strings.EmojiInput_PremiumEmojiToast_Text, undoText: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, customAction: { [weak controllerInteraction] in
premiumToastCounter += 1
let suggestSavedMessages = premiumToastCounter % 2 == 0
let text: String
let actionTitle: String
if suggestSavedMessages {
text = presentationData.strings.EmojiInput_PremiumEmojiToast_TryText
actionTitle = presentationData.strings.EmojiInput_PremiumEmojiToast_TryAction
} else {
text = presentationData.strings.EmojiInput_PremiumEmojiToast_Text
actionTitle = presentationData.strings.EmojiInput_PremiumEmojiToast_Action
}
let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: text, undoText: actionTitle, customAction: { [weak controllerInteraction] in
guard let controllerInteraction = controllerInteraction else {
return
}
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumDemoScreen(context: context, subject: .animatedEmoji, action: {
let controller = PremiumIntroScreen(context: context, source: .animatedEmoji)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
if suggestSavedMessages, let navigationController = controllerInteraction.navigationController() {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
navigationController: navigationController,
chatController: nil,
context: context,
chatLocation: .peer(id: context.account.peerId),
subject: nil,
updateTextInputState: nil,
activateInput: .entityInput,
keepStack: .always,
completion: { _ in
})
)
} else {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumDemoScreen(context: context, subject: .animatedEmoji, action: {
let controller = PremiumIntroScreen(context: context, source: .animatedEmoji)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
controllerInteraction.navigationController()?.pushViewController(controller)
}
controllerInteraction.navigationController()?.pushViewController(controller)
/*let controller = PremiumIntroScreen(context: context, source: .stickers)
controllerInteraction.navigationController()?.pushViewController(controller)*/
}), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false })
strongSelf.currentUndoOverlayController = controller
controllerInteraction.presentController(controller, nil)
@ -1247,7 +1339,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
self.stickerInputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak controllerInteraction, weak interfaceInteraction] groupId, item, view, rect, layer in
let _ = (hasPremium |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in
let _ = (ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false) |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in
guard let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else {
return
}
@ -1401,6 +1493,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
let _ = ApplicationSpecificNotice.setDismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager, values: stickerPackIds).start()
})
} else if groupId == AnyHashable("peerSpecific") {
}
},
pushController: { [weak controllerInteraction] controller in
@ -2060,19 +2153,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV
let inputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak self] _, item, _, _, _ in
guard let strongSelf = self else {
return
}
let hasPremium = strongSelf.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId))
|> map { peer -> Bool in
guard case let .user(user) = peer else {
return false
}
return user.isPremium
}
|> distinctUntilChanged
let _ = (hasPremium |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in
let _ = (ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: nil, premiumIfSavedMessages: false) |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in
guard let strongSelf = self else {
return
}
@ -2168,7 +2249,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV
let semaphore = DispatchSemaphore(value: 0)
var emojiComponent: EmojiPagerContentComponent?
let _ = ChatEntityKeyboardInputNode.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, areCustomEmojiEnabled: areCustomEmojiEnabled).start(next: { value in
let _ = ChatEntityKeyboardInputNode.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil).start(next: { value in
emojiComponent = value
semaphore.signal()
})
@ -2183,7 +2264,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV
gifs: nil,
availableGifSearchEmojies: []
),
updatedInputData: ChatEntityKeyboardInputNode.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, areCustomEmojiEnabled: areCustomEmojiEnabled) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in
updatedInputData: ChatEntityKeyboardInputNode.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in
return ChatEntityKeyboardInputNode.InputData(
emoji: emojiComponent,
stickers: nil,

View File

@ -7,11 +7,17 @@ import TelegramCore
import AccountContext
import ChatPresentationInterfaceState
protocol ChatInputPanelViewForOverlayContent: UIView {
func maybeDismissContent(point: CGPoint)
}
class ChatInputPanelNode: ASDisplayNode {
var context: AccountContext?
var interfaceInteraction: ChatPanelInterfaceInteraction?
var prevInputPanelNode: ChatInputPanelNode?
var viewForOverlayContent: ChatInputPanelViewForOverlayContent?
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
}

View File

@ -115,14 +115,14 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) ->
return [(NSRange(location: 0, length: inputString.length - (string.count - trimmedString.count)), [.emoji], nil)]
}
} else {
let activeString = inputText.attributedSubstring(from: NSRange(location: 0, length: inputState.selectionRange.upperBound))
/*let activeString = inputText.attributedSubstring(from: NSRange(location: 0, length: inputState.selectionRange.upperBound))
if let lastCharacter = activeString.string.last, String(lastCharacter).isSingleEmoji {
let matchLength = (String(lastCharacter) as NSString).length
if activeString.attribute(ChatTextInputAttributes.customEmoji, at: activeString.length - matchLength, effectiveRange: nil) == nil {
return [(NSRange(location: inputState.selectionRange.upperBound - matchLength, length: matchLength), [.emojiSearch], nil)]
}
}
}*/
}
var possibleTypes = PossibleContextQueryTypes([.command, .mention, .hashtag, .emojiSearch])

View File

@ -628,14 +628,16 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode {
videoNode.isUserInteractionEnabled = false
videoNode.isHidden = true
videoNode.playbackCompleted = { [weak self] in
if let strongSelf = self {
strongSelf.videoLoopCount += 1
if strongSelf.videoLoopCount == maxVideoLoopCount {
if let videoNode = strongSelf.videoNode {
strongSelf.videoNode = nil
videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in
videoNode?.removeFromSupernode()
})
Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.videoLoopCount += 1
if strongSelf.videoLoopCount == maxVideoLoopCount {
if let videoNode = strongSelf.videoNode {
strongSelf.videoNode = nil
videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in
videoNode?.removeFromSupernode()
})
}
}
}
}

View File

@ -27,6 +27,8 @@ import EditableChatTextNode
import EmojiTextAttachmentView
import LottieAnimationComponent
import ComponentFlow
import EmojiSuggestionsComponent
import AudioToolbox
private let accessoryButtonFont = Font.medium(14.0)
private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers])
@ -417,6 +419,47 @@ enum ChatTextInputPanelPasteData {
case sticker(UIImage, Bool)
}
final class ChatTextViewForOverlayContent: UIView, ChatInputPanelViewForOverlayContent {
let ignoreHit: (UIView, CGPoint) -> Bool
let dismissSuggestions: () -> Void
init(ignoreHit: @escaping (UIView, CGPoint) -> Bool, dismissSuggestions: @escaping () -> Void) {
self.ignoreHit = ignoreHit
self.dismissSuggestions = dismissSuggestions
super.init(frame: CGRect())
}
required init(coder: NSCoder) {
preconditionFailure()
}
func maybeDismissContent(point: CGPoint) {
for subview in self.subviews.reversed() {
if let _ = subview.hitTest(self.convert(point, to: subview), with: nil) {
return
}
}
self.dismissSuggestions()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for subview in self.subviews.reversed() {
if let result = subview.hitTest(self.convert(point, to: subview), with: event) {
return result
}
}
if event == nil || self.ignoreHit(self, point) {
return nil
}
self.dismissSuggestions()
return nil
}
}
final class CustomEmojiContainerView: UIView {
private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView?
@ -706,8 +749,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
private let presentationContext: ChatPresentationContext?
init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) {
self.presentationInterfaceState = presentationInterfaceState
self.presentationContext = presentationContext
var hasSpoilers = true
if presentationInterfaceState.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat {
@ -770,6 +816,29 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
super.init()
self.viewForOverlayContent = ChatTextViewForOverlayContent(
ignoreHit: { [weak self] view, point in
guard let strongSelf = self else {
return false
}
if strongSelf.view.hitTest(view.convert(point, to: strongSelf.view), with: nil) != nil {
return true
}
if view.convert(point, to: strongSelf.view).y > strongSelf.view.bounds.maxY {
return true
}
return false
},
dismissSuggestions: { [weak self] in
guard let strongSelf = self, let currentEmojiSuggestion = strongSelf.currentEmojiSuggestion, let textInputNode = strongSelf.textInputNode else {
return
}
strongSelf.dismissedEmojiSuggestionPosition = currentEmojiSuggestion.position
strongSelf.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
}
)
self.context = context
self.addSubnode(self.clippingNode)
@ -1942,6 +2011,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
let textFieldFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - textInputViewInternalInsets.bottom))
let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size
transition.updateFrame(node: textInputNode, frame: textFieldFrame)
self.updateInputField(textInputFrame: textFieldFrame, transition: Transition(transition))
if shouldUpdateLayout {
textInputNode.layout()
}
@ -2370,6 +2440,220 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
}
}
private struct EmojiSuggestionPosition: Equatable {
var range: NSRange
var value: String
}
private final class CurrentEmojiSuggestion {
var localPosition: CGPoint
var position: EmojiSuggestionPosition
let disposable: MetaDisposable
var value: [TelegramMediaFile]?
init(localPosition: CGPoint, position: EmojiSuggestionPosition, disposable: MetaDisposable, value: [TelegramMediaFile]?) {
self.localPosition = localPosition
self.position = position
self.disposable = disposable
self.value = value
}
}
private var currentEmojiSuggestion: CurrentEmojiSuggestion?
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
private var dismissedEmojiSuggestionPosition: EmojiSuggestionPosition?
private func updateInputField(textInputFrame: CGRect, transition: Transition) {
guard let textInputNode = self.textInputNode, let context = self.context else {
return
}
var hasTracking = false
var hasTrackingView = false
if textInputNode.selectedRange.length == 0 && textInputNode.selectedRange.location > 0 {
let selectedSubstring = textInputNode.textView.attributedText.attributedSubstring(from: NSRange(location: 0, length: textInputNode.selectedRange.location))
if let lastCharacter = selectedSubstring.string.last, String(lastCharacter).isSingleEmoji {
let queryLength = (String(lastCharacter) as NSString).length
if selectedSubstring.attribute(ChatTextInputAttributes.customEmoji, at: selectedSubstring.length - queryLength, effectiveRange: nil) == nil {
let beginning = textInputNode.textView.beginningOfDocument
let characterRange = NSRange(location: selectedSubstring.length - queryLength, length: queryLength)
let start = textInputNode.textView.position(from: beginning, offset: selectedSubstring.length - queryLength)
let end = textInputNode.textView.position(from: beginning, offset: selectedSubstring.length)
if let start = start, let end = end, let textRange = textInputNode.textView.textRange(from: start, to: end) {
let selectionRects = textInputNode.textView.selectionRects(for: textRange)
let emojiSuggestionPosition = EmojiSuggestionPosition(range: characterRange, value: String(lastCharacter))
hasTracking = true
if let trackingRect = selectionRects.first?.rect {
let trackingPosition = CGPoint(x: trackingRect.midX, y: trackingRect.minY)
if self.dismissedEmojiSuggestionPosition == emojiSuggestionPosition {
} else {
hasTrackingView = true
var beginRequest = false
let suggestionContext: CurrentEmojiSuggestion
if let current = self.currentEmojiSuggestion, current.position.value == emojiSuggestionPosition.value {
suggestionContext = current
} else {
beginRequest = true
suggestionContext = CurrentEmojiSuggestion(localPosition: trackingPosition, position: emojiSuggestionPosition, disposable: MetaDisposable(), value: nil)
self.currentEmojiSuggestion = suggestionContext
}
suggestionContext.localPosition = trackingPosition
suggestionContext.position = emojiSuggestionPosition
self.dismissedEmojiSuggestionPosition = nil
if beginRequest {
suggestionContext.disposable.set((EmojiSuggestionsComponent.suggestionData(context: context, isSavedMessages: self.presentationInterfaceState?.chatLocation.peerId == self.context?.account.peerId, query: String(lastCharacter))
|> deliverOnMainQueue).start(next: { [weak self, weak suggestionContext] result in
guard let strongSelf = self, let suggestionContext = suggestionContext, strongSelf.currentEmojiSuggestion === suggestionContext else {
return
}
suggestionContext.value = result
if let textInputNode = strongSelf.textInputNode {
strongSelf.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
}
}))
}
}
}
}
}
}
}
if !hasTracking {
self.dismissedEmojiSuggestionPosition = nil
}
if let currentEmojiSuggestion = self.currentEmojiSuggestion, let value = currentEmojiSuggestion.value, value.isEmpty {
hasTrackingView = false
}
if !textInputNode.textView.isFirstResponder {
hasTrackingView = false
}
if !hasTrackingView {
if let currentEmojiSuggestion = self.currentEmojiSuggestion {
self.currentEmojiSuggestion = nil
currentEmojiSuggestion.disposable.dispose()
}
if let currentEmojiSuggestionView = self.currentEmojiSuggestionView {
self.currentEmojiSuggestionView = nil
currentEmojiSuggestionView.alpha = 0.0
currentEmojiSuggestionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, completion: { [weak currentEmojiSuggestionView] _ in
currentEmojiSuggestionView?.removeFromSuperview()
})
}
}
if let context = self.context, let theme = self.theme, let viewForOverlayContent = self.viewForOverlayContent, let presentationContext = self.presentationContext, let currentEmojiSuggestion = self.currentEmojiSuggestion, let value = currentEmojiSuggestion.value {
let currentEmojiSuggestionView: ComponentHostView<Empty>
if let current = self.currentEmojiSuggestionView {
currentEmojiSuggestionView = current
} else {
currentEmojiSuggestionView = ComponentHostView<Empty>()
self.currentEmojiSuggestionView = currentEmojiSuggestionView
viewForOverlayContent.addSubview(currentEmojiSuggestionView)
currentEmojiSuggestionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
let globalPosition = textInputNode.textView.convert(currentEmojiSuggestion.localPosition, to: self.view)
let sideInset: CGFloat = 16.0
let viewSize = currentEmojiSuggestionView.update(
transition: .immediate,
component: AnyComponent(EmojiSuggestionsComponent(
context: context,
theme: theme,
animationCache: presentationContext.animationCache,
animationRenderer: presentationContext.animationRenderer,
files: value,
action: { [weak self] file in
guard let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction, let currentEmojiSuggestion = strongSelf.currentEmojiSuggestion else {
return
}
AudioServicesPlaySystemSound(0x450)
interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in
let inputText = NSMutableAttributedString(attributedString: textInputState.inputText)
var text: String?
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
loop: for attribute in file.attributes {
switch attribute {
case let .CustomEmoji(_, displayText, packReference):
text = displayText
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(stickerPack: packReference, fileId: file.fileId.id, file: file)
break loop
default:
break
}
}
if let emojiAttribute = emojiAttribute, let text = text {
let replacementText = NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])
let range = currentEmojiSuggestion.position.range
let previousText = inputText.attributedSubstring(from: range)
inputText.replaceCharacters(in: range, with: replacementText)
var replacedUpperBound = range.lowerBound
while true {
if inputText.attributedSubstring(from: NSRange(location: 0, length: replacedUpperBound)).string.hasSuffix(previousText.string) {
let replaceRange = NSRange(location: replacedUpperBound - previousText.length, length: previousText.length)
if replaceRange.location < 0 {
break
}
if inputText.attributedSubstring(from: replaceRange).string != previousText.string {
break
}
inputText.replaceCharacters(in: replaceRange, with: NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: emojiAttribute.stickerPack, fileId: emojiAttribute.fileId, file: emojiAttribute.file)]))
replacedUpperBound = replaceRange.lowerBound
} else {
break
}
}
let selectionPosition = range.lowerBound + (replacementText.string as NSString).length
return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode)
}
return (textInputState, inputMode)
}
if let textInputNode = strongSelf.textInputNode {
strongSelf.dismissedEmojiSuggestionPosition = currentEmojiSuggestion.position
strongSelf.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
}
}
)),
environment: {},
containerSize: CGSize(width: self.bounds.width - sideInset * 2.0, height: 100.0)
)
let viewFrame = CGRect(origin: CGPoint(x: min(self.bounds.width - sideInset - viewSize.width, max(sideInset, floor(globalPosition.x - viewSize.width / 2.0))), y: globalPosition.y - 2.0 - viewSize.height), size: viewSize)
currentEmojiSuggestionView.frame = viewFrame
if let componentView = currentEmojiSuggestionView.componentView as? EmojiSuggestionsComponent.View {
componentView.adjustBackground(relativePositionX: floor(globalPosition.x - viewFrame.minX))
}
}
}
private func updateCounterTextNode(transition: ContainedViewLayoutTransition) {
if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let editMessage = presentationInterfaceState.interfaceState.editMessage, let inputTextMaxLength = editMessage.inputTextMaxLength {
let textCount = Int32(textInputNode.textView.text.count)
@ -2580,6 +2864,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics)
if !self.bounds.size.height.isEqual(to: panelHeight) {
self.updateHeight(animated)
} else {
if let textInputNode = self.textInputNode {
self.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
}
}
}
}
@ -2644,6 +2932,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize)
self.updateSpoilersRevealed()
self.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
}
}
@ -2671,6 +2961,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
self.storedInputLanguage = editableTextNode.textInputMode.primaryLanguage
self.inputMenu.deactivate()
self.dismissedEmojiSuggestionPosition = nil
if let presentationInterfaceState = self.presentationInterfaceState {
if let peer = presentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo != nil, presentationInterfaceState.keyboardButtonsMessage != nil {
@ -3128,6 +3419,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
return result
}
}
if self.bounds.contains(point), let textInputNode = self.textInputNode, let currentEmojiSuggestion = self.currentEmojiSuggestion, let currentEmojiSuggestionView = self.currentEmojiSuggestionView {
if let result = currentEmojiSuggestionView.hitTest(self.view.convert(point, to: currentEmojiSuggestionView), with: event) {
return result
}
self.dismissedEmojiSuggestionPosition = currentEmojiSuggestion.position
self.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate)
}
let result = super.hitTest(point, with: event)
return result
}

View File

@ -20,8 +20,8 @@ private enum EmojisChatInputContextPanelEntryStableId: Hashable, Equatable {
}
private func backgroundCenterImage(_ theme: PresentationTheme) -> UIImage? {
return generateImage(CGSize(width: 8.0, height: 16.0), rotatedContext: { size, context in
/*context.clear(CGRect(origin: CGPoint(), size: size))
return generateImage(CGSize(width: 30.0, height: 55.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(theme.list.itemPlainSeparatorColor.cgColor)
context.setFillColor(theme.list.plainBackgroundColor.cgColor)
let lineWidth = UIScreenPixel
@ -37,17 +37,8 @@ private func backgroundCenterImage(_ theme: PresentationTheme) -> UIImage? {
context.translateBy(x: -460.5, y: -lineWidth / 2.0 - 364.0 + 27.0)
context.move(to: CGPoint(x: 0.0, y: lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width, y: lineWidth / 2.0))
context.strokePath()*/
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(theme.list.itemPlainSeparatorColor.cgColor)
context.setFillColor(theme.list.plainBackgroundColor.cgColor)
let lineWidth = UIScreenPixel
context.setLineWidth(lineWidth)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.height, height: size.height)))
context.stroke(CGRect(origin: CGPoint(x: -lineWidth / 2.0, y: lineWidth / 2.0), size: CGSize(width: size.height + lineWidth, height: size.height - lineWidth)))
})?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 8)
context.strokePath()
})
}
private func backgroundLeftImage(_ theme: PresentationTheme) -> UIImage? {
@ -201,10 +192,8 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode {
inner: for (range, type, _) in textInputStateContextQueryRangeAndType(textInputState) {
if type == [.emojiSearch] {
var range = range
if textInputState.inputText.attributedSubstring(from: range).string.hasPrefix(":") {
range.location -= 1
range.length += 1
}
range.location -= 1
range.length += 1
hashtagQueryRange = range
break inner
}
@ -278,7 +267,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode {
self.presentationInterfaceState = interfaceState
let sideInsets: CGFloat = 10.0 + leftInset
let contentWidth = min(size.width - sideInsets - sideInsets, max(24.0, CGFloat(self.currentEntries?.count ?? 0) * 45.0 + 5.0))
let contentWidth = min(size.width - sideInsets - sideInsets, max(24.0, CGFloat(self.currentEntries?.count ?? 0) * 45.0))
var contentLeftInset: CGFloat = 40.0
var leftOffset: CGFloat = 0.0
@ -290,7 +279,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode {
let backgroundFrame = CGRect(origin: CGPoint(x: sideInsets + leftOffset, y: size.height - 55.0 + 4.0), size: CGSize(width: contentWidth, height: 55.0))
let backgroundLeftFrame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: contentLeftInset, height: backgroundFrame.size.height - 10.0 + UIScreenPixel))
let backgroundCenterFrame = CGRect(origin: CGPoint(x: backgroundLeftFrame.maxX, y: backgroundFrame.minY), size: CGSize(width: 30.0, height: backgroundFrame.size.height - 10.0 + UIScreenPixel))
let backgroundCenterFrame = CGRect(origin: CGPoint(x: backgroundLeftFrame.maxX, y: backgroundFrame.minY), size: CGSize(width: 30.0, height: 55.0))
let backgroundRightFrame = CGRect(origin: CGPoint(x: backgroundCenterFrame.maxX, y: backgroundFrame.minY), size: CGSize(width: max(0.0, backgroundFrame.minX + backgroundFrame.size.width - backgroundCenterFrame.maxX), height: backgroundFrame.size.height - 10.0 + UIScreenPixel))
transition.updateFrame(node: self.backgroundLeftNode, frame: backgroundLeftFrame)
transition.updateFrame(node: self.backgroundNode, frame: backgroundCenterFrame)

View File

@ -54,8 +54,8 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
}
controller.purposefulAction = params.purposefulAction
if params.activateInput {
controller.activateInput()
if let activateInput = params.activateInput {
controller.activateInput(type: activateInput)
}
if params.changeColors {
controller.presentThemeSelection()
@ -138,8 +138,8 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
}
}
}
if params.activateInput {
controller.activateInput()
if let activateInput = params.activateInput {
controller.activateInput(type: activateInput)
}
if params.changeColors {
Queue.mainQueue().after(0.1) {

View File

@ -5749,8 +5749,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: title),
ActionSheetButtonItem(title: text, color: .destructive, action: {
ActionSheetTextItem(title: text),
ActionSheetButtonItem(title: title, color: .destructive, action: {
dismissAction()
self?.deletePeerChat(peer: peer._asPeer(), globally: true)
}),
@ -7077,7 +7077,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
subject: .message(id: .id(index.id), highlight: false, timecode: nil),
botStart: nil,
updateTextInputState: nil,
activateInput: false,
keepStack: .never,
useExisting: true,
purposefulAction: nil,

View File

@ -12,6 +12,12 @@ import PhotoResources
import UIKitRuntimeUtils
import RangeSet
private extension CGRect {
var center: CGPoint {
return CGPoint(x: self.midX, y: self.midY)
}
}
public enum NativeVideoContentId: Hashable {
case message(UInt32, MediaId)
case instantPage(MediaId, MediaId)

View File

@ -6,6 +6,12 @@ import Display
import TelegramPresentationData
import TextFormat
private extension CGRect {
var center: CGPoint {
return CGPoint(x: self.midX, y: self.midY)
}
}
private func findScrollView(view: UIView?) -> UIScrollView? {
if let view = view {
if let view = view as? UIScrollView {

@ -1 +1 @@
Subproject commit c2620d42ee6cff0c9fb078c1a6956c5c7fb6448e
Subproject commit c8f6a173253e88fc77ef4ceece919a786023108e