mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Tag saved message
This commit is contained in:
parent
2d23d6c497
commit
dc7541065d
@ -10964,3 +10964,5 @@ Sorry for the inconvenience.";
|
||||
"PrivacyInfo.ShowReadTime.ButtonTitle" = "Show My Read Time";
|
||||
"PrivacyInfo.ShowReadTime.PremiumInfo" = "Subscription will let you see **%@'s** read time without showing yours.";
|
||||
"PrivacyInfo.ShowReadTime.AlwaysToast.Text" = "Set **Last Seen** privacy to 'Nobody' or 'My Contacts.'";
|
||||
|
||||
"Chat.ToastMessageTagged.Text" = "Message tagged with %@";
|
||||
|
@ -469,6 +469,13 @@ public final class ShareController: ViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
public var enqueued: (([PeerId], [Int64]) -> Void)? {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
self.controllerNode.enqueued = enqueued
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var openShareAsImage: (([Message]) -> Void)?
|
||||
|
||||
@ -713,6 +720,7 @@ public final class ShareController: ViewController {
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, fromForeignApp: self.fromForeignApp, forceTheme: self.forceTheme, fromPublicChannel: fromPublicChannel, segmentedValues: self.segmentedValues, shareStory: self.shareStory)
|
||||
self.controllerNode.completed = self.completed
|
||||
self.controllerNode.enqueued = self.enqueued
|
||||
self.controllerNode.present = { [weak self] c in
|
||||
self?.presentInGlobalOverlay(c)
|
||||
}
|
||||
@ -1841,7 +1849,7 @@ public final class ShareController: ViewController {
|
||||
case let .progress(value):
|
||||
return .progress(value)
|
||||
case .done:
|
||||
return .done
|
||||
return .done([])
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1876,21 +1884,21 @@ public final class ShareController: ViewController {
|
||||
}
|
||||
|> mapToSignal { progressSets -> Signal<ShareState, ShareControllerError> in
|
||||
if progressSets.isEmpty {
|
||||
return .single(.done)
|
||||
return .single(.done([]))
|
||||
}
|
||||
for item in progressSets {
|
||||
if case .progress = item {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
return .single(.done)
|
||||
return .single(.done([]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func shareLegacy(text: String, peerIds: [EnginePeer.Id], topicIds: [EnginePeer.Id: Int64], showNames: Bool, silently: Bool) -> Signal<ShareState, ShareControllerError> {
|
||||
guard let currentContext = self.currentContext as? ShareControllerAppAccountContext else {
|
||||
return .single(.done)
|
||||
return .single(.done([]))
|
||||
}
|
||||
return currentContext.context.engine.data.get(EngineDataMap(
|
||||
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))
|
||||
@ -1924,6 +1932,8 @@ public final class ShareController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
var correlationIds: [Int64] = []
|
||||
|
||||
switch subject {
|
||||
case let .url(url):
|
||||
for peerId in peerIds {
|
||||
@ -2205,7 +2215,9 @@ public final class ShareController: ViewController {
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
||||
messagesToEnqueue.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
|
||||
let correlationId = Int64.random(in: Int64.min ... Int64.max)
|
||||
correlationIds.append(correlationId)
|
||||
messagesToEnqueue.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []))
|
||||
}
|
||||
for message in messages {
|
||||
for media in message.media {
|
||||
@ -2277,7 +2289,7 @@ public final class ShareController: ViewController {
|
||||
case let .progress(value):
|
||||
return .progress(value)
|
||||
case .done:
|
||||
return .done
|
||||
return .done([])
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2326,7 +2338,7 @@ public final class ShareController: ViewController {
|
||||
}
|
||||
}
|
||||
if !hasStatuses {
|
||||
return .single(.done)
|
||||
return .single(.done(correlationIds))
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import ContextUI
|
||||
enum ShareState {
|
||||
case preparing(Bool)
|
||||
case progress(Float)
|
||||
case done
|
||||
case done([Int64])
|
||||
}
|
||||
|
||||
enum ShareExternalState {
|
||||
@ -67,6 +67,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
var debugAction: (() -> Void)?
|
||||
var openStats: (() -> Void)?
|
||||
var completed: (([PeerId]) -> Void)?
|
||||
var enqueued: (([PeerId], [Int64]) -> Void)?
|
||||
var present: ((ViewController) -> Void)?
|
||||
var disabledPeerSelected: ((EnginePeer) -> Void)?
|
||||
|
||||
@ -954,7 +955,8 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
}
|
||||
}
|
||||
|
||||
if case .done = status, !fromForeignApp {
|
||||
if case let .done(correlationIds) = status, !fromForeignApp {
|
||||
strongSelf.enqueued?(peerIds, correlationIds)
|
||||
strongSelf.dismiss?(true)
|
||||
return
|
||||
}
|
||||
|
@ -1037,7 +1037,8 @@ public extension TelegramEngine.EngineData.Item {
|
||||
func keys(data: TelegramEngine.EngineData) -> [PostboxViewKey] {
|
||||
return [
|
||||
.cachedPeerData(peerId: self.id),
|
||||
.basicPeer(data.accountPeerId)
|
||||
.basicPeer(data.accountPeerId),
|
||||
.basicPeer(self.id)
|
||||
]
|
||||
}
|
||||
|
||||
@ -1046,6 +1047,10 @@ public extension TelegramEngine.EngineData.Item {
|
||||
assertionFailure()
|
||||
return false
|
||||
}
|
||||
guard let basicTargetPeerView = views[.basicPeer(self.id)] as? BasicPeerView else {
|
||||
assertionFailure()
|
||||
return false
|
||||
}
|
||||
guard let view = views[.cachedPeerData(peerId: self.id)] as? CachedPeerDataView else {
|
||||
assertionFailure()
|
||||
return false
|
||||
@ -1055,6 +1060,13 @@ public extension TelegramEngine.EngineData.Item {
|
||||
return false
|
||||
}
|
||||
|
||||
guard let targetPeer = basicTargetPeerView.peer as? TelegramUser else {
|
||||
return false
|
||||
}
|
||||
if !targetPeer.flags.contains(.requirePremium) {
|
||||
return false
|
||||
}
|
||||
|
||||
if self.id.namespace == Namespaces.Peer.CloudUser {
|
||||
if let cachedData = view.cachedPeerData as? CachedUserData {
|
||||
return cachedData.flags.contains(.premiumRequired)
|
||||
|
@ -2,6 +2,7 @@ import Postbox
|
||||
|
||||
public final class EngineMessage: Equatable {
|
||||
public typealias Id = MessageId
|
||||
public typealias StableId = UInt32
|
||||
public typealias Index = MessageIndex
|
||||
public typealias Tags = MessageTags
|
||||
public typealias Attribute = MessageAttribute
|
||||
|
@ -489,13 +489,13 @@ public extension Message {
|
||||
|
||||
public extension Message {
|
||||
func areReactionsTags(accountPeerId: PeerId) -> Bool {
|
||||
/*if self.id.peerId == accountPeerId {
|
||||
if self.id.peerId == accountPeerId {
|
||||
if let reactionsAttribute = self.reactionsAttribute, !reactionsAttribute.reactions.isEmpty {
|
||||
return reactionsAttribute.isTags
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}*/
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ public enum PresentationResourceKey: Int32 {
|
||||
case navigationShareIcon
|
||||
case navigationSearchIcon
|
||||
case navigationCompactSearchIcon
|
||||
case navigationCompactTagsSearchIcon
|
||||
case navigationCalendarIcon
|
||||
case navigationMoreIcon
|
||||
case navigationMoreCircledIcon
|
||||
|
@ -73,6 +73,12 @@ public struct PresentationResourcesRootController {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/SearchIcon"), color: theme.rootController.navigationBar.accentTextColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func navigationCompactTagsSearchIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.navigationCompactTagsSearchIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/NavigationSearchTagsIcon"), color: theme.rootController.navigationBar.accentTextColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func navigationCalendarIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.navigationCalendarIcon.rawValue, { theme in
|
||||
|
@ -425,6 +425,7 @@ swift_library(
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/TelegramUI/Components/VideoMessageCameraScreen",
|
||||
"//submodules/TelegramUI/Components/MediaScrubberComponent",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatShareMessageTagView",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
"//build-system:ios_sim_arm64": [],
|
||||
|
@ -6,7 +6,7 @@ public enum ChatNavigationButtonAction: Equatable {
|
||||
case clearHistory
|
||||
case clearCache
|
||||
case cancelMessageSelection
|
||||
case search
|
||||
case search(hasTags: Bool)
|
||||
case dismiss
|
||||
case toggleInfoPanel
|
||||
case spacer
|
||||
|
@ -175,7 +175,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
|
||||
|
||||
self.navigationItem.titleView = self.titleView
|
||||
|
||||
let rightButton = ChatNavigationButton(action: .search, buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch)))
|
||||
let rightButton = ChatNavigationButton(action: .search(hasTags: false), buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch)))
|
||||
self.navigationItem.setRightBarButton(rightButton.buttonItem, animated: false)
|
||||
|
||||
self.titleView.title = self.presentationData.strings.Channel_AdminLog_TitleAllEvents
|
||||
@ -235,7 +235,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
|
||||
self.titleView.color = self.presentationData.theme.rootController.navigationBar.primaryTextColor
|
||||
self.updateTitle()
|
||||
|
||||
let rightButton = ChatNavigationButton(action: .search, buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch)))
|
||||
let rightButton = ChatNavigationButton(action: .search(hasTags: false), buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch)))
|
||||
self.navigationItem.setRightBarButton(rightButton.buttonItem, animated: false)
|
||||
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||||
|
@ -0,0 +1,28 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatShareMessageTagView",
|
||||
module_name = "ChatShareMessageTagView",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/ContextUI",
|
||||
"//submodules/ReactionSelectionNode",
|
||||
"//submodules/TelegramUI/Components/EntityKeyboard",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,155 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import UndoUI
|
||||
import ReactionSelectionNode
|
||||
import EntityKeyboard
|
||||
|
||||
public final class ChatShareMessageTagView: UIView, UndoOverlayControllerAdditionalView {
|
||||
private struct Params: Equatable {
|
||||
var size: CGSize
|
||||
|
||||
init(size: CGSize) {
|
||||
self.size = size
|
||||
}
|
||||
}
|
||||
|
||||
public var interaction: UndoOverlayControllerAdditionalViewInteraction?
|
||||
|
||||
private var reactionContextNode: ReactionContextNode?
|
||||
private var params: Params?
|
||||
|
||||
public init(context: AccountContext, presentationData: PresentationData, reactionItems: [ReactionItem], completion: @escaping (TelegramMediaFile, UpdateMessageReaction) -> Void) {
|
||||
super.init(frame: CGRect())
|
||||
|
||||
let reactionContextNode = ReactionContextNode(
|
||||
context: context,
|
||||
animationCache: context.animationCache,
|
||||
presentationData: presentationData,
|
||||
items: reactionItems.map(ReactionContextItem.reaction),
|
||||
selectedItems: Set(),
|
||||
title: presentationData.strings.Chat_ContextMenuTagsTitle,
|
||||
alwaysAllowPremiumReactions: false,
|
||||
allPresetReactionsAreAvailable: true,
|
||||
getEmojiContent: { animationCache, animationRenderer in
|
||||
let mappedReactionItems: [EmojiComponentReactionItem] = reactionItems.map { reaction -> EmojiComponentReactionItem in
|
||||
return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation)
|
||||
}
|
||||
|
||||
return EmojiPagerContentComponent.emojiInputData(
|
||||
context: context,
|
||||
animationCache: animationCache,
|
||||
animationRenderer: animationRenderer,
|
||||
isStandalone: false,
|
||||
subject: .messageTag,
|
||||
hasTrending: false,
|
||||
topReactionItems: mappedReactionItems,
|
||||
areUnicodeEmojiEnabled: false,
|
||||
areCustomEmojiEnabled: true,
|
||||
chatPeerId: context.account.peerId,
|
||||
selectedItems: Set(),
|
||||
premiumIfSavedMessages: false
|
||||
)
|
||||
},
|
||||
isExpandedUpdated: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.interaction?.disableTimeout()
|
||||
self.update(transition: transition)
|
||||
},
|
||||
requestLayout: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.update(transition: transition)
|
||||
},
|
||||
requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.update(transition: transition)
|
||||
}
|
||||
)
|
||||
reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (context.engine.stickers.availableReactions()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] availableReactions in
|
||||
guard let self, let availableReactions else {
|
||||
return
|
||||
}
|
||||
|
||||
var file: TelegramMediaFile?
|
||||
switch updateReaction {
|
||||
case .builtin:
|
||||
for reaction in availableReactions.reactions {
|
||||
if reaction.value == updateReaction.reaction {
|
||||
file = reaction.centerAnimation
|
||||
break
|
||||
}
|
||||
}
|
||||
case let .custom(_, fileValue):
|
||||
file = fileValue
|
||||
}
|
||||
|
||||
guard let file else {
|
||||
return
|
||||
}
|
||||
|
||||
completion(file, updateReaction)
|
||||
|
||||
self.interaction?.dismiss()
|
||||
})
|
||||
}
|
||||
reactionContextNode.displayTail = false
|
||||
reactionContextNode.forceTailToRight = true
|
||||
reactionContextNode.forceDark = false
|
||||
self.reactionContextNode = reactionContextNode
|
||||
|
||||
self.addSubnode(reactionContextNode)
|
||||
}
|
||||
|
||||
required public init(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
public func update(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
let params = Params(size: size)
|
||||
if self.params == params {
|
||||
return
|
||||
}
|
||||
self.params = params
|
||||
self.update(params: params, transition: transition)
|
||||
}
|
||||
|
||||
private func update(transition: ContainedViewLayoutTransition) {
|
||||
if let params = self.params {
|
||||
self.update(params: params, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private func update(params: Params, transition: ContainedViewLayoutTransition) {
|
||||
guard let reactionContextNode = self.reactionContextNode else {
|
||||
return
|
||||
}
|
||||
|
||||
let isFirstTime = reactionContextNode.bounds.isEmpty
|
||||
|
||||
let reactionsAnchorRect = CGRect(origin: CGPoint(x: params.size.width - 1.0, y: 0.0), size: CGSize(width: 1.0, height: 1.0))
|
||||
|
||||
transition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: params.size))
|
||||
reactionContextNode.updateLayout(size: params.size, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: transition)
|
||||
if isFirstTime {
|
||||
reactionContextNode.animateIn(from: reactionsAnchorRect)
|
||||
}
|
||||
}
|
||||
}
|
@ -357,19 +357,69 @@ public class ImmediateTextNodeWithEntities: TextNode {
|
||||
public var tapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)?
|
||||
public var longTapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)?
|
||||
|
||||
public var customItemLayout: ((CGSize, TelegramMediaFile) -> CGSize)?
|
||||
|
||||
private func processedAttributedText() -> NSAttributedString? {
|
||||
var updatedString: NSAttributedString?
|
||||
if let sourceString = self.attributedText {
|
||||
let string = NSMutableAttributedString(attributedString: sourceString)
|
||||
|
||||
let fullRange = NSRange(location: 0, length: string.length)
|
||||
string.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: fullRange, options: [], using: { value, range, _ in
|
||||
if let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
||||
if let font = string.attribute(.font, at: range.location, effectiveRange: nil) as? UIFont {
|
||||
string.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize), range: range)
|
||||
var fullRange = NSRange(location: 0, length: string.length)
|
||||
var originalTextId = 0
|
||||
while true {
|
||||
var found = false
|
||||
string.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: fullRange, options: [], using: { value, range, stop in
|
||||
if let value = value as? ChatTextInputTextCustomEmojiAttribute, let font = string.attribute(.font, at: range.location, effectiveRange: nil) as? UIFont {
|
||||
let updatedSubstring = NSMutableAttributedString(string: "&")
|
||||
|
||||
let replacementRange = NSRange(location: 0, length: updatedSubstring.length)
|
||||
updatedSubstring.addAttributes(string.attributes(at: range.location, effectiveRange: nil), range: replacementRange)
|
||||
updatedSubstring.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize), range: replacementRange)
|
||||
updatedSubstring.addAttribute(originalTextAttributeKey, value: OriginalTextAttribute(id: originalTextId, string: string.attributedSubstring(from: range).string), range: replacementRange)
|
||||
originalTextId += 1
|
||||
|
||||
let itemSize = (font.pointSize * 24.0 / 17.0)
|
||||
|
||||
let runDelegateData = RunDelegateData(
|
||||
ascent: font.ascender,
|
||||
descent: font.descender,
|
||||
width: itemSize
|
||||
)
|
||||
var callbacks = CTRunDelegateCallbacks(
|
||||
version: kCTRunDelegateCurrentVersion,
|
||||
dealloc: { dataRef in
|
||||
Unmanaged<RunDelegateData>.fromOpaque(dataRef).release()
|
||||
},
|
||||
getAscent: { dataRef in
|
||||
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
|
||||
return data.takeUnretainedValue().ascent
|
||||
},
|
||||
getDescent: { dataRef in
|
||||
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
|
||||
return data.takeUnretainedValue().descent
|
||||
},
|
||||
getWidth: { dataRef in
|
||||
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
|
||||
return data.takeUnretainedValue().width
|
||||
}
|
||||
)
|
||||
|
||||
if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) {
|
||||
updatedSubstring.addAttribute(NSAttributedString.Key(kCTRunDelegateAttributeName as String), value: runDelegate, range: replacementRange)
|
||||
}
|
||||
|
||||
string.replaceCharacters(in: range, with: updatedSubstring)
|
||||
let updatedRange = NSRange(location: range.location, length: updatedSubstring.length)
|
||||
|
||||
found = true
|
||||
stop.pointee = ObjCBool(true)
|
||||
fullRange = NSRange(location: updatedRange.upperBound, length: fullRange.upperBound - range.upperBound)
|
||||
}
|
||||
})
|
||||
if !found {
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
updatedString = string
|
||||
}
|
||||
@ -418,16 +468,20 @@ public class ImmediateTextNodeWithEntities: TextNode {
|
||||
let id = InlineStickerItemLayer.Key(id: stickerItem.emoji.fileId, index: index)
|
||||
validIds.append(id)
|
||||
|
||||
let itemSize = floor(stickerItem.fontSize * 24.0 / 17.0)
|
||||
let itemSide = floor(stickerItem.fontSize * 24.0 / 17.0)
|
||||
var itemSize = CGSize(width: itemSide, height: itemSide)
|
||||
if let file = stickerItem.file, let customItemLayout = self.customItemLayout {
|
||||
itemSize = customItemLayout(itemSize, file)
|
||||
}
|
||||
|
||||
let itemFrame = CGRect(origin: item.rect.offsetBy(dx: textLayout.insets.left, dy: textLayout.insets.top + 0.0).center, size: CGSize()).insetBy(dx: -itemSize / 2.0, dy: -itemSize / 2.0)
|
||||
let itemFrame = CGRect(origin: item.rect.offsetBy(dx: textLayout.insets.left, dy: textLayout.insets.top + 0.0).center, size: CGSize()).insetBy(dx: -itemSize.width / 2.0, dy: -itemSize.height / 2.0)
|
||||
|
||||
let itemLayer: InlineStickerItemLayer
|
||||
if let current = self.inlineStickerItemLayers[id] {
|
||||
itemLayer = current
|
||||
itemLayer.dynamicColor = item.textColor
|
||||
} else {
|
||||
let pointSize = floor(itemSize * 1.3)
|
||||
let pointSize = floor(itemSize.width * 1.3)
|
||||
itemLayer = InlineStickerItemLayer(context: context, userLocation: .other, attemptSynchronousLoad: false, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: item.textColor)
|
||||
self.inlineStickerItemLayers[id] = itemLayer
|
||||
self.layer.addSublayer(itemLayer)
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Chat/NavigationSearchTagsIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/NavigationSearchTagsIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "savedsearch_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
118
submodules/TelegramUI/Images.xcassets/Chat/NavigationSearchTagsIcon.imageset/savedsearch_30.pdf
vendored
Normal file
118
submodules/TelegramUI/Images.xcassets/Chat/NavigationSearchTagsIcon.imageset/savedsearch_30.pdf
vendored
Normal file
@ -0,0 +1,118 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 9.334961 9.588989 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
1.330000 10.411050 m
|
||||
1.330000 13.357489 3.718561 15.746050 6.665000 15.746050 c
|
||||
9.611439 15.746050 12.000000 13.357489 12.000000 10.411050 c
|
||||
12.000000 7.464611 9.611439 5.076050 6.665000 5.076050 c
|
||||
3.718561 5.076050 1.330000 7.464611 1.330000 10.411050 c
|
||||
h
|
||||
6.665000 17.076050 m
|
||||
2.984022 17.076050 0.000000 14.092028 0.000000 10.411050 c
|
||||
0.000000 6.730072 2.984022 3.746050 6.665000 3.746050 c
|
||||
8.206339 3.746050 9.625477 4.269255 10.754500 5.147752 c
|
||||
15.578101 0.324150 l
|
||||
15.902237 0.000015 16.427763 0.000015 16.751900 0.324150 c
|
||||
17.076035 0.648287 17.076035 1.173813 16.751900 1.497949 c
|
||||
11.928298 6.321549 l
|
||||
12.806795 7.450573 13.330000 8.869711 13.330000 10.411050 c
|
||||
13.330000 14.092028 10.345978 17.076050 6.665000 17.076050 c
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 2.334961 2.835022 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
2.665000 9.330017 m
|
||||
1.193161 9.330017 0.000000 8.136856 0.000000 6.665017 c
|
||||
0.000000 2.665017 l
|
||||
0.000000 1.193178 1.193161 0.000017 2.665000 0.000017 c
|
||||
8.665000 0.000017 l
|
||||
9.503828 0.000017 10.293703 0.394955 10.797000 1.066017 c
|
||||
12.522000 3.366017 l
|
||||
13.099334 4.135795 13.099333 5.194240 12.522000 5.964017 c
|
||||
10.797000 8.264017 l
|
||||
10.293703 8.935080 9.503828 9.330017 8.665000 9.330017 c
|
||||
2.665000 9.330017 l
|
||||
h
|
||||
1.330000 6.665017 m
|
||||
1.330000 7.402317 1.927700 8.000017 2.665000 8.000017 c
|
||||
8.665000 8.000017 l
|
||||
9.085201 8.000017 9.480880 7.802178 9.733000 7.466017 c
|
||||
11.458000 5.166018 l
|
||||
11.680667 4.869128 11.680667 4.460906 11.458000 4.164017 c
|
||||
9.733000 1.864017 l
|
||||
9.480880 1.527856 9.085201 1.330017 8.665000 1.330017 c
|
||||
2.665000 1.330017 l
|
||||
1.927700 1.330017 1.330000 1.927717 1.330000 2.665017 c
|
||||
1.330000 6.665017 l
|
||||
h
|
||||
8.665000 3.665017 m
|
||||
9.217285 3.665017 9.665000 4.112732 9.665000 4.665017 c
|
||||
9.665000 5.217302 9.217285 5.665017 8.665000 5.665017 c
|
||||
8.112715 5.665017 7.665000 5.217302 7.665000 4.665017 c
|
||||
7.665000 4.112732 8.112715 3.665017 8.665000 3.665017 c
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
2019
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000002109 00000 n
|
||||
0000002132 00000 n
|
||||
0000002305 00000 n
|
||||
0000002379 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
2438
|
||||
%%EOF
|
@ -2258,129 +2258,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return $0.updatedInputMode(f)
|
||||
})
|
||||
}, openMessageShareMenu: { [weak self] id in
|
||||
if let strongSelf = self, let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id), let message = messages.first {
|
||||
let chatPresentationInterfaceState = strongSelf.presentationInterfaceState
|
||||
var warnAboutPrivate = false
|
||||
var canShareToStory = false
|
||||
if case .peer = chatPresentationInterfaceState.chatLocation, let channel = message.peers[message.id.peerId] as? TelegramChannel {
|
||||
if case .broadcast = channel.info {
|
||||
canShareToStory = true
|
||||
}
|
||||
if channel.addressName == nil {
|
||||
warnAboutPrivate = true
|
||||
}
|
||||
}
|
||||
let shareController = ShareController(context: strongSelf.context, subject: .messages(messages), updatedPresentationData: strongSelf.updatedPresentationData, shareAsLink: true)
|
||||
|
||||
shareController.parentNavigationController = strongSelf.navigationController as? NavigationController
|
||||
|
||||
if let message = messages.first, message.media.contains(where: { media in
|
||||
if media is TelegramMediaContact || media is TelegramMediaPoll {
|
||||
return true
|
||||
} else if let file = media as? TelegramMediaFile, file.isSticker || file.isAnimatedSticker || file.isVideoSticker {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
canShareToStory = false
|
||||
}
|
||||
if message.text.containsOnlyEmoji {
|
||||
canShareToStory = false
|
||||
}
|
||||
|
||||
if canShareToStory {
|
||||
shareController.shareStory = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
Queue.mainQueue().after(0.15) {
|
||||
self.openStorySharing(messages: messages)
|
||||
}
|
||||
}
|
||||
}
|
||||
shareController.openShareAsImage = { [weak self] messages in
|
||||
if let strongSelf = self {
|
||||
strongSelf.present(ChatQrCodeScreen(context: strongSelf.context, subject: .messages(messages)), in: .window(.root))
|
||||
}
|
||||
}
|
||||
shareController.dismissed = { [weak self] shared in
|
||||
if shared {
|
||||
self?.commitPurposefulAction()
|
||||
}
|
||||
}
|
||||
shareController.actionCompleted = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let content: UndoOverlayContent
|
||||
if warnAboutPrivate {
|
||||
content = .linkCopied(text: strongSelf.presentationData.strings.Conversation_PrivateMessageLinkCopiedLong)
|
||||
} else {
|
||||
content = .linkCopied(text: strongSelf.presentationData.strings.Conversation_LinkCopied)
|
||||
}
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
|
||||
}
|
||||
}
|
||||
shareController.completed = { [weak self] peerIds in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (strongSelf.context.engine.data.get(
|
||||
EngineDataList(
|
||||
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
|
||||
)
|
||||
)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let peers = peerList.compactMap { $0 }
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let text: String
|
||||
var savedMessages = false
|
||||
if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId {
|
||||
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many
|
||||
savedMessages = true
|
||||
} else {
|
||||
if peers.count == 1, let peer = peers.first {
|
||||
var peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
peerName = peerName.replacingOccurrences(of: "**", with: "")
|
||||
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string : presentationData.strings.Conversation_ForwardTooltip_Chat_Many(peerName).string
|
||||
} else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last {
|
||||
var firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "")
|
||||
var secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "")
|
||||
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string : presentationData.strings.Conversation_ForwardTooltip_TwoChats_Many(firstPeerName, secondPeerName).string
|
||||
} else if let peer = peers.first {
|
||||
var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
peerName = peerName.replacingOccurrences(of: "**", with: "")
|
||||
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string : presentationData.strings.Conversation_ForwardTooltip_ManyChats_Many(peerName, "\(peers.count - 1)").string
|
||||
} else {
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in
|
||||
if savedMessages, let self, action == .info {
|
||||
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self, let peer else {
|
||||
return
|
||||
}
|
||||
guard let navigationController = self.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer)))
|
||||
})
|
||||
}
|
||||
return false
|
||||
}), in: .current)
|
||||
})
|
||||
}
|
||||
strongSelf.chatDisplayNode.dismissInput()
|
||||
strongSelf.present(shareController, in: .window(.root), blockInteraction: true)
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openMessageShareMenu(id: id)
|
||||
}, presentController: { [weak self] controller, arguments in
|
||||
self?.present(controller, in: .window(.root), with: arguments)
|
||||
}, presentControllerInCurrent: { [weak self] controller, arguments in
|
||||
|
@ -0,0 +1,215 @@
|
||||
import Foundation
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import ContextUI
|
||||
import ChatControllerInteraction
|
||||
import Display
|
||||
import UIKit
|
||||
import UndoUI
|
||||
import ShareController
|
||||
import ChatQrCodeScreen
|
||||
import ChatShareMessageTagView
|
||||
import ReactionSelectionNode
|
||||
|
||||
extension ChatControllerImpl {
|
||||
func openMessageShareMenu(id: EngineMessage.Id) {
|
||||
guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id), let message = messages.first else {
|
||||
return
|
||||
}
|
||||
|
||||
let chatPresentationInterfaceState = self.presentationInterfaceState
|
||||
var warnAboutPrivate = false
|
||||
var canShareToStory = false
|
||||
if case .peer = chatPresentationInterfaceState.chatLocation, let channel = message.peers[message.id.peerId] as? TelegramChannel {
|
||||
if case .broadcast = channel.info {
|
||||
canShareToStory = true
|
||||
}
|
||||
if channel.addressName == nil {
|
||||
warnAboutPrivate = true
|
||||
}
|
||||
}
|
||||
let shareController = ShareController(context: self.context, subject: .messages(messages), updatedPresentationData: self.updatedPresentationData, shareAsLink: true)
|
||||
shareController.parentNavigationController = self.navigationController as? NavigationController
|
||||
|
||||
if let message = messages.first, message.media.contains(where: { media in
|
||||
if media is TelegramMediaContact || media is TelegramMediaPoll {
|
||||
return true
|
||||
} else if let file = media as? TelegramMediaFile, file.isSticker || file.isAnimatedSticker || file.isVideoSticker {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
canShareToStory = false
|
||||
}
|
||||
if message.text.containsOnlyEmoji {
|
||||
canShareToStory = false
|
||||
}
|
||||
|
||||
if canShareToStory {
|
||||
shareController.shareStory = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
Queue.mainQueue().after(0.15) {
|
||||
self.openStorySharing(messages: messages)
|
||||
}
|
||||
}
|
||||
}
|
||||
shareController.openShareAsImage = { [weak self] messages in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.present(ChatQrCodeScreen(context: self.context, subject: .messages(messages)), in: .window(.root))
|
||||
}
|
||||
shareController.dismissed = { [weak self] shared in
|
||||
if shared {
|
||||
self?.commitPurposefulAction()
|
||||
}
|
||||
}
|
||||
shareController.actionCompleted = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let content: UndoOverlayContent
|
||||
if warnAboutPrivate {
|
||||
content = .linkCopied(text: self.presentationData.strings.Conversation_PrivateMessageLinkCopiedLong)
|
||||
} else {
|
||||
content = .linkCopied(text: self.presentationData.strings.Conversation_LinkCopied)
|
||||
}
|
||||
self.present(UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
|
||||
}
|
||||
shareController.enqueued = { [weak self] peerIds, correlationIds in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (self.context.engine.data.get(
|
||||
EngineDataList(
|
||||
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
|
||||
)
|
||||
)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let peers = peerList.compactMap { $0 }
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let text: String
|
||||
var savedMessages = false
|
||||
if peerIds.count == 1, let peerId = peerIds.first, peerId == self.context.account.peerId {
|
||||
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many
|
||||
savedMessages = true
|
||||
} else {
|
||||
if peers.count == 1, let peer = peers.first {
|
||||
var peerName = peer.id == self.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
peerName = peerName.replacingOccurrences(of: "**", with: "")
|
||||
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string : presentationData.strings.Conversation_ForwardTooltip_Chat_Many(peerName).string
|
||||
} else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last {
|
||||
var firstPeerName = firstPeer.id == self.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "")
|
||||
var secondPeerName = secondPeer.id == self.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "")
|
||||
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string : presentationData.strings.Conversation_ForwardTooltip_TwoChats_Many(firstPeerName, secondPeerName).string
|
||||
} else if let peer = peers.first {
|
||||
var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
peerName = peerName.replacingOccurrences(of: "**", with: "")
|
||||
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string : presentationData.strings.Conversation_ForwardTooltip_ManyChats_Many(peerName, "\(peers.count - 1)").string
|
||||
} else {
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
|
||||
let reactionItems: Signal<[ReactionItem], NoError>
|
||||
if savedMessages {
|
||||
reactionItems = tagMessageReactions(context: self.context)
|
||||
} else {
|
||||
reactionItems = .single([])
|
||||
}
|
||||
|
||||
let _ = (reactionItems
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] reactionItems in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, position: savedMessages ? .top : .bottom, animateInAsReplacement: !savedMessages, action: { [weak self] action in
|
||||
if savedMessages, let self, action == .info {
|
||||
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self, let peer else {
|
||||
return
|
||||
}
|
||||
guard let navigationController = self.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer)))
|
||||
})
|
||||
}
|
||||
return false
|
||||
}, additionalView: savedMessages ? { [weak self] () -> UndoOverlayControllerAdditionalView? in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
return ChatShareMessageTagView(context: self.context, presentationData: self.presentationData, reactionItems: reactionItems, completion: { [weak self] file, updateReaction in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (self.context.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: context.account.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 45, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: [])
|
||||
|> map { view, _, _ -> [EngineMessage.Id] in
|
||||
//TODO:filter?
|
||||
for entry in view.entries.reversed() {
|
||||
if entry.message.id.namespace == Namespaces.Message.Cloud {
|
||||
return [entry.message.id]
|
||||
}
|
||||
}
|
||||
|
||||
return view.entries.compactMap { entry -> EngineMessage.Id? in
|
||||
for attribute in entry.message.attributes {
|
||||
if let attribute = attribute as? OutgoingMessageInfoAttribute {
|
||||
if let correlationId = attribute.correlationId {
|
||||
if correlationIds.contains(correlationId) {
|
||||
if entry.message.id.namespace == Namespaces.Message.Cloud {
|
||||
return entry.message.id
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|> filter { !$0.isEmpty }
|
||||
|> take(1)
|
||||
|> timeout(5.0, queue: .mainQueue(), alternate: .single([]))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] messageIds in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let messageId = messageIds.first {
|
||||
let _ = context.engine.messages.setMessageReactions(id: messageId, reactions: [updateReaction])
|
||||
|
||||
var isBuiltinReaction = false
|
||||
if case .builtin = updateReaction {
|
||||
isBuiltinReaction = true
|
||||
}
|
||||
self.present(UndoOverlayController(presentationData: presentationData, content: .messageTagged(context: self.context, customEmoji: file, isBuiltinReaction: isBuiltinReaction), elevatedLayout: false, position: .top, animateInAsReplacement: true, action: { _ in
|
||||
return false
|
||||
}), in: .current)
|
||||
}
|
||||
})
|
||||
})
|
||||
} : nil), in: .current)
|
||||
})
|
||||
})
|
||||
}
|
||||
self.chatDisplayNode.dismissInput()
|
||||
self.present(shareController, in: .window(.root), blockInteraction: true)
|
||||
}
|
||||
}
|
@ -108,7 +108,7 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present
|
||||
} else {
|
||||
let buttonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector)
|
||||
buttonItem.accessibilityLabel = strings.Conversation_Search
|
||||
return ChatNavigationButton(action: .search, buttonItem: buttonItem)
|
||||
return ChatNavigationButton(action: .search(hasTags: false), buttonItem: buttonItem)
|
||||
}
|
||||
} else {
|
||||
if case .spacer = currentButton?.action {
|
||||
@ -126,7 +126,7 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present
|
||||
} else {
|
||||
let buttonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector)
|
||||
buttonItem.accessibilityLabel = strings.Conversation_Search
|
||||
return ChatNavigationButton(action: .search, buttonItem: buttonItem)
|
||||
return ChatNavigationButton(action: .search(hasTags: false), buttonItem: buttonItem)
|
||||
}
|
||||
} else {
|
||||
if case .spacer = currentButton?.action {
|
||||
@ -149,13 +149,15 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present
|
||||
if case .scheduledMessages = presentationInterfaceState.subject {
|
||||
return chatInfoNavigationButton
|
||||
} else {
|
||||
if presentationInterfaceState.hasPlentyOfMessages {
|
||||
if case .search = currentButton?.action {
|
||||
let isTags = presentationInterfaceState.hasSearchTags
|
||||
|
||||
if presentationInterfaceState.hasPlentyOfMessages || isTags {
|
||||
if case .search(isTags) = currentButton?.action {
|
||||
return currentButton
|
||||
} else {
|
||||
let buttonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector)
|
||||
let buttonItem = UIBarButtonItem(image: isTags ? PresentationResourcesRootController.navigationCompactTagsSearchIcon(presentationInterfaceState.theme) : PresentationResourcesRootController.navigationCompactSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector)
|
||||
buttonItem.accessibilityLabel = strings.Conversation_Search
|
||||
return ChatNavigationButton(action: .search, buttonItem: buttonItem)
|
||||
return ChatNavigationButton(action: .search(hasTags: isTags), buttonItem: buttonItem)
|
||||
}
|
||||
} else {
|
||||
if case .spacer = currentButton?.action {
|
||||
|
@ -5,6 +5,80 @@ import Postbox
|
||||
import AccountContext
|
||||
import ReactionSelectionNode
|
||||
|
||||
func tagMessageReactions(context: AccountContext) -> Signal<[ReactionItem], NoError> {
|
||||
return combineLatest(
|
||||
context.engine.stickers.availableReactions(),
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudDefaultTagReactions], namespaces: [ItemCollectionId.Namespace.max - 1], aroundIndex: nil, count: 10000000)
|
||||
)
|
||||
|> take(1)
|
||||
|> map { availableReactions, view -> [ReactionItem] in
|
||||
var defaultTagReactions: OrderedItemListView?
|
||||
for orderedView in view.orderedItemListsViews {
|
||||
if orderedView.collectionId == Namespaces.OrderedItemList.CloudDefaultTagReactions {
|
||||
defaultTagReactions = orderedView
|
||||
}
|
||||
}
|
||||
|
||||
var result: [ReactionItem] = []
|
||||
var existingIds = Set<MessageReaction.Reaction>()
|
||||
|
||||
if let defaultTagReactions {
|
||||
for item in defaultTagReactions.items {
|
||||
guard let topReaction = item.contents.get(RecentReactionItem.self) else {
|
||||
continue
|
||||
}
|
||||
switch topReaction.content {
|
||||
case let .builtin(value):
|
||||
if let reaction = availableReactions?.reactions.first(where: { $0.value == .builtin(value) }) {
|
||||
guard let centerAnimation = reaction.centerAnimation else {
|
||||
continue
|
||||
}
|
||||
guard let aroundAnimation = reaction.aroundAnimation else {
|
||||
continue
|
||||
}
|
||||
|
||||
if existingIds.contains(reaction.value) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(reaction.value)
|
||||
|
||||
result.append(ReactionItem(
|
||||
reaction: ReactionItem.Reaction(rawValue: reaction.value),
|
||||
appearAnimation: reaction.appearAnimation,
|
||||
stillAnimation: reaction.selectAnimation,
|
||||
listAnimation: centerAnimation,
|
||||
largeListAnimation: reaction.activateAnimation,
|
||||
applicationAnimation: aroundAnimation,
|
||||
largeApplicationAnimation: reaction.effectAnimation,
|
||||
isCustom: false
|
||||
))
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
case let .custom(file):
|
||||
if existingIds.contains(.custom(file.fileId.id)) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(.custom(file.fileId.id))
|
||||
|
||||
result.append(ReactionItem(
|
||||
reaction: ReactionItem.Reaction(rawValue: .custom(file.fileId.id)),
|
||||
appearAnimation: file,
|
||||
stillAnimation: file,
|
||||
listAnimation: file,
|
||||
largeListAnimation: file,
|
||||
applicationAnimation: nil,
|
||||
largeApplicationAnimation: nil,
|
||||
isCustom: true
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
func topMessageReactions(context: AccountContext, message: Message) -> Signal<[ReactionItem], NoError> {
|
||||
if message.id.peerId == context.account.peerId {
|
||||
var loadTags = false
|
||||
@ -24,77 +98,7 @@ func topMessageReactions(context: AccountContext, message: Message) -> Signal<[R
|
||||
}
|
||||
|
||||
if loadTags {
|
||||
return combineLatest(
|
||||
context.engine.stickers.availableReactions(),
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudDefaultTagReactions], namespaces: [ItemCollectionId.Namespace.max - 1], aroundIndex: nil, count: 10000000)
|
||||
)
|
||||
|> take(1)
|
||||
|> map { availableReactions, view -> [ReactionItem] in
|
||||
var defaultTagReactions: OrderedItemListView?
|
||||
for orderedView in view.orderedItemListsViews {
|
||||
if orderedView.collectionId == Namespaces.OrderedItemList.CloudDefaultTagReactions {
|
||||
defaultTagReactions = orderedView
|
||||
}
|
||||
}
|
||||
|
||||
var result: [ReactionItem] = []
|
||||
var existingIds = Set<MessageReaction.Reaction>()
|
||||
|
||||
if let defaultTagReactions {
|
||||
for item in defaultTagReactions.items {
|
||||
guard let topReaction = item.contents.get(RecentReactionItem.self) else {
|
||||
continue
|
||||
}
|
||||
switch topReaction.content {
|
||||
case let .builtin(value):
|
||||
if let reaction = availableReactions?.reactions.first(where: { $0.value == .builtin(value) }) {
|
||||
guard let centerAnimation = reaction.centerAnimation else {
|
||||
continue
|
||||
}
|
||||
guard let aroundAnimation = reaction.aroundAnimation else {
|
||||
continue
|
||||
}
|
||||
|
||||
if existingIds.contains(reaction.value) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(reaction.value)
|
||||
|
||||
result.append(ReactionItem(
|
||||
reaction: ReactionItem.Reaction(rawValue: reaction.value),
|
||||
appearAnimation: reaction.appearAnimation,
|
||||
stillAnimation: reaction.selectAnimation,
|
||||
listAnimation: centerAnimation,
|
||||
largeListAnimation: reaction.activateAnimation,
|
||||
applicationAnimation: aroundAnimation,
|
||||
largeApplicationAnimation: reaction.effectAnimation,
|
||||
isCustom: false
|
||||
))
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
case let .custom(file):
|
||||
if existingIds.contains(.custom(file.fileId.id)) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(.custom(file.fileId.id))
|
||||
|
||||
result.append(ReactionItem(
|
||||
reaction: ReactionItem.Reaction(rawValue: .custom(file.fileId.id)),
|
||||
appearAnimation: file,
|
||||
stillAnimation: file,
|
||||
listAnimation: file,
|
||||
largeListAnimation: file,
|
||||
applicationAnimation: nil,
|
||||
largeApplicationAnimation: nil,
|
||||
isCustom: true
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
return tagMessageReactions(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,7 @@ swift_library(
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
|
||||
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -45,6 +45,7 @@ public enum UndoOverlayContent {
|
||||
case universal(animation: String, scale: CGFloat, colors: [String: UIColor], title: String?, text: String, customUndoText: String?, timeout: Double?)
|
||||
case premiumPaywall(title: String?, text: String, customUndoText: String?, timeout: Double?, linkAction: ((String) -> Void)?)
|
||||
case peers(context: AccountContext, peers: [EnginePeer], title: String?, text: String, customUndoText: String?)
|
||||
case messageTagged(context: AccountContext, customEmoji: TelegramMediaFile, isBuiltinReaction: Bool)
|
||||
}
|
||||
|
||||
public enum UndoOverlayAction {
|
||||
@ -53,6 +54,22 @@ public enum UndoOverlayAction {
|
||||
case commit
|
||||
}
|
||||
|
||||
public final class UndoOverlayControllerAdditionalViewInteraction {
|
||||
public let disableTimeout: () -> Void
|
||||
public let dismiss: () -> Void
|
||||
|
||||
public init(disableTimeout: @escaping () -> Void, dismiss: @escaping () -> Void) {
|
||||
self.disableTimeout = disableTimeout
|
||||
self.dismiss = dismiss
|
||||
}
|
||||
}
|
||||
|
||||
public protocol UndoOverlayControllerAdditionalView: UIView {
|
||||
var interaction: UndoOverlayControllerAdditionalViewInteraction? { get set }
|
||||
|
||||
func update(size: CGSize, transition: ContainedViewLayoutTransition)
|
||||
}
|
||||
|
||||
public final class UndoOverlayController: ViewController {
|
||||
public enum Position {
|
||||
case top
|
||||
@ -69,6 +86,7 @@ public final class UndoOverlayController: ViewController {
|
||||
private let position: Position
|
||||
private let animateInAsReplacement: Bool
|
||||
private var action: (UndoOverlayAction) -> Bool
|
||||
private let additionalView: (() -> UndoOverlayControllerAdditionalView?)?
|
||||
|
||||
private let blurred: Bool
|
||||
private var didPlayPresentationAnimation = false
|
||||
@ -78,7 +96,7 @@ public final class UndoOverlayController: ViewController {
|
||||
|
||||
public var tag: Any?
|
||||
|
||||
public init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, position: Position = .bottom, animateInAsReplacement: Bool = false, blurred: Bool = false, action: @escaping (UndoOverlayAction) -> Bool) {
|
||||
public init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, position: Position = .bottom, animateInAsReplacement: Bool = false, blurred: Bool = false, action: @escaping (UndoOverlayAction) -> Bool, additionalView: (() -> UndoOverlayControllerAdditionalView?)? = nil) {
|
||||
self.presentationData = presentationData
|
||||
self.content = content
|
||||
self.elevatedLayout = elevatedLayout
|
||||
@ -86,6 +104,7 @@ public final class UndoOverlayController: ViewController {
|
||||
self.animateInAsReplacement = animateInAsReplacement
|
||||
self.blurred = blurred
|
||||
self.action = action
|
||||
self.additionalView = additionalView
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
@ -97,7 +116,7 @@ public final class UndoOverlayController: ViewController {
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = UndoOverlayControllerNode(presentationData: self.presentationData, content: self.content, elevatedLayout: self.elevatedLayout, placementPosition: self.position, blurred: self.blurred, action: { [weak self] value in
|
||||
self.displayNode = UndoOverlayControllerNode(presentationData: self.presentationData, content: self.content, elevatedLayout: self.elevatedLayout, placementPosition: self.position, blurred: self.blurred, additionalView: self.additionalView, action: { [weak self] value in
|
||||
return self?.action(value) ?? false
|
||||
}, dismiss: { [weak self] in
|
||||
self?.dismiss()
|
||||
|
@ -19,6 +19,7 @@ import AccountContext
|
||||
import AnimatedAvatarSetNode
|
||||
import ComponentFlow
|
||||
import EmojiStatusComponent
|
||||
import TextNodeWithEntities
|
||||
|
||||
final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
private let presentationData: PresentationData
|
||||
@ -40,7 +41,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
private var stickerOffset: CGPoint?
|
||||
private var emojiStatus: ComponentView<Empty>?
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let textNode: ImmediateTextNode
|
||||
private let textNode: ImmediateTextNodeWithEntities
|
||||
private let buttonNode: HighlightTrackingButtonNode
|
||||
private let undoButtonTextNode: ImmediateTextNode
|
||||
private let undoButtonNode: HighlightTrackingButtonNode
|
||||
@ -52,10 +53,13 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
private var content: UndoOverlayContent
|
||||
private let blurred: Bool
|
||||
|
||||
private let additionalView: UndoOverlayControllerAdditionalView?
|
||||
|
||||
private let effectView: UIView
|
||||
|
||||
private let animationBackgroundColor: UIColor
|
||||
|
||||
private var isTimeoutDisabled: Bool = false
|
||||
private var originalRemainingSeconds: Double
|
||||
private var remainingSeconds: Double
|
||||
private var timer: SwiftSignalKit.Timer?
|
||||
@ -64,13 +68,15 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
|
||||
private var fetchResourceDisposable: Disposable?
|
||||
|
||||
init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, placementPosition: UndoOverlayController.Position, blurred: Bool, action: @escaping (UndoOverlayAction) -> Bool, dismiss: @escaping () -> Void) {
|
||||
init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, placementPosition: UndoOverlayController.Position, blurred: Bool, additionalView: (() -> UndoOverlayControllerAdditionalView?)?, action: @escaping (UndoOverlayAction) -> Bool, dismiss: @escaping () -> Void) {
|
||||
self.presentationData = presentationData
|
||||
self.elevatedLayout = elevatedLayout
|
||||
self.placementPosition = placementPosition
|
||||
self.blurred = blurred
|
||||
self.content = content
|
||||
|
||||
self.additionalView = additionalView?()
|
||||
|
||||
self.action = action
|
||||
self.dismiss = dismiss
|
||||
|
||||
@ -81,7 +87,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
self.titleNode.maximumNumberOfLines = 0
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode = ImmediateTextNodeWithEntities()
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
|
||||
@ -1153,6 +1159,39 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
} else {
|
||||
displayUndo = false
|
||||
}
|
||||
case let .messageTagged(context, customEmoji, isBuiltinReaction):
|
||||
self.avatarNode = nil
|
||||
self.iconNode = nil
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_savedmessages", colors: [:], scale: 0.066)
|
||||
self.animatedStickerNode = nil
|
||||
|
||||
let rawText = presentationData.strings.Chat_ToastMessageTagged_Text(".")
|
||||
let attributedText = NSMutableAttributedString(string: rawText.string, font: Font.regular(14.0), textColor: .white)
|
||||
for range in rawText.ranges {
|
||||
attributedText.addAttributes([ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: customEmoji.fileId.id, file: customEmoji, custom: nil)], range: range.range)
|
||||
}
|
||||
self.textNode.customItemLayout = { size, _ in
|
||||
if isBuiltinReaction {
|
||||
return CGSize(width: size.width * 2.0, height: size.height * 2.0)
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
self.textNode.arguments = TextNodeWithEntities.Arguments(
|
||||
context: context,
|
||||
cache: context.animationCache,
|
||||
renderer: context.animationRenderer,
|
||||
placeholderColor: UIColor(white: 1.0, alpha: 0.1),
|
||||
attemptSynchronous: false
|
||||
)
|
||||
self.textNode.visibility = true
|
||||
|
||||
self.textNode.attributedText = attributedText
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
|
||||
displayUndo = false
|
||||
self.originalRemainingSeconds = 3
|
||||
}
|
||||
|
||||
self.remainingSeconds = self.originalRemainingSeconds
|
||||
@ -1181,7 +1220,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
switch content {
|
||||
case .removedChat:
|
||||
self.panelWrapperNode.addSubnode(self.timerTextNode)
|
||||
case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal, .premiumPaywall, .peers:
|
||||
case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal, .premiumPaywall, .peers, .messageTagged:
|
||||
if self.textNode.tapAttributeAction != nil || displayUndo {
|
||||
self.isUserInteractionEnabled = true
|
||||
} else {
|
||||
@ -1251,6 +1290,24 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.animatedStickerNode?.started = { [weak self] in
|
||||
self?.stillStickerNode?.isHidden = true
|
||||
}
|
||||
|
||||
if let additionalView = self.additionalView {
|
||||
additionalView.interaction = UndoOverlayControllerAdditionalViewInteraction(disableTimeout: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isTimeoutDisabled = true
|
||||
self.timer?.invalidate()
|
||||
self.remainingSeconds = self.originalRemainingSeconds
|
||||
self.checkTimer()
|
||||
}, dismiss: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.dismiss()
|
||||
})
|
||||
self.view.addSubview(additionalView)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -1265,6 +1322,16 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
}
|
||||
|
||||
self.panelNode.view.addSubview(self.effectView)
|
||||
|
||||
if self.additionalView != nil {
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
@ -1318,11 +1385,13 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.containerLayoutUpdated(layout: validLayout, transition: .immediate)
|
||||
}
|
||||
}
|
||||
let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: false, completion: { [weak self] in
|
||||
self?.checkTimer()
|
||||
}, queue: .mainQueue())
|
||||
self.timer = timer
|
||||
timer.start()
|
||||
if !self.isTimeoutDisabled {
|
||||
let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: false, completion: { [weak self] in
|
||||
self?.checkTimer()
|
||||
}, queue: .mainQueue())
|
||||
self.timer = timer
|
||||
timer.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1566,6 +1635,12 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
let avatarsFrame = CGRect(origin: CGPoint(x: 13.0, y: floor((contentHeight - multiAvatarsSize.height) / 2.0) + verticalOffset), size: multiAvatarsSize)
|
||||
transition.updateFrame(node: multiAvatarsNode, frame: avatarsFrame)
|
||||
}
|
||||
|
||||
if let additionalView = self.additionalView {
|
||||
let additionalViewFrame = CGRect(origin: CGPoint(x: 0.0, y: panelWrapperFrame.maxY), size: CGSize(width: layout.size.width, height: layout.size.height - insets.bottom - panelWrapperFrame.maxY))
|
||||
transition.updateFrame(view: additionalView, frame: additionalViewFrame)
|
||||
additionalView.update(size: additionalViewFrame.size, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn(asReplacement: Bool) {
|
||||
@ -1602,6 +1677,10 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
|
||||
self.animatedStickerNode?.visibility = true
|
||||
|
||||
if let additionalView = self.additionalView {
|
||||
additionalView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
}
|
||||
|
||||
self.checkTimer()
|
||||
}
|
||||
|
||||
@ -1617,6 +1696,10 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
}
|
||||
self.panelNode.layer.animateScale(from: 1.0, to: 0.96, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
self.panelWrapperNode.layer.animateScale(from: 1.0, to: 0.96, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
|
||||
if let additionalView = self.additionalView {
|
||||
additionalView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
func animateOutWithReplacement(completion: @escaping () -> Void) {
|
||||
@ -1631,9 +1714,24 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !self.panelNode.frame.insetBy(dx: -60.0, dy: 0.0).contains(point) {
|
||||
return nil
|
||||
if let additionalView = self.additionalView {
|
||||
if let result = additionalView.hitTest(self.view.convert(point, to: additionalView), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
|
||||
if !self.panelNode.frame.insetBy(dx: -60.0, dy: 0.0).contains(point) {
|
||||
if self.additionalView != nil && self.isTimeoutDisabled {
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
let result = super.hitTest(point, with: event)
|
||||
if result == self {
|
||||
if self.additionalView != nil && self.isTimeoutDisabled {
|
||||
return self.view
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user