mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-06-20 18:24:43 +00:00
69bfc65da7
Consolidates 137 wave commits + 31 supporting commits (CLAUDE.md bump, typealias additions, AnyObject→EngineMedia restoration) into one squashed commit. Migrates dozens of consumer-side public APIs, struct fields, protocol methods, and enum payloads from Postbox protocols/structs to TelegramEngine engine wrappers and typealiases. Drops `import Postbox` from many files. Adds new TelegramCore typealiases and one TelegramEngineUnauthorized facade. Notable changes by category: **TelegramCore typealias additions** (rule 2 — narrow utility typealiases): - EngineChatListIndex, EngineTempBoxFile, EngineItemCollectionItemIndex, EngineItemCollectionViewEntryIndex, EngineValueBoxEncryptionParameters, EngineMessageAndThreadId, EnginePeerStoryStats, EngineMessageHistoryAnchorIndex, EngineChatListTotalUnreadStateCategory, EngineChatListTotalUnreadStateStats, EnginePeerSummaryCounterTags, EngineChatListTotalUnreadState, EngineItemCacheEntryId, EngineHashFunctions, EngineCachedMediaResourceRepresentationResult, EngineMediaResourceDataFetchResult, EngineMediaResourceDataFetchError, EngineMediaResourceStatus, EngineCachedPeerData **TelegramCore engine extensions/forwarders**: - EngineMessage.engineMedia, EngineMessage.enginePeers, EngineMessage.adAttribute, EngineMessage.effectivelyIncoming - engineFileSize forwarder - TelegramEngine.Resources.clearCachedMediaResources(mediaResourceIds: Set<EngineMediaResource.Id>) - TelegramEngine.Resources.fetchStatus(id:resourceSize:) - TelegramEngineUnauthorized.UnauthorizedResources facade with storeResourceData **Public API/struct migrations to engine types**: - ChatAvailableMessageActions.banAuthor/banAuthors → EnginePeer?/[EnginePeer] - WebSessionsContextState.peers → [EnginePeer.Id: EnginePeer] - CacheUsageStats.peers → [EnginePeer.Id: EnginePeer] - PeerCommand.peer → EnginePeer - PeerInfoControllerMode.calls(messages:) → [EngineMessage] - CallControllerNodeProtocol.updatePeer → EnginePeer params - ChatHistoryListNode.messageInCurrentHistoryView (and 4 variants) → EngineMessage? - ChatHistorySearchContainerNode.messageForGallery → EngineMessage? - PeerInfoPaneNode.findLoadedMessage / ensureMessageIsVisible / transitionNodeForGallery → engine-typed - GalleryHiddenMediaTarget.getTransitionInfo / GalleryHiddenMediaManager.findTarget → engine-typed - ChatPanelInterfaceInteraction.presentReactionDeletionOptions / presentBan*MessageOptions → EnginePeer - DrawingMessageRenderer.messages → [EngineMessage] - ChatVideoGalleryItemScrubberView.setFetchStatusSignal → EngineMediaResource.FetchStatus - ChannelDiscussionGroupActionSheetItem.peer, VoiceChatPeerEntry.peer, VoiceChatFullscreenParticipantItem.peer, MediaStreamComponent.chatPeer, MediaStreamVideoComponent.callPeer, ChatMessageContactBubbleContentNode.contactPeer, ChatMessageForwardInfoNode.peer, ChatMessageCommentFooterContentNode.replyPeers, ChatReportPeerTitlePanelNode.peer, ChatMessageActionUrlAuthController.bot, PeerMediaCollectionInterfaceState.peer, ChatMessageCallBubbleContentNode.peopleAvatars, ChatLoadingNode.renderedPeer (→ EngineRenderedPeer) — all to engine types **Wave-71-shadow stored-field migrations** (Postbox Peer/Message → Engine wrapper): - LegacyCallControllerNode.peer - CallStatusBarNode.currentPeer **Dead-code / dead-field removals**: - CallController.peer, CallControllerNodeV2.account, ContactMultiselectionController PeerNameIndex fields, preparedChatListNodeViewTransition account: Account param, FetchResource.swift entirely (unused function) **Module-level Postbox import drops**: 30+ files including TelegramRootController, EditStories, GiftViewScreen, AnimatedStickerUtils, FetchPhotoLibraryImageResource, PeerInfoGiftsPaneNode, PeerInfoPaneContainerNode, PresentAddMembers, PeerInfoProfileItems, ChatControllerAdminBanUsers, PresentationData typealiases, DefaultDayPresentationTheme, ChatListViewTransition, GalleryHiddenMediaManager, RecentSessionsController, GifContext, AuthorizationSequenceController, PeerInfoHeaderEditingContentNode, PeerInfoHeaderNode, PeerAllowedReactionListController, CallControllerNodeV2, and 6 PeerInfo pane files. **AnyObject restoration**: rule 8 added (never substitute Postbox protocols with Any/AnyObject) — undid previous AnyObject substitutions in waves 141/143 back to EngineMedia. Doc maintenance: CLAUDE.md updated to reflect new typealiases and forwarders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
311 lines
17 KiB
Swift
311 lines
17 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import SwiftSignalKit
|
|
import TelegramCore
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import ContextUI
|
|
import UndoUI
|
|
import AccountContext
|
|
import ChatMessageItemView
|
|
import ChatMessageItemCommon
|
|
import AvatarNode
|
|
import ChatControllerInteraction
|
|
import Pasteboard
|
|
import TelegramStringFormatting
|
|
import TelegramPresentationData
|
|
import ChatPresentationInterfaceState
|
|
|
|
private enum OptionsId: Hashable {
|
|
case item
|
|
case message
|
|
}
|
|
|
|
extension ChatControllerImpl {
|
|
func openTodoItemContextMenu(todoItemId: Int32, params: ChatControllerInteraction.LongTapParams) -> Void {
|
|
guard let message = params.message, let todo = message.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo, let todoItem = todo.items.first(where: { $0.id == todoItemId }), let contentNode = params.contentNode else {
|
|
return
|
|
}
|
|
|
|
let completion = todo.completions.first(where: { $0.id == todoItemId })
|
|
|
|
var canMark = false
|
|
if (todo.flags.contains(.othersCanComplete) || message.author?.id == context.account.peerId) {
|
|
canMark = true
|
|
}
|
|
let canEdit = canEditMessage(context: self.context, limitsConfiguration: self.context.currentLimitsConfiguration.with { EngineConfiguration.Limits($0) }, message: message)
|
|
|
|
let _ = (contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: self.presentationInterfaceState, context: self.context, messages: [message], controllerInteraction: self.controllerInteraction, selectAll: false, interfaceInteraction: self.interfaceInteraction, messageNode: params.messageNode as? ChatMessageItemView)
|
|
|> deliverOnMainQueue).start(next: { [weak self] actions in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
var items: [ContextMenuItem] = []
|
|
if let completion {
|
|
let dateText = humanReadableStringForTimestamp(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, timestamp: completion.date, alwaysShowTime: true, allowYesterday: true, format: HumanReadableStringFormat(
|
|
dateFormatString: { value in
|
|
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_TodoItemCompletionTimestamp_Date(value).string, ranges: [])
|
|
},
|
|
tomorrowFormatString: { value in
|
|
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_TodoItemCompletionTimestamp_TodayAt(value).string, ranges: [])
|
|
},
|
|
todayFormatString: { value in
|
|
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_TodoItemCompletionTimestamp_TodayAt(value).string, ranges: [])
|
|
},
|
|
yesterdayFormatString: { value in
|
|
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_TodoItemCompletionTimestamp_YesterdayAt(value).string, ranges: [])
|
|
}
|
|
)).string
|
|
|
|
let nop: ((ContextMenuActionItem.Action) -> Void)? = nil
|
|
items.append(.action(ContextMenuActionItem(text: dateText, textFont: .small, icon: { _ in return nil }, action: nop)))
|
|
items.append(.separator)
|
|
|
|
if canMark {
|
|
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Todo_ContextMenu_UncheckTask, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
if !self.context.isPremium {
|
|
f(.default)
|
|
let controller = UndoOverlayController(
|
|
presentationData: self.presentationData,
|
|
content: .premiumPaywall(title: nil, text: self.presentationData.strings.Chat_Todo_PremiumRequired, customUndoText: nil, timeout: nil, linkAction: nil),
|
|
action: { [weak self] action in
|
|
guard let self else {
|
|
return false
|
|
}
|
|
if case .info = action {
|
|
let controller = self.context.sharedContext.makePremiumIntroController(context: context, source: .presence, forceDark: false, dismissed: nil)
|
|
self.push(controller)
|
|
}
|
|
return false
|
|
}
|
|
)
|
|
self.present(controller, in: .current)
|
|
} else {
|
|
c?.dismiss(completion: {
|
|
let _ = self.context.engine.messages.requestUpdateTodoMessageItems(messageId: message.id, completedIds: [], incompletedIds: [todoItemId]).start()
|
|
})
|
|
}
|
|
})))
|
|
}
|
|
} else {
|
|
if canMark {
|
|
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Todo_ContextMenu_CheckTask, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
if !self.context.isPremium {
|
|
f(.default)
|
|
let controller = UndoOverlayController(
|
|
presentationData: self.presentationData,
|
|
content: .premiumPaywall(title: nil, text: self.presentationData.strings.Chat_Todo_PremiumRequired, customUndoText: nil, timeout: nil, linkAction: nil),
|
|
action: { [weak self] action in
|
|
guard let self else {
|
|
return false
|
|
}
|
|
if case .info = action {
|
|
let controller = self.context.sharedContext.makePremiumIntroController(context: context, source: .presence, forceDark: false, dismissed: nil)
|
|
self.push(controller)
|
|
}
|
|
return false
|
|
}
|
|
)
|
|
self.present(controller, in: .current)
|
|
} else {
|
|
c?.dismiss(completion: {
|
|
let _ = self.context.engine.messages.requestUpdateTodoMessageItems(messageId: message.id, completedIds: [todoItemId], incompletedIds: []).start()
|
|
})
|
|
}
|
|
})))
|
|
}
|
|
}
|
|
|
|
var canReply = canReplyInChat(self.presentationInterfaceState, accountPeerId: self.context.account.peerId)
|
|
if !canSendMessagesToChat(self.presentationInterfaceState) && (self.presentationInterfaceState.copyProtectionEnabled || message.isCopyProtected()) {
|
|
canReply = false
|
|
}
|
|
|
|
if canReply {
|
|
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Todo_ReplyToItem, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.actionSheet.primaryTextColor)
|
|
}, action: { [weak self] c, _ in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.interfaceInteraction?.setupReplyMessage(message.id, .todoItem(todoItem.id), { transition, completed in
|
|
c?.dismiss(result: .custom(transition), completion: {
|
|
completed()
|
|
})
|
|
})
|
|
})))
|
|
}
|
|
|
|
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuCopy, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
|
|
f(.default)
|
|
|
|
guard let self else {
|
|
return
|
|
}
|
|
storeMessageTextInPasteboard(todoItem.text, entities: todoItem.entities)
|
|
|
|
self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
|
|
})))
|
|
|
|
var isReplyThreadHead = false
|
|
if case let .replyThread(replyThreadMessage) = self.presentationInterfaceState.chatLocation {
|
|
isReplyThreadHead = message.id == replyThreadMessage.effectiveTopId
|
|
}
|
|
|
|
if message.id.namespace == Namespaces.Message.Cloud, let channel = message.peers[message.id.peerId] as? TelegramChannel, !channel.isMonoForum, !isReplyThreadHead {
|
|
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuCopyLink, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
|
|
}, action: { [weak self] _, f in
|
|
guard let self else {
|
|
return
|
|
}
|
|
var threadMessageId: EngineMessage.Id?
|
|
if case let .replyThread(replyThreadMessage) = self.presentationInterfaceState.chatLocation {
|
|
threadMessageId = replyThreadMessage.effectiveMessageId
|
|
}
|
|
let _ = (self.context.engine.messages.exportMessageLink(peerId: message.id.peerId, messageId: message.id, isThread: threadMessageId != nil)
|
|
|> map { result -> String? in
|
|
return result
|
|
}
|
|
|> deliverOnMainQueue).startStandalone(next: { [weak self] link in
|
|
guard let self, let link else {
|
|
return
|
|
}
|
|
UIPasteboard.general.string = link + "?task=\(todoItemId)"
|
|
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
var warnAboutPrivate = false
|
|
if case .peer = self.presentationInterfaceState.chatLocation {
|
|
if channel.addressName == nil {
|
|
warnAboutPrivate = true
|
|
}
|
|
}
|
|
Queue.mainQueue().after(0.2, {
|
|
if warnAboutPrivate {
|
|
self.controllerInteraction?.displayUndo(.linkCopied(title: nil, text: presentationData.strings.Conversation_PrivateMessageLinkCopiedLong))
|
|
} else {
|
|
self.controllerInteraction?.displayUndo(.linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied))
|
|
}
|
|
})
|
|
})
|
|
f(.default)
|
|
})))
|
|
}
|
|
|
|
if canEdit {
|
|
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Todo_ContextMenu_EditTask, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
|
|
f(.default)
|
|
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
self.interfaceInteraction?.editTodoMessage(message.id, todoItemId, false)
|
|
})))
|
|
|
|
if todo.items.count > 1 {
|
|
items.append(.separator)
|
|
|
|
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Todo_ContextMenu_DeleteTask, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, f in
|
|
f(.default)
|
|
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
let updatedItems = todo.items.filter { $0.id != todoItemId }
|
|
let updatedTodo = todo.withUpdated(items: updatedItems)
|
|
|
|
let _ = self.context.engine.messages.requestEditMessage(
|
|
messageId: message.id,
|
|
text: "",
|
|
media: .update(.standalone(media: updatedTodo)),
|
|
entities: nil,
|
|
inlineStickers: [:]
|
|
).start()
|
|
})))
|
|
}
|
|
}
|
|
|
|
self.canReadHistory.set(false)
|
|
|
|
var sources: [ContextController.Source] = []
|
|
sources.append(
|
|
ContextController.Source(
|
|
id: AnyHashable(OptionsId.item),
|
|
title: self.presentationData.strings.Chat_Todo_ContextMenu_SectionTask,
|
|
footer: self.presentationData.strings.Chat_Todo_ContextMenu_SectionsInfo,
|
|
source: .extracted(ChatTodoItemContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)),
|
|
items: .single(ContextController.Items(content: .list(items)))
|
|
)
|
|
)
|
|
|
|
let messageContentSource = ChatMessageContextExtractedContentSource(chatController: self, chatNode: self.chatDisplayNode, engine: self.context.engine, message: EngineMessage(message), selectAll: false, snapshot: true)
|
|
|
|
sources.append(
|
|
ContextController.Source(
|
|
id: AnyHashable(OptionsId.message),
|
|
title: self.presentationData.strings.Chat_Todo_ContextMenu_SectionList,
|
|
source: .extracted(messageContentSource),
|
|
items: .single(actions)
|
|
)
|
|
)
|
|
|
|
contentNode.onDismiss = { [weak messageContentSource] in
|
|
messageContentSource?.snapshotView?.removeFromSuperview()
|
|
}
|
|
|
|
let contextController = makeContextController(
|
|
presentationData: self.presentationData,
|
|
configuration: ContextController.Configuration(
|
|
sources: sources,
|
|
initialId: AnyHashable(OptionsId.item)
|
|
)
|
|
)
|
|
contextController.dismissed = { [weak self] in
|
|
self?.canReadHistory.set(true)
|
|
}
|
|
|
|
self.window?.presentInGlobalOverlay(contextController)
|
|
})
|
|
}
|
|
}
|
|
|
|
final class ChatTodoItemContextExtractedContentSource: ContextExtractedContentSource {
|
|
let keepInPlace: Bool = false
|
|
let ignoreContentTouches: Bool = false
|
|
let blurBackground: Bool = true
|
|
|
|
private weak var chatNode: ChatControllerNode?
|
|
private let contentNode: ContextExtractedContentContainingNode
|
|
|
|
init(chatNode: ChatControllerNode, contentNode: ContextExtractedContentContainingNode) {
|
|
self.chatNode = chatNode
|
|
self.contentNode = contentNode
|
|
}
|
|
|
|
func takeView() -> ContextControllerTakeViewInfo? {
|
|
guard let chatNode = self.chatNode else {
|
|
return nil
|
|
}
|
|
return ContextControllerTakeViewInfo(containingItem: .node(self.contentNode), contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil))
|
|
}
|
|
|
|
func putBack() -> ContextControllerPutBackViewInfo? {
|
|
guard let chatNode = self.chatNode else {
|
|
return nil
|
|
}
|
|
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil))
|
|
}
|
|
}
|