Tag saved message

This commit is contained in:
Isaac 2024-01-16 21:14:24 +04:00
parent 2d23d6c497
commit dc7541065d
23 changed files with 861 additions and 237 deletions

View File

@ -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 %@";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,7 @@ public enum PresentationResourceKey: Int32 {
case navigationShareIcon
case navigationSearchIcon
case navigationCompactSearchIcon
case navigationCompactTagsSearchIcon
case navigationCalendarIcon
case navigationMoreIcon
case navigationMoreCircledIcon

View File

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

View File

@ -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": [],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "savedsearch_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,6 +29,7 @@ swift_library(
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
],
visibility = [
"//visibility:public",

View File

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

View File

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