mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
Story caption improvements
This commit is contained in:
parent
3cb9b21c72
commit
09e2e5bdc2
@ -214,6 +214,8 @@ private final class CameraContext {
|
||||
|
||||
func stopCapture(invalidate: Bool = false) {
|
||||
if invalidate {
|
||||
self.setZoomLevel(1.0)
|
||||
|
||||
self.configure {
|
||||
self.mainDeviceContext.invalidate()
|
||||
}
|
||||
|
21
submodules/ChatContextQuery/BUILD
Normal file
21
submodules/ChatContextQuery/BUILD
Normal file
@ -0,0 +1,21 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatContextQuery",
|
||||
module_name = "ChatContextQuery",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
241
submodules/ChatContextQuery/Sources/ChatContextQuery.swift
Normal file
241
submodules/ChatContextQuery/Sources/ChatContextQuery.swift
Normal file
@ -0,0 +1,241 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import TextFormat
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
|
||||
public struct PossibleContextQueryTypes: OptionSet {
|
||||
public var rawValue: Int32
|
||||
|
||||
public init() {
|
||||
self.rawValue = 0
|
||||
}
|
||||
|
||||
public init(rawValue: Int32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public static let emoji = PossibleContextQueryTypes(rawValue: (1 << 0))
|
||||
public static let hashtag = PossibleContextQueryTypes(rawValue: (1 << 1))
|
||||
public static let mention = PossibleContextQueryTypes(rawValue: (1 << 2))
|
||||
public static let command = PossibleContextQueryTypes(rawValue: (1 << 3))
|
||||
public static let contextRequest = PossibleContextQueryTypes(rawValue: (1 << 4))
|
||||
public static let emojiSearch = PossibleContextQueryTypes(rawValue: (1 << 5))
|
||||
}
|
||||
|
||||
private func scalarCanPrependQueryControl(_ c: UnicodeScalar?) -> Bool {
|
||||
if let c = c {
|
||||
if c == " " || c == "\n" || c == "." || c == "," {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private func makeScalar(_ c: Character) -> Character {
|
||||
return c
|
||||
}
|
||||
|
||||
private let spaceScalar = " " as UnicodeScalar
|
||||
private let newlineScalar = "\n" as UnicodeScalar
|
||||
private let hashScalar = "#" as UnicodeScalar
|
||||
private let atScalar = "@" as UnicodeScalar
|
||||
private let slashScalar = "/" as UnicodeScalar
|
||||
private let colonScalar = ":" as UnicodeScalar
|
||||
private let alphanumerics = CharacterSet.alphanumerics
|
||||
|
||||
public func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> [(NSRange, PossibleContextQueryTypes, NSRange?)] {
|
||||
return textInputStateContextQueryRangeAndType(inputText: inputState.inputText, selectionRange: inputState.selectionRange)
|
||||
}
|
||||
|
||||
public func textInputStateContextQueryRangeAndType(inputText: NSAttributedString, selectionRange: Range<Int>) -> [(NSRange, PossibleContextQueryTypes, NSRange?)] {
|
||||
if selectionRange.count != 0 {
|
||||
return []
|
||||
}
|
||||
|
||||
let inputString: NSString = inputText.string as NSString
|
||||
var results: [(NSRange, PossibleContextQueryTypes, NSRange?)] = []
|
||||
let inputLength = inputString.length
|
||||
|
||||
if inputLength != 0 {
|
||||
if inputString.hasPrefix("@") && inputLength != 1 {
|
||||
let startIndex = 1
|
||||
var index = startIndex
|
||||
var contextAddressRange: NSRange?
|
||||
|
||||
while true {
|
||||
if index == inputLength {
|
||||
break
|
||||
}
|
||||
if let c = UnicodeScalar(inputString.character(at: index)) {
|
||||
if c == " " {
|
||||
if index != startIndex {
|
||||
contextAddressRange = NSRange(location: startIndex, length: index - startIndex)
|
||||
index += 1
|
||||
}
|
||||
break
|
||||
} else {
|
||||
if !((c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || (c >= "0" && c <= "9") || c == "_") {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if index == inputLength {
|
||||
break
|
||||
} else {
|
||||
index += 1
|
||||
}
|
||||
} else {
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
if let contextAddressRange = contextAddressRange {
|
||||
results.append((contextAddressRange, [.contextRequest], NSRange(location: index, length: inputLength - index)))
|
||||
}
|
||||
}
|
||||
|
||||
let maxIndex = min(selectionRange.lowerBound, inputLength)
|
||||
if maxIndex == 0 {
|
||||
return results
|
||||
}
|
||||
var index = maxIndex - 1
|
||||
|
||||
var possibleQueryRange: NSRange?
|
||||
|
||||
let string = (inputString as String)
|
||||
let trimmedString = string.trimmingTrailingSpaces()
|
||||
if string.count < 3, trimmedString.isSingleEmoji {
|
||||
if inputText.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) == nil {
|
||||
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))
|
||||
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])
|
||||
var definedType = false
|
||||
|
||||
while true {
|
||||
var previousC: UnicodeScalar?
|
||||
if index != 0 {
|
||||
previousC = UnicodeScalar(inputString.character(at: index - 1))
|
||||
}
|
||||
if let c = UnicodeScalar(inputString.character(at: index)) {
|
||||
if c == spaceScalar || c == newlineScalar {
|
||||
possibleTypes = []
|
||||
} else if c == hashScalar {
|
||||
if scalarCanPrependQueryControl(previousC) {
|
||||
possibleTypes = possibleTypes.intersection([.hashtag])
|
||||
definedType = true
|
||||
index += 1
|
||||
possibleQueryRange = NSRange(location: index, length: maxIndex - index)
|
||||
}
|
||||
break
|
||||
} else if c == atScalar {
|
||||
if scalarCanPrependQueryControl(previousC) {
|
||||
possibleTypes = possibleTypes.intersection([.mention])
|
||||
definedType = true
|
||||
index += 1
|
||||
possibleQueryRange = NSRange(location: index, length: maxIndex - index)
|
||||
}
|
||||
break
|
||||
} else if c == slashScalar {
|
||||
if scalarCanPrependQueryControl(previousC) {
|
||||
possibleTypes = possibleTypes.intersection([.command])
|
||||
definedType = true
|
||||
index += 1
|
||||
possibleQueryRange = NSRange(location: index, length: maxIndex - index)
|
||||
}
|
||||
break
|
||||
} else if c == colonScalar {
|
||||
if scalarCanPrependQueryControl(previousC) {
|
||||
possibleTypes = possibleTypes.intersection([.emojiSearch])
|
||||
definedType = true
|
||||
index += 1
|
||||
possibleQueryRange = NSRange(location: index, length: maxIndex - index)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if index == 0 {
|
||||
break
|
||||
} else {
|
||||
index -= 1
|
||||
possibleQueryRange = NSRange(location: index, length: maxIndex - index)
|
||||
}
|
||||
}
|
||||
|
||||
if let possibleQueryRange = possibleQueryRange, definedType && !possibleTypes.isEmpty {
|
||||
results.append((possibleQueryRange, possibleTypes, nil))
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
public enum ChatPresentationInputQueryKind: Int32 {
|
||||
case emoji
|
||||
case hashtag
|
||||
case mention
|
||||
case command
|
||||
case contextRequest
|
||||
case emojiSearch
|
||||
}
|
||||
|
||||
public struct ChatInputQueryMentionTypes: OptionSet, Hashable {
|
||||
public var rawValue: Int32
|
||||
|
||||
public init(rawValue: Int32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public static let contextBots = ChatInputQueryMentionTypes(rawValue: 1 << 0)
|
||||
public static let members = ChatInputQueryMentionTypes(rawValue: 1 << 1)
|
||||
public static let accountPeer = ChatInputQueryMentionTypes(rawValue: 1 << 2)
|
||||
}
|
||||
|
||||
public enum ChatPresentationInputQuery: Hashable, Equatable {
|
||||
case emoji(String)
|
||||
case hashtag(String)
|
||||
case mention(query: String, types: ChatInputQueryMentionTypes)
|
||||
case command(String)
|
||||
case emojiSearch(query: String, languageCode: String, range: NSRange)
|
||||
case contextRequest(addressName: String, query: String)
|
||||
|
||||
public var kind: ChatPresentationInputQueryKind {
|
||||
switch self {
|
||||
case .emoji:
|
||||
return .emoji
|
||||
case .hashtag:
|
||||
return .hashtag
|
||||
case .mention:
|
||||
return .mention
|
||||
case .command:
|
||||
return .command
|
||||
case .contextRequest:
|
||||
return .contextRequest
|
||||
case .emojiSearch:
|
||||
return .emojiSearch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChatContextQueryError {
|
||||
case generic
|
||||
case inlineBotLocationRequest(EnginePeer.Id)
|
||||
}
|
||||
|
||||
public enum ChatContextQueryUpdate {
|
||||
case remove
|
||||
case update(ChatPresentationInputQuery, Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError>)
|
||||
}
|
@ -19,6 +19,7 @@ swift_library(
|
||||
"//submodules/ChatInterfaceState:ChatInterfaceState",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/ChatContextQuery",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -6,6 +6,7 @@ import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import ChatInterfaceState
|
||||
import ChatContextQuery
|
||||
|
||||
public extension ChatLocation {
|
||||
var peerId: PeerId? {
|
||||
@ -31,53 +32,6 @@ public extension ChatLocation {
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChatPresentationInputQueryKind: Int32 {
|
||||
case emoji
|
||||
case hashtag
|
||||
case mention
|
||||
case command
|
||||
case contextRequest
|
||||
case emojiSearch
|
||||
}
|
||||
|
||||
public struct ChatInputQueryMentionTypes: OptionSet, Hashable {
|
||||
public var rawValue: Int32
|
||||
|
||||
public init(rawValue: Int32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public static let contextBots = ChatInputQueryMentionTypes(rawValue: 1 << 0)
|
||||
public static let members = ChatInputQueryMentionTypes(rawValue: 1 << 1)
|
||||
public static let accountPeer = ChatInputQueryMentionTypes(rawValue: 1 << 2)
|
||||
}
|
||||
|
||||
public enum ChatPresentationInputQuery: Hashable, Equatable {
|
||||
case emoji(String)
|
||||
case hashtag(String)
|
||||
case mention(query: String, types: ChatInputQueryMentionTypes)
|
||||
case command(String)
|
||||
case emojiSearch(query: String, languageCode: String, range: NSRange)
|
||||
case contextRequest(addressName: String, query: String)
|
||||
|
||||
public var kind: ChatPresentationInputQueryKind {
|
||||
switch self {
|
||||
case .emoji:
|
||||
return .emoji
|
||||
case .hashtag:
|
||||
return .hashtag
|
||||
case .mention:
|
||||
return .mention
|
||||
case .command:
|
||||
return .command
|
||||
case .contextRequest:
|
||||
return .contextRequest
|
||||
case .emojiSearch:
|
||||
return .emojiSearch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChatMediaInputMode {
|
||||
case gif
|
||||
case other
|
||||
|
@ -14,7 +14,7 @@ open class PagerExternalTopPanelContainer: SparseContainerView {
|
||||
}
|
||||
|
||||
public protocol PagerContentViewWithBackground: UIView {
|
||||
func pagerUpdateBackground(backgroundFrame: CGRect, transition: Transition)
|
||||
func pagerUpdateBackground(backgroundFrame: CGRect, topPanelHeight: CGFloat, transition: Transition)
|
||||
}
|
||||
|
||||
public final class PagerComponentChildEnvironment: Equatable {
|
||||
@ -904,7 +904,7 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
|
||||
}
|
||||
|
||||
if let contentViewWithBackground = contentView.view.componentView as? PagerContentViewWithBackground {
|
||||
contentViewWithBackground.pagerUpdateBackground(backgroundFrame: backgroundFrame, transition: contentTransition)
|
||||
contentViewWithBackground.pagerUpdateBackground(backgroundFrame: backgroundFrame, topPanelHeight: topPanelHeight, transition: contentTransition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -758,6 +758,48 @@ public class ContactsController: ViewController {
|
||||
let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ContactsTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
|
||||
self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
|
||||
}
|
||||
|
||||
private var storyCameraTransitionInCoordinator: StoryCameraTransitionInCoordinator?
|
||||
var hasStoryCameraTransition: Bool {
|
||||
return self.storyCameraTransitionInCoordinator != nil
|
||||
}
|
||||
func storyCameraPanGestureChanged(transitionFraction: CGFloat) {
|
||||
guard let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface else {
|
||||
return
|
||||
}
|
||||
|
||||
let coordinator: StoryCameraTransitionInCoordinator?
|
||||
if let current = self.storyCameraTransitionInCoordinator {
|
||||
coordinator = current
|
||||
} else {
|
||||
coordinator = rootController.openStoryCamera(transitionIn: nil, transitionedIn: {}, transitionOut: { [weak self] finished in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let _ = self
|
||||
// if finished, let componentView = self.chatListHeaderView() {
|
||||
// if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
|
||||
// return StoryCameraTransitionOut(
|
||||
// destinationView: transitionView,
|
||||
// destinationRect: transitionView.bounds,
|
||||
// destinationCornerRadius: transitionView.bounds.height * 0.5
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
return nil
|
||||
})
|
||||
self.storyCameraTransitionInCoordinator = coordinator
|
||||
}
|
||||
coordinator?.updateTransitionProgress(transitionFraction)
|
||||
}
|
||||
|
||||
func storyCameraPanGestureEnded(transitionFraction: CGFloat, velocity: CGFloat) {
|
||||
if let coordinator = self.storyCameraTransitionInCoordinator {
|
||||
coordinator.completeWithTransitionProgressAndVelocity(transitionFraction, velocity)
|
||||
self.storyCameraTransitionInCoordinator = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ContactsTabBarContextExtractedContentSource: ContextExtractedContentSource {
|
||||
|
@ -44,7 +44,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
|
||||
}
|
||||
}
|
||||
|
||||
final class ContactsControllerNode: ASDisplayNode {
|
||||
final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
let contactListNode: ContactListNode
|
||||
|
||||
private let context: AccountContext
|
||||
@ -82,6 +82,8 @@ final class ContactsControllerNode: ASDisplayNode {
|
||||
|
||||
let storiesReady = Promise<Bool>()
|
||||
|
||||
private var panRecognizer: InteractiveTransitionGestureRecognizer?
|
||||
|
||||
init(context: AccountContext, sortOrder: Signal<ContactsSortOrder, NoError>, present: @escaping (ViewController, Any?) -> Void, controller: ContactsController) {
|
||||
self.context = context
|
||||
self.controller = controller
|
||||
@ -263,6 +265,34 @@ final class ContactsControllerNode: ASDisplayNode {
|
||||
self.storySubscriptionsDisposable?.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { _ in
|
||||
let directions: InteractiveTransitionGestureRecognizerDirections = [.rightCenter, .rightEdge]
|
||||
return directions
|
||||
}, edgeWidth: .widthMultiplier(factor: 1.0 / 6.0, min: 22.0, max: 80.0))
|
||||
panRecognizer.delegate = self
|
||||
panRecognizer.delaysTouchesBegan = false
|
||||
panRecognizer.cancelsTouchesInView = true
|
||||
self.panRecognizer = panRecognizer
|
||||
self.view.addGestureRecognizer(panRecognizer)
|
||||
}
|
||||
|
||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
|
||||
return false
|
||||
}
|
||||
if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func updateThemeAndStrings() {
|
||||
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
|
||||
self.searchDisplayController?.updatePresentationData(self.presentationData)
|
||||
@ -512,4 +542,36 @@ final class ContactsControllerNode: ASDisplayNode {
|
||||
placeholderNode.frame = previousFrame
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
guard let (layout, _) = self.containerLayout else {
|
||||
return
|
||||
}
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
break
|
||||
case .changed:
|
||||
let translation = recognizer.translation(in: self.view)
|
||||
if case .compact = layout.metrics.widthClass {
|
||||
let cameraIsAlreadyOpened = self.controller?.hasStoryCameraTransition ?? false
|
||||
if translation.x > 0.0 {
|
||||
self.controller?.storyCameraPanGestureChanged(transitionFraction: translation.x / layout.size.width)
|
||||
} else if translation.x <= 0.0 && cameraIsAlreadyOpened {
|
||||
self.controller?.storyCameraPanGestureChanged(transitionFraction: 0.0)
|
||||
}
|
||||
if cameraIsAlreadyOpened {
|
||||
return
|
||||
}
|
||||
}
|
||||
case .cancelled, .ended:
|
||||
let translation = recognizer.translation(in: self.view)
|
||||
let velocity = recognizer.velocity(in: self.view)
|
||||
let hasStoryCameraTransition = self.controller?.hasStoryCameraTransition ?? false
|
||||
if hasStoryCameraTransition {
|
||||
self.controller?.storyCameraPanGestureEnded(transitionFraction: translation.x / layout.size.width, velocity: velocity.x)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -160,6 +160,6 @@ public extension ContainerViewLayout {
|
||||
}
|
||||
|
||||
var standardInputHeight: CGFloat {
|
||||
return self.deviceMetrics.keyboardHeight(inLandscape: self.orientation == .landscape) + self.deviceMetrics.predictiveInputHeight(inLandscape: self.orientation == .landscape)
|
||||
return self.deviceMetrics.standardInputHeight(inLandscape: self.orientation == .landscape)
|
||||
}
|
||||
}
|
||||
|
@ -266,7 +266,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
|
||||
return 20.0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func keyboardHeight(inLandscape: Bool) -> CGFloat {
|
||||
if inLandscape {
|
||||
switch self {
|
||||
@ -337,6 +337,10 @@ public enum DeviceMetrics: CaseIterable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public func standardInputHeight(inLandscape: Bool) -> CGFloat {
|
||||
return self.keyboardHeight(inLandscape: inLandscape) + predictiveInputHeight(inLandscape: inLandscape)
|
||||
}
|
||||
|
||||
public var hasTopNotch: Bool {
|
||||
switch self {
|
||||
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax:
|
||||
|
@ -582,7 +582,7 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
}
|
||||
}
|
||||
|
||||
private let snapTimeout = 2.0
|
||||
private let snapTimeout = 1.0
|
||||
|
||||
class DrawingEntitySnapTool {
|
||||
enum SnapType {
|
||||
|
@ -31,7 +31,6 @@ swift_library(
|
||||
"//submodules/StickerResources:StickerResources",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||
"//submodules/TelegramUI/Components/ChatControllerInteraction:ChatControllerInteraction",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -12,7 +12,6 @@ import AccountContext
|
||||
import StickerPackPreviewUI
|
||||
import PresentationDataUtils
|
||||
import UndoUI
|
||||
import ChatControllerInteraction
|
||||
|
||||
public final class TrendingPaneInteraction {
|
||||
public let installPack: (ItemCollectionInfo) -> Void
|
||||
@ -192,8 +191,24 @@ private func trendingPaneEntries(trendingEntries: [FeaturedStickerPackItem], ins
|
||||
}
|
||||
|
||||
public final class ChatMediaInputTrendingPane: ChatMediaInputPane {
|
||||
public final class Interaction {
|
||||
let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool
|
||||
let presentController: (ViewController, Any?) -> Void
|
||||
let getNavigationController: () -> NavigationController?
|
||||
|
||||
public init(
|
||||
sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool,
|
||||
presentController: @escaping (ViewController, Any?) -> Void,
|
||||
getNavigationController: @escaping () -> NavigationController?
|
||||
) {
|
||||
self.sendSticker = sendSticker
|
||||
self.presentController = presentController
|
||||
self.getNavigationController = getNavigationController
|
||||
}
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let controllerInteraction: ChatControllerInteraction
|
||||
private let interaction: ChatMediaInputTrendingPane.Interaction
|
||||
private let getItemIsPreviewed: (StickerPackItem) -> Bool
|
||||
private let isPane: Bool
|
||||
|
||||
@ -215,9 +230,9 @@ public final class ChatMediaInputTrendingPane: ChatMediaInputPane {
|
||||
|
||||
private let installDisposable = MetaDisposable()
|
||||
|
||||
public init(context: AccountContext, controllerInteraction: ChatControllerInteraction, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, isPane: Bool) {
|
||||
public init(context: AccountContext, interaction: ChatMediaInputTrendingPane.Interaction, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, isPane: Bool) {
|
||||
self.context = context
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.interaction = interaction
|
||||
self.getItemIsPreviewed = getItemIsPreviewed
|
||||
self.isPane = isPane
|
||||
|
||||
@ -279,7 +294,7 @@ public final class ChatMediaInputTrendingPane: ChatMediaInputPane {
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
cancelImpl?()
|
||||
}))
|
||||
self?.controllerInteraction.presentController(controller, nil)
|
||||
self?.interaction.presentController(controller, nil)
|
||||
return ActionDisposable { [weak controller] in
|
||||
Queue.mainQueue().async() {
|
||||
controller?.dismiss()
|
||||
@ -306,7 +321,7 @@ public final class ChatMediaInputTrendingPane: ChatMediaInputPane {
|
||||
}
|
||||
|
||||
var animateInAsReplacement = false
|
||||
if let navigationController = strongSelf.controllerInteraction.navigationController() {
|
||||
if let navigationController = strongSelf.interaction.getNavigationController() {
|
||||
for controller in navigationController.overlayControllers {
|
||||
if let controller = controller as? UndoOverlayController {
|
||||
controller.dismissWithCommitActionAndReplacementAnimation()
|
||||
@ -316,7 +331,7 @@ public final class ChatMediaInputTrendingPane: ChatMediaInputPane {
|
||||
}
|
||||
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
strongSelf.controllerInteraction.navigationController()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: strongSelf.context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in
|
||||
strongSelf.interaction.getNavigationController()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: strongSelf.context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in
|
||||
return true
|
||||
}))
|
||||
}))
|
||||
@ -325,14 +340,14 @@ public final class ChatMediaInputTrendingPane: ChatMediaInputPane {
|
||||
if let strongSelf = self, let info = info as? StickerPackCollectionInfo {
|
||||
strongSelf.view.window?.endEditing(true)
|
||||
let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash)
|
||||
let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { fileReference, sourceNode, sourceRect in
|
||||
let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.interaction.getNavigationController(), sendSticker: { fileReference, sourceNode, sourceRect in
|
||||
if let strongSelf = self {
|
||||
return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, [])
|
||||
return strongSelf.interaction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, [])
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
strongSelf.controllerInteraction.presentController(controller, nil)
|
||||
strongSelf.interaction.presentController(controller, nil)
|
||||
}
|
||||
}, getItemIsPreviewed: self.getItemIsPreviewed,
|
||||
openSearch: { [weak self] in
|
||||
|
@ -47,6 +47,322 @@ func generateMaskImage(size originalSize: CGSize, position: CGPoint, inverse: Bo
|
||||
})
|
||||
}
|
||||
|
||||
public class InvisibleInkDustView: UIView {
|
||||
private var currentParams: (size: CGSize, color: UIColor, textColor: UIColor, rects: [CGRect], wordRects: [CGRect])?
|
||||
private var animColor: CGColor?
|
||||
private let enableAnimations: Bool
|
||||
|
||||
private weak var textNode: TextNode?
|
||||
private let textMaskNode: ASDisplayNode
|
||||
private let textSpotNode: ASImageNode
|
||||
|
||||
private var emitterNode: ASDisplayNode
|
||||
private var emitter: CAEmitterCell?
|
||||
private var emitterLayer: CAEmitterLayer?
|
||||
private let emitterMaskNode: ASDisplayNode
|
||||
private let emitterSpotNode: ASImageNode
|
||||
private let emitterMaskFillNode: ASDisplayNode
|
||||
|
||||
private var staticNode: ASImageNode?
|
||||
private var staticParams: (size: CGSize, color: UIColor, rects: [CGRect])?
|
||||
|
||||
public var isRevealed = false
|
||||
private var isExploding = false
|
||||
|
||||
public init(textNode: TextNode?, enableAnimations: Bool) {
|
||||
self.textNode = textNode
|
||||
self.enableAnimations = enableAnimations
|
||||
|
||||
self.emitterNode = ASDisplayNode()
|
||||
self.emitterNode.isUserInteractionEnabled = false
|
||||
self.emitterNode.clipsToBounds = true
|
||||
|
||||
self.textMaskNode = ASDisplayNode()
|
||||
self.textMaskNode.isUserInteractionEnabled = false
|
||||
self.textSpotNode = ASImageNode()
|
||||
self.textSpotNode.contentMode = .scaleToFill
|
||||
self.textSpotNode.isUserInteractionEnabled = false
|
||||
|
||||
self.emitterMaskNode = ASDisplayNode()
|
||||
self.emitterSpotNode = ASImageNode()
|
||||
self.emitterSpotNode.contentMode = .scaleToFill
|
||||
self.emitterSpotNode.isUserInteractionEnabled = false
|
||||
|
||||
self.emitterMaskFillNode = ASDisplayNode()
|
||||
self.emitterMaskFillNode.backgroundColor = .white
|
||||
self.emitterMaskFillNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.addSubnode(self.emitterNode)
|
||||
|
||||
self.textMaskNode.addSubnode(self.textSpotNode)
|
||||
self.emitterMaskNode.addSubnode(self.emitterSpotNode)
|
||||
self.emitterMaskNode.addSubnode(self.emitterMaskFillNode)
|
||||
|
||||
self.didLoad()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func didLoad() {
|
||||
if self.enableAnimations {
|
||||
let emitter = CAEmitterCell()
|
||||
emitter.contents = UIImage(bundleImageName: "Components/TextSpeckle")?.cgImage
|
||||
emitter.contentsScale = 1.8
|
||||
emitter.emissionRange = .pi * 2.0
|
||||
emitter.lifetime = 1.0
|
||||
emitter.scale = 0.5
|
||||
emitter.velocityRange = 20.0
|
||||
emitter.name = "dustCell"
|
||||
emitter.alphaRange = 1.0
|
||||
emitter.setValue("point", forKey: "particleType")
|
||||
emitter.setValue(3.0, forKey: "mass")
|
||||
emitter.setValue(2.0, forKey: "massRange")
|
||||
self.emitter = emitter
|
||||
|
||||
let fingerAttractor = createEmitterBehavior(type: "simpleAttractor")
|
||||
fingerAttractor.setValue("fingerAttractor", forKey: "name")
|
||||
|
||||
let alphaBehavior = createEmitterBehavior(type: "valueOverLife")
|
||||
alphaBehavior.setValue("color.alpha", forKey: "keyPath")
|
||||
alphaBehavior.setValue([0.0, 0.0, 1.0, 0.0, -1.0], forKey: "values")
|
||||
alphaBehavior.setValue(true, forKey: "additive")
|
||||
|
||||
let behaviors = [fingerAttractor, alphaBehavior]
|
||||
|
||||
let emitterLayer = CAEmitterLayer()
|
||||
emitterLayer.masksToBounds = true
|
||||
emitterLayer.allowsGroupOpacity = true
|
||||
emitterLayer.lifetime = 1
|
||||
emitterLayer.emitterCells = [emitter]
|
||||
emitterLayer.emitterPosition = CGPoint(x: 0, y: 0)
|
||||
emitterLayer.seed = arc4random()
|
||||
emitterLayer.emitterSize = CGSize(width: 1, height: 1)
|
||||
emitterLayer.emitterShape = CAEmitterLayerEmitterShape(rawValue: "rectangles")
|
||||
emitterLayer.setValue(behaviors, forKey: "emitterBehaviors")
|
||||
|
||||
emitterLayer.setValue(4.0, forKeyPath: "emitterBehaviors.fingerAttractor.stiffness")
|
||||
emitterLayer.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
|
||||
|
||||
self.emitterLayer = emitterLayer
|
||||
|
||||
self.emitterNode.layer.addSublayer(emitterLayer)
|
||||
} else {
|
||||
let staticNode = ASImageNode()
|
||||
self.staticNode = staticNode
|
||||
self.addSubnode(staticNode)
|
||||
}
|
||||
|
||||
self.updateEmitter()
|
||||
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap(_:))))
|
||||
}
|
||||
|
||||
public func update(revealed: Bool, animated: Bool = true) {
|
||||
guard self.isRevealed != revealed, let textNode = self.textNode else {
|
||||
return
|
||||
}
|
||||
|
||||
self.isRevealed = revealed
|
||||
|
||||
if revealed {
|
||||
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .linear) : .immediate
|
||||
transition.updateAlpha(layer: self.layer, alpha: 0.0)
|
||||
transition.updateAlpha(node: textNode, alpha: 1.0)
|
||||
} else {
|
||||
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .linear) : .immediate
|
||||
transition.updateAlpha(layer: self.layer, alpha: 1.0)
|
||||
transition.updateAlpha(node: textNode, alpha: 0.0)
|
||||
|
||||
if self.isExploding {
|
||||
self.isExploding = false
|
||||
self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
guard let (_, _, textColor, _, _) = self.currentParams, let textNode = self.textNode, !self.isRevealed else {
|
||||
return
|
||||
}
|
||||
|
||||
self.isRevealed = true
|
||||
|
||||
if self.enableAnimations {
|
||||
self.isExploding = true
|
||||
|
||||
let position = gestureRecognizer.location(in: self)
|
||||
self.emitterLayer?.setValue(true, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
|
||||
self.emitterLayer?.setValue(position, forKeyPath: "emitterBehaviors.fingerAttractor.position")
|
||||
|
||||
let maskSize = self.emitterNode.frame.size
|
||||
Queue.concurrentDefaultQueue().async {
|
||||
let textMaskImage = generateMaskImage(size: maskSize, position: position, inverse: false)
|
||||
let emitterMaskImage = generateMaskImage(size: maskSize, position: position, inverse: true)
|
||||
|
||||
Queue.mainQueue().async {
|
||||
self.textSpotNode.image = textMaskImage
|
||||
self.emitterSpotNode.image = emitterMaskImage
|
||||
}
|
||||
}
|
||||
|
||||
Queue.mainQueue().after(0.1 * UIView.animationDurationFactor()) {
|
||||
textNode.alpha = 1.0
|
||||
|
||||
textNode.view.mask = self.textMaskNode.view
|
||||
self.textSpotNode.frame = CGRect(x: 0.0, y: 0.0, width: self.emitterMaskNode.frame.width * 3.0, height: self.emitterMaskNode.frame.height * 3.0)
|
||||
|
||||
let xFactor = (position.x / self.emitterNode.frame.width - 0.5) * 2.0
|
||||
let yFactor = (position.y / self.emitterNode.frame.height - 0.5) * 2.0
|
||||
let maxFactor = max(abs(xFactor), abs(yFactor))
|
||||
|
||||
var scaleAddition = maxFactor * 4.0
|
||||
var durationAddition = -maxFactor * 0.2
|
||||
if self.emitterNode.frame.height > 0.0, self.emitterNode.frame.width / self.emitterNode.frame.height < 0.7 {
|
||||
scaleAddition *= 5.0
|
||||
durationAddition *= 2.0
|
||||
}
|
||||
|
||||
self.textSpotNode.layer.anchorPoint = CGPoint(x: position.x / self.emitterMaskNode.frame.width, y: position.y / self.emitterMaskNode.frame.height)
|
||||
self.textSpotNode.position = position
|
||||
self.textSpotNode.layer.animateScale(from: 0.3333, to: 10.5 + scaleAddition, duration: 0.55 + durationAddition, removeOnCompletion: false, completion: { _ in
|
||||
textNode.view.mask = nil
|
||||
})
|
||||
self.textSpotNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
|
||||
self.emitterNode.view.mask = self.emitterMaskNode.view
|
||||
self.emitterSpotNode.frame = CGRect(x: 0.0, y: 0.0, width: self.emitterMaskNode.frame.width * 3.0, height: self.emitterMaskNode.frame.height * 3.0)
|
||||
|
||||
self.emitterSpotNode.layer.anchorPoint = CGPoint(x: position.x / self.emitterMaskNode.frame.width, y: position.y / self.emitterMaskNode.frame.height)
|
||||
self.emitterSpotNode.position = position
|
||||
self.emitterSpotNode.layer.animateScale(from: 0.3333, to: 10.5 + scaleAddition, duration: 0.55 + durationAddition, removeOnCompletion: false, completion: { [weak self] _ in
|
||||
self?.alpha = 0.0
|
||||
self?.emitterNode.view.mask = nil
|
||||
|
||||
self?.emitter?.color = textColor.cgColor
|
||||
})
|
||||
self.emitterMaskFillNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
Queue.mainQueue().after(0.8 * UIView.animationDurationFactor()) {
|
||||
self.isExploding = false
|
||||
self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
|
||||
self.textSpotNode.layer.removeAllAnimations()
|
||||
|
||||
self.emitterSpotNode.layer.removeAllAnimations()
|
||||
self.emitterMaskFillNode.layer.removeAllAnimations()
|
||||
}
|
||||
} else {
|
||||
textNode.alpha = 1.0
|
||||
textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
|
||||
self.staticNode?.alpha = 0.0
|
||||
self.staticNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateEmitter() {
|
||||
guard let (size, color, _, lineRects, wordRects) = self.currentParams else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.enableAnimations {
|
||||
self.emitter?.color = self.animColor ?? color.cgColor
|
||||
self.emitterLayer?.setValue(wordRects, forKey: "emitterRects")
|
||||
self.emitterLayer?.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
let radius = max(size.width, size.height)
|
||||
self.emitterLayer?.setValue(max(size.width, size.height), forKeyPath: "emitterBehaviors.fingerAttractor.radius")
|
||||
self.emitterLayer?.setValue(radius * -0.5, forKeyPath: "emitterBehaviors.fingerAttractor.falloff")
|
||||
|
||||
var square: Float = 0.0
|
||||
for rect in wordRects {
|
||||
square += Float(rect.width * rect.height)
|
||||
}
|
||||
|
||||
Queue.mainQueue().async {
|
||||
self.emitter?.birthRate = min(100000, square * 0.35)
|
||||
}
|
||||
} else {
|
||||
if let staticParams = self.staticParams, staticParams.size == size && staticParams.color == color && staticParams.rects == lineRects && self.staticNode?.image != nil {
|
||||
return
|
||||
}
|
||||
self.staticParams = (size, color, lineRects)
|
||||
|
||||
var combinedRect: CGRect?
|
||||
var combinedRects: [CGRect] = []
|
||||
for rect in lineRects {
|
||||
if let currentRect = combinedRect {
|
||||
if abs(currentRect.minY - rect.minY) < 1.0 && abs(currentRect.maxY - rect.maxY) < 1.0 {
|
||||
combinedRect = currentRect.union(rect)
|
||||
} else {
|
||||
combinedRects.append(currentRect.insetBy(dx: 0.0, dy: -1.0 + UIScreenPixel))
|
||||
combinedRect = rect
|
||||
}
|
||||
} else {
|
||||
combinedRect = rect
|
||||
}
|
||||
}
|
||||
if let combinedRect {
|
||||
combinedRects.append(combinedRect.insetBy(dx: 0.0, dy: -1.0))
|
||||
}
|
||||
|
||||
Queue.concurrentDefaultQueue().async {
|
||||
var generator = ArbitraryRandomNumberGenerator(seed: 1)
|
||||
let image = generateImage(size, rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
context.setFillColor(color.cgColor)
|
||||
for rect in combinedRects {
|
||||
if rect.width > 10.0 {
|
||||
let rate = Int(rect.width * rect.height * 0.25)
|
||||
for _ in 0 ..< rate {
|
||||
let location = CGPoint(x: .random(in: rect.minX ..< rect.maxX, using: &generator), y: .random(in: rect.minY ..< rect.maxY, using: &generator))
|
||||
context.fillEllipse(in: CGRect(origin: location, size: CGSize(width: 1.0, height: 1.0)))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Queue.mainQueue().async {
|
||||
self.staticNode?.image = image
|
||||
}
|
||||
}
|
||||
self.staticNode?.frame = CGRect(origin: CGPoint(), size: size)
|
||||
}
|
||||
}
|
||||
|
||||
public func update(size: CGSize, color: UIColor, textColor: UIColor, rects: [CGRect], wordRects: [CGRect]) {
|
||||
self.currentParams = (size, color, textColor, rects, wordRects)
|
||||
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
self.emitterNode.frame = bounds
|
||||
self.emitterMaskNode.frame = bounds
|
||||
self.emitterMaskFillNode.frame = bounds
|
||||
self.textMaskNode.frame = CGRect(origin: CGPoint(x: 3.0, y: 3.0), size: size)
|
||||
|
||||
self.staticNode?.frame = bounds
|
||||
|
||||
self.updateEmitter()
|
||||
}
|
||||
|
||||
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
if let (_, _, _, rects, _) = self.currentParams, !self.isRevealed {
|
||||
for rect in rects {
|
||||
if rect.contains(point) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class InvisibleInkDustNode: ASDisplayNode {
|
||||
private var currentParams: (size: CGSize, color: UIColor, textColor: UIColor, rects: [CGRect], wordRects: [CGRect])?
|
||||
private var animColor: CGColor?
|
||||
|
@ -256,7 +256,7 @@ public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32,
|
||||
return true
|
||||
}
|
||||
}
|
||||
presentInCurrentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .autoDelete(isOn: isOn, title: nil, text: text), elevatedLayout: false, animateInAsReplacement: animateAsReplacement, action: { _ in return false }))
|
||||
presentInCurrentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .autoDelete(isOn: isOn, title: nil, text: text, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: animateAsReplacement, action: { _ in return false }))
|
||||
}
|
||||
|
||||
updateTimeoutDisposable.set((context.engine.privacy.updateGlobalMessageRemovalTimeout(timeout: timeout == 0 ? nil : timeout)
|
||||
@ -408,7 +408,7 @@ public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32,
|
||||
return true
|
||||
}
|
||||
}
|
||||
presentInCurrentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .autoDelete(isOn: isOn, title: nil, text: text), elevatedLayout: false, animateInAsReplacement: animateAsReplacement, action: { _ in return false }))
|
||||
presentInCurrentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .autoDelete(isOn: isOn, title: nil, text: text, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: animateAsReplacement, action: { _ in return false }))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -376,6 +376,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
|
||||
"//submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent",
|
||||
"//submodules/Utils/VolumeButtons",
|
||||
"//submodules/ChatContextQuery",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
|
@ -32,6 +32,12 @@ import FeaturedStickersScreen
|
||||
import Pasteboard
|
||||
import StickerPackPreviewUI
|
||||
|
||||
public final class EmptyInputView: UIView, UIInputViewAudioFeedback {
|
||||
public var enableInputClicksWhenVisible: Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatMediaInputPaneScrollState {
|
||||
let absoluteOffset: CGFloat?
|
||||
let relativeChange: CGFloat
|
||||
@ -68,6 +74,72 @@ public final class EntityKeyboardGifContent: Equatable {
|
||||
}
|
||||
|
||||
public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
public final class Interaction {
|
||||
let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool
|
||||
let sendEmoji: (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void
|
||||
let sendGif: (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool
|
||||
let sendBotContextResultAsGif: (ChatContextResultCollection, ChatContextResult, UIView, CGRect, Bool, Bool) -> Bool
|
||||
let updateChoosingSticker: (Bool) -> Void
|
||||
let switchToTextInput: () -> Void
|
||||
let dismissTextInput: () -> Void
|
||||
let insertText: (NSAttributedString) -> Void
|
||||
let backwardsDeleteText: () -> Void
|
||||
let presentController: (ViewController, Any?) -> Void
|
||||
let presentGlobalOverlayController: (ViewController, Any?) -> Void
|
||||
let getNavigationController: () -> NavigationController?
|
||||
let requestLayout: (ContainedViewLayoutTransition) -> Void
|
||||
|
||||
public init(
|
||||
sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool,
|
||||
sendEmoji: @escaping (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void,
|
||||
sendGif: @escaping (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool,
|
||||
sendBotContextResultAsGif: @escaping (ChatContextResultCollection, ChatContextResult, UIView, CGRect, Bool, Bool) -> Bool,
|
||||
updateChoosingSticker: @escaping (Bool) -> Void,
|
||||
switchToTextInput: @escaping () -> Void,
|
||||
dismissTextInput: @escaping () -> Void,
|
||||
insertText: @escaping (NSAttributedString) -> Void,
|
||||
backwardsDeleteText: @escaping () -> Void,
|
||||
presentController: @escaping (ViewController, Any?) -> Void,
|
||||
presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void,
|
||||
getNavigationController: @escaping () -> NavigationController?,
|
||||
requestLayout: @escaping (ContainedViewLayoutTransition) -> Void
|
||||
) {
|
||||
self.sendSticker = sendSticker
|
||||
self.sendEmoji = sendEmoji
|
||||
self.sendGif = sendGif
|
||||
self.sendBotContextResultAsGif = sendBotContextResultAsGif
|
||||
self.updateChoosingSticker = updateChoosingSticker
|
||||
self.switchToTextInput = switchToTextInput
|
||||
self.dismissTextInput = dismissTextInput
|
||||
self.insertText = insertText
|
||||
self.backwardsDeleteText = backwardsDeleteText
|
||||
self.presentController = presentController
|
||||
self.presentGlobalOverlayController = presentGlobalOverlayController
|
||||
self.getNavigationController = getNavigationController
|
||||
self.requestLayout = requestLayout
|
||||
}
|
||||
|
||||
public init(chatControllerInteraction: ChatControllerInteraction, panelInteraction: ChatPanelInterfaceInteraction) {
|
||||
self.sendSticker = chatControllerInteraction.sendSticker
|
||||
self.sendEmoji = chatControllerInteraction.sendEmoji
|
||||
self.sendGif = chatControllerInteraction.sendGif
|
||||
self.sendBotContextResultAsGif = chatControllerInteraction.sendBotContextResultAsGif
|
||||
self.updateChoosingSticker = chatControllerInteraction.updateChoosingSticker
|
||||
self.switchToTextInput = { [weak chatControllerInteraction] in
|
||||
chatControllerInteraction?.updateInputMode { _ in
|
||||
return .text
|
||||
}
|
||||
}
|
||||
self.dismissTextInput = chatControllerInteraction.dismissTextInput
|
||||
self.insertText = panelInteraction.insertText
|
||||
self.backwardsDeleteText = panelInteraction.backwardsDeleteText
|
||||
self.presentController = chatControllerInteraction.presentController
|
||||
self.presentGlobalOverlayController = chatControllerInteraction.presentGlobalOverlayController
|
||||
self.getNavigationController = chatControllerInteraction.navigationController
|
||||
self.requestLayout = panelInteraction.requestLayout
|
||||
}
|
||||
}
|
||||
|
||||
public struct InputData: Equatable {
|
||||
public var emoji: EmojiPagerContentComponent?
|
||||
public var stickers: EmojiPagerContentComponent?
|
||||
@ -111,7 +183,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
return hasPremium
|
||||
}
|
||||
|
||||
public static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction?, chatPeerId: PeerId?, areCustomEmojiEnabled: Bool) -> Signal<InputData, NoError> {
|
||||
public static func inputData(context: AccountContext, chatPeerId: PeerId?, areCustomEmojiEnabled: Bool, sendGif: ((FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool)?) -> Signal<InputData, NoError> {
|
||||
let animationCache = context.animationCache
|
||||
let animationRenderer = context.animationRenderer
|
||||
|
||||
@ -156,11 +228,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
|
||||
let gifInputInteraction = GifPagerContentComponent.InputInteraction(
|
||||
performItemAction: { [weak controllerInteraction] item, view, rect in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
return
|
||||
performItemAction: { item, view, rect in
|
||||
if let sendGif {
|
||||
let _ = sendGif(item.file, view, rect, false, false)
|
||||
}
|
||||
let _ = controllerInteraction.sendGif(item.file, view, rect, false, false)
|
||||
},
|
||||
openGifContextMenu: { _, _, _, _, _ in
|
||||
},
|
||||
@ -287,8 +358,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
}
|
||||
|
||||
private let controllerInteraction: ChatControllerInteraction?
|
||||
|
||||
private let interaction: ChatEntityKeyboardInputNode.Interaction?
|
||||
private var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
||||
|
||||
private let trendingGifsPromise = Promise<ChatMediaInputGifPaneTrendingState?>(nil)
|
||||
@ -303,7 +373,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
|
||||
fileprivate var clipContentToTopPanel: Bool = false
|
||||
|
||||
var externalTopPanelContainerImpl: PagerExternalTopPanelContainer?
|
||||
public var externalTopPanelContainerImpl: PagerExternalTopPanelContainer?
|
||||
public override var externalTopPanelContainer: UIView? {
|
||||
return self.externalTopPanelContainerImpl
|
||||
}
|
||||
@ -589,14 +659,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
|> distinctUntilChanged
|
||||
}
|
||||
|
||||
public init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal<InputData, NoError>, defaultToEmojiTab: Bool, opaqueTopPanelBackground: Bool = false, controllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?, chatPeerId: PeerId?, stateContext: StateContext?) {
|
||||
public init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal<InputData, NoError>, defaultToEmojiTab: Bool, opaqueTopPanelBackground: Bool = false, interaction: ChatEntityKeyboardInputNode.Interaction?, chatPeerId: PeerId?, stateContext: StateContext?) {
|
||||
self.context = context
|
||||
self.currentInputData = currentInputData
|
||||
self.defaultToEmojiTab = defaultToEmojiTab
|
||||
self.opaqueTopPanelBackground = opaqueTopPanelBackground
|
||||
self.stateContext = stateContext
|
||||
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.interaction = interaction
|
||||
|
||||
self.entityKeyboardView = ComponentHostView<Empty>()
|
||||
|
||||
@ -612,13 +682,13 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
self.externalTopPanelContainerImpl = PagerExternalTopPanelContainer()
|
||||
|
||||
var stickerPeekBehavior: EmojiContentPeekBehaviorImpl?
|
||||
if let controllerInteraction = controllerInteraction {
|
||||
if let interaction {
|
||||
let context = self.context
|
||||
|
||||
stickerPeekBehavior = EmojiContentPeekBehaviorImpl(
|
||||
context: self.context,
|
||||
interaction: EmojiContentPeekBehaviorImpl.Interaction(
|
||||
sendSticker: controllerInteraction.sendSticker,
|
||||
sendSticker: interaction.sendSticker,
|
||||
sendEmoji: { file in
|
||||
var text = "."
|
||||
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
|
||||
@ -639,7 +709,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
|
||||
if let emojiAttribute {
|
||||
controllerInteraction.sendEmoji(text, emojiAttribute, true)
|
||||
interaction.sendEmoji(text, emojiAttribute, true)
|
||||
}
|
||||
},
|
||||
setStatus: { [weak self] file in
|
||||
@ -659,7 +729,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
|
||||
let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.EmojiStatus_AppliedText, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false })
|
||||
strongSelf.currentUndoOverlayController = controller
|
||||
controllerInteraction.presentController(controller, nil)
|
||||
interaction.presentController(controller, nil)
|
||||
},
|
||||
copyEmoji: { [weak self] file in
|
||||
guard let strongSelf = self else {
|
||||
@ -692,28 +762,28 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.EmojiPreview_CopyEmoji, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false })
|
||||
let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.Conversation_EmojiCopied, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false })
|
||||
strongSelf.currentUndoOverlayController = controller
|
||||
controllerInteraction.presentController(controller, nil)
|
||||
interaction.presentController(controller, nil)
|
||||
}
|
||||
},
|
||||
presentController: controllerInteraction.presentController,
|
||||
presentGlobalOverlayController: controllerInteraction.presentGlobalOverlayController,
|
||||
navigationController: controllerInteraction.navigationController,
|
||||
presentController: interaction.presentController,
|
||||
presentGlobalOverlayController: interaction.presentGlobalOverlayController,
|
||||
navigationController: interaction.getNavigationController,
|
||||
updateIsPreviewing: { [weak self] value in
|
||||
self?.previewingStickersPromise.set(value)
|
||||
}
|
||||
),
|
||||
chatPeerId: chatPeerId,
|
||||
present: { c, a in
|
||||
controllerInteraction.presentGlobalOverlayController(c, a)
|
||||
interaction.presentGlobalOverlayController(c, a)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var premiumToastCounter = 0
|
||||
self.emojiInputInteraction = EmojiPagerContentComponent.InputInteraction(
|
||||
performItemAction: { [weak self, weak interfaceInteraction, weak controllerInteraction] groupId, item, _, _, _, _ in
|
||||
performItemAction: { [weak self, weak interaction] groupId, item, _, _, _, _ in
|
||||
let _ = (
|
||||
combineLatest(
|
||||
ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true),
|
||||
@ -721,7 +791,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
)
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { hasPremium, hasGlobalPremium in
|
||||
guard let strongSelf = self, let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else {
|
||||
guard let strongSelf = self, let interaction else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -732,8 +802,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
context.account.postbox.combinedView(keys: [viewKey])
|
||||
)
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak interfaceInteraction, weak controllerInteraction] emojiPacksView, views in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
|> deliverOnMainQueue).start(next: { [weak interaction] emojiPacksView, views in
|
||||
guard let interaction else {
|
||||
return
|
||||
}
|
||||
guard let view = views.views[viewKey] as? OrderedItemListView else {
|
||||
@ -743,8 +813,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = interfaceInteraction
|
||||
let _ = controllerInteraction
|
||||
let _ = interaction
|
||||
|
||||
var installedCollectionIds = Set<ItemCollectionId>()
|
||||
for (id, _, _) in emojiPacksView.collectionInfos {
|
||||
@ -831,15 +900,15 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
actionTitle = presentationData.strings.EmojiInput_PremiumEmojiToast_Action
|
||||
}
|
||||
|
||||
let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: text, undoText: actionTitle, customAction: { [weak controllerInteraction] in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: text, undoText: actionTitle, customAction: { [weak interaction] in
|
||||
guard let interaction else {
|
||||
return
|
||||
}
|
||||
|
||||
if suggestSavedMessages {
|
||||
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
guard let peer = peer, let navigationController = controllerInteraction.navigationController() else {
|
||||
guard let peer = peer, let navigationController = interaction.getNavigationController() else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -865,29 +934,28 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
}
|
||||
controllerInteraction.navigationController()?.pushViewController(controller)
|
||||
interaction.getNavigationController()?.pushViewController(controller)
|
||||
}
|
||||
}), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false })
|
||||
strongSelf.currentUndoOverlayController = controller
|
||||
controllerInteraction.presentController(controller, nil)
|
||||
interaction.presentController(controller, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if let emojiAttribute = emojiAttribute {
|
||||
AudioServicesPlaySystemSound(0x450)
|
||||
interfaceInteraction.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute]))
|
||||
interaction.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute]))
|
||||
}
|
||||
} else if case let .staticEmoji(staticEmoji) = item.content {
|
||||
AudioServicesPlaySystemSound(0x450)
|
||||
interfaceInteraction.insertText(NSAttributedString(string: staticEmoji, attributes: [:]))
|
||||
interaction.insertText(NSAttributedString(string: staticEmoji, attributes: [:]))
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteBackwards: { [weak interfaceInteraction] in
|
||||
guard let interfaceInteraction = interfaceInteraction else {
|
||||
return
|
||||
deleteBackwards: { [weak interaction] in
|
||||
if let interaction {
|
||||
interaction.backwardsDeleteText()
|
||||
}
|
||||
interfaceInteraction.backwardsDeleteText()
|
||||
},
|
||||
openStickerSettings: {
|
||||
},
|
||||
@ -895,8 +963,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
},
|
||||
openSearch: {
|
||||
},
|
||||
addGroupAction: { [weak self, weak controllerInteraction] groupId, isPremiumLocked, scrollToGroup in
|
||||
guard let controllerInteraction = controllerInteraction, let collectionId = groupId.base as? ItemCollectionId else {
|
||||
addGroupAction: { [weak self, weak interaction] groupId, isPremiumLocked, scrollToGroup in
|
||||
guard let interaction, let collectionId = groupId.base as? ItemCollectionId else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -909,7 +977,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
}
|
||||
controllerInteraction.navigationController()?.pushViewController(controller)
|
||||
interaction.getNavigationController()?.pushViewController(controller)
|
||||
|
||||
return
|
||||
}
|
||||
@ -933,12 +1001,12 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
})
|
||||
},
|
||||
clearGroup: { [weak controllerInteraction] groupId in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
clearGroup: { [weak interaction] groupId in
|
||||
guard let interaction else {
|
||||
return
|
||||
}
|
||||
if groupId == AnyHashable("recent") {
|
||||
controllerInteraction.dismissTextInput()
|
||||
interaction.dismissTextInput()
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize))
|
||||
var items: [ActionSheetItem] = []
|
||||
@ -951,7 +1019,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
controllerInteraction.presentController(actionSheet, nil)
|
||||
interaction.presentController(actionSheet, nil)
|
||||
} else if groupId == AnyHashable("featuredTop") {
|
||||
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)
|
||||
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|
||||
@ -968,33 +1036,33 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
})
|
||||
}
|
||||
},
|
||||
pushController: { [weak controllerInteraction] controller in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
pushController: { [weak interaction] controller in
|
||||
guard let interaction else {
|
||||
return
|
||||
}
|
||||
controllerInteraction.navigationController()?.pushViewController(controller)
|
||||
interaction.getNavigationController()?.pushViewController(controller)
|
||||
},
|
||||
presentController: { [weak controllerInteraction] controller in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
presentController: { [weak interaction] controller in
|
||||
guard let interaction else {
|
||||
return
|
||||
}
|
||||
controllerInteraction.presentController(controller, nil)
|
||||
interaction.presentController(controller, nil)
|
||||
},
|
||||
presentGlobalOverlayController: { [weak controllerInteraction] controller in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
presentGlobalOverlayController: { [weak interaction] controller in
|
||||
guard let interaction else {
|
||||
return
|
||||
}
|
||||
controllerInteraction.presentGlobalOverlayController(controller, nil)
|
||||
interaction.presentGlobalOverlayController(controller, nil)
|
||||
},
|
||||
navigationController: { [weak controllerInteraction] in
|
||||
return controllerInteraction?.navigationController()
|
||||
navigationController: { [weak interaction] in
|
||||
return interaction?.getNavigationController()
|
||||
},
|
||||
requestUpdate: { [weak self] transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !transition.animation.isImmediate {
|
||||
strongSelf.interfaceInteraction?.requestLayout(transition.containedViewLayoutTransition)
|
||||
strongSelf.interaction?.requestLayout(transition.containedViewLayoutTransition)
|
||||
}
|
||||
},
|
||||
updateSearchQuery: { [weak self] query in
|
||||
@ -1231,9 +1299,9 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
)
|
||||
|
||||
self.stickerInputInteraction = EmojiPagerContentComponent.InputInteraction(
|
||||
performItemAction: { [weak controllerInteraction, weak interfaceInteraction] groupId, item, view, rect, layer, _ in
|
||||
performItemAction: { [weak interaction] groupId, item, view, rect, layer, _ 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 {
|
||||
guard let interaction else {
|
||||
return
|
||||
}
|
||||
guard let file = item.itemFile else {
|
||||
@ -1244,8 +1312,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
|
||||
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak controllerInteraction] views in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
|> deliverOnMainQueue).start(next: { [weak interaction] views in
|
||||
guard let interaction else {
|
||||
return
|
||||
}
|
||||
guard let view = views.views[viewKey] as? OrderedItemListView else {
|
||||
@ -1253,14 +1321,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
|
||||
if featuredStickerPack.topItems.contains(where: { $0.file.fileId == file.fileId }) {
|
||||
controllerInteraction.navigationController()?.pushViewController(FeaturedStickersScreen(
|
||||
interaction.getNavigationController()?.pushViewController(FeaturedStickersScreen(
|
||||
context: context,
|
||||
highlightedPackId: featuredStickerPack.info.id,
|
||||
sendSticker: { [weak controllerInteraction] fileReference, sourceNode, sourceRect in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
sendSticker: { [weak interaction] fileReference, sourceNode, sourceRect in
|
||||
guard let interaction else {
|
||||
return false
|
||||
}
|
||||
return controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, [])
|
||||
return interaction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, [])
|
||||
}
|
||||
))
|
||||
|
||||
@ -1271,7 +1339,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
} else {
|
||||
if file.isPremiumSticker && !hasPremium {
|
||||
let controller = PremiumIntroScreen(context: context, source: .stickers)
|
||||
controllerInteraction.navigationController()?.pushViewController(controller)
|
||||
interaction.getNavigationController()?.pushViewController(controller)
|
||||
|
||||
return
|
||||
}
|
||||
@ -1279,37 +1347,36 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
if let id = groupId.base as? ItemCollectionId, context.sharedContext.currentStickerSettings.with({ $0 }).dynamicPackOrder {
|
||||
bubbleUpEmojiOrStickersets.append(id)
|
||||
}
|
||||
let _ = interfaceInteraction.sendSticker(.standalone(media: file), false, view, rect, layer, bubbleUpEmojiOrStickersets)
|
||||
let _ = interaction.sendSticker(.standalone(media: file), false, false, nil, false, view, rect, layer, bubbleUpEmojiOrStickersets)
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteBackwards: { [weak interfaceInteraction] in
|
||||
guard let interfaceInteraction = interfaceInteraction else {
|
||||
return
|
||||
deleteBackwards: { [weak interaction] in
|
||||
if let interaction {
|
||||
interaction.backwardsDeleteText()
|
||||
}
|
||||
interfaceInteraction.backwardsDeleteText()
|
||||
},
|
||||
openStickerSettings: { [weak controllerInteraction] in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
openStickerSettings: { [weak interaction] in
|
||||
guard let interaction else {
|
||||
return
|
||||
}
|
||||
let controller = context.sharedContext.makeInstalledStickerPacksController(context: context, mode: .modal)
|
||||
controller.navigationPresentation = .modal
|
||||
controllerInteraction.navigationController()?.pushViewController(controller)
|
||||
interaction.getNavigationController()?.pushViewController(controller)
|
||||
},
|
||||
openFeatured: { [weak controllerInteraction] in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
openFeatured: { [weak interaction] in
|
||||
guard let interaction else {
|
||||
return
|
||||
}
|
||||
|
||||
controllerInteraction.navigationController()?.pushViewController(FeaturedStickersScreen(
|
||||
interaction.getNavigationController()?.pushViewController(FeaturedStickersScreen(
|
||||
context: context,
|
||||
highlightedPackId: nil,
|
||||
sendSticker: { [weak controllerInteraction] fileReference, sourceNode, sourceRect in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
sendSticker: { [weak interaction] fileReference, sourceNode, sourceRect in
|
||||
guard let interaction else {
|
||||
return false
|
||||
}
|
||||
return controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, [])
|
||||
return interaction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, [])
|
||||
}
|
||||
))
|
||||
},
|
||||
@ -1318,14 +1385,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
pagerView.openSearch()
|
||||
}
|
||||
},
|
||||
addGroupAction: { groupId, isPremiumLocked, _ in
|
||||
guard let controllerInteraction = controllerInteraction, let collectionId = groupId.base as? ItemCollectionId else {
|
||||
addGroupAction: { [weak interaction] groupId, isPremiumLocked, _ in
|
||||
guard let interaction, let collectionId = groupId.base as? ItemCollectionId else {
|
||||
return
|
||||
}
|
||||
|
||||
if isPremiumLocked {
|
||||
let controller = PremiumIntroScreen(context: context, source: .stickers)
|
||||
controllerInteraction.navigationController()?.pushViewController(controller)
|
||||
interaction.getNavigationController()?.pushViewController(controller)
|
||||
|
||||
return
|
||||
}
|
||||
@ -1363,12 +1430,12 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
})
|
||||
},
|
||||
clearGroup: { [weak controllerInteraction] groupId in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
clearGroup: { [weak interaction] groupId in
|
||||
guard let interaction else {
|
||||
return
|
||||
}
|
||||
if groupId == AnyHashable("recent") {
|
||||
controllerInteraction.dismissTextInput()
|
||||
interaction.dismissTextInput()
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize))
|
||||
var items: [ActionSheetItem] = []
|
||||
@ -1381,7 +1448,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
controllerInteraction.presentController(actionSheet, nil)
|
||||
interaction.presentController(actionSheet, nil)
|
||||
} else if groupId == AnyHashable("featuredTop") {
|
||||
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
|
||||
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|
||||
@ -1399,26 +1466,26 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
} else if groupId == AnyHashable("peerSpecific") {
|
||||
}
|
||||
},
|
||||
pushController: { [weak controllerInteraction] controller in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
pushController: { [weak interaction] controller in
|
||||
guard let interaction else {
|
||||
return
|
||||
}
|
||||
controllerInteraction.navigationController()?.pushViewController(controller)
|
||||
interaction.getNavigationController()?.pushViewController(controller)
|
||||
},
|
||||
presentController: { [weak controllerInteraction] controller in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
presentController: { [weak interaction] controller in
|
||||
guard let interaction else {
|
||||
return
|
||||
}
|
||||
controllerInteraction.presentController(controller, nil)
|
||||
interaction.presentController(controller, nil)
|
||||
},
|
||||
presentGlobalOverlayController: { [weak controllerInteraction] controller in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
presentGlobalOverlayController: { [weak interaction] controller in
|
||||
guard let interaction else {
|
||||
return
|
||||
}
|
||||
controllerInteraction.presentGlobalOverlayController(controller, nil)
|
||||
interaction.presentGlobalOverlayController(controller, nil)
|
||||
},
|
||||
navigationController: { [weak controllerInteraction] in
|
||||
return controllerInteraction?.navigationController()
|
||||
navigationController: { [weak interaction] in
|
||||
return interaction?.getNavigationController()
|
||||
},
|
||||
requestUpdate: { _ in
|
||||
},
|
||||
@ -1634,15 +1701,15 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
})
|
||||
|
||||
self.gifInputInteraction = GifPagerContentComponent.InputInteraction(
|
||||
performItemAction: { [weak controllerInteraction] item, view, rect in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
performItemAction: { [weak interaction] item, view, rect in
|
||||
guard let interaction else {
|
||||
return
|
||||
}
|
||||
|
||||
if let (collection, result) = item.contextResult {
|
||||
let _ = controllerInteraction.sendBotContextResultAsGif(collection, result, view, rect, false, false)
|
||||
let _ = interaction.sendBotContextResultAsGif(collection, result, view, rect, false, false)
|
||||
} else {
|
||||
let _ = controllerInteraction.sendGif(item.file, view, rect, false, false)
|
||||
let _ = interaction.sendGif(item.file, view, rect, false, false)
|
||||
}
|
||||
},
|
||||
openGifContextMenu: { [weak self] item, sourceView, sourceRect, gesture, isSaved in
|
||||
@ -1675,11 +1742,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
)
|
||||
|
||||
self.switchToTextInput = { [weak self] in
|
||||
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
|
||||
return
|
||||
}
|
||||
controllerInteraction.updateInputMode { _ in
|
||||
return .text
|
||||
if let self {
|
||||
self.interaction?.switchToTextInput()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1707,8 +1771,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
|
||||
self.choosingStickerDisposable = (self.choosingSticker
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerInteraction?.updateChoosingSticker(value)
|
||||
if let self {
|
||||
self.interaction?.updateChoosingSticker(value)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1771,7 +1835,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
|
||||
let context = self.context
|
||||
let controllerInteraction = self.controllerInteraction
|
||||
let interaction = self.interaction
|
||||
let inputNodeInteraction = self.inputNodeInteraction!
|
||||
let trendingGifsPromise = self.trendingGifsPromise
|
||||
|
||||
@ -1883,8 +1947,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
strongSelf.reorderItems(category: category, items: items)
|
||||
},
|
||||
makeSearchContainerNode: { [weak self, weak controllerInteraction] content in
|
||||
guard let self, let controllerInteraction = controllerInteraction else {
|
||||
makeSearchContainerNode: { [weak self, weak interaction] content in
|
||||
guard let self, let interaction else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1901,7 +1965,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
context: context,
|
||||
theme: presentationData.theme,
|
||||
strings: presentationData.strings,
|
||||
controllerInteraction: controllerInteraction,
|
||||
interaction: interaction,
|
||||
inputNodeInteraction: inputNodeInteraction,
|
||||
mode: mappedMode,
|
||||
trendingGifsPromise: trendingGifsPromise,
|
||||
@ -2072,7 +2136,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
|
||||
if self.context.sharedContext.currentStickerSettings.with({ $0 }).dynamicPackOrder {
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_reorder", scale: 0.05, colors: [:], title: presentationData.strings.StickerPacksSettings_DynamicOrderOff, text: presentationData.strings.StickerPacksSettings_DynamicOrderOffInfo, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
|
||||
self.interaction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_reorder", scale: 0.05, colors: [:], title: presentationData.strings.StickerPacksSettings_DynamicOrderOff, text: presentationData.strings.StickerPacksSettings_DynamicOrderOffInfo, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
|
||||
return false
|
||||
}), nil)
|
||||
|
||||
@ -2110,12 +2174,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
var items: [ContextMenuItem] = []
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Send, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
}, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
if isSaved {
|
||||
let _ = self?.controllerInteraction?.sendGif(file, sourceView, sourceRect, false, false)
|
||||
} else if let (collection, result) = contextResult {
|
||||
let _ = self?.controllerInteraction?.sendBotContextResultAsGif(collection, result, sourceView, sourceRect, false, false)
|
||||
if let self {
|
||||
if isSaved {
|
||||
let _ = self.interaction?.sendGif(file, sourceView, sourceRect, false, false)
|
||||
} else if let (collection, result) = contextResult {
|
||||
let _ = self.interaction?.sendBotContextResultAsGif(collection, result, sourceView, sourceRect, false, false)
|
||||
}
|
||||
}
|
||||
})))
|
||||
|
||||
@ -2131,12 +2197,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
if peerId != self?.context.account.peerId && peerId.namespace != Namespaces.Peer.SecretChat {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
}, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
if isSaved {
|
||||
let _ = self?.controllerInteraction?.sendGif(file, sourceView, sourceRect, true, false)
|
||||
} else if let (collection, result) = contextResult {
|
||||
let _ = self?.controllerInteraction?.sendBotContextResultAsGif(collection, result, sourceView, sourceRect, true, false)
|
||||
if let self {
|
||||
if isSaved {
|
||||
let _ = self.interaction?.sendGif(file, sourceView, sourceRect, true, false)
|
||||
} else if let (collection, result) = contextResult {
|
||||
let _ = self.interaction?.sendBotContextResultAsGif(collection, result, sourceView, sourceRect, true, false)
|
||||
}
|
||||
}
|
||||
})))
|
||||
}
|
||||
@ -2144,10 +2212,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
if isSaved {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
}, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
|
||||
let _ = self?.controllerInteraction?.sendGif(file, sourceView, sourceRect, false, true)
|
||||
if let self {
|
||||
let _ = self.interaction?.sendGif(file, sourceView, sourceRect, false, true)
|
||||
}
|
||||
})))
|
||||
}
|
||||
}
|
||||
@ -2157,18 +2226,17 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
if isSaved || isGifSaved {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
|
||||
}, action: { _, f in
|
||||
}, action: { [weak self] _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
if let self {
|
||||
let _ = removeSavedGif(postbox: self.context.account.postbox, mediaId: file.media.fileId).start()
|
||||
}
|
||||
let _ = removeSavedGif(postbox: strongSelf.context.account.postbox, mediaId: file.media.fileId).start()
|
||||
})))
|
||||
} else if canSaveGif && !isGifSaved {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Preview_SaveGif, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
}, action: { [weak self] _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
guard let strongSelf = self else {
|
||||
@ -2178,13 +2246,13 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
let context = strongSelf.context
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let _ = (toggleGifSaved(account: context.account, fileReference: file, saved: true)
|
||||
|> deliverOnMainQueue).start(next: { result in
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
switch result {
|
||||
case .generic:
|
||||
strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
strongSelf.interaction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
case let .limitExceeded(limit, premiumLimit):
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
||||
let text: String
|
||||
@ -2193,14 +2261,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
} else {
|
||||
text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string
|
||||
}
|
||||
strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
|
||||
strongSelf.interaction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
|
||||
guard let strongSelf = self else {
|
||||
return false
|
||||
}
|
||||
|
||||
if case .info = action {
|
||||
let controller = PremiumIntroScreen(context: context, source: .savedGifs)
|
||||
strongSelf.controllerInteraction?.navigationController()?.pushViewController(controller)
|
||||
strongSelf.interaction?.getNavigationController()?.pushViewController(controller)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@ -2211,7 +2279,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
|
||||
let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceView: sourceView, sourceRect: sourceRect)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
strongSelf.controllerInteraction?.presentGlobalOverlayController(contextController, nil)
|
||||
strongSelf.interaction?.presentGlobalOverlayController(contextController, nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -2281,7 +2349,6 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi
|
||||
}
|
||||
|
||||
super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), inputViewStyle: .default)
|
||||
// super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)))
|
||||
|
||||
self.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
self.clipsToBounds = true
|
||||
@ -2428,7 +2495,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi
|
||||
gifs: nil,
|
||||
availableGifSearchEmojies: []
|
||||
),
|
||||
updatedInputData: EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: false, hasTrending: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil, forceHasPremium: forceHasPremium) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in
|
||||
updatedInputData: EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: false, hasTrending: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil, forceHasPremium: forceHasPremium, hideBackground: hideBackground) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in
|
||||
return ChatEntityKeyboardInputNode.InputData(
|
||||
emoji: emojiComponent,
|
||||
stickers: nil,
|
||||
@ -2437,9 +2504,8 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi
|
||||
)
|
||||
},
|
||||
defaultToEmojiTab: true,
|
||||
opaqueTopPanelBackground: true,
|
||||
controllerInteraction: nil,
|
||||
interfaceInteraction: nil,
|
||||
opaqueTopPanelBackground: !hideBackground,
|
||||
interaction: nil,
|
||||
chatPeerId: nil,
|
||||
stateContext: nil
|
||||
)
|
||||
@ -2450,7 +2516,9 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi
|
||||
inputNode.switchToTextInput = { [weak self] in
|
||||
self?.switchToKeyboard?()
|
||||
}
|
||||
inputNode.backgroundColor = self.presentationData.theme.chat.inputMediaPanel.backgroundColor
|
||||
if !hideBackground {
|
||||
inputNode.backgroundColor = self.presentationData.theme.chat.inputMediaPanel.backgroundColor
|
||||
}
|
||||
self.addSubnode(inputNode)
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ import ChatPresentationInterfaceState
|
||||
|
||||
final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
private let context: AccountContext
|
||||
private let controllerInteraction: ChatControllerInteraction
|
||||
private let interaction: ChatEntityKeyboardInputNode.Interaction
|
||||
private let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
|
||||
private var theme: PresentationTheme
|
||||
@ -44,9 +44,9 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
|
||||
private var hasInitialText = false
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingPromise: Promise<ChatMediaInputGifPaneTrendingState?>) {
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, interaction: ChatEntityKeyboardInputNode.Interaction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingPromise: Promise<ChatMediaInputGifPaneTrendingState?>) {
|
||||
self.context = context
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.interaction = interaction
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
self.trendingPromise = trendingPromise
|
||||
|
||||
@ -226,9 +226,9 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
|
||||
multiplexedNode.fileSelected = { [weak self] file, sourceNode, sourceRect in
|
||||
if let (collection, result) = file.contextResult {
|
||||
let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode.view, sourceRect, false, false)
|
||||
let _ = self?.interaction.sendBotContextResultAsGif(collection, result, sourceNode.view, sourceRect, false, false)
|
||||
} else {
|
||||
let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, false, false)
|
||||
let _ = self?.interaction.sendGif(file.file, sourceNode.view, sourceRect, false, false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
|
||||
private let context: AccountContext
|
||||
private let mode: ChatMediaInputSearchMode
|
||||
public private(set) var contentNode: PaneSearchContentNode & ASDisplayNode
|
||||
private let controllerInteraction: ChatControllerInteraction
|
||||
private let interaction: ChatEntityKeyboardInputNode.Interaction
|
||||
private let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
private let peekBehavior: EmojiContentPeekBehavior?
|
||||
|
||||
@ -53,17 +53,17 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
|
||||
return self.contentNode.ready
|
||||
}
|
||||
|
||||
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, trendingGifsPromise: Promise<ChatMediaInputGifPaneTrendingState?>, cancel: @escaping () -> Void, peekBehavior: EmojiContentPeekBehavior?) {
|
||||
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, interaction: ChatEntityKeyboardInputNode.Interaction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, trendingGifsPromise: Promise<ChatMediaInputGifPaneTrendingState?>, cancel: @escaping () -> Void, peekBehavior: EmojiContentPeekBehavior?) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.interaction = interaction
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
self.peekBehavior = peekBehavior
|
||||
switch mode {
|
||||
case .gif:
|
||||
self.contentNode = GifPaneSearchContentNode(context: context, theme: theme, strings: strings, controllerInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction, trendingPromise: trendingGifsPromise)
|
||||
self.contentNode = GifPaneSearchContentNode(context: context, theme: theme, strings: strings, interaction: interaction, inputNodeInteraction: inputNodeInteraction, trendingPromise: trendingGifsPromise)
|
||||
case .sticker, .trending:
|
||||
self.contentNode = StickerPaneSearchContentNode(context: context, theme: theme, strings: strings, controllerInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction)
|
||||
self.contentNode = StickerPaneSearchContentNode(context: context, theme: theme, strings: strings, interaction: interaction, inputNodeInteraction: inputNodeInteraction)
|
||||
}
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
|
||||
|
@ -19,7 +19,6 @@ import UndoUI
|
||||
import ChatControllerInteraction
|
||||
import FeaturedStickersScreen
|
||||
import ChatPresentationInterfaceState
|
||||
import FeaturedStickersScreen
|
||||
|
||||
private enum StickerSearchEntryId: Equatable, Hashable {
|
||||
case sticker(String?, Int64)
|
||||
@ -136,9 +135,9 @@ private func preparedChatMediaInputGridEntryTransition(context: AccountContext,
|
||||
|
||||
final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode {
|
||||
private let context: AccountContext
|
||||
private let controllerInteraction: ChatControllerInteraction
|
||||
private let interaction: ChatEntityKeyboardInputNode.Interaction
|
||||
private let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
private var interaction: StickerPaneSearchInteraction?
|
||||
private var searchInteraction: StickerPaneSearchInteraction?
|
||||
|
||||
private var theme: PresentationTheme
|
||||
private var strings: PresentationStrings
|
||||
@ -168,15 +167,21 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode {
|
||||
|
||||
private let installDisposable = MetaDisposable()
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) {
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, interaction: ChatEntityKeyboardInputNode.Interaction, inputNodeInteraction: ChatMediaInputNodeInteraction) {
|
||||
self.context = context
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.interaction = interaction
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
|
||||
self.trendingPane = ChatMediaInputTrendingPane(context: context, controllerInteraction: controllerInteraction, getItemIsPreviewed: { [weak inputNodeInteraction] item in
|
||||
let trendingPaneInteraction = ChatMediaInputTrendingPane.Interaction(
|
||||
sendSticker: interaction.sendSticker,
|
||||
presentController: interaction.presentController,
|
||||
getNavigationController: interaction.getNavigationController
|
||||
)
|
||||
|
||||
self.trendingPane = ChatMediaInputTrendingPane(context: context, interaction: trendingPaneInteraction, getItemIsPreviewed: { [weak inputNodeInteraction] item in
|
||||
return inputNodeInteraction?.previewedStickerPackItemFile?.id == item.file.id
|
||||
}, isPane: false)
|
||||
|
||||
@ -211,18 +216,18 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode {
|
||||
self?.deactivateSearchBar?()
|
||||
}
|
||||
|
||||
self.interaction = StickerPaneSearchInteraction(open: { [weak self] info in
|
||||
self.searchInteraction = StickerPaneSearchInteraction(open: { [weak self] info in
|
||||
if let strongSelf = self {
|
||||
strongSelf.view.window?.endEditing(true)
|
||||
let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash)
|
||||
let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { [weak self] fileReference, sourceNode, sourceRect in
|
||||
let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.interaction.getNavigationController(), sendSticker: { [weak self] fileReference, sourceNode, sourceRect in
|
||||
if let strongSelf = self {
|
||||
return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, [])
|
||||
return strongSelf.interaction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, [])
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
strongSelf.controllerInteraction.presentController(controller, nil)
|
||||
strongSelf.interaction.presentController(controller, nil)
|
||||
}
|
||||
}, install: { [weak self] info, items, install in
|
||||
guard let strongSelf = self else {
|
||||
@ -264,7 +269,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode {
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
cancelImpl?()
|
||||
}))
|
||||
self?.controllerInteraction.presentController(controller, nil)
|
||||
self?.interaction.presentController(controller, nil)
|
||||
return ActionDisposable { [weak controller] in
|
||||
Queue.mainQueue().async() {
|
||||
controller?.dismiss()
|
||||
@ -291,7 +296,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode {
|
||||
}
|
||||
|
||||
var animateInAsReplacement = false
|
||||
if let navigationController = strongSelf.controllerInteraction.navigationController() {
|
||||
if let navigationController = strongSelf.interaction.getNavigationController() {
|
||||
for controller in navigationController.overlayControllers {
|
||||
if let controller = controller as? UndoOverlayController {
|
||||
controller.dismissWithCommitActionAndReplacementAnimation()
|
||||
@ -301,7 +306,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode {
|
||||
}
|
||||
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
strongSelf.controllerInteraction.navigationController()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: strongSelf.context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in
|
||||
strongSelf.interaction.getNavigationController()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: strongSelf.context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in
|
||||
return true
|
||||
}))
|
||||
}))
|
||||
@ -312,7 +317,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode {
|
||||
}
|
||||
}, sendSticker: { [weak self] file, sourceView, sourceRect in
|
||||
if let strongSelf = self {
|
||||
let _ = strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil, [])
|
||||
let _ = strongSelf.interaction.sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil, [])
|
||||
}
|
||||
}, getItemIsPreviewed: { item in
|
||||
return inputNodeInteraction.previewedStickerPackItemFile?.id == item.file.id
|
||||
@ -451,7 +456,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode {
|
||||
self.searchDisposable.set((signal
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] result in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self, let interaction = strongSelf.interaction else {
|
||||
guard let strongSelf = self, let interaction = strongSelf.searchInteraction else {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -2633,6 +2633,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
public let itemContentUniqueId: ContentId?
|
||||
public let searchState: SearchState
|
||||
public let warpContentsOnEdges: Bool
|
||||
public let hideBackground: Bool
|
||||
public let displaySearchWithPlaceholder: String?
|
||||
public let searchCategories: EmojiSearchCategories?
|
||||
public let searchInitiallyHidden: Bool
|
||||
@ -2655,6 +2656,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
itemContentUniqueId: ContentId?,
|
||||
searchState: SearchState,
|
||||
warpContentsOnEdges: Bool,
|
||||
hideBackground: Bool,
|
||||
displaySearchWithPlaceholder: String?,
|
||||
searchCategories: EmojiSearchCategories?,
|
||||
searchInitiallyHidden: Bool,
|
||||
@ -2676,6 +2678,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
self.itemContentUniqueId = itemContentUniqueId
|
||||
self.searchState = searchState
|
||||
self.warpContentsOnEdges = warpContentsOnEdges
|
||||
self.hideBackground = hideBackground
|
||||
self.displaySearchWithPlaceholder = displaySearchWithPlaceholder
|
||||
self.searchCategories = searchCategories
|
||||
self.searchInitiallyHidden = searchInitiallyHidden
|
||||
@ -2700,6 +2703,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
itemContentUniqueId: itemContentUniqueId,
|
||||
searchState: searchState,
|
||||
warpContentsOnEdges: self.warpContentsOnEdges,
|
||||
hideBackground: self.hideBackground,
|
||||
displaySearchWithPlaceholder: self.displaySearchWithPlaceholder,
|
||||
searchCategories: self.searchCategories,
|
||||
searchInitiallyHidden: self.searchInitiallyHidden,
|
||||
@ -2751,6 +2755,9 @@ public final class EmojiPagerContentComponent: Component {
|
||||
if lhs.warpContentsOnEdges != rhs.warpContentsOnEdges {
|
||||
return false
|
||||
}
|
||||
if lhs.hideBackground != rhs.hideBackground {
|
||||
return false
|
||||
}
|
||||
if lhs.displaySearchWithPlaceholder != rhs.displaySearchWithPlaceholder {
|
||||
return false
|
||||
}
|
||||
@ -3619,6 +3626,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
private var isSearchActivated: Bool = false
|
||||
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
private var fadingMaskLayer: FadingMaskLayer?
|
||||
private var vibrancyClippingView: UIView
|
||||
private var vibrancyEffectView: UIVisualEffectView?
|
||||
public private(set) var mirrorContentClippingView: UIView?
|
||||
@ -6185,7 +6193,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(ContentAnimation(type: .groupExpanded(id: groupId))))
|
||||
}
|
||||
|
||||
public func pagerUpdateBackground(backgroundFrame: CGRect, transition: Transition) {
|
||||
public func pagerUpdateBackground(backgroundFrame: CGRect, topPanelHeight: CGFloat, transition: Transition) {
|
||||
guard let component = self.component, let keyboardChildEnvironment = self.keyboardChildEnvironment, let pagerEnvironment = self.pagerEnvironment else {
|
||||
return
|
||||
}
|
||||
@ -6232,7 +6240,21 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if component.warpContentsOnEdges {
|
||||
if component.hideBackground {
|
||||
self.backgroundView.isHidden = true
|
||||
|
||||
let maskLayer: FadingMaskLayer
|
||||
if let current = self.fadingMaskLayer {
|
||||
maskLayer = current
|
||||
} else {
|
||||
maskLayer = FadingMaskLayer()
|
||||
self.fadingMaskLayer = maskLayer
|
||||
}
|
||||
if self.layer.mask == nil {
|
||||
self.layer.mask = maskLayer
|
||||
}
|
||||
maskLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: (topPanelHeight - 34.0) * 0.75), size: backgroundFrame.size)
|
||||
} else if component.warpContentsOnEdges {
|
||||
self.backgroundView.isHidden = true
|
||||
} else {
|
||||
self.backgroundView.isHidden = false
|
||||
@ -7005,7 +7027,8 @@ public final class EmojiPagerContentComponent: Component {
|
||||
topicColor: Int32? = nil,
|
||||
hasSearch: Bool = true,
|
||||
forceHasPremium: Bool = false,
|
||||
premiumIfSavedMessages: Bool = true
|
||||
premiumIfSavedMessages: Bool = true,
|
||||
hideBackground: Bool = false
|
||||
) -> Signal<EmojiPagerContentComponent, NoError> {
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
||||
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
|
||||
@ -7990,6 +8013,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
itemContentUniqueId: nil,
|
||||
searchState: .empty(hasResults: false),
|
||||
warpContentsOnEdges: isReactionSelection || isStatusSelection || isProfilePhotoEmojiSelection || isGroupPhotoEmojiSelection,
|
||||
hideBackground: hideBackground,
|
||||
displaySearchWithPlaceholder: displaySearchWithPlaceholder,
|
||||
searchCategories: searchCategories,
|
||||
searchInitiallyHidden: searchInitiallyHidden,
|
||||
@ -8015,7 +8039,8 @@ public final class EmojiPagerContentComponent: Component {
|
||||
forceHasPremium: Bool,
|
||||
searchIsPlaceholderOnly: Bool = true,
|
||||
isProfilePhotoEmojiSelection: Bool = false,
|
||||
isGroupPhotoEmojiSelection: Bool = false
|
||||
isGroupPhotoEmojiSelection: Bool = false,
|
||||
hideBackground: Bool = false
|
||||
) -> Signal<EmojiPagerContentComponent, NoError> {
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
||||
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
|
||||
@ -8456,6 +8481,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
itemContentUniqueId: nil,
|
||||
searchState: .empty(hasResults: false),
|
||||
warpContentsOnEdges: isProfilePhotoEmojiSelection || isGroupPhotoEmojiSelection,
|
||||
hideBackground: hideBackground,
|
||||
displaySearchWithPlaceholder: hasSearch ? strings.StickersSearch_SearchStickersPlaceholder : nil,
|
||||
searchCategories: searchCategories,
|
||||
searchInitiallyHidden: true,
|
||||
@ -8522,3 +8548,24 @@ func generateTopicIcon(backgroundColors: [UIColor], strokeColors: [UIColor], tit
|
||||
context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y)
|
||||
})
|
||||
}
|
||||
|
||||
private final class FadingMaskLayer: SimpleLayer {
|
||||
let gradientLayer = SimpleLayer()
|
||||
let fillLayer = SimpleLayer()
|
||||
|
||||
override func layoutSublayers() {
|
||||
let gradientHeight: CGFloat = 66.0
|
||||
if self.gradientLayer.contents == nil {
|
||||
self.addSublayer(self.gradientLayer)
|
||||
self.addSublayer(self.fillLayer)
|
||||
|
||||
let gradientImage = generateGradientImage(size: CGSize(width: 1.0, height: gradientHeight), colors: [UIColor.white.withAlphaComponent(0.0), UIColor.white.withAlphaComponent(0.0), UIColor.white, UIColor.white], locations: [0.0, 0.4, 0.9, 1.0], direction: .vertical)
|
||||
self.gradientLayer.contents = gradientImage?.cgImage
|
||||
self.gradientLayer.contentsGravity = .resize
|
||||
self.fillLayer.backgroundColor = UIColor.white.cgColor
|
||||
}
|
||||
|
||||
self.gradientLayer.frame = CGRect(origin: .zero, size: CGSize(width: self.bounds.width, height: gradientHeight))
|
||||
self.fillLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: gradientHeight), size: CGSize(width: self.bounds.width, height: self.bounds.height - gradientHeight))
|
||||
}
|
||||
}
|
||||
|
@ -451,6 +451,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
|
||||
itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: "main", version: 0),
|
||||
searchState: .empty(hasResults: false),
|
||||
warpContentsOnEdges: false,
|
||||
hideBackground: false,
|
||||
displaySearchWithPlaceholder: self.presentationData.strings.EmojiSearch_SearchEmojiPlaceholder,
|
||||
searchCategories: nil,
|
||||
searchInitiallyHidden: false,
|
||||
|
@ -960,7 +960,7 @@ public final class GifPagerContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public func pagerUpdateBackground(backgroundFrame: CGRect, transition: Transition) {
|
||||
public func pagerUpdateBackground(backgroundFrame: CGRect, topPanelHeight: CGFloat, transition: Transition) {
|
||||
guard let theme = self.theme else {
|
||||
return
|
||||
}
|
||||
|
@ -282,6 +282,19 @@ public final class MediaEditorVideoExport {
|
||||
self.outputPath = outputPath
|
||||
|
||||
self.setup()
|
||||
|
||||
let _ = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil, using: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.resume()
|
||||
})
|
||||
let _ = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil, using: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.pause()
|
||||
})
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
|
@ -32,12 +32,14 @@ swift_library(
|
||||
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
|
||||
"//submodules/Components/BundleIconComponent:BundleIconComponent",
|
||||
"//submodules/TelegramUI/Components/MessageInputPanelComponent",
|
||||
"//submodules/TelegramUI/Components/ChatInputNode",
|
||||
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
|
||||
"//submodules/TooltipUI",
|
||||
"//submodules/Components/BlurredBackgroundComponent",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/TelegramUI/Components/ShareWithPeersScreen",
|
||||
"//submodules/TelegramUI/Components/CameraButtonComponent",
|
||||
"//submodules/ChatPresentationInterfaceState",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -25,6 +25,9 @@ import PresentationDataUtils
|
||||
import ContextUI
|
||||
import BundleIconComponent
|
||||
import CameraButtonComponent
|
||||
import UndoUI
|
||||
import ChatEntityKeyboardInputNode
|
||||
import ChatPresentationInterfaceState
|
||||
|
||||
enum DrawingScreenType {
|
||||
case drawing
|
||||
@ -39,12 +42,21 @@ private let saveButtonTag = GenericComponentViewTag()
|
||||
final class MediaEditorScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
public final class ExternalState {
|
||||
public fileprivate(set) var derivedInputHeight: CGFloat = 0.0
|
||||
|
||||
public init() {
|
||||
}
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let externalState: ExternalState
|
||||
let isDisplayingTool: Bool
|
||||
let isInteractingWithEntities: Bool
|
||||
let isSavingAvailable: Bool
|
||||
let hasAppeared: Bool
|
||||
let isDismissing: Bool
|
||||
let bottomSafeInset: CGFloat
|
||||
let mediaEditor: MediaEditor?
|
||||
let privacy: MediaEditorResultPrivacy
|
||||
let selectedEntity: DrawingEntity?
|
||||
@ -54,11 +66,13 @@ final class MediaEditorScreenComponent: Component {
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
externalState: ExternalState,
|
||||
isDisplayingTool: Bool,
|
||||
isInteractingWithEntities: Bool,
|
||||
isSavingAvailable: Bool,
|
||||
hasAppeared: Bool,
|
||||
isDismissing: Bool,
|
||||
bottomSafeInset: CGFloat,
|
||||
mediaEditor: MediaEditor?,
|
||||
privacy: MediaEditorResultPrivacy,
|
||||
selectedEntity: DrawingEntity?,
|
||||
@ -67,11 +81,13 @@ final class MediaEditorScreenComponent: Component {
|
||||
openTools: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.externalState = externalState
|
||||
self.isDisplayingTool = isDisplayingTool
|
||||
self.isInteractingWithEntities = isInteractingWithEntities
|
||||
self.isSavingAvailable = isSavingAvailable
|
||||
self.hasAppeared = hasAppeared
|
||||
self.isDismissing = isDismissing
|
||||
self.bottomSafeInset = bottomSafeInset
|
||||
self.mediaEditor = mediaEditor
|
||||
self.privacy = privacy
|
||||
self.selectedEntity = selectedEntity
|
||||
@ -99,6 +115,9 @@ final class MediaEditorScreenComponent: Component {
|
||||
if lhs.isDismissing != rhs.isDismissing {
|
||||
return false
|
||||
}
|
||||
if lhs.bottomSafeInset != rhs.bottomSafeInset {
|
||||
return false
|
||||
}
|
||||
if lhs.privacy != rhs.privacy {
|
||||
return false
|
||||
}
|
||||
@ -206,6 +225,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
|
||||
private let inputPanel = ComponentView<Empty>()
|
||||
private let inputPanelExternalState = MessageInputPanelComponent.ExternalState()
|
||||
private let inputPanelBackground = ComponentView<Empty>()
|
||||
|
||||
private let scrubber = ComponentView<Empty>()
|
||||
|
||||
@ -221,6 +241,17 @@ final class MediaEditorScreenComponent: Component {
|
||||
|
||||
private var isDismissed = false
|
||||
|
||||
private var isEditingCaption = false
|
||||
private var currentInputMode: MessageInputPanelComponent.InputMode = .keyboard
|
||||
|
||||
private var didInitializeInputMediaNodeDataPromise = false
|
||||
private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData?
|
||||
private var inputMediaNodeDataPromise = Promise<ChatEntityKeyboardInputNode.InputData>()
|
||||
private var inputMediaNodeDataDisposable: Disposable?
|
||||
private var inputMediaNodeStateContext = ChatEntityKeyboardInputNode.StateContext()
|
||||
private var inputMediaInteraction: ChatEntityKeyboardInputNode.Interaction?
|
||||
private var inputMediaNode: ChatEntityKeyboardInputNode?
|
||||
|
||||
private var component: MediaEditorScreenComponent?
|
||||
private weak var state: State?
|
||||
private var environment: ViewControllerComponentContainer.Environment?
|
||||
@ -235,13 +266,114 @@ final class MediaEditorScreenComponent: Component {
|
||||
self.fadeView.alpha = 0.0
|
||||
|
||||
self.addSubview(self.fadeView)
|
||||
|
||||
self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.inputMediaNodeData = value
|
||||
})
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.inputMediaNodeDataDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func setupIfNeeded() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
if !self.didInitializeInputMediaNodeDataPromise {
|
||||
self.didInitializeInputMediaNodeDataPromise = true
|
||||
|
||||
let context = component.context
|
||||
self.inputMediaNodeDataPromise.set(
|
||||
EmojiPagerContentComponent.emojiInputData(
|
||||
context: context,
|
||||
animationCache: context.animationCache,
|
||||
animationRenderer: context.animationRenderer,
|
||||
isStandalone: true,
|
||||
isStatusSelection: false,
|
||||
isReactionSelection: false,
|
||||
isEmojiSelection: false,
|
||||
hasTrending: false,
|
||||
topReactionItems: [],
|
||||
areUnicodeEmojiEnabled: true,
|
||||
areCustomEmojiEnabled: true,
|
||||
chatPeerId: nil,
|
||||
forceHasPremium: false,
|
||||
hideBackground: true
|
||||
) |> map { emoji -> ChatEntityKeyboardInputNode.InputData in
|
||||
return ChatEntityKeyboardInputNode.InputData(
|
||||
emoji: emoji,
|
||||
stickers: nil,
|
||||
gifs: nil,
|
||||
availableGifSearchEmojies: []
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction(
|
||||
sendSticker: { _, _, _, _, _, _, _, _, _ in
|
||||
return false
|
||||
},
|
||||
sendEmoji: { [weak self] text, attribute, bool1 in
|
||||
if let self {
|
||||
let _ = self
|
||||
}
|
||||
},
|
||||
sendGif: { _, _, _, _, _ in
|
||||
return false
|
||||
},
|
||||
sendBotContextResultAsGif: { _, _, _, _, _, _ in
|
||||
return false
|
||||
},
|
||||
updateChoosingSticker: { _ in },
|
||||
switchToTextInput: { [weak self] in
|
||||
if let self {
|
||||
self.currentInputMode = .keyboard
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
},
|
||||
dismissTextInput: {
|
||||
|
||||
},
|
||||
insertText: { [weak self] text in
|
||||
if let self {
|
||||
self.inputPanelExternalState.insertText(text)
|
||||
}
|
||||
},
|
||||
backwardsDeleteText: { [weak self] in
|
||||
if let self {
|
||||
self.inputPanelExternalState.deleteBackward()
|
||||
}
|
||||
},
|
||||
presentController: { [weak self] c, a in
|
||||
if let self {
|
||||
self.environment?.controller()?.present(c, in: .window(.root), with: a)
|
||||
}
|
||||
},
|
||||
presentGlobalOverlayController: { [weak self] c, a in
|
||||
if let self {
|
||||
self.environment?.controller()?.presentInGlobalOverlay(c, with: a)
|
||||
}
|
||||
},
|
||||
getNavigationController: { return nil },
|
||||
requestLayout: { _ in
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func fadePressed() {
|
||||
self.currentInputMode = .keyboard
|
||||
self.endEditing(true)
|
||||
}
|
||||
|
||||
@ -347,7 +479,6 @@ final class MediaEditorScreenComponent: Component {
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
}
|
||||
|
||||
|
||||
if case .camera = source {
|
||||
if let view = self.inputPanel.view {
|
||||
view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
|
||||
@ -453,7 +584,6 @@ final class MediaEditorScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private var isEditingCaption = false
|
||||
func update(component: MediaEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
|
||||
guard !self.isDismissed else {
|
||||
return availableSize
|
||||
@ -464,6 +594,8 @@ final class MediaEditorScreenComponent: Component {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
self.setupIfNeeded()
|
||||
|
||||
let isTablet: Bool
|
||||
if case .regular = environment.metrics.widthClass {
|
||||
isTablet = true
|
||||
@ -757,15 +889,27 @@ final class MediaEditorScreenComponent: Component {
|
||||
timeoutValue = "\(timeout ?? 1)"
|
||||
timeoutSelected = timeout != nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
var inputPanelAvailableWidth = previewSize.width
|
||||
var inputPanelAvailableHeight = 115.0
|
||||
if case .regular = environment.metrics.widthClass {
|
||||
if (self.inputPanelExternalState.isEditing || self.inputPanelExternalState.hasText) {
|
||||
inputPanelAvailableWidth += 200.0
|
||||
}
|
||||
}
|
||||
if environment.inputHeight > 0.0 || self.currentInputMode == .emoji {
|
||||
inputPanelAvailableHeight = 200.0
|
||||
}
|
||||
|
||||
let nextInputMode: MessageInputPanelComponent.InputMode
|
||||
switch self.currentInputMode {
|
||||
case .keyboard:
|
||||
nextInputMode = .emoji
|
||||
case .emoji:
|
||||
nextInputMode = .keyboard
|
||||
default:
|
||||
nextInputMode = .emoji
|
||||
}
|
||||
self.inputPanel.parentState = state
|
||||
let inputPanelSize = self.inputPanel.update(
|
||||
transition: transition,
|
||||
@ -777,6 +921,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
style: .editor,
|
||||
placeholder: "Add a caption...",
|
||||
alwaysDarkWhenHasText: false,
|
||||
nextInputMode: nextInputMode,
|
||||
areVoiceMessagesAvailable: false,
|
||||
presentController: { [weak self] c in
|
||||
guard let self, let _ = self.component else {
|
||||
@ -788,6 +933,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.currentInputMode = .keyboard
|
||||
self.endEditing(true)
|
||||
},
|
||||
setMediaRecordingActive: nil,
|
||||
@ -795,11 +941,34 @@ final class MediaEditorScreenComponent: Component {
|
||||
stopAndPreviewMediaRecording: nil,
|
||||
discardMediaRecordingPreview: nil,
|
||||
attachmentAction: nil,
|
||||
inputModeAction: { [weak self] in
|
||||
if let self {
|
||||
switch self.currentInputMode {
|
||||
case .keyboard:
|
||||
self.currentInputMode = .emoji
|
||||
case .emoji:
|
||||
self.currentInputMode = .keyboard
|
||||
default:
|
||||
self.currentInputMode = .emoji
|
||||
}
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
},
|
||||
timeoutAction: { [weak self] view in
|
||||
guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else {
|
||||
return
|
||||
}
|
||||
controller.presentTimeoutSetup(sourceView: view)
|
||||
let context = controller.context
|
||||
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak controller] peer in
|
||||
let hasPremium: Bool
|
||||
if case let .user(user) = peer {
|
||||
hasPremium = user.isPremium
|
||||
} else {
|
||||
hasPremium = false
|
||||
}
|
||||
controller?.presentTimeoutSetup(sourceView: view, hasPremium: hasPremium)
|
||||
})
|
||||
},
|
||||
forwardAction: nil,
|
||||
presentVoiceMessagesUnavailableTooltip: nil,
|
||||
@ -811,10 +980,11 @@ final class MediaEditorScreenComponent: Component {
|
||||
timeoutValue: timeoutValue,
|
||||
timeoutSelected: timeoutSelected,
|
||||
displayGradient: false,
|
||||
bottomInset: 0.0
|
||||
bottomInset: 0.0,
|
||||
hideKeyboard: self.currentInputMode == .emoji
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0)
|
||||
containerSize: CGSize(width: inputPanelAvailableWidth, height: inputPanelAvailableHeight)
|
||||
)
|
||||
|
||||
let fadeTransition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut))
|
||||
@ -836,6 +1006,29 @@ final class MediaEditorScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
var inputHeight = environment.inputHeight
|
||||
if self.inputPanelExternalState.isEditing {
|
||||
if self.currentInputMode == .emoji || inputHeight.isZero {
|
||||
inputHeight = environment.deviceMetrics.standardInputHeight(inLandscape: false)
|
||||
}
|
||||
}
|
||||
|
||||
let inputPanelBackgroundSize = self.inputPanelBackground.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(BlurredGradientComponent(position: .bottom, tag: nil)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: environment.deviceMetrics.standardInputHeight(inLandscape: false) + 100.0)
|
||||
)
|
||||
if let inputPanelBackgroundView = self.inputPanelBackground.view {
|
||||
if inputPanelBackgroundView.superview == nil {
|
||||
self.addSubview(inputPanelBackgroundView)
|
||||
}
|
||||
let isVisible = inputHeight > 44.0
|
||||
transition.setFrame(view: inputPanelBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: isVisible ? availableSize.height - inputPanelBackgroundSize.height : availableSize.height), size: inputPanelBackgroundSize))
|
||||
transition.setAlpha(view: inputPanelBackgroundView, alpha: isVisible ? 1.0 : 0.0, delay: isVisible ? 0.0 : 0.4)
|
||||
}
|
||||
|
||||
|
||||
var isEditingTextEntity = false
|
||||
var sizeSliderVisible = false
|
||||
var sizeValue: CGFloat?
|
||||
@ -845,11 +1038,9 @@ final class MediaEditorScreenComponent: Component {
|
||||
sizeValue = textEntity.fontSize
|
||||
}
|
||||
|
||||
var inputPanelOffset: CGFloat = 0.0
|
||||
var inputPanelBottomInset: CGFloat = scrubberBottomInset
|
||||
if environment.inputHeight > 0.0 {
|
||||
inputPanelBottomInset = environment.inputHeight - environment.safeInsets.bottom
|
||||
inputPanelOffset = inputPanelBottomInset
|
||||
if inputHeight > 0.0 {
|
||||
inputPanelBottomInset = inputHeight - environment.safeInsets.bottom
|
||||
}
|
||||
let inputPanelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - inputPanelSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - inputPanelBottomInset - inputPanelSize.height - 3.0), size: inputPanelSize)
|
||||
if let inputPanelView = self.inputPanel.view {
|
||||
@ -910,7 +1101,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
)
|
||||
} else {
|
||||
privacyButtonFrame = CGRect(
|
||||
origin: CGPoint(x: 16.0, y: environment.safeInsets.top + 20.0 - inputPanelOffset),
|
||||
origin: CGPoint(x: 16.0, y: environment.safeInsets.top + 20.0),
|
||||
size: privacyButtonSize
|
||||
)
|
||||
}
|
||||
@ -969,7 +1160,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
containerSize: CGSize(width: 44.0, height: 44.0)
|
||||
)
|
||||
let saveButtonFrame = CGRect(
|
||||
origin: CGPoint(x: availableSize.width - 20.0 - saveButtonSize.width, y: environment.safeInsets.top + 20.0 - inputPanelOffset),
|
||||
origin: CGPoint(x: availableSize.width - 20.0 - saveButtonSize.width, y: environment.safeInsets.top + 20.0),
|
||||
size: saveButtonSize
|
||||
)
|
||||
if let saveButtonView = self.saveButton.view {
|
||||
@ -1042,7 +1233,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
containerSize: CGSize(width: 44.0, height: 44.0)
|
||||
)
|
||||
let muteButtonFrame = CGRect(
|
||||
origin: CGPoint(x: availableSize.width - 20.0 - muteButtonSize.width - 50.0, y: environment.safeInsets.top + 20.0 - inputPanelOffset),
|
||||
origin: CGPoint(x: availableSize.width - 20.0 - muteButtonSize.width - 50.0, y: environment.safeInsets.top + 20.0),
|
||||
size: muteButtonSize
|
||||
)
|
||||
if let muteButtonView = self.muteButton.view {
|
||||
@ -1080,7 +1271,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
containerSize: CGSize(width: 44.0, height: 44.0)
|
||||
)
|
||||
let settingsButtonFrame = CGRect(
|
||||
origin: CGPoint(x: floorToScreenPixels((availableSize.width - settingsButtonSize.width) / 2.0), y: environment.safeInsets.top + 20.0 - inputPanelOffset),
|
||||
origin: CGPoint(x: floorToScreenPixels((availableSize.width - settingsButtonSize.width) / 2.0), y: environment.safeInsets.top + 20.0),
|
||||
size: settingsButtonSize
|
||||
)
|
||||
if let settingsButtonView = self.settingsButton.view {
|
||||
@ -1172,7 +1363,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 30.0, height: 240.0)
|
||||
)
|
||||
let bottomInset: CGFloat = environment.inputHeight > 0.0 ? environment.inputHeight : environment.safeInsets.bottom
|
||||
let bottomInset: CGFloat = inputHeight > 0.0 ? inputHeight : environment.safeInsets.bottom
|
||||
let textSizeFrame = CGRect(
|
||||
origin: CGPoint(x: 0.0, y: environment.safeInsets.top + (availableSize.height - environment.safeInsets.top - bottomInset) / 2.0 - textSizeSize.height / 2.0),
|
||||
size: textSizeSize
|
||||
@ -1185,6 +1376,78 @@ final class MediaEditorScreenComponent: Component {
|
||||
transition.setBounds(view: textSizeView, bounds: CGRect(origin: .zero, size: textSizeFrame.size))
|
||||
transition.setAlpha(view: textSizeView, alpha: sizeSliderVisible && !component.isInteractingWithEntities ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData {
|
||||
let inputMediaNode: ChatEntityKeyboardInputNode
|
||||
if let current = self.inputMediaNode {
|
||||
inputMediaNode = current
|
||||
} else {
|
||||
inputMediaNode = ChatEntityKeyboardInputNode(
|
||||
context: component.context,
|
||||
currentInputData: inputData,
|
||||
updatedInputData: self.inputMediaNodeDataPromise.get(),
|
||||
defaultToEmojiTab: true,
|
||||
opaqueTopPanelBackground: false,
|
||||
interaction: self.inputMediaInteraction,
|
||||
chatPeerId: nil,
|
||||
stateContext: self.inputMediaNodeStateContext
|
||||
)
|
||||
inputMediaNode.externalTopPanelContainerImpl = nil
|
||||
if let inputPanelView = self.inputPanel.view {
|
||||
self.insertSubview(inputMediaNode.view, belowSubview: inputPanelView)
|
||||
}
|
||||
self.inputMediaNode = inputMediaNode
|
||||
}
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
|
||||
let presentationInterfaceState = ChatPresentationInterfaceState(
|
||||
chatWallpaper: .builtin(WallpaperSettings()),
|
||||
theme: presentationData.theme,
|
||||
strings: presentationData.strings,
|
||||
dateTimeFormat: presentationData.dateTimeFormat,
|
||||
nameDisplayOrder: presentationData.nameDisplayOrder,
|
||||
limitsConfiguration: component.context.currentLimitsConfiguration.with { $0 },
|
||||
fontSize: presentationData.chatFontSize,
|
||||
bubbleCorners: presentationData.chatBubbleCorners,
|
||||
accountPeerId: component.context.account.peerId,
|
||||
mode: .standard(previewing: false),
|
||||
chatLocation: .peer(id: component.context.account.peerId),
|
||||
subject: nil,
|
||||
peerNearbyData: nil,
|
||||
greetingData: nil,
|
||||
pendingUnpinnedAllMessages: false,
|
||||
activeGroupCallInfo: nil,
|
||||
hasActiveGroupCall: false,
|
||||
importState: nil,
|
||||
threadData: nil,
|
||||
isGeneralThreadClosed: nil
|
||||
)
|
||||
|
||||
let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: component.bottomSafeInset, standardInputHeight: environment.deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: environment.inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: environment.metrics, deviceMetrics: environment.deviceMetrics, isVisible: true, isExpanded: false)
|
||||
let inputNodeHeight = heightAndOverflow.0
|
||||
let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight))
|
||||
transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
|
||||
} else if let inputMediaNode = self.inputMediaNode {
|
||||
self.inputMediaNode = nil
|
||||
|
||||
var targetFrame = inputMediaNode.frame
|
||||
if inputHeight > 0.0 {
|
||||
targetFrame.origin.y = availableSize.height - inputHeight
|
||||
} else {
|
||||
targetFrame.origin.y = availableSize.height
|
||||
}
|
||||
transition.setFrame(view: inputMediaNode.view, frame: targetFrame, completion: { [weak inputMediaNode] _ in
|
||||
if let inputMediaNode {
|
||||
Queue.mainQueue().after(0.3) {
|
||||
inputMediaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak inputMediaNode] _ in
|
||||
inputMediaNode?.view.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
component.externalState.derivedInputHeight = inputHeight
|
||||
|
||||
return availableSize
|
||||
}
|
||||
@ -1263,6 +1526,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
|
||||
private let backgroundDimView: UIView
|
||||
fileprivate let containerView: UIView
|
||||
fileprivate let componentExternalState = MediaEditorScreenComponent.ExternalState()
|
||||
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
|
||||
fileprivate let storyPreview: ComponentView<Empty>
|
||||
fileprivate let toolValue: ComponentView<Empty>
|
||||
@ -2231,9 +2495,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
}
|
||||
let bottomInset = layout.size.height - previewSize.height - topInset
|
||||
|
||||
var inputHeight = layout.inputHeight ?? 0.0
|
||||
var layoutInputHeight = layout.inputHeight ?? 0.0
|
||||
if self.stickerScreen != nil {
|
||||
inputHeight = 0.0
|
||||
layoutInputHeight = 0.0
|
||||
}
|
||||
|
||||
let environment = ViewControllerComponentContainer.Environment(
|
||||
@ -2245,12 +2509,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
bottom: bottomInset,
|
||||
right: layout.safeInsets.right
|
||||
),
|
||||
inputHeight: inputHeight,
|
||||
inputHeight: layoutInputHeight,
|
||||
metrics: layout.metrics,
|
||||
deviceMetrics: layout.deviceMetrics,
|
||||
orientation: nil,
|
||||
isVisible: true,
|
||||
theme: self.presentationData.theme,
|
||||
theme: defaultDarkPresentationTheme,
|
||||
strings: self.presentationData.strings,
|
||||
dateTimeFormat: self.presentationData.dateTimeFormat,
|
||||
controller: { [weak self] in
|
||||
@ -2267,11 +2531,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
component: AnyComponent(
|
||||
MediaEditorScreenComponent(
|
||||
context: self.context,
|
||||
externalState: self.componentExternalState,
|
||||
isDisplayingTool: self.isDisplayingTool,
|
||||
isInteractingWithEntities: self.isInteractingWithEntities,
|
||||
isSavingAvailable: controller.isSavingAvailable,
|
||||
hasAppeared: self.hasAppeared,
|
||||
isDismissing: self.isDismissing,
|
||||
bottomSafeInset: layout.intrinsicInsets.bottom,
|
||||
mediaEditor: self.mediaEditor,
|
||||
privacy: controller.state.privacy,
|
||||
selectedEntity: self.isDisplayingTool ? nil : self.entitiesView.selectedEntityView?.entity,
|
||||
@ -2309,7 +2575,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
}
|
||||
}
|
||||
self.stickerScreen = controller
|
||||
self.controller?.present(controller, in: .current)
|
||||
self.controller?.present(controller, in: .window(.root))
|
||||
return
|
||||
case .text:
|
||||
let textEntity = DrawingTextEntity(text: NSAttributedString(), style: .regular, animation: .none, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: DrawingColor(color: .white))
|
||||
@ -2352,7 +2618,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
self?.interaction?.activate()
|
||||
self?.entitiesView.selectEntity(nil)
|
||||
}
|
||||
self.controller?.present(controller, in: .current)
|
||||
self.controller?.present(controller, in: .window(.root))
|
||||
self.animateOutToTool()
|
||||
}
|
||||
}
|
||||
@ -2368,7 +2634,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
self.animateInFromTool()
|
||||
}
|
||||
}
|
||||
self.controller?.present(controller, in: .current)
|
||||
self.controller?.present(controller, in: .window(.root))
|
||||
self.animateOutToTool()
|
||||
}
|
||||
}
|
||||
@ -2388,6 +2654,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.dismissOffset), size: componentSize))
|
||||
}
|
||||
|
||||
let inputHeight = self.componentExternalState.derivedInputHeight
|
||||
|
||||
let storyPreviewSize = self.storyPreview.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
@ -2441,7 +2709,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
if self.entitiesView.selectedEntityView != nil || self.isDisplayingTool {
|
||||
bottomInputOffset = inputHeight / 2.0
|
||||
} else {
|
||||
bottomInputOffset = inputHeight - bottomInset - 17.0
|
||||
bottomInputOffset = 0.0 //inputHeight - bottomInset - 17.0
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2461,6 +2729,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
|
||||
self.interaction?.containerLayoutUpdated(layout: layout, transition: transition)
|
||||
|
||||
var layout = layout
|
||||
layout.intrinsicInsets.bottom = bottomInset + 60.0
|
||||
controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
if isFirstTime {
|
||||
self.animateIn()
|
||||
}
|
||||
@ -2575,6 +2847,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.automaticallyControlPresentationContextLayout = false
|
||||
|
||||
self.navigationPresentation = .flatModal
|
||||
|
||||
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
@ -2719,7 +2993,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
})
|
||||
}
|
||||
|
||||
func presentTimeoutSetup(sourceView: UIView) {
|
||||
func presentTimeoutSetup(sourceView: UIView, hasPremium: Bool) {
|
||||
self.hapticFeedback.impact(.light)
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
@ -2755,18 +3029,34 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
switch self.state.privacy {
|
||||
case .story:
|
||||
items.append(.action(ContextMenuActionItem(text: "6 Hours", icon: { theme in
|
||||
return currentValue == 3600 * 6 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
|
||||
}, action: { _, a in
|
||||
if !hasPremium {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: theme.contextMenu.secondaryColor)
|
||||
} else {
|
||||
return currentValue == 3600 * 6 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
|
||||
}
|
||||
}, action: { [weak self] _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(3600 * 6, false)
|
||||
if hasPremium {
|
||||
updateTimeout(3600 * 6, false)
|
||||
} else {
|
||||
self?.presentTimeoutPremiumSuggestion(3600 * 6)
|
||||
}
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "12 Hours", icon: { theme in
|
||||
return currentValue == 3600 * 12 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
|
||||
}, action: { _, a in
|
||||
if !hasPremium {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: theme.contextMenu.secondaryColor)
|
||||
} else {
|
||||
return currentValue == 3600 * 12 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
|
||||
}
|
||||
}, action: { [weak self] _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(3600 * 12, false)
|
||||
if hasPremium {
|
||||
updateTimeout(3600 * 12, false)
|
||||
} else {
|
||||
self?.presentTimeoutPremiumSuggestion(3600 * 12)
|
||||
}
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "24 Hours", icon: { theme in
|
||||
return currentValue == 86400 && !currentArchived ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
|
||||
@ -2776,11 +3066,19 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
updateTimeout(86400, false)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "48 Hours", icon: { theme in
|
||||
return currentValue == 86400 * 2 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
|
||||
}, action: { _, a in
|
||||
if !hasPremium {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: theme.contextMenu.secondaryColor)
|
||||
} else {
|
||||
return currentValue == 86400 * 2 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
|
||||
}
|
||||
}, action: { [weak self] _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(86400 * 2, false)
|
||||
if hasPremium {
|
||||
updateTimeout(86400 * 2, false)
|
||||
} else {
|
||||
self?.presentTimeoutPremiumSuggestion(86400 * 2)
|
||||
}
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "Keep Always", icon: { theme in
|
||||
return currentArchived ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
|
||||
@ -2790,7 +3088,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
updateTimeout(86400, true)
|
||||
})))
|
||||
items.append(.separator)
|
||||
items.append(.action(ContextMenuActionItem(text: "Select 'Keep Always' to always show the story in your profile.", textLayout: .multiline, textFont: .small, icon: { theme in
|
||||
items.append(.action(ContextMenuActionItem(text: "Select 'Keep Always' to show the story on your page.", textLayout: .multiline, textFont: .small, icon: { theme in
|
||||
return nil
|
||||
}, action: { _, _ in
|
||||
})))
|
||||
@ -2837,6 +3135,23 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
self.present(contextController, in: .window(.root))
|
||||
}
|
||||
|
||||
private func presentTimeoutPremiumSuggestion(_ timeout: Int32) {
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let timeoutString = presentationData.strings.MuteExpires_Hours(max(1, timeout / (60 * 60)))
|
||||
let text = "Subscribe to **Telegram Premium** to make your stories disappear \(timeoutString)."
|
||||
|
||||
let context = self.context
|
||||
let controller = UndoOverlayController(presentationData: presentationData, content: .autoDelete(isOn: true, title: nil, text: text, customUndoText: "More"), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { [weak self] action in
|
||||
if case .undo = action, let self {
|
||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings)
|
||||
self.push(controller)
|
||||
}
|
||||
return false }
|
||||
)
|
||||
self.present(controller, in: .current)
|
||||
}
|
||||
|
||||
func maybePresentDiscardAlert() {
|
||||
self.hapticFeedback.impact(.light)
|
||||
if "".isEmpty {
|
||||
@ -3531,3 +3846,84 @@ private final class ToolValueComponent: Component {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class BlurredGradientComponent: Component {
|
||||
public enum Position {
|
||||
case top
|
||||
case bottom
|
||||
}
|
||||
|
||||
let position: Position
|
||||
let tag: AnyObject?
|
||||
|
||||
public init(
|
||||
position: Position,
|
||||
tag: AnyObject?
|
||||
) {
|
||||
self.position = position
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
public static func ==(lhs: BlurredGradientComponent, rhs: BlurredGradientComponent) -> Bool {
|
||||
if lhs.position != rhs.position {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: BlurredBackgroundView, ComponentTaggedView {
|
||||
private var component: BlurredGradientComponent?
|
||||
|
||||
public func matches(tag: Any) -> Bool {
|
||||
if let component = self.component, let componentTag = component.tag {
|
||||
let tag = tag as AnyObject
|
||||
if componentTag === tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private var gradientMask = UIImageView()
|
||||
private var gradientForeground = SimpleGradientLayer()
|
||||
|
||||
public func update(component: BlurredGradientComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
|
||||
self.updateColor(color: UIColor(rgb: 0x000000, alpha: component.position == .top ? 0.15 : 0.25), transition: transition.containedViewLayoutTransition)
|
||||
|
||||
if self.mask == nil {
|
||||
self.mask = self.gradientMask
|
||||
self.gradientMask.image = generateGradientImage(
|
||||
size: CGSize(width: 1.0, height: availableSize.height),
|
||||
colors: [UIColor(rgb: 0xffffff, alpha: 1.0), UIColor(rgb: 0xffffff, alpha: 1.0), UIColor(rgb: 0xffffff, alpha: 0.0)],
|
||||
locations: component.position == .top ? [0.0, 0.8, 1.0] : [1.0, 0.20, 0.0],
|
||||
direction: .vertical
|
||||
)
|
||||
|
||||
self.gradientForeground.colors = [UIColor(rgb: 0x000000, alpha: 0.35).cgColor, UIColor(rgb: 0x000000, alpha: 0.0).cgColor]
|
||||
self.gradientForeground.startPoint = CGPoint(x: 0.5, y: component.position == .top ? 0.0 : 1.0)
|
||||
self.gradientForeground.endPoint = CGPoint(x: 0.5, y: component.position == .top ? 1.0 : 0.0)
|
||||
|
||||
self.layer.addSublayer(self.gradientForeground)
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.gradientMask, frame: CGRect(origin: .zero, size: availableSize))
|
||||
transition.setFrame(layer: self.gradientForeground, frame: CGRect(origin: .zero, size: availableSize))
|
||||
|
||||
self.update(size: availableSize, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(color: nil, enableBlur: true)
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
@ -251,6 +251,7 @@ final class StoryPreviewComponent: Component {
|
||||
style: .story,
|
||||
placeholder: "Reply Privately...",
|
||||
alwaysDarkWhenHasText: false,
|
||||
nextInputMode: nil,
|
||||
areVoiceMessagesAvailable: false,
|
||||
presentController: { _ in
|
||||
},
|
||||
@ -261,6 +262,7 @@ final class StoryPreviewComponent: Component {
|
||||
stopAndPreviewMediaRecording: nil,
|
||||
discardMediaRecordingPreview: nil,
|
||||
attachmentAction: { },
|
||||
inputModeAction: nil,
|
||||
timeoutAction: nil,
|
||||
forwardAction: nil,
|
||||
presentVoiceMessagesUnavailableTooltip: nil,
|
||||
@ -272,7 +274,8 @@ final class StoryPreviewComponent: Component {
|
||||
timeoutValue: nil,
|
||||
timeoutSelected: false,
|
||||
displayGradient: false,
|
||||
bottomInset: 0.0
|
||||
bottomInset: 0.0,
|
||||
hideKeyboard: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 200.0)
|
||||
|
@ -24,6 +24,9 @@ swift_library(
|
||||
"//submodules/Components/HierarchyTrackingLayer",
|
||||
"//submodules/TelegramUI/Components/AudioWaveformComponent",
|
||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||
"//submodules/ChatContextQuery",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -0,0 +1,375 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import TelegramPresentationData
|
||||
import PeerListItemComponent
|
||||
|
||||
extension ChatPresentationInputQueryResult {
|
||||
var count: Int {
|
||||
switch self {
|
||||
case let .stickers(stickers):
|
||||
return stickers.count
|
||||
case let .hashtags(hashtags):
|
||||
return hashtags.count
|
||||
case let .mentions(peers):
|
||||
return peers.count
|
||||
case let .commands(commands):
|
||||
return commands.count
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final class ContextResultPanelComponent: Component {
|
||||
final class ExternalState {
|
||||
fileprivate(set) var minimizedHeight: CGFloat = 0.0
|
||||
fileprivate(set) var effectiveHeight: CGFloat = 0.0
|
||||
|
||||
init() {
|
||||
}
|
||||
}
|
||||
|
||||
enum ResultAction {
|
||||
case mention(EnginePeer)
|
||||
case hashtag(String)
|
||||
}
|
||||
|
||||
let externalState: ExternalState
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let results: ChatPresentationInputQueryResult
|
||||
let action: (ResultAction) -> Void
|
||||
|
||||
init(
|
||||
externalState: ExternalState,
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
results: ChatPresentationInputQueryResult,
|
||||
action: @escaping (ResultAction) -> Void
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.results = results
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: ContextResultPanelComponent, rhs: ContextResultPanelComponent) -> Bool {
|
||||
if lhs.externalState !== rhs.externalState {
|
||||
return false
|
||||
}
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.results != rhs.results {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private struct ItemLayout: Equatable {
|
||||
var containerSize: CGSize
|
||||
var bottomInset: CGFloat
|
||||
var topInset: CGFloat
|
||||
var sideInset: CGFloat
|
||||
var itemHeight: CGFloat
|
||||
var itemCount: Int
|
||||
|
||||
var contentSize: CGSize
|
||||
|
||||
init(containerSize: CGSize, bottomInset: CGFloat, topInset: CGFloat, sideInset: CGFloat, itemHeight: CGFloat, itemCount: Int) {
|
||||
self.containerSize = containerSize
|
||||
self.bottomInset = bottomInset
|
||||
self.topInset = topInset
|
||||
self.sideInset = sideInset
|
||||
self.itemHeight = itemHeight
|
||||
self.itemCount = itemCount
|
||||
|
||||
self.contentSize = CGSize(width: containerSize.width, height: topInset + CGFloat(itemCount) * itemHeight + bottomInset)
|
||||
}
|
||||
|
||||
func visibleItems(for rect: CGRect) -> Range<Int>? {
|
||||
let offsetRect = rect.offsetBy(dx: 0.0, dy: -self.topInset)
|
||||
var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight)))
|
||||
minVisibleRow = max(0, minVisibleRow)
|
||||
let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight)))
|
||||
|
||||
let minVisibleIndex = minVisibleRow
|
||||
let maxVisibleIndex = maxVisibleRow
|
||||
|
||||
if maxVisibleIndex >= minVisibleIndex {
|
||||
return minVisibleIndex ..< (maxVisibleIndex + 1)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func itemFrame(for index: Int) -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: 0.0, y: self.topInset + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerSize.width, height: self.itemHeight))
|
||||
}
|
||||
}
|
||||
|
||||
private final class ScrollView: UIScrollView {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
if result === self {
|
||||
return nil
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
private let scrollView: UIScrollView
|
||||
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
private let measureItem = ComponentView<Empty>()
|
||||
|
||||
private var visibleItems: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
|
||||
private var ignoreScrolling = false
|
||||
|
||||
private var component: ContextResultPanelComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||
|
||||
self.scrollView = ScrollView()
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.delaysContentTouches = false
|
||||
self.scrollView.showsVerticalScrollIndicator = true
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
self.scrollView.indicatorStyle = .white
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.scrollView.delegate = self
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
self.addSubview(self.scrollView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func animateIn(transition: Transition) {
|
||||
let offset = self.scrollView.contentOffset.y * -1.0 + 10.0
|
||||
Transition.immediate.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset))
|
||||
transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: 0.0))
|
||||
}
|
||||
|
||||
func animateOut(transition: Transition, completion: @escaping () -> Void) {
|
||||
let offset = self.scrollView.contentOffset.y * -1.0 + 10.0
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset), completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: Transition) {
|
||||
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -200.0)
|
||||
|
||||
var synchronousLoad = false
|
||||
if let hint = transition.userData(PeerListItemComponent.TransitionHint.self) {
|
||||
synchronousLoad = hint.synchronousLoad
|
||||
}
|
||||
|
||||
var validIds: [AnyHashable] = []
|
||||
if let range = itemLayout.visibleItems(for: visibleBounds), case let .mentions(peers) = component.results {
|
||||
for index in range.lowerBound ..< range.upperBound {
|
||||
guard index < peers.count else {
|
||||
continue
|
||||
}
|
||||
|
||||
let itemFrame = itemLayout.itemFrame(for: index)
|
||||
|
||||
var itemTransition = transition
|
||||
let peer = peers[index]
|
||||
validIds.append(peer.id)
|
||||
|
||||
let visibleItem: ComponentView<Empty>
|
||||
if let current = self.visibleItems[peer.id] {
|
||||
visibleItem = current
|
||||
} else {
|
||||
if !transition.animation.isImmediate {
|
||||
itemTransition = .immediate
|
||||
}
|
||||
visibleItem = ComponentView()
|
||||
self.visibleItems[peer.id] = visibleItem
|
||||
}
|
||||
|
||||
let _ = visibleItem.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(PeerListItemComponent(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
style: .compact,
|
||||
sideInset: itemLayout.sideInset,
|
||||
title: peer.displayTitle(strings: component.strings, displayOrder: .firstLast),
|
||||
peer: peer,
|
||||
subtitle: peer.addressName.flatMap { "@\($0)" },
|
||||
subtitleAccessory: .none,
|
||||
selectionState: .none,
|
||||
hasNext: index != peers.count - 1,
|
||||
action: { [weak self] peer in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.action(.mention(peer))
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: itemFrame.size
|
||||
)
|
||||
if let itemView = visibleItem.view {
|
||||
var animateIn = false
|
||||
if itemView.superview == nil {
|
||||
animateIn = true
|
||||
self.scrollView.addSubview(itemView)
|
||||
}
|
||||
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||
|
||||
if animateIn, synchronousLoad {
|
||||
itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [AnyHashable] = []
|
||||
for (id, visibleItem) in self.visibleItems {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
if let itemView = visibleItem.view {
|
||||
itemView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.visibleItems.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
let backgroundSize = CGSize(width: self.scrollView.frame.width, height: self.scrollView.frame.height + 20.0)
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: max(0.0, self.scrollView.contentOffset.y * -1.0)), size: backgroundSize))
|
||||
self.backgroundView.update(size: backgroundSize, cornerRadius: 11.0, transition: transition.containedViewLayoutTransition)
|
||||
}
|
||||
|
||||
func update(component: ContextResultPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
//let itemUpdated = self.component?.results != component.results
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let minimizedHeight = min(availableSize.height, 500.0)
|
||||
|
||||
let sideInset: CGFloat = 3.0
|
||||
self.backgroundView.updateColor(color: UIColor(white: 0.0, alpha: 0.7), transition: transition.containedViewLayoutTransition)
|
||||
|
||||
let measureItemSize = self.measureItem.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(PeerListItemComponent(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
style: .compact,
|
||||
sideInset: sideInset,
|
||||
title: "AAAAAAAAAAAA",
|
||||
peer: nil,
|
||||
subtitle: "BBBBBBB",
|
||||
subtitleAccessory: .none,
|
||||
selectionState: .none,
|
||||
hasNext: true,
|
||||
action: { _ in
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
)
|
||||
|
||||
let itemLayout = ItemLayout(
|
||||
containerSize: CGSize(width: availableSize.width, height: minimizedHeight),
|
||||
bottomInset: 0.0,
|
||||
topInset: 0.0,
|
||||
sideInset: sideInset,
|
||||
itemHeight: measureItemSize.height,
|
||||
itemCount: component.results.count
|
||||
)
|
||||
self.itemLayout = itemLayout
|
||||
|
||||
let scrollContentSize = itemLayout.contentSize
|
||||
|
||||
self.ignoreScrolling = true
|
||||
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: minimizedHeight)))
|
||||
|
||||
let visibleTopContentHeight = min(scrollContentSize.height, measureItemSize.height * 3.5 + 19.0)
|
||||
let topInset = availableSize.height - visibleTopContentHeight
|
||||
|
||||
let scrollContentInsets = UIEdgeInsets(top: topInset, left: 0.0, bottom: 19.0, right: 0.0)
|
||||
let scrollIndicatorInsets = UIEdgeInsets(top: topInset + 17.0, left: 0.0, bottom: 19.0, right: 0.0)
|
||||
if self.scrollView.contentInset != scrollContentInsets {
|
||||
self.scrollView.contentInset = scrollContentInsets
|
||||
}
|
||||
if self.scrollView.scrollIndicatorInsets != scrollIndicatorInsets {
|
||||
self.scrollView.scrollIndicatorInsets = scrollIndicatorInsets
|
||||
}
|
||||
if self.scrollView.contentSize != scrollContentSize {
|
||||
self.scrollView.contentSize = scrollContentSize
|
||||
}
|
||||
|
||||
self.ignoreScrolling = false
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
// component.externalState.minimizedHeight = minimizedHeight
|
||||
|
||||
// let effectiveHeight: CGFloat = minimizedHeight * dismissFraction + (1.0 - dismissFraction) * (60.0 + component.safeInsets.bottom + 1.0)
|
||||
// component.externalState.effectiveHeight = min(minimizedHeight, max(0.0, effectiveHeight))
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import TextFieldComponent
|
||||
import ChatContextQuery
|
||||
import AccountContext
|
||||
|
||||
func textInputStateContextQueryRangeAndType(inputState: TextFieldComponent.InputState) -> [(NSRange, PossibleContextQueryTypes, NSRange?)] {
|
||||
return textInputStateContextQueryRangeAndType(inputText: inputState.inputText, selectionRange: inputState.selectionRange)
|
||||
}
|
||||
|
||||
func inputContextQueries(_ inputState: TextFieldComponent.InputState) -> [ChatPresentationInputQuery] {
|
||||
let inputString: NSString = inputState.inputText.string as NSString
|
||||
var result: [ChatPresentationInputQuery] = []
|
||||
for (possibleQueryRange, possibleTypes, additionalStringRange) in textInputStateContextQueryRangeAndType(inputText: inputState.inputText, selectionRange: inputState.selectionRange) {
|
||||
let query = inputString.substring(with: possibleQueryRange)
|
||||
if possibleTypes == [.emoji] {
|
||||
result.append(.emoji(query.basicEmoji.0))
|
||||
} else if possibleTypes == [.hashtag] {
|
||||
result.append(.hashtag(query))
|
||||
} else if possibleTypes == [.mention] {
|
||||
let types: ChatInputQueryMentionTypes = [.members]
|
||||
// if possibleQueryRange.lowerBound == 1 {
|
||||
// types.insert(.contextBots)
|
||||
// }
|
||||
result.append(.mention(query: query, types: types))
|
||||
} else if possibleTypes == [.command] {
|
||||
result.append(.command(query))
|
||||
} else if possibleTypes == [.contextRequest], let additionalStringRange = additionalStringRange {
|
||||
let additionalString = inputString.substring(with: additionalStringRange)
|
||||
result.append(.contextRequest(addressName: query, query: additionalString))
|
||||
}
|
||||
// else if possibleTypes == [.emojiSearch], !query.isEmpty, let inputLanguage = chatPresentationInterfaceState.interfaceState.inputLanguage {
|
||||
// result.append(.emojiSearch(query: query, languageCode: inputLanguage, range: possibleQueryRange))
|
||||
// }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func contextQueryResultState(context: AccountContext, inputState: TextFieldComponent.InputState, currentQueryStates: inout [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)]) -> [ChatPresentationInputQueryKind: ChatContextQueryUpdate] {
|
||||
let inputQueries = inputContextQueries(inputState).filter({ query in
|
||||
switch query {
|
||||
case .contextRequest, .command, .emoji:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
var updates: [ChatPresentationInputQueryKind: ChatContextQueryUpdate] = [:]
|
||||
|
||||
for query in inputQueries {
|
||||
let previousQuery = currentQueryStates[query.kind]?.0
|
||||
if previousQuery != query {
|
||||
let signal = updatedContextQueryResultStateForQuery(context: context, inputQuery: query, previousQuery: previousQuery)
|
||||
updates[query.kind] = .update(query, signal)
|
||||
}
|
||||
}
|
||||
|
||||
for currentQueryKind in currentQueryStates.keys {
|
||||
var found = false
|
||||
inner: for query in inputQueries {
|
||||
if query.kind == currentQueryKind {
|
||||
found = true
|
||||
break inner
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
updates[currentQueryKind] = .remove
|
||||
}
|
||||
}
|
||||
|
||||
return updates
|
||||
}
|
||||
|
||||
private func updatedContextQueryResultStateForQuery(context: AccountContext, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> {
|
||||
switch inputQuery {
|
||||
case let .hashtag(query):
|
||||
var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete()
|
||||
if let previousQuery = previousQuery {
|
||||
switch previousQuery {
|
||||
case .hashtag:
|
||||
break
|
||||
default:
|
||||
signal = .single({ _ in return .hashtags([]) })
|
||||
}
|
||||
} else {
|
||||
signal = .single({ _ in return .hashtags([]) })
|
||||
}
|
||||
|
||||
let hashtags: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.messages.recentlyUsedHashtags()
|
||||
|> map { hashtags -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
||||
let normalizedQuery = query.lowercased()
|
||||
var result: [String] = []
|
||||
for hashtag in hashtags {
|
||||
if hashtag.lowercased().hasPrefix(normalizedQuery) {
|
||||
result.append(hashtag)
|
||||
}
|
||||
}
|
||||
return { _ in return .hashtags(result) }
|
||||
}
|
||||
|> castError(ChatContextQueryError.self)
|
||||
|
||||
return signal |> then(hashtags)
|
||||
case let .mention(query, _):
|
||||
var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete()
|
||||
if let previousQuery = previousQuery {
|
||||
switch previousQuery {
|
||||
case .mention:
|
||||
break
|
||||
default:
|
||||
signal = .single({ _ in return .mentions([]) })
|
||||
}
|
||||
} else {
|
||||
signal = .single({ _ in return .mentions([]) })
|
||||
}
|
||||
|
||||
let normalizedQuery = query.lowercased()
|
||||
let peers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.contacts.searchLocalPeers(query: normalizedQuery)
|
||||
|> map { peersAndPresences -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
||||
let peers = peersAndPresences.filter { peer in
|
||||
if let peer = peer.peer, case .user = peer {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}.compactMap { $0.peer }
|
||||
return { _ in return .mentions(peers) }
|
||||
}
|
||||
|> castError(ChatContextQueryError.self)
|
||||
|
||||
return signal |> then(peers)
|
||||
default:
|
||||
return .complete()
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import AppBundle
|
||||
import TextFieldComponent
|
||||
import BundleIconComponent
|
||||
@ -9,16 +10,28 @@ import AccountContext
|
||||
import TelegramPresentationData
|
||||
import ChatPresentationInterfaceState
|
||||
import LottieComponent
|
||||
import ChatContextQuery
|
||||
import TextFormat
|
||||
|
||||
public final class MessageInputPanelComponent: Component {
|
||||
public enum Style {
|
||||
case story
|
||||
case editor
|
||||
}
|
||||
|
||||
public enum InputMode: Hashable {
|
||||
case keyboard
|
||||
case stickers
|
||||
case emoji
|
||||
}
|
||||
|
||||
public final class ExternalState {
|
||||
public fileprivate(set) var isEditing: Bool = false
|
||||
public fileprivate(set) var hasText: Bool = false
|
||||
|
||||
public fileprivate(set) var insertText: (NSAttributedString) -> Void = { _ in }
|
||||
public fileprivate(set) var deleteBackward: () -> Void = { }
|
||||
|
||||
public init() {
|
||||
}
|
||||
}
|
||||
@ -30,6 +43,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
public let style: Style
|
||||
public let placeholder: String
|
||||
public let alwaysDarkWhenHasText: Bool
|
||||
public let nextInputMode: InputMode?
|
||||
public let areVoiceMessagesAvailable: Bool
|
||||
public let presentController: (ViewController) -> Void
|
||||
public let sendMessageAction: () -> Void
|
||||
@ -38,6 +52,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
public let stopAndPreviewMediaRecording: (() -> Void)?
|
||||
public let discardMediaRecordingPreview: (() -> Void)?
|
||||
public let attachmentAction: (() -> Void)?
|
||||
public let inputModeAction: (() -> Void)?
|
||||
public let timeoutAction: ((UIView) -> Void)?
|
||||
public let forwardAction: (() -> Void)?
|
||||
public let presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?
|
||||
@ -50,6 +65,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
public let timeoutSelected: Bool
|
||||
public let displayGradient: Bool
|
||||
public let bottomInset: CGFloat
|
||||
public let hideKeyboard: Bool
|
||||
|
||||
public init(
|
||||
externalState: ExternalState,
|
||||
@ -59,6 +75,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
style: Style,
|
||||
placeholder: String,
|
||||
alwaysDarkWhenHasText: Bool,
|
||||
nextInputMode: InputMode?,
|
||||
areVoiceMessagesAvailable: Bool,
|
||||
presentController: @escaping (ViewController) -> Void,
|
||||
sendMessageAction: @escaping () -> Void,
|
||||
@ -67,6 +84,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
stopAndPreviewMediaRecording: (() -> Void)?,
|
||||
discardMediaRecordingPreview: (() -> Void)?,
|
||||
attachmentAction: (() -> Void)?,
|
||||
inputModeAction: (() -> Void)?,
|
||||
timeoutAction: ((UIView) -> Void)?,
|
||||
forwardAction: (() -> Void)?,
|
||||
presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?,
|
||||
@ -78,13 +96,15 @@ public final class MessageInputPanelComponent: Component {
|
||||
timeoutValue: String?,
|
||||
timeoutSelected: Bool,
|
||||
displayGradient: Bool,
|
||||
bottomInset: CGFloat
|
||||
bottomInset: CGFloat,
|
||||
hideKeyboard: Bool
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.style = style
|
||||
self.nextInputMode = nextInputMode
|
||||
self.placeholder = placeholder
|
||||
self.alwaysDarkWhenHasText = alwaysDarkWhenHasText
|
||||
self.areVoiceMessagesAvailable = areVoiceMessagesAvailable
|
||||
@ -95,6 +115,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.stopAndPreviewMediaRecording = stopAndPreviewMediaRecording
|
||||
self.discardMediaRecordingPreview = discardMediaRecordingPreview
|
||||
self.attachmentAction = attachmentAction
|
||||
self.inputModeAction = inputModeAction
|
||||
self.timeoutAction = timeoutAction
|
||||
self.forwardAction = forwardAction
|
||||
self.presentVoiceMessagesUnavailableTooltip = presentVoiceMessagesUnavailableTooltip
|
||||
@ -107,6 +128,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.timeoutSelected = timeoutSelected
|
||||
self.displayGradient = displayGradient
|
||||
self.bottomInset = bottomInset
|
||||
self.hideKeyboard = hideKeyboard
|
||||
}
|
||||
|
||||
public static func ==(lhs: MessageInputPanelComponent, rhs: MessageInputPanelComponent) -> Bool {
|
||||
@ -125,6 +147,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
if lhs.style != rhs.style {
|
||||
return false
|
||||
}
|
||||
if lhs.nextInputMode != rhs.nextInputMode {
|
||||
return false
|
||||
}
|
||||
if lhs.placeholder != rhs.placeholder {
|
||||
return false
|
||||
}
|
||||
@ -164,6 +189,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
if (lhs.forwardAction == nil) != (rhs.forwardAction == nil) {
|
||||
return false
|
||||
}
|
||||
if lhs.hideKeyboard != rhs.hideKeyboard {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -199,6 +227,12 @@ public final class MessageInputPanelComponent: Component {
|
||||
private var currentMediaInputIsVoice: Bool = true
|
||||
private var mediaCancelFraction: CGFloat = 0.0
|
||||
|
||||
private var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:]
|
||||
private var contextQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult] = [:]
|
||||
|
||||
private var contextQueryResultPanel: ComponentView<Empty>?
|
||||
private var contextQueryResultPanelExternalState: ContextResultPanelComponent.ExternalState?
|
||||
|
||||
private var component: MessageInputPanelComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
@ -256,9 +290,56 @@ public final class MessageInputPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public func updateContextQueries() {
|
||||
guard let component = self.component, let textFieldView = self.textField.view as? TextFieldComponent.View else {
|
||||
return
|
||||
}
|
||||
let context = component.context
|
||||
let inputState = textFieldView.getInputState()
|
||||
|
||||
let contextQueryUpdates = contextQueryResultState(context: context, inputState: inputState, currentQueryStates: &self.contextQueryStates)
|
||||
|
||||
for (kind, update) in contextQueryUpdates {
|
||||
switch update {
|
||||
case .remove:
|
||||
if let (_, disposable) = self.contextQueryStates[kind] {
|
||||
disposable.dispose()
|
||||
self.contextQueryStates.removeValue(forKey: kind)
|
||||
self.contextQueryResults[kind] = nil
|
||||
}
|
||||
case let .update(query, signal):
|
||||
let currentQueryAndDisposable = self.contextQueryStates[kind]
|
||||
currentQueryAndDisposable?.1.dispose()
|
||||
|
||||
var inScope = true
|
||||
var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)?
|
||||
self.contextQueryStates[kind] = (query, (signal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
if let self {
|
||||
if Thread.isMainThread && inScope {
|
||||
inScope = false
|
||||
inScopeResult = result
|
||||
} else {
|
||||
self.contextQueryResults[kind] = result(self.contextQueryResults[kind])
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
}
|
||||
}))
|
||||
inScope = false
|
||||
if let inScopeResult = inScopeResult {
|
||||
self.contextQueryResults[kind] = inScopeResult(self.contextQueryResults[kind])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
|
||||
if result == nil, let contextQueryResultPanel = self.contextQueryResultPanel?.view, let panelResult = contextQueryResultPanel.hitTest(self.convert(point, to: contextQueryResultPanel), with: event), panelResult !== contextQueryResultPanel {
|
||||
return panelResult
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@ -276,6 +357,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
|
||||
let baseFieldHeight: CGFloat = 40.0
|
||||
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
@ -317,9 +399,16 @@ public final class MessageInputPanelComponent: Component {
|
||||
let textFieldSize = self.textField.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(TextFieldComponent(
|
||||
context: component.context,
|
||||
strings: component.strings,
|
||||
externalState: self.textFieldExternalState,
|
||||
placeholder: ""
|
||||
fontSize: 17.0,
|
||||
textColor: UIColor(rgb: 0xffffff),
|
||||
insets: UIEdgeInsets(top: 9.0, left: 8.0, bottom: 10.0, right: 48.0),
|
||||
hideKeyboard: component.hideKeyboard,
|
||||
present: { c in
|
||||
component.presentController(c)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableTextFieldSize
|
||||
@ -644,36 +733,100 @@ public final class MessageInputPanelComponent: Component {
|
||||
}
|
||||
|
||||
var fieldIconNextX = fieldBackgroundFrame.maxX - 4.0
|
||||
if case .story = component.style {
|
||||
let stickerButtonSize = self.stickerButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Input/Text/AccessoryIconStickers",
|
||||
tintColor: .white
|
||||
)),
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.component?.attachmentAction?()
|
||||
|
||||
var inputModeVisible = false
|
||||
if component.style == .story || self.textFieldExternalState.isEditing {
|
||||
inputModeVisible = true
|
||||
}
|
||||
|
||||
let animationName: String
|
||||
var animationPlay = false
|
||||
|
||||
if let inputMode = component.nextInputMode {
|
||||
switch inputMode {
|
||||
case .keyboard:
|
||||
if let previousInputMode = previousComponent?.nextInputMode {
|
||||
if case .stickers = previousInputMode {
|
||||
animationName = "input_anim_stickerToKey"
|
||||
animationPlay = true
|
||||
} else if case .emoji = previousInputMode {
|
||||
animationName = "input_anim_smileToKey"
|
||||
animationPlay = true
|
||||
} else {
|
||||
animationName = "input_anim_stickerToKey"
|
||||
}
|
||||
).minSize(CGSize(width: 32.0, height: 32.0))),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 32.0, height: 32.0)
|
||||
)
|
||||
if let stickerButtonView = self.stickerButton.view {
|
||||
if stickerButtonView.superview == nil {
|
||||
self.addSubview(stickerButtonView)
|
||||
} else {
|
||||
animationName = "input_anim_stickerToKey"
|
||||
}
|
||||
let stickerIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - stickerButtonSize.width, y: fieldBackgroundFrame.minY + floor((fieldBackgroundFrame.height - stickerButtonSize.height) * 0.5)), size: stickerButtonSize)
|
||||
transition.setPosition(view: stickerButtonView, position: stickerIconFrame.center)
|
||||
transition.setBounds(view: stickerButtonView, bounds: CGRect(origin: CGPoint(), size: stickerIconFrame.size))
|
||||
|
||||
transition.setAlpha(view: stickerButtonView, alpha: (self.textFieldExternalState.hasText || hasMediaRecording || hasMediaEditing) ? 0.0 : 1.0)
|
||||
transition.setScale(view: stickerButtonView, scale: (self.textFieldExternalState.hasText || hasMediaRecording || hasMediaEditing) ? 0.1 : 1.0)
|
||||
|
||||
case .stickers:
|
||||
if let previousInputMode = previousComponent?.nextInputMode {
|
||||
if case .keyboard = previousInputMode {
|
||||
animationName = "input_anim_keyToSticker"
|
||||
animationPlay = true
|
||||
} else if case .emoji = previousInputMode {
|
||||
animationName = "input_anim_smileToSticker"
|
||||
animationPlay = true
|
||||
} else {
|
||||
animationName = "input_anim_keyToSticker"
|
||||
}
|
||||
} else {
|
||||
animationName = "input_anim_keyToSticker"
|
||||
}
|
||||
case .emoji:
|
||||
if let previousInputMode = previousComponent?.nextInputMode {
|
||||
if case .keyboard = previousInputMode {
|
||||
animationName = "input_anim_keyToSmile"
|
||||
animationPlay = true
|
||||
} else if case .stickers = previousInputMode {
|
||||
animationName = "input_anim_stickerToSmile"
|
||||
animationPlay = true
|
||||
} else {
|
||||
animationName = "input_anim_keyToSmile"
|
||||
}
|
||||
} else {
|
||||
animationName = "input_anim_keyToSmile"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
animationName = ""
|
||||
}
|
||||
|
||||
let stickerButtonSize = self.stickerButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(name: animationName),
|
||||
color: .white
|
||||
)),
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.component?.inputModeAction?()
|
||||
}
|
||||
).minSize(CGSize(width: 32.0, height: 32.0))),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 32.0, height: 32.0)
|
||||
)
|
||||
if let stickerButtonView = self.stickerButton.view as? Button.View {
|
||||
if stickerButtonView.superview == nil {
|
||||
self.addSubview(stickerButtonView)
|
||||
}
|
||||
let stickerIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - stickerButtonSize.width, y: fieldFrame.maxY - 4.0 - stickerButtonSize.height), size: stickerButtonSize)
|
||||
transition.setPosition(view: stickerButtonView, position: stickerIconFrame.center)
|
||||
transition.setBounds(view: stickerButtonView, bounds: CGRect(origin: CGPoint(), size: stickerIconFrame.size))
|
||||
|
||||
transition.setAlpha(view: stickerButtonView, alpha: (hasMediaRecording || hasMediaEditing || !inputModeVisible) ? 0.0 : 1.0)
|
||||
transition.setScale(view: stickerButtonView, scale: (hasMediaRecording || hasMediaEditing || !inputModeVisible) ? 0.1 : 1.0)
|
||||
|
||||
if inputModeVisible {
|
||||
fieldIconNextX -= stickerButtonSize.width + 2.0
|
||||
|
||||
if let animationView = stickerButtonView.content as? LottieComponent.View {
|
||||
if animationPlay {
|
||||
animationView.playOnce()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -723,14 +876,13 @@ public final class MessageInputPanelComponent: Component {
|
||||
if timeoutButtonView.superview == nil {
|
||||
self.addSubview(timeoutButtonView)
|
||||
}
|
||||
let timeoutIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - timeoutButtonSize.width, y: fieldFrame.maxY - 4.0 - timeoutButtonSize.height), size: timeoutButtonSize)
|
||||
let originX = fieldBackgroundFrame.maxX - 4.0
|
||||
let timeoutIconFrame = CGRect(origin: CGPoint(x: originX - timeoutButtonSize.width, y: fieldFrame.maxY - 4.0 - timeoutButtonSize.height), size: timeoutButtonSize)
|
||||
transition.setPosition(view: timeoutButtonView, position: timeoutIconFrame.center)
|
||||
transition.setBounds(view: timeoutButtonView, bounds: CGRect(origin: CGPoint(), size: timeoutIconFrame.size))
|
||||
|
||||
transition.setAlpha(view: timeoutButtonView, alpha: self.textFieldExternalState.isEditing ? 0.0 : 1.0)
|
||||
transition.setScale(view: timeoutButtonView, scale: self.textFieldExternalState.isEditing ? 0.1 : 1.0)
|
||||
|
||||
fieldIconNextX -= timeoutButtonSize.width + 2.0
|
||||
}
|
||||
}
|
||||
|
||||
@ -748,6 +900,16 @@ public final class MessageInputPanelComponent: Component {
|
||||
|
||||
component.externalState.isEditing = self.textFieldExternalState.isEditing
|
||||
component.externalState.hasText = self.textFieldExternalState.hasText
|
||||
component.externalState.insertText = { [weak self] text in
|
||||
if let self, let view = self.textField.view as? TextFieldComponent.View {
|
||||
view.insertText(text)
|
||||
}
|
||||
}
|
||||
component.externalState.deleteBackward = { [weak self] in
|
||||
if let self, let view = self.textField.view as? TextFieldComponent.View {
|
||||
view.deleteBackward()
|
||||
}
|
||||
}
|
||||
|
||||
if hasMediaRecording {
|
||||
if let dismissingMediaRecordingPanel = self.dismissingMediaRecordingPanel {
|
||||
@ -894,6 +1056,94 @@ public final class MessageInputPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
self.updateContextQueries()
|
||||
|
||||
if let result = self.contextQueryResults[.mention], result.count > 0 && self.textFieldExternalState.isEditing {
|
||||
let availablePanelHeight: CGFloat = 413.0
|
||||
|
||||
var animateIn = false
|
||||
let panel: ComponentView<Empty>
|
||||
let externalState: ContextResultPanelComponent.ExternalState
|
||||
var transition = transition
|
||||
if let current = self.contextQueryResultPanel, let currentState = self.contextQueryResultPanelExternalState {
|
||||
panel = current
|
||||
externalState = currentState
|
||||
} else {
|
||||
panel = ComponentView<Empty>()
|
||||
externalState = ContextResultPanelComponent.ExternalState()
|
||||
self.contextQueryResultPanel = panel
|
||||
self.contextQueryResultPanelExternalState = externalState
|
||||
animateIn = true
|
||||
transition = .immediate
|
||||
}
|
||||
let panelLeftInset: CGFloat = max(insets.left, 7.0)
|
||||
let panelRightInset: CGFloat = max(insets.right, 41.0)
|
||||
let panelSize = panel.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ContextResultPanelComponent(
|
||||
externalState: externalState,
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
results: result,
|
||||
action: { [weak self] action in
|
||||
if let self, case let .mention(peer) = action, let textView = self.textField.view as? TextFieldComponent.View {
|
||||
let inputState = textView.getInputState()
|
||||
|
||||
var mentionQueryRange: NSRange?
|
||||
inner: for (range, type, _) in textInputStateContextQueryRangeAndType(inputState: inputState) {
|
||||
if type == [.mention] {
|
||||
mentionQueryRange = range
|
||||
break inner
|
||||
}
|
||||
}
|
||||
|
||||
if let range = mentionQueryRange {
|
||||
let inputText = NSMutableAttributedString(attributedString: inputState.inputText)
|
||||
if let addressName = peer.addressName, !addressName.isEmpty {
|
||||
let replacementText = addressName + " "
|
||||
inputText.replaceCharacters(in: range, with: replacementText)
|
||||
|
||||
let selectionPosition = range.lowerBound + (replacementText as NSString).length
|
||||
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
|
||||
} else if !peer.compactDisplayTitle.isEmpty {
|
||||
let replacementText = NSMutableAttributedString()
|
||||
replacementText.append(NSAttributedString(string: peer.compactDisplayTitle, attributes: [ChatTextInputAttributes.textMention: ChatTextInputTextMentionAttribute(peerId: peer.id)]))
|
||||
replacementText.append(NSAttributedString(string: " "))
|
||||
|
||||
let updatedRange = NSRange(location: range.location - 1, length: range.length + 1)
|
||||
inputText.replaceCharacters(in: updatedRange, with: replacementText)
|
||||
|
||||
let selectionPosition = updatedRange.lowerBound + replacementText.length
|
||||
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - panelLeftInset - panelRightInset, height: availablePanelHeight)
|
||||
)
|
||||
|
||||
let panelFrame = CGRect(origin: CGPoint(x: insets.left, y: -panelSize.height + 33.0), size: panelSize)
|
||||
if let panelView = panel.view as? ContextResultPanelComponent.View {
|
||||
if panelView.superview == nil {
|
||||
self.insertSubview(panelView, at: 0)
|
||||
}
|
||||
transition.setFrame(view: panelView, frame: panelFrame)
|
||||
|
||||
if animateIn {
|
||||
panelView.animateIn(transition: .spring(duration: 0.4))
|
||||
}
|
||||
}
|
||||
|
||||
} else if let contextQueryResultPanel = self.contextQueryResultPanel?.view as? ContextResultPanelComponent.View {
|
||||
self.contextQueryResultPanel = nil
|
||||
contextQueryResultPanel.animateOut(transition: .spring(duration: 0.4), completion: { [weak contextQueryResultPanel] in
|
||||
contextQueryResultPanel?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "PeerListItemComponent",
|
||||
module_name = "PeerListItemComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/CheckNode",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/AppBundle",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
@ -16,39 +16,53 @@ import AppBundle
|
||||
private let avatarFont = avatarPlaceholderFont(size: 15.0)
|
||||
private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
|
||||
final class PeerListItemComponent: Component {
|
||||
final class TransitionHint {
|
||||
let synchronousLoad: Bool
|
||||
public final class PeerListItemComponent: Component {
|
||||
public final class TransitionHint {
|
||||
public let synchronousLoad: Bool
|
||||
|
||||
init(synchronousLoad: Bool) {
|
||||
public init(synchronousLoad: Bool) {
|
||||
self.synchronousLoad = synchronousLoad
|
||||
}
|
||||
}
|
||||
|
||||
enum SelectionState: Equatable {
|
||||
public enum Style {
|
||||
case generic
|
||||
case compact
|
||||
}
|
||||
|
||||
public enum SelectionState: Equatable {
|
||||
case none
|
||||
case editing(isSelected: Bool, isTinted: Bool)
|
||||
}
|
||||
|
||||
public enum SubtitleAccessory: Equatable {
|
||||
case none
|
||||
case checks
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let style: Style
|
||||
let sideInset: CGFloat
|
||||
let title: String
|
||||
let peer: EnginePeer?
|
||||
let subtitle: String?
|
||||
let subtitleAccessory: SubtitleAccessory
|
||||
let selectionState: SelectionState
|
||||
let hasNext: Bool
|
||||
let action: (EnginePeer) -> Void
|
||||
|
||||
init(
|
||||
public init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
style: Style,
|
||||
sideInset: CGFloat,
|
||||
title: String,
|
||||
peer: EnginePeer?,
|
||||
subtitle: String?,
|
||||
subtitleAccessory: SubtitleAccessory,
|
||||
selectionState: SelectionState,
|
||||
hasNext: Bool,
|
||||
action: @escaping (EnginePeer) -> Void
|
||||
@ -56,16 +70,18 @@ final class PeerListItemComponent: Component {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.style = style
|
||||
self.sideInset = sideInset
|
||||
self.title = title
|
||||
self.peer = peer
|
||||
self.subtitle = subtitle
|
||||
self.subtitleAccessory = subtitleAccessory
|
||||
self.selectionState = selectionState
|
||||
self.hasNext = hasNext
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool {
|
||||
public static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
@ -75,6 +91,9 @@ final class PeerListItemComponent: Component {
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.style != rhs.style {
|
||||
return false
|
||||
}
|
||||
if lhs.sideInset != rhs.sideInset {
|
||||
return false
|
||||
}
|
||||
@ -87,6 +106,9 @@ final class PeerListItemComponent: Component {
|
||||
if lhs.subtitle != rhs.subtitle {
|
||||
return false
|
||||
}
|
||||
if lhs.subtitleAccessory != rhs.subtitleAccessory {
|
||||
return false
|
||||
}
|
||||
if lhs.selectionState != rhs.selectionState {
|
||||
return false
|
||||
}
|
||||
@ -96,7 +118,7 @@ final class PeerListItemComponent: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
public final class View: UIView {
|
||||
private let containerButton: HighlightTrackingButton
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
@ -110,15 +132,15 @@ final class PeerListItemComponent: Component {
|
||||
private var component: PeerListItemComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
var avatarFrame: CGRect {
|
||||
public var avatarFrame: CGRect {
|
||||
return self.avatarNode.frame
|
||||
}
|
||||
|
||||
var titleFrame: CGRect? {
|
||||
public var titleFrame: CGRect? {
|
||||
return self.title.view?.frame
|
||||
}
|
||||
|
||||
var labelFrame: CGRect? {
|
||||
public var labelFrame: CGRect? {
|
||||
guard var value = self.label.view?.frame else {
|
||||
return nil
|
||||
}
|
||||
@ -186,9 +208,26 @@ final class PeerListItemComponent: Component {
|
||||
|
||||
let contextInset: CGFloat = 0.0
|
||||
|
||||
let height: CGFloat = 60.0
|
||||
let height: CGFloat
|
||||
let titleFont: UIFont
|
||||
let subtitleFont: UIFont
|
||||
switch component.style {
|
||||
case .generic:
|
||||
titleFont = Font.semibold(17.0)
|
||||
subtitleFont = Font.regular(15.0)
|
||||
height = 60.0
|
||||
case .compact:
|
||||
titleFont = Font.semibold(14.0)
|
||||
subtitleFont = Font.regular(14.0)
|
||||
height = 42.0
|
||||
}
|
||||
|
||||
|
||||
let verticalInset: CGFloat = 1.0
|
||||
var leftInset: CGFloat = 62.0 + component.sideInset
|
||||
var leftInset: CGFloat = 53.0 + component.sideInset
|
||||
if case .generic = component.style {
|
||||
leftInset += 9.0
|
||||
}
|
||||
let rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset
|
||||
var avatarLeftInset: CGFloat = component.sideInset + 10.0
|
||||
|
||||
@ -230,7 +269,7 @@ final class PeerListItemComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
let avatarSize: CGFloat = 40.0
|
||||
let avatarSize: CGFloat = component.style == .compact ? 30.0 : 40.0
|
||||
|
||||
let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
|
||||
if self.avatarNode.bounds.isEmpty {
|
||||
@ -251,22 +290,14 @@ final class PeerListItemComponent: Component {
|
||||
let labelData: (String, Bool)
|
||||
if let subtitle = component.subtitle {
|
||||
labelData = (subtitle, false)
|
||||
} else if case .legacyGroup = component.peer {
|
||||
labelData = (component.strings.Group_Status, false)
|
||||
} else if case let .channel(channel) = component.peer {
|
||||
if case .group = channel.info {
|
||||
labelData = (component.strings.Group_Status, false)
|
||||
} else {
|
||||
labelData = (component.strings.Channel_Status, false)
|
||||
}
|
||||
} else {
|
||||
labelData = (component.strings.Group_Status, false)
|
||||
labelData = ("", false)
|
||||
}
|
||||
|
||||
let labelSize = self.label.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: labelData.0, font: Font.regular(15.0), textColor: labelData.1 ? component.theme.list.itemAccentColor : component.theme.list.itemSecondaryTextColor))
|
||||
text: .plain(NSAttributedString(string: labelData.0, font: subtitleFont, textColor: labelData.1 ? component.theme.list.itemAccentColor : component.theme.list.itemSecondaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||
@ -281,14 +312,19 @@ final class PeerListItemComponent: Component {
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor))
|
||||
text: .plain(NSAttributedString(string: component.title, font: titleFont, textColor: component.theme.list.itemPrimaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||
)
|
||||
|
||||
let titleSpacing: CGFloat = 1.0
|
||||
let centralContentHeight: CGFloat = titleSize.height + labelSize.height + titleSpacing
|
||||
let centralContentHeight: CGFloat
|
||||
if labelSize.height > 0.0, case .generic = component.style {
|
||||
centralContentHeight = titleSize.height + labelSize.height + titleSpacing
|
||||
} else {
|
||||
centralContentHeight = titleSize.height
|
||||
}
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
@ -315,26 +351,40 @@ final class PeerListItemComponent: Component {
|
||||
if let labelView = self.label.view {
|
||||
var iconLabelOffset: CGFloat = 0.0
|
||||
|
||||
let iconView: UIImageView
|
||||
if let current = self.iconView {
|
||||
iconView = current
|
||||
} else {
|
||||
iconView = UIImageView(image: readIconImage)
|
||||
iconView.tintColor = component.theme.list.itemSecondaryTextColor
|
||||
self.iconView = iconView
|
||||
self.containerButton.addSubview(iconView)
|
||||
}
|
||||
|
||||
if let image = iconView.image {
|
||||
iconLabelOffset = image.size.width + 4.0
|
||||
transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing + 3.0 + floor((labelSize.height - image.size.height) * 0.5)), size: image.size))
|
||||
if case .checks = component.subtitleAccessory {
|
||||
let iconView: UIImageView
|
||||
if let current = self.iconView {
|
||||
iconView = current
|
||||
} else {
|
||||
iconView = UIImageView(image: readIconImage)
|
||||
iconView.tintColor = component.theme.list.itemSecondaryTextColor
|
||||
self.iconView = iconView
|
||||
self.containerButton.addSubview(iconView)
|
||||
}
|
||||
|
||||
if let image = iconView.image {
|
||||
iconLabelOffset = image.size.width + 4.0
|
||||
transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing + 3.0 + floor((labelSize.height - image.size.height) * 0.5)), size: image.size))
|
||||
}
|
||||
} else if let iconView = self.iconView {
|
||||
self.iconView = nil
|
||||
iconView.removeFromSuperview()
|
||||
}
|
||||
|
||||
if labelView.superview == nil {
|
||||
labelView.isUserInteractionEnabled = false
|
||||
self.containerButton.addSubview(labelView)
|
||||
}
|
||||
transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: titleFrame.minX + iconLabelOffset, y: titleFrame.maxY + titleSpacing), size: labelSize))
|
||||
|
||||
let labelFrame: CGRect
|
||||
switch component.style {
|
||||
case .generic:
|
||||
labelFrame = CGRect(origin: CGPoint(x: titleFrame.minX + iconLabelOffset, y: titleFrame.maxY + titleSpacing), size: labelSize)
|
||||
case .compact:
|
||||
labelFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: labelSize)
|
||||
}
|
||||
|
||||
transition.setFrame(view: labelView, frame: labelFrame)
|
||||
}
|
||||
|
||||
if themeUpdated {
|
||||
@ -350,11 +400,11 @@ final class PeerListItemComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ swift_library(
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/TelegramUI/Components/MessageInputPanelComponent",
|
||||
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
|
||||
"//submodules/TelegramUI/Components/ChatInputNode",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/AppBundle",
|
||||
@ -45,6 +46,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/LegacyInstantVideoController",
|
||||
"//submodules/TelegramUI/Components/EntityKeyboard",
|
||||
"//submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent",
|
||||
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
|
||||
"//submodules/TelegramUI/Components/ShareWithPeersScreen",
|
||||
"//submodules/TelegramUI/Components/MediaEditorScreen",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
|
@ -241,6 +241,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
var captionItem: CaptionItem?
|
||||
|
||||
let inputBackground = ComponentView<Empty>()
|
||||
let inputPanel = ComponentView<Empty>()
|
||||
let footerPanel = ComponentView<Empty>()
|
||||
let inputPanelExternalState = MessageInputPanelComponent.ExternalState()
|
||||
@ -1013,6 +1014,13 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
let nextInputMode: MessageInputPanelComponent.InputMode
|
||||
if self.inputPanelExternalState.hasText {
|
||||
nextInputMode = .emoji
|
||||
} else {
|
||||
nextInputMode = .stickers
|
||||
}
|
||||
|
||||
self.inputPanel.parentState = state
|
||||
let inputPanelSize = self.inputPanel.update(
|
||||
transition: inputPanelTransition,
|
||||
@ -1024,6 +1032,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
style: .story,
|
||||
placeholder: "Reply Privately...",
|
||||
alwaysDarkWhenHasText: component.metrics.widthClass == .regular,
|
||||
nextInputMode: nextInputMode,
|
||||
areVoiceMessagesAvailable: component.slice.additionalPeerData.areVoiceMessagesAvailable,
|
||||
presentController: { [weak self] c in
|
||||
guard let self, let component = self.component else {
|
||||
@ -1068,6 +1077,13 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
self.sendMessageContext.presentAttachmentMenu(view: self, subject: .default)
|
||||
},
|
||||
inputModeAction: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.sendMessageContext.toggleInputMode()
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)))
|
||||
},
|
||||
timeoutAction: nil,
|
||||
forwardAction: component.slice.item.storyItem.isPublic ? { [weak self] in
|
||||
guard let self else {
|
||||
@ -1106,7 +1122,8 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
timeoutValue: nil,
|
||||
timeoutSelected: false,
|
||||
displayGradient: component.inputHeight != 0.0 && component.metrics.widthClass != .regular,
|
||||
bottomInset: component.inputHeight != 0.0 ? 0.0 : bottomContentInset
|
||||
bottomInset: component.inputHeight != 0.0 ? 0.0 : bottomContentInset,
|
||||
hideKeyboard: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0)
|
||||
|
@ -34,9 +34,17 @@ import ChatPresentationInterfaceState
|
||||
import Postbox
|
||||
|
||||
final class StoryItemSetContainerSendMessage {
|
||||
enum InputMode {
|
||||
case text
|
||||
case emoji
|
||||
case sticker
|
||||
}
|
||||
|
||||
weak var attachmentController: AttachmentController?
|
||||
weak var shareController: ShareController?
|
||||
|
||||
var inputMode: InputMode = .text
|
||||
|
||||
var audioRecorderValue: ManagedAudioRecorder?
|
||||
var audioRecorder = Promise<ManagedAudioRecorder?>()
|
||||
var recordedAudioPreview: ChatRecordedMediaPreview?
|
||||
@ -55,6 +63,9 @@ final class StoryItemSetContainerSendMessage {
|
||||
self.enqueueMediaMessageDisposable.dispose()
|
||||
}
|
||||
|
||||
func toggleInputMode() {
|
||||
}
|
||||
|
||||
func performSendMessageAction(
|
||||
view: StoryItemSetContainerComponent.View
|
||||
) {
|
||||
|
@ -11,6 +11,7 @@ import SwiftSignalKit
|
||||
import TelegramStringFormatting
|
||||
import ShimmerEffect
|
||||
import StoryFooterPanelComponent
|
||||
import PeerListItemComponent
|
||||
|
||||
final class StoryItemSetViewListComponent: Component {
|
||||
final class ExternalState {
|
||||
@ -429,10 +430,12 @@ final class StoryItemSetViewListComponent: Component {
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
style: .generic,
|
||||
sideInset: itemLayout.sideInset,
|
||||
title: item.peer.displayTitle(strings: component.strings, displayOrder: .firstLast),
|
||||
peer: item.peer,
|
||||
subtitle: dateText,
|
||||
subtitleAccessory: .checks,
|
||||
selectionState: .none,
|
||||
hasNext: index != viewListState.totalCount - 1,
|
||||
action: { [weak self] peer in
|
||||
@ -627,10 +630,12 @@ final class StoryItemSetViewListComponent: Component {
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
style: .generic,
|
||||
sideInset: sideInset,
|
||||
title: "AAAAAAAAAAAA",
|
||||
peer: nil,
|
||||
subtitle: "BBBBBBB",
|
||||
subtitleAccessory: .checks,
|
||||
selectionState: .none,
|
||||
hasNext: true,
|
||||
action: { _ in
|
||||
|
@ -99,7 +99,8 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
}
|
||||
let additionalPeerData: StoryContentContextState.AdditionalPeerData
|
||||
if let cachedPeerDataView = views.views[PostboxViewKey.cachedPeerData(peerId: peerId)] as? CachedPeerDataView, let cachedUserData = cachedPeerDataView.cachedPeerData as? CachedUserData {
|
||||
additionalPeerData = StoryContentContextState.AdditionalPeerData(areVoiceMessagesAvailable: cachedUserData.voiceMessagesAvailable)
|
||||
let _ = cachedUserData
|
||||
additionalPeerData = StoryContentContextState.AdditionalPeerData(areVoiceMessagesAvailable: false) //cachedUserData.voiceMessagesAvailable)
|
||||
} else {
|
||||
additionalPeerData = StoryContentContextState.AdditionalPeerData(areVoiceMessagesAvailable: true)
|
||||
}
|
||||
|
@ -14,6 +14,10 @@ swift_library(
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/InvisibleInkDustNode",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
||||
"//submodules/ChatTextLinkEditUI"
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -4,6 +4,17 @@ import Display
|
||||
import ComponentFlow
|
||||
import TextFormat
|
||||
import TelegramPresentationData
|
||||
import InvisibleInkDustNode
|
||||
import EmojiTextAttachmentView
|
||||
import AccountContext
|
||||
import TextFormat
|
||||
import ChatTextLinkEditUI
|
||||
|
||||
public final class EmptyInputView: UIView, UIInputViewAudioFeedback {
|
||||
public var enableInputClicksWhenVisible: Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public final class TextFieldComponent: Component {
|
||||
public final class ExternalState {
|
||||
@ -27,18 +38,33 @@ public final class TextFieldComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public let context: AccountContext
|
||||
public let strings: PresentationStrings
|
||||
public let externalState: ExternalState
|
||||
public let placeholder: String
|
||||
public let fontSize: CGFloat
|
||||
public let textColor: UIColor
|
||||
public let insets: UIEdgeInsets
|
||||
public let hideKeyboard: Bool
|
||||
public let present: (ViewController) -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
strings: PresentationStrings,
|
||||
externalState: ExternalState,
|
||||
placeholder: String
|
||||
fontSize: CGFloat,
|
||||
textColor: UIColor,
|
||||
insets: UIEdgeInsets,
|
||||
hideKeyboard: Bool,
|
||||
present: @escaping (ViewController) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.strings = strings
|
||||
self.externalState = externalState
|
||||
self.placeholder = placeholder
|
||||
self.fontSize = fontSize
|
||||
self.textColor = textColor
|
||||
self.insets = insets
|
||||
self.hideKeyboard = hideKeyboard
|
||||
self.present = present
|
||||
}
|
||||
|
||||
public static func ==(lhs: TextFieldComponent, rhs: TextFieldComponent) -> Bool {
|
||||
@ -48,7 +74,16 @@ public final class TextFieldComponent: Component {
|
||||
if lhs.externalState !== rhs.externalState {
|
||||
return false
|
||||
}
|
||||
if lhs.placeholder != rhs.placeholder {
|
||||
if lhs.fontSize != rhs.fontSize {
|
||||
return false
|
||||
}
|
||||
if lhs.textColor != rhs.textColor {
|
||||
return false
|
||||
}
|
||||
if lhs.insets != rhs.insets {
|
||||
return false
|
||||
}
|
||||
if lhs.hideKeyboard != rhs.hideKeyboard {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -60,14 +95,21 @@ public final class TextFieldComponent: Component {
|
||||
}
|
||||
|
||||
public final class View: UIView, UITextViewDelegate, UIScrollViewDelegate {
|
||||
private let placeholder = ComponentView<Empty>()
|
||||
|
||||
private let textContainer: NSTextContainer
|
||||
private let textStorage: NSTextStorage
|
||||
private let layoutManager: NSLayoutManager
|
||||
private let textView: UITextView
|
||||
|
||||
private var inputState = InputState(inputText: NSAttributedString(), selectionRange: 0 ..< 0)
|
||||
private var spoilerView: InvisibleInkDustView?
|
||||
private var customEmojiContainerView: CustomEmojiContainerView?
|
||||
private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
|
||||
|
||||
//private var inputState = InputState(inputText: NSAttributedString(), selectionRange: 0 ..< 0)
|
||||
|
||||
private var inputState: InputState {
|
||||
let selectionRange: Range<Int> = self.textView.selectedRange.location ..< (self.textView.selectedRange.location + self.textView.selectedRange.length)
|
||||
return InputState(inputText: stateAttributedStringForText(self.textView.attributedText ?? NSAttributedString()), selectionRange: selectionRange)
|
||||
}
|
||||
|
||||
private var component: TextFieldComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
@ -88,7 +130,6 @@ public final class TextFieldComponent: Component {
|
||||
|
||||
self.textView = UITextView(frame: CGRect(), textContainer: self.textContainer)
|
||||
self.textView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.textView.textContainerInset = UIEdgeInsets(top: 9.0, left: 8.0, bottom: 10.0, right: 8.0)
|
||||
self.textView.backgroundColor = nil
|
||||
self.textView.layer.isOpaque = false
|
||||
self.textView.keyboardAppearance = .dark
|
||||
@ -115,58 +156,116 @@ public final class TextFieldComponent: Component {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func textViewDidChange(_ textView: UITextView) {
|
||||
private func updateInputState(_ f: (InputState) -> InputState) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
let inputState = f(self.inputState)
|
||||
|
||||
self.textView.attributedText = textAttributedStringForStateText(inputState.inputText, fontSize: component.fontSize, textColor: component.textColor, accentTextColor: component.textColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
|
||||
self.textView.selectedRange = NSMakeRange(inputState.selectionRange.lowerBound, inputState.selectionRange.count)
|
||||
|
||||
refreshChatTextInputAttributes(textView: self.textView, primaryTextColor: component.textColor, accentTextColor: component.textColor, baseFontSize: component.fontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
|
||||
|
||||
self.updateEntities()
|
||||
}
|
||||
|
||||
public func insertText(_ text: NSAttributedString) {
|
||||
self.updateInputState { state in
|
||||
return state.insertText(text)
|
||||
}
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
|
||||
}
|
||||
|
||||
public func deleteBackward() {
|
||||
self.textView.deleteBackward()
|
||||
}
|
||||
|
||||
public func updateText(_ text: NSAttributedString, selectionRange: Range<Int>) {
|
||||
self.updateInputState { _ in
|
||||
return TextFieldComponent.InputState(inputText: text, selectionRange: selectionRange)
|
||||
}
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
|
||||
}
|
||||
|
||||
public func textViewDidChange(_ textView: UITextView) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
refreshChatTextInputAttributes(textView: self.textView, primaryTextColor: component.textColor, accentTextColor: component.textColor, baseFontSize: component.fontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
|
||||
refreshChatTextInputTypingAttributes(self.textView, textColor: component.textColor, baseFontSize: component.fontSize)
|
||||
|
||||
self.updateEntities()
|
||||
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
|
||||
}
|
||||
|
||||
public func textViewDidChangeSelection(_ textView: UITextView) {
|
||||
guard let _ = self.component else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
public func textViewDidBeginEditing(_ textView: UITextView) {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged)))
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged)))
|
||||
}
|
||||
|
||||
public func textViewDidEndEditing(_ textView: UITextView) {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged)))
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged)))
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
public func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
|
||||
let filteredActions: Set<String> = Set([
|
||||
"com.apple.menu.format",
|
||||
"com.apple.menu.replace"
|
||||
])
|
||||
let suggestedActions = suggestedActions.filter {
|
||||
if let action = $0 as? UIMenu, filteredActions.contains(action.identifier.rawValue) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
guard let component = self.component, !textView.attributedText.string.isEmpty && textView.selectedRange.length > 0 else {
|
||||
return UIMenu(children: suggestedActions)
|
||||
}
|
||||
|
||||
|
||||
let strings = component.strings
|
||||
var actions: [UIAction] = [
|
||||
UIAction(title: strings.TextFormat_Bold, image: nil) { [weak self] (action) in
|
||||
UIAction(title: strings.TextFormat_Bold, image: nil) { [weak self] action in
|
||||
if let self {
|
||||
self.toggleAttribute(key: ChatTextInputAttributes.bold)
|
||||
}
|
||||
},
|
||||
UIAction(title: strings.TextFormat_Italic, image: nil) { [weak self] (action) in
|
||||
UIAction(title: strings.TextFormat_Italic, image: nil) { [weak self] action in
|
||||
if let self {
|
||||
self.toggleAttribute(key: ChatTextInputAttributes.italic)
|
||||
}
|
||||
},
|
||||
UIAction(title: strings.TextFormat_Monospace, image: nil) { [weak self] (action) in
|
||||
UIAction(title: strings.TextFormat_Monospace, image: nil) { [weak self] action in
|
||||
if let self {
|
||||
self.toggleAttribute(key: ChatTextInputAttributes.monospace)
|
||||
}
|
||||
},
|
||||
UIAction(title: strings.TextFormat_Link, image: nil) { [weak self] (action) in
|
||||
UIAction(title: strings.TextFormat_Link, image: nil) { [weak self] action in
|
||||
if let self {
|
||||
let _ = self
|
||||
self.openLinkEditing()
|
||||
}
|
||||
},
|
||||
UIAction(title: strings.TextFormat_Strikethrough, image: nil) { [weak self] (action) in
|
||||
UIAction(title: strings.TextFormat_Strikethrough, image: nil) { [weak self] action in
|
||||
if let self {
|
||||
self.toggleAttribute(key: ChatTextInputAttributes.strikethrough)
|
||||
}
|
||||
},
|
||||
UIAction(title: strings.TextFormat_Underline, image: nil) { [weak self] (action) in
|
||||
UIAction(title: strings.TextFormat_Underline, image: nil) { [weak self] action in
|
||||
if let self {
|
||||
self.toggleAttribute(key: ChatTextInputAttributes.underline)
|
||||
}
|
||||
}
|
||||
]
|
||||
actions.append(UIAction(title: strings.TextFormat_Spoiler, image: nil) { [weak self] (action) in
|
||||
actions.append(UIAction(title: strings.TextFormat_Spoiler, image: nil) { [weak self] action in
|
||||
if let self {
|
||||
self.toggleAttribute(key: ChatTextInputAttributes.spoiler)
|
||||
}
|
||||
@ -174,27 +273,64 @@ public final class TextFieldComponent: Component {
|
||||
|
||||
var updatedActions = suggestedActions
|
||||
let formatMenu = UIMenu(title: strings.TextFormat_Format, image: nil, children: actions)
|
||||
updatedActions.insert(formatMenu, at: 3)
|
||||
updatedActions.insert(formatMenu, at: 1)
|
||||
|
||||
return UIMenu(children: updatedActions)
|
||||
}
|
||||
|
||||
private func toggleAttribute(key: NSAttributedString.Key) {
|
||||
self.updateInputState { state in
|
||||
return state.addFormattingAttribute(attribute: key)
|
||||
}
|
||||
}
|
||||
|
||||
private func openLinkEditing() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
let selectionRange = self.inputState.selectionRange
|
||||
let text = self.inputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count))
|
||||
var link: String?
|
||||
text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in
|
||||
if let linkAttribute = attributes[ChatTextInputAttributes.textUrl] as? ChatTextInputTextUrlAttribute {
|
||||
link = linkAttribute.url
|
||||
}
|
||||
}
|
||||
|
||||
let controller = chatTextLinkEditController(sharedContext: component.context.sharedContext, account: component.context.account, text: text.string, link: link, apply: { [weak self] link in
|
||||
if let self {
|
||||
if let link = link {
|
||||
self.updateInputState { state in
|
||||
return state.addLinkAttribute(selectionRange: selectionRange, url: link)
|
||||
}
|
||||
}
|
||||
// strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, {
|
||||
// return $0.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({
|
||||
// $0.withUpdatedEffectiveInputState(ChatTextInputState(inputText: $0.effectiveInputState.inputText, selectionRange: selectionRange.endIndex ..< selectionRange.endIndex))
|
||||
// })
|
||||
// })
|
||||
}
|
||||
})
|
||||
component.present(controller)
|
||||
}
|
||||
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
//print("didScroll \(scrollView.bounds)")
|
||||
}
|
||||
|
||||
public func getInputState() -> TextFieldComponent.InputState {
|
||||
return self.inputState
|
||||
}
|
||||
|
||||
public func getAttributedText() -> NSAttributedString {
|
||||
Keyboard.applyAutocorrection(textView: self.textView)
|
||||
return NSAttributedString(string: self.textView.text ?? "")
|
||||
//return self.inputState.inputText
|
||||
return self.inputState.inputText
|
||||
}
|
||||
|
||||
public func setAttributedText(_ string: NSAttributedString) {
|
||||
self.textView.text = string.string
|
||||
self.updateInputState { _ in
|
||||
return TextFieldComponent.InputState(inputText: string, selectionRange: string.length ..< string.length)
|
||||
}
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
|
||||
}
|
||||
|
||||
@ -202,90 +338,167 @@ public final class TextFieldComponent: Component {
|
||||
self.textView.becomeFirstResponder()
|
||||
}
|
||||
|
||||
var spoilersRevealed = false
|
||||
|
||||
func updateEntities() {
|
||||
// var spoilerRects: [CGRect] = []
|
||||
// var customEmojiRects: [CGRect: ChatTextInputTextCustomEmojiAttribute] = []
|
||||
//
|
||||
// if !spoilerRects.isEmpty {
|
||||
// let dustNode: InvisibleInkDustNode
|
||||
// if let current = self.dustNode {
|
||||
// dustNode = current
|
||||
// } else {
|
||||
// dustNode = InvisibleInkDustNode(textNode: nil, enableAnimations: self.context?.sharedContext.energyUsageSettings.fullTranslucency ?? true)
|
||||
// dustNode.alpha = self.spoilersRevealed ? 0.0 : 1.0
|
||||
// dustNode.isUserInteractionEnabled = false
|
||||
// textInputNode.textView.addSubview(dustNode.view)
|
||||
// self.dustNode = dustNode
|
||||
// }
|
||||
// dustNode.frame = CGRect(origin: CGPoint(), size: textInputNode.textView.contentSize)
|
||||
// dustNode.update(size: textInputNode.textView.contentSize, color: textColor, textColor: textColor, rects: rects, wordRects: rects)
|
||||
// } else if let dustNode = self.dustNode {
|
||||
// dustNode.removeFromSupernode()
|
||||
// self.dustNode = nil
|
||||
// }
|
||||
//
|
||||
// if !customEmojiRects.isEmpty {
|
||||
// let customEmojiContainerView: CustomEmojiContainerView
|
||||
// if let current = self.customEmojiContainerView {
|
||||
// customEmojiContainerView = current
|
||||
// } else {
|
||||
// customEmojiContainerView = CustomEmojiContainerView(emojiViewProvider: { [weak self] emoji in
|
||||
// guard let strongSelf = self, let emojiViewProvider = strongSelf.emojiViewProvider else {
|
||||
// return nil
|
||||
// }
|
||||
// return emojiViewProvider(emoji)
|
||||
// })
|
||||
// customEmojiContainerView.isUserInteractionEnabled = false
|
||||
// textInputNode.textView.addSubview(customEmojiContainerView)
|
||||
// self.customEmojiContainerView = customEmojiContainerView
|
||||
// }
|
||||
//
|
||||
// customEmojiContainerView.update(fontSize: fontSize, textColor: textColor, emojiRects: customEmojiRects)
|
||||
// } else if let customEmojiContainerView = self.customEmojiContainerView {
|
||||
// customEmojiContainerView.removeFromSuperview()
|
||||
// self.customEmojiContainerView = nil
|
||||
// }
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
var spoilerRects: [CGRect] = []
|
||||
var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = []
|
||||
|
||||
let textView = self.textView
|
||||
if let attributedText = textView.attributedText {
|
||||
let beginning = textView.beginningOfDocument
|
||||
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in
|
||||
if let _ = attributes[ChatTextInputAttributes.spoiler] {
|
||||
func addSpoiler(startIndex: Int, endIndex: Int) {
|
||||
if let start = textView.position(from: beginning, offset: startIndex), let end = textView.position(from: start, offset: endIndex - startIndex), let textRange = textView.textRange(from: start, to: end) {
|
||||
let textRects = textView.selectionRects(for: textRange)
|
||||
for textRect in textRects {
|
||||
if textRect.rect.width > 1.0 && textRect.rect.size.height > 1.0 {
|
||||
spoilerRects.append(textRect.rect.insetBy(dx: 1.0, dy: 1.0).offsetBy(dx: 0.0, dy: 1.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var startIndex: Int?
|
||||
var currentIndex: Int?
|
||||
|
||||
let nsString = (attributedText.string as NSString)
|
||||
nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in
|
||||
if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil {
|
||||
if let currentStartIndex = startIndex {
|
||||
startIndex = nil
|
||||
let endIndex = range.location
|
||||
addSpoiler(startIndex: currentStartIndex, endIndex: endIndex)
|
||||
}
|
||||
} else if startIndex == nil {
|
||||
startIndex = range.location
|
||||
}
|
||||
currentIndex = range.location + range.length
|
||||
}
|
||||
|
||||
if let currentStartIndex = startIndex, let currentIndex = currentIndex {
|
||||
startIndex = nil
|
||||
let endIndex = currentIndex
|
||||
addSpoiler(startIndex: currentStartIndex, endIndex: endIndex)
|
||||
}
|
||||
}
|
||||
|
||||
if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
||||
if let start = textView.position(from: beginning, offset: range.location), let end = textView.position(from: start, offset: range.length), let textRange = textView.textRange(from: start, to: end) {
|
||||
let textRects = textView.selectionRects(for: textRange)
|
||||
for textRect in textRects {
|
||||
customEmojiRects.append((textRect.rect, value))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if !spoilerRects.isEmpty {
|
||||
let spoilerView: InvisibleInkDustView
|
||||
if let current = self.spoilerView {
|
||||
spoilerView = current
|
||||
} else {
|
||||
spoilerView = InvisibleInkDustView(textNode: nil, enableAnimations: component.context.sharedContext.energyUsageSettings.fullTranslucency)
|
||||
spoilerView.alpha = self.spoilersRevealed ? 0.0 : 1.0
|
||||
spoilerView.isUserInteractionEnabled = false
|
||||
self.textView.addSubview(spoilerView)
|
||||
self.spoilerView = spoilerView
|
||||
}
|
||||
spoilerView.frame = CGRect(origin: CGPoint(), size: self.textView.contentSize)
|
||||
spoilerView.update(size: self.textView.contentSize, color: component.textColor, textColor: component.textColor, rects: spoilerRects, wordRects: spoilerRects)
|
||||
} else if let spoilerView = self.spoilerView {
|
||||
spoilerView.removeFromSuperview()
|
||||
self.spoilerView = nil
|
||||
}
|
||||
|
||||
if !customEmojiRects.isEmpty {
|
||||
let customEmojiContainerView: CustomEmojiContainerView
|
||||
if let current = self.customEmojiContainerView {
|
||||
customEmojiContainerView = current
|
||||
} else {
|
||||
customEmojiContainerView = CustomEmojiContainerView(emojiViewProvider: { [weak self] emoji in
|
||||
guard let strongSelf = self, let emojiViewProvider = strongSelf.emojiViewProvider else {
|
||||
return nil
|
||||
}
|
||||
return emojiViewProvider(emoji)
|
||||
})
|
||||
customEmojiContainerView.isUserInteractionEnabled = false
|
||||
self.textView.addSubview(customEmojiContainerView)
|
||||
self.customEmojiContainerView = customEmojiContainerView
|
||||
}
|
||||
|
||||
customEmojiContainerView.update(fontSize: component.fontSize, textColor: component.textColor, emojiRects: customEmojiRects)
|
||||
} else if let customEmojiContainerView = self.customEmojiContainerView {
|
||||
customEmojiContainerView.removeFromSuperview()
|
||||
self.customEmojiContainerView = nil
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: TextFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
if self.emojiViewProvider == nil {
|
||||
self.emojiViewProvider = { [weak self] emoji in
|
||||
guard let component = self?.component else {
|
||||
return UIView()
|
||||
}
|
||||
let pointSize = floor(24.0 * 1.3)
|
||||
return EmojiTextAttachmentView(context: component.context, userLocation: .other, emoji: emoji, file: emoji.file, cache: component.context.animationCache, renderer: component.context.animationRenderer, placeholderColor: UIColor.white.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize))
|
||||
}
|
||||
}
|
||||
|
||||
if self.textView.textContainerInset != component.insets {
|
||||
self.textView.textContainerInset = component.insets
|
||||
}
|
||||
self.textContainer.size = CGSize(width: availableSize.width - self.textView.textContainerInset.left - self.textView.textContainerInset.right, height: 10000000.0)
|
||||
self.layoutManager.ensureLayout(for: self.textContainer)
|
||||
|
||||
let boundingRect = self.layoutManager.boundingRect(forGlyphRange: NSRange(location: 0, length: self.textStorage.length), in: self.textContainer)
|
||||
let size = CGSize(width: availableSize.width, height: min(200.0, ceil(boundingRect.height) + self.textView.textContainerInset.top + self.textView.textContainerInset.bottom))
|
||||
let size = CGSize(width: availableSize.width, height: min(availableSize.height, ceil(boundingRect.height) + self.textView.textContainerInset.top + self.textView.textContainerInset.bottom))
|
||||
|
||||
let wasEditing = component.externalState.isEditing
|
||||
let isEditing = self.textView.isFirstResponder
|
||||
|
||||
let refreshScrolling = self.textView.bounds.size != size
|
||||
self.textView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.textView.panGestureRecognizer.isEnabled = isEditing
|
||||
|
||||
if refreshScrolling {
|
||||
self.textView.setContentOffset(CGPoint(x: 0.0, y: max(0.0, self.textView.contentSize.height - self.textView.bounds.height)), animated: false)
|
||||
}
|
||||
|
||||
let placeholderSize = self.placeholder.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: component.placeholder, font: Font.regular(17.0), color: UIColor(white: 1.0, alpha: 0.25))),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let placeholderView = self.placeholder.view {
|
||||
if placeholderView.superview == nil {
|
||||
placeholderView.layer.anchorPoint = CGPoint()
|
||||
placeholderView.isUserInteractionEnabled = false
|
||||
self.insertSubview(placeholderView, belowSubview: self.textView)
|
||||
if isEditing {
|
||||
if wasEditing {
|
||||
self.textView.setContentOffset(CGPoint(x: 0.0, y: max(0.0, self.textView.contentSize.height - self.textView.bounds.height)), animated: false)
|
||||
}
|
||||
} else {
|
||||
self.textView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: true)
|
||||
}
|
||||
|
||||
let placeholderFrame = CGRect(origin: CGPoint(x: self.textView.textContainerInset.left + 5.0, y: self.textView.textContainerInset.top), size: placeholderSize)
|
||||
placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size)
|
||||
transition.setPosition(view: placeholderView, position: placeholderFrame.origin)
|
||||
|
||||
placeholderView.isHidden = self.textStorage.length != 0
|
||||
}
|
||||
|
||||
component.externalState.hasText = self.textStorage.length != 0
|
||||
component.externalState.isEditing = self.textView.isFirstResponder
|
||||
component.externalState.isEditing = isEditing
|
||||
|
||||
if component.hideKeyboard {
|
||||
if self.textView.inputView == nil {
|
||||
self.textView.inputView = EmptyInputView()
|
||||
if self.textView.isFirstResponder {
|
||||
self.textView.reloadInputViews()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.textView.inputView != nil {
|
||||
self.textView.inputView = nil
|
||||
if self.textView.isFirstResponder {
|
||||
self.textView.reloadInputViews()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
@ -299,3 +512,89 @@ public final class TextFieldComponent: Component {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
extension TextFieldComponent.InputState {
|
||||
public func insertText(_ text: NSAttributedString) -> TextFieldComponent.InputState {
|
||||
let inputText = NSMutableAttributedString(attributedString: self.inputText)
|
||||
let range = self.selectionRange
|
||||
|
||||
inputText.replaceCharacters(in: NSMakeRange(range.lowerBound, range.count), with: text)
|
||||
|
||||
let selectionPosition = range.lowerBound + (text.string as NSString).length
|
||||
return TextFieldComponent.InputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition)
|
||||
}
|
||||
|
||||
public func addFormattingAttribute(attribute: NSAttributedString.Key) -> TextFieldComponent.InputState {
|
||||
if !self.selectionRange.isEmpty {
|
||||
let nsRange = NSRange(location: self.selectionRange.lowerBound, length: self.selectionRange.count)
|
||||
var addAttribute = true
|
||||
var attributesToRemove: [NSAttributedString.Key] = []
|
||||
self.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in
|
||||
for (key, _) in attributes {
|
||||
if key == attribute && range == nsRange {
|
||||
addAttribute = false
|
||||
attributesToRemove.append(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = NSMutableAttributedString(attributedString: self.inputText)
|
||||
for attribute in attributesToRemove {
|
||||
result.removeAttribute(attribute, range: nsRange)
|
||||
}
|
||||
if addAttribute {
|
||||
result.addAttribute(attribute, value: true as Bool, range: nsRange)
|
||||
}
|
||||
return TextFieldComponent.InputState(inputText: result, selectionRange: self.selectionRange)
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
public func clearFormattingAttributes() -> TextFieldComponent.InputState {
|
||||
if !self.selectionRange.isEmpty {
|
||||
let nsRange = NSRange(location: self.selectionRange.lowerBound, length: self.selectionRange.count)
|
||||
var attributesToRemove: [NSAttributedString.Key] = []
|
||||
self.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in
|
||||
for (key, _) in attributes {
|
||||
attributesToRemove.append(key)
|
||||
}
|
||||
}
|
||||
|
||||
let result = NSMutableAttributedString(attributedString: self.inputText)
|
||||
for attribute in attributesToRemove {
|
||||
result.removeAttribute(attribute, range: nsRange)
|
||||
}
|
||||
return TextFieldComponent.InputState(inputText: result, selectionRange: self.selectionRange)
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
public func addLinkAttribute(selectionRange: Range<Int>, url: String) -> TextFieldComponent.InputState {
|
||||
if !selectionRange.isEmpty {
|
||||
let nsRange = NSRange(location: selectionRange.lowerBound, length: selectionRange.count)
|
||||
var linkRange = nsRange
|
||||
var attributesToRemove: [(NSAttributedString.Key, NSRange)] = []
|
||||
self.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in
|
||||
for (key, _) in attributes {
|
||||
if key == ChatTextInputAttributes.textUrl {
|
||||
attributesToRemove.append((key, range))
|
||||
linkRange = linkRange.union(range)
|
||||
} else {
|
||||
attributesToRemove.append((key, nsRange))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = NSMutableAttributedString(attributedString: self.inputText)
|
||||
for (attribute, range) in attributesToRemove {
|
||||
result.removeAttribute(attribute, range: range)
|
||||
}
|
||||
result.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: nsRange)
|
||||
return TextFieldComponent.InputState(inputText: result, selectionRange: selectionRange)
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -98,6 +98,7 @@ import StoryContainerScreen
|
||||
import StoryContentComponent
|
||||
import MoreHeaderButton
|
||||
import VolumeButtons
|
||||
import ChatContextQuery
|
||||
|
||||
#if DEBUG
|
||||
import os.signpost
|
||||
@ -18572,7 +18573,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
text = strongSelf.presentationData.strings.Conversation_AutoremoveOff
|
||||
}
|
||||
if let text = text {
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .autoDelete(isOn: isOn, title: nil, text: text), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .autoDelete(isOn: isOn, title: nil, text: text, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -2282,13 +2282,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private final class EmptyInputView: UIView, UIInputViewAudioFeedback {
|
||||
var enableInputClicksWhenVisible: Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private let emptyInputView = EmptyInputView()
|
||||
private func chatPresentationInterfaceStateInputView(_ state: ChatPresentationInterfaceState) -> UIView? {
|
||||
switch state.inputMode {
|
||||
@ -2634,13 +2628,16 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
peerId = id
|
||||
}
|
||||
|
||||
guard let interfaceInteraction = self.interfaceInteraction else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let inputNode = ChatEntityKeyboardInputNode(
|
||||
context: self.context,
|
||||
currentInputData: inputMediaNodeData,
|
||||
updatedInputData: self.inputMediaNodeDataPromise.get(),
|
||||
defaultToEmojiTab: !self.chatPresentationInterfaceState.interfaceState.effectiveInputState.inputText.string.isEmpty || self.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil || self.openStickersBeginWithEmoji,
|
||||
controllerInteraction: self.controllerInteraction,
|
||||
interfaceInteraction: self.interfaceInteraction,
|
||||
interaction: ChatEntityKeyboardInputNode.Interaction(chatControllerInteraction: self.controllerInteraction, panelInteraction: interfaceInteraction),
|
||||
chatPeerId: peerId,
|
||||
stateContext: self.inputMediaNodeStateContext
|
||||
)
|
||||
@ -2650,12 +2647,23 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
func loadInputPanels(theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) {
|
||||
if !self.didInitializeInputMediaNodeDataPromise, let interfaceInteraction = self.interfaceInteraction {
|
||||
if !self.didInitializeInputMediaNodeDataPromise {
|
||||
self.didInitializeInputMediaNodeDataPromise = true
|
||||
|
||||
let areCustomEmojiEnabled = self.chatPresentationInterfaceState.customEmojiAvailable
|
||||
|
||||
self.inputMediaNodeDataPromise.set(ChatEntityKeyboardInputNode.inputData(context: self.context, interfaceInteraction: interfaceInteraction, controllerInteraction: self.controllerInteraction, chatPeerId: self.chatLocation.peerId, areCustomEmojiEnabled: areCustomEmojiEnabled))
|
||||
|
||||
self.inputMediaNodeDataPromise.set(
|
||||
ChatEntityKeyboardInputNode.inputData(
|
||||
context: self.context,
|
||||
chatPeerId: self.chatLocation.peerId,
|
||||
areCustomEmojiEnabled: self.chatPresentationInterfaceState.customEmojiAvailable,
|
||||
sendGif: { [weak self] fileReference, sourceView, sourceRect, silentPosting, schedule in
|
||||
if let self {
|
||||
return self.controllerInteraction.sendGif(fileReference, sourceView, sourceRect, silentPosting, schedule)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
self.textInputPanelNode?.loadTextInputNodeIfNeeded()
|
||||
|
@ -9,182 +9,7 @@ import ChatInterfaceState
|
||||
import ChatPresentationInterfaceState
|
||||
import SwiftSignalKit
|
||||
import TextFormat
|
||||
|
||||
struct PossibleContextQueryTypes: OptionSet {
|
||||
var rawValue: Int32
|
||||
|
||||
init() {
|
||||
self.rawValue = 0
|
||||
}
|
||||
|
||||
init(rawValue: Int32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
static let emoji = PossibleContextQueryTypes(rawValue: (1 << 0))
|
||||
static let hashtag = PossibleContextQueryTypes(rawValue: (1 << 1))
|
||||
static let mention = PossibleContextQueryTypes(rawValue: (1 << 2))
|
||||
static let command = PossibleContextQueryTypes(rawValue: (1 << 3))
|
||||
static let contextRequest = PossibleContextQueryTypes(rawValue: (1 << 4))
|
||||
static let emojiSearch = PossibleContextQueryTypes(rawValue: (1 << 5))
|
||||
}
|
||||
|
||||
private func makeScalar(_ c: Character) -> Character {
|
||||
return c
|
||||
}
|
||||
|
||||
private let spaceScalar = " " as UnicodeScalar
|
||||
private let newlineScalar = "\n" as UnicodeScalar
|
||||
private let hashScalar = "#" as UnicodeScalar
|
||||
private let atScalar = "@" as UnicodeScalar
|
||||
private let slashScalar = "/" as UnicodeScalar
|
||||
private let colonScalar = ":" as UnicodeScalar
|
||||
private let alphanumerics = CharacterSet.alphanumerics
|
||||
|
||||
private func scalarCanPrependQueryControl(_ c: UnicodeScalar?) -> Bool {
|
||||
if let c = c {
|
||||
if c == " " || c == "\n" || c == "." || c == "," {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> [(NSRange, PossibleContextQueryTypes, NSRange?)] {
|
||||
if inputState.selectionRange.count != 0 {
|
||||
return []
|
||||
}
|
||||
|
||||
let inputText = inputState.inputText
|
||||
let inputString: NSString = inputText.string as NSString
|
||||
var results: [(NSRange, PossibleContextQueryTypes, NSRange?)] = []
|
||||
let inputLength = inputString.length
|
||||
|
||||
if inputLength != 0 {
|
||||
if inputString.hasPrefix("@") && inputLength != 1 {
|
||||
let startIndex = 1
|
||||
var index = startIndex
|
||||
var contextAddressRange: NSRange?
|
||||
|
||||
while true {
|
||||
if index == inputLength {
|
||||
break
|
||||
}
|
||||
if let c = UnicodeScalar(inputString.character(at: index)) {
|
||||
if c == " " {
|
||||
if index != startIndex {
|
||||
contextAddressRange = NSRange(location: startIndex, length: index - startIndex)
|
||||
index += 1
|
||||
}
|
||||
break
|
||||
} else {
|
||||
if !((c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || (c >= "0" && c <= "9") || c == "_") {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if index == inputLength {
|
||||
break
|
||||
} else {
|
||||
index += 1
|
||||
}
|
||||
} else {
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
if let contextAddressRange = contextAddressRange {
|
||||
results.append((contextAddressRange, [.contextRequest], NSRange(location: index, length: inputLength - index)))
|
||||
}
|
||||
}
|
||||
|
||||
let maxIndex = min(inputState.selectionRange.lowerBound, inputLength)
|
||||
if maxIndex == 0 {
|
||||
return results
|
||||
}
|
||||
var index = maxIndex - 1
|
||||
|
||||
var possibleQueryRange: NSRange?
|
||||
|
||||
let string = (inputString as String)
|
||||
let trimmedString = string.trimmingTrailingSpaces()
|
||||
if string.count < 3, trimmedString.isSingleEmoji {
|
||||
if inputText.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) == nil {
|
||||
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))
|
||||
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])
|
||||
var definedType = false
|
||||
|
||||
while true {
|
||||
var previousC: UnicodeScalar?
|
||||
if index != 0 {
|
||||
previousC = UnicodeScalar(inputString.character(at: index - 1))
|
||||
}
|
||||
if let c = UnicodeScalar(inputString.character(at: index)) {
|
||||
if c == spaceScalar || c == newlineScalar {
|
||||
possibleTypes = []
|
||||
} else if c == hashScalar {
|
||||
if scalarCanPrependQueryControl(previousC) {
|
||||
possibleTypes = possibleTypes.intersection([.hashtag])
|
||||
definedType = true
|
||||
index += 1
|
||||
possibleQueryRange = NSRange(location: index, length: maxIndex - index)
|
||||
}
|
||||
break
|
||||
} else if c == atScalar {
|
||||
if scalarCanPrependQueryControl(previousC) {
|
||||
possibleTypes = possibleTypes.intersection([.mention])
|
||||
definedType = true
|
||||
index += 1
|
||||
possibleQueryRange = NSRange(location: index, length: maxIndex - index)
|
||||
}
|
||||
break
|
||||
} else if c == slashScalar {
|
||||
if scalarCanPrependQueryControl(previousC) {
|
||||
possibleTypes = possibleTypes.intersection([.command])
|
||||
definedType = true
|
||||
index += 1
|
||||
possibleQueryRange = NSRange(location: index, length: maxIndex - index)
|
||||
}
|
||||
break
|
||||
} else if c == colonScalar {
|
||||
if scalarCanPrependQueryControl(previousC) {
|
||||
possibleTypes = possibleTypes.intersection([.emojiSearch])
|
||||
definedType = true
|
||||
index += 1
|
||||
possibleQueryRange = NSRange(location: index, length: maxIndex - index)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if index == 0 {
|
||||
break
|
||||
} else {
|
||||
index -= 1
|
||||
possibleQueryRange = NSRange(location: index, length: maxIndex - index)
|
||||
}
|
||||
}
|
||||
|
||||
if let possibleQueryRange = possibleQueryRange, definedType && !possibleTypes.isEmpty {
|
||||
results.append((possibleQueryRange, possibleTypes, nil))
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
import ChatContextQuery
|
||||
|
||||
func serviceTasksForChatPresentationIntefaceState(context: AccountContext, chatPresentationInterfaceState: ChatPresentationInterfaceState, updateState: @escaping ((ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void) -> [AnyHashable: () -> Disposable] {
|
||||
var missingEmoji = Set<Int64>()
|
||||
|
@ -12,16 +12,7 @@ import SearchPeerMembers
|
||||
import DeviceLocationManager
|
||||
import TelegramNotices
|
||||
import ChatPresentationInterfaceState
|
||||
|
||||
enum ChatContextQueryError {
|
||||
case generic
|
||||
case inlineBotLocationRequest(PeerId)
|
||||
}
|
||||
|
||||
enum ChatContextQueryUpdate {
|
||||
case remove
|
||||
case update(ChatPresentationInputQuery, Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError>)
|
||||
}
|
||||
import ChatContextQuery
|
||||
|
||||
func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentQueryStates: inout [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)], requestBotLocationStatus: @escaping (PeerId) -> Void) -> [ChatPresentationInputQueryKind: ChatContextQueryUpdate] {
|
||||
guard let peer = chatPresentationInterfaceState.renderedPeer?.peer else {
|
||||
@ -331,16 +322,16 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
|
||||
|
||||
return signal |> then(contextBot)
|
||||
case let .emojiSearch(query, languageCode, range):
|
||||
if query.isSingleEmoji {
|
||||
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
|
||||
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
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
return user.isPremium
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
if query.isSingleEmoji {
|
||||
return combineLatest(
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
|
||||
hasPremium
|
||||
@ -385,15 +376,6 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
return signal
|
||||
|> castError(ChatContextQueryError.self)
|
||||
|
@ -1091,7 +1091,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
text = strongSelf.presentationData.strings.Conversation_AutoremoveOff
|
||||
}
|
||||
if let text = text {
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .autoDelete(isOn: isOn, title: nil, text: text), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .autoDelete(isOn: isOn, title: nil, text: text, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -37,6 +37,7 @@ import LottieComponent
|
||||
import SolidRoundedButtonNode
|
||||
import TooltipUI
|
||||
import ChatTextInputMediaRecordingButton
|
||||
import ChatContextQuery
|
||||
|
||||
private let accessoryButtonFont = Font.medium(14.0)
|
||||
private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers])
|
||||
|
@ -11,6 +11,7 @@ import AccountContext
|
||||
import ChatPresentationInterfaceState
|
||||
import ChatControllerInteraction
|
||||
import ItemListUI
|
||||
import ChatContextQuery
|
||||
|
||||
private struct CommandChatInputContextPanelEntryStableId: Hashable {
|
||||
let command: PeerCommand
|
||||
|
@ -11,6 +11,7 @@ import MergeLists
|
||||
import AccountContext
|
||||
import ChatPresentationInterfaceState
|
||||
import ChatControllerInteraction
|
||||
import ChatContextQuery
|
||||
|
||||
private struct CommandMenuChatInputContextPanelEntryStableId: Hashable {
|
||||
let command: PeerCommand
|
||||
|
@ -20,6 +20,7 @@ import PremiumUI
|
||||
import StickerPeekUI
|
||||
import UndoUI
|
||||
import Pasteboard
|
||||
import ChatContextQuery
|
||||
|
||||
private enum EmojisChatInputContextPanelEntryStableId: Hashable, Equatable {
|
||||
case symbol(String)
|
||||
|
@ -12,6 +12,7 @@ import AccountContext
|
||||
import ItemListUI
|
||||
import ChatPresentationInterfaceState
|
||||
import ChatControllerInteraction
|
||||
import ChatContextQuery
|
||||
|
||||
private struct HashtagChatInputContextPanelEntryStableId: Hashable {
|
||||
let text: String
|
||||
|
@ -12,6 +12,7 @@ import LocalizedPeerData
|
||||
import ItemListUI
|
||||
import ChatPresentationInterfaceState
|
||||
import ChatControllerInteraction
|
||||
import ChatContextQuery
|
||||
|
||||
private struct MentionChatInputContextPanelEntry: Comparable, Identifiable {
|
||||
let index: Int
|
||||
|
@ -5662,7 +5662,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
text = strongSelf.presentationData.strings.Conversation_AutoremoveOff
|
||||
}
|
||||
if let text = text {
|
||||
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .autoDelete(isOn: isOn, title: nil, text: text), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||||
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .autoDelete(isOn: isOn, title: nil, text: text, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -5704,7 +5704,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
text = strongSelf.presentationData.strings.Conversation_AutoremoveOff
|
||||
}
|
||||
if let text = text {
|
||||
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .autoDelete(isOn: isOn, title: nil, text: text), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||||
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .autoDelete(isOn: isOn, title: nil, text: text, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -361,7 +361,9 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
if let imageData = compressImageToJPEG(image, quality: 0.6) {
|
||||
switch privacy {
|
||||
case let .story(storyPrivacy, period, pin):
|
||||
self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: period, randomId: randomId)
|
||||
let text = caption ?? NSAttributedString()
|
||||
let entities = generateChatInputTextEntities(text)
|
||||
self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: text.string, entities: entities, pin: pin, privacy: storyPrivacy, period: period, randomId: randomId)
|
||||
|
||||
/*let _ = (self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: period, randomId: randomId)
|
||||
|> deliverOnMainQueue).start(next: { [weak chatListController] result in
|
||||
@ -465,7 +467,9 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) }
|
||||
|
||||
if case let .story(storyPrivacy, period, pin) = privacy {
|
||||
self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameImageData: imageData), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: period, randomId: randomId)
|
||||
let text = caption ?? NSAttributedString()
|
||||
let entities = generateChatInputTextEntities(text)
|
||||
self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameImageData: imageData), text: text.string, entities: entities, pin: pin, privacy: storyPrivacy, period: period, randomId: randomId)
|
||||
/*let _ = (self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: period, randomId: randomId)
|
||||
|> deliverOnMainQueue).start(next: { [weak chatListController] result in
|
||||
if let chatListController {
|
||||
|
@ -114,6 +114,7 @@ public func textAttributedStringForStateText(_ stateText: NSAttributedString, fo
|
||||
}
|
||||
} else if key == ChatTextInputAttributes.customEmoji {
|
||||
result.addAttribute(key, value: value, range: range)
|
||||
result.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range)
|
||||
}
|
||||
}
|
||||
|
||||
@ -715,6 +716,31 @@ public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, th
|
||||
}
|
||||
}
|
||||
|
||||
public func refreshChatTextInputTypingAttributes(_ textView: UITextView, textColor: UIColor, baseFontSize: CGFloat) {
|
||||
var filteredAttributes: [NSAttributedString.Key: Any] = [
|
||||
NSAttributedString.Key.font: Font.regular(baseFontSize),
|
||||
NSAttributedString.Key.foregroundColor: textColor
|
||||
]
|
||||
let style = NSMutableParagraphStyle()
|
||||
style.baseWritingDirection = .natural
|
||||
filteredAttributes[NSAttributedString.Key.paragraphStyle] = style
|
||||
if let attributedText = textView.attributedText, attributedText.length != 0 {
|
||||
let attributes = attributedText.attributes(at: max(0, min(textView.selectedRange.location - 1, attributedText.length - 1)), effectiveRange: nil)
|
||||
for (key, value) in attributes {
|
||||
if key == ChatTextInputAttributes.bold {
|
||||
filteredAttributes[key] = value
|
||||
} else if key == ChatTextInputAttributes.italic {
|
||||
filteredAttributes[key] = value
|
||||
} else if key == ChatTextInputAttributes.monospace {
|
||||
filteredAttributes[key] = value
|
||||
} else if key == NSAttributedString.Key.font {
|
||||
filteredAttributes[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
textView.typingAttributes = filteredAttributes
|
||||
}
|
||||
|
||||
public func refreshChatTextInputTypingAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat) {
|
||||
var filteredAttributes: [NSAttributedString.Key: Any] = [
|
||||
NSAttributedString.Key.font: Font.regular(baseFontSize),
|
||||
|
@ -28,7 +28,7 @@ public enum UndoOverlayContent {
|
||||
case importedMessage(text: String)
|
||||
case audioRate(rate: CGFloat, text: String)
|
||||
case forward(savedMessages: Bool, text: String)
|
||||
case autoDelete(isOn: Bool, title: String?, text: String)
|
||||
case autoDelete(isOn: Bool, title: String?, text: String, customUndoText: String?)
|
||||
case gigagroupConversion(text: String)
|
||||
case linkRevoked(text: String)
|
||||
case voiceChatRecording(text: String)
|
||||
|
@ -154,7 +154,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white)
|
||||
displayUndo = undo
|
||||
self.originalRemainingSeconds = 3
|
||||
case let .autoDelete(isOn, title, text):
|
||||
case let .autoDelete(isOn, title, text, customUndoText):
|
||||
self.avatarNode = nil
|
||||
self.iconNode = nil
|
||||
self.iconCheckNode = nil
|
||||
@ -164,8 +164,18 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
if let title = title {
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
|
||||
}
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white)
|
||||
displayUndo = false
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural)
|
||||
self.textNode.attributedText = attributedText
|
||||
if let customUndoText {
|
||||
|
||||
undoText = customUndoText
|
||||
displayUndo = true
|
||||
} else {
|
||||
displayUndo = false
|
||||
}
|
||||
self.originalRemainingSeconds = 4.5
|
||||
case let .succeed(text):
|
||||
self.avatarNode = nil
|
||||
|
Loading…
x
Reference in New Issue
Block a user