mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
b87e3e0b1a
@ -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";
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
|
@ -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?
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
@ -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?
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
}
|
||||
|
||||
|
@ -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])
|
||||
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user