[WIP] Chat Import

This commit is contained in:
Ali 2021-01-21 20:04:13 +04:00
parent fe491b6831
commit 85402a67cb
35 changed files with 3759 additions and 3454 deletions

View file

@ -5903,3 +5903,4 @@ Sorry for the inconvenience.";
"Common.Save" = "Save";
"ChatList.HeaderImportIntoAnExistingGroup" = "OR IMPORT INTO AN EXISTING GROUP";
"Conversation.ImportedMessageHint" = "The messages was imported from another app. We can't guarantee it's real.";

View file

@ -0,0 +1,18 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatHistoryImportTasks",
module_name = "ChatHistoryImportTasks",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/SyncCore:SyncCore",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,6 @@
import Foundation
import Postbox
import TelegramCore
import SyncCore
import SwiftSignalKit

View file

@ -20,6 +20,7 @@ swift_library(
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/RadialStatusNode:RadialStatusNode",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/ChatHistoryImportTasks:ChatHistoryImportTasks",
],
visibility = [
"//visibility:public",

View file

@ -201,9 +201,11 @@ public final class ChatImportActivityScreen: ViewController {
private let peerId: PeerId
private let archive: Archive
private let mainEntry: TempBoxFile
private let mainEntrySize: Int
private let otherEntries: [(Entry, String, ChatHistoryImport.MediaType)]
private let totalBytes: Int
private var pendingEntries = Set<String>()
private var pendingEntries: [String: (Int, Float)] = [:]
private let disposable = MetaDisposable()
@ -222,7 +224,21 @@ public final class ChatImportActivityScreen: ViewController {
self.mainEntry = mainEntry
self.otherEntries = otherEntries
self.pendingEntries = Set(otherEntries.map { $0.1 })
if let size = fileSize(self.mainEntry.path) {
self.mainEntrySize = size
} else {
self.mainEntrySize = 0
}
for (entry, fileName, _) in otherEntries {
self.pendingEntries[fileName] = (entry.uncompressedSize, 0.0)
}
var totalBytes: Int = self.mainEntrySize
for entry in self.otherEntries {
totalBytes += entry.0.uncompressedSize
}
self.totalBytes = totalBytes
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
@ -253,14 +269,7 @@ public final class ChatImportActivityScreen: ViewController {
}
override public func loadDisplayNode() {
var totalBytes: Int = 0
if let size = fileSize(self.mainEntry.path) {
totalBytes += size
}
for entry in self.otherEntries {
totalBytes += entry.0.uncompressedSize
}
self.displayNode = Node(controller: self, context: self.context, totalBytes: totalBytes)
self.displayNode = Node(controller: self, context: self.context, totalBytes: self.totalBytes)
self.displayNodeDidLoad()
}
@ -298,8 +307,8 @@ public final class ChatImportActivityScreen: ViewController {
return .generic
}
}
|> mapToSignal { session -> Signal<String, ImportError> in
var importSignal: Signal<String, ImportError> = .single("")
|> mapToSignal { session -> Signal<(String, Float), ImportError> in
var importSignal: Signal<(String, Float), ImportError> = .single(("", 0.0))
for (entry, fileName, mediaType) in otherEntries {
let unpackedFile = Signal<TempBoxFile, ImportError> { subscriber in
@ -315,14 +324,14 @@ public final class ChatImportActivityScreen: ViewController {
return EmptyDisposable
}
let uploadedMedia = unpackedFile
|> mapToSignal { tempFile -> Signal<String, ImportError> in
|> mapToSignal { tempFile -> Signal<(String, Float), ImportError> in
return ChatHistoryImport.uploadMedia(account: context.account, session: session, file: tempFile, fileName: fileName, type: mediaType)
|> mapError { _ -> ImportError in
return .generic
}
|> map { _ -> String in
|> map { progress -> (String, Float) in
return (fileName, progress)
}
|> then(.single(fileName))
}
importSignal = importSignal
@ -334,19 +343,28 @@ public final class ChatImportActivityScreen: ViewController {
|> mapError { _ -> ImportError in
return .generic
}
|> map { _ -> String in
|> map { _ -> (String, Float) in
})
return importSignal
}
|> deliverOnMainQueue).start(next: { [weak self] fileName in
|> deliverOnMainQueue).start(next: { [weak self] (fileName, progress) in
guard let strongSelf = self else {
return
}
strongSelf.pendingEntries.remove(fileName)
if let (fileSize, _) = strongSelf.pendingEntries[fileName] {
strongSelf.pendingEntries[fileName] = (fileSize, progress)
}
var totalDoneBytes = strongSelf.mainEntrySize
for (_, sizeAndProgress) in strongSelf.pendingEntries {
totalDoneBytes += Int(Float(sizeAndProgress.0) * sizeAndProgress.1)
}
var totalProgress: CGFloat = 1.0
if !strongSelf.otherEntries.isEmpty {
totalProgress = CGFloat(strongSelf.otherEntries.count - strongSelf.pendingEntries.count) / CGFloat(strongSelf.otherEntries.count)
totalProgress = CGFloat(totalDoneBytes) / CGFloat(strongSelf.totalBytes)
}
strongSelf.controllerNode.updateProgress(totalProgress: totalProgress, isDone: false, animated: true)
}, error: { [weak self] _ in

View file

@ -460,6 +460,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[649453030] = { return Api.messages.MessageEditData.parse_messageEditData($0) }
dict[-886477832] = { return Api.LabeledPrice.parse_labeledPrice($0) }
dict[-438840932] = { return Api.messages.ChatFull.parse_chatFull($0) }
dict[-1919636670] = { return Api.messages.HistoryImportParsed.parse_historyImportParsed($0) }
dict[-618540889] = { return Api.InputSecureValue.parse_inputSecureValue($0) }
dict[-170029155] = { return Api.messages.DiscussionMessage.parse_discussionMessage($0) }
dict[1722786150] = { return Api.help.DeepLinkInfo.parse_deepLinkInfoEmpty($0) }
@ -1193,6 +1194,8 @@ public struct Api {
_1.serialize(buffer, boxed)
case let _1 as Api.messages.ChatFull:
_1.serialize(buffer, boxed)
case let _1 as Api.messages.HistoryImportParsed:
_1.serialize(buffer, boxed)
case let _1 as Api.InputSecureValue:
_1.serialize(buffer, boxed)
case let _1 as Api.messages.DiscussionMessage:

View file

@ -977,6 +977,44 @@ public struct messages {
}
}
}
public enum HistoryImportParsed: TypeConstructorDescription {
case historyImportParsed(flags: Int32, title: String?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .historyImportParsed(let flags, let title):
if boxed {
buffer.appendInt32(-1919636670)
}
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {serializeString(title!, buffer: buffer, boxed: false)}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .historyImportParsed(let flags, let title):
return ("historyImportParsed", [("flags", flags), ("title", title)])
}
}
public static func parse_historyImportParsed(_ reader: BufferReader) -> HistoryImportParsed? {
var _1: Int32?
_1 = reader.readInt32()
var _2: String?
if Int(_1!) & Int(1 << 1) != 0 {_2 = parseString(reader) }
let _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil
if _c1 && _c2 {
return Api.messages.HistoryImportParsed.historyImportParsed(flags: _1!, title: _2)
}
else {
return nil
}
}
}
public enum DiscussionMessage: TypeConstructorDescription {
case discussionMessage(flags: Int32, messages: [Api.Message], maxId: Int32?, readInboxMaxId: Int32?, readOutboxMaxId: Int32?, chats: [Api.Chat], users: [Api.User])

View file

@ -4080,6 +4080,20 @@ public extension Api {
return result
})
}
public static func checkHistoryImport(importHead: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.HistoryImportParsed>) {
let buffer = Buffer()
buffer.appendInt32(1140726259)
serializeString(importHead, buffer: buffer, boxed: false)
return (FunctionDescription(name: "messages.checkHistoryImport", parameters: [("importHead", importHead)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.HistoryImportParsed? in
let reader = BufferReader(buffer)
var result: Api.messages.HistoryImportParsed?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.messages.HistoryImportParsed
}
return result
})
}
}
public struct channels {
public static func readHistory(channel: Api.InputChannel, maxId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {

View file

@ -419,7 +419,7 @@ private func initialStateWithPeerIds(_ transaction: Transaction, peerIds: Set<Pe
}
} else {
if let peer = transaction.getPeer(peerId) {
if let _ = peer as? TelegramChannel {
if let channel = peer as? TelegramChannel, channel.participationStatus != .member {
if let notificationSettings = transaction.getPeerNotificationSettings(peerId) {
peerChatInfos[peerId] = PeerChatInfo(notificationSettings: notificationSettings)
Logger.shared.log("State", "Peer \(peerId) (\(peer.debugDisplayTitle) has no stored inclusion, using synthesized one")
@ -447,7 +447,8 @@ private func initialStateWithPeerIds(_ transaction: Transaction, peerIds: Set<Pe
}
}
return AccountMutableState(initialState: AccountInitialState(state: (transaction.getState() as? AuthorizedAccountState)!.state!, peerIds: peerIds, peerIdsRequiringLocalChatState: peerIdsRequiringLocalChatState, channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: locallyGeneratedMessageTimestamps, cloudReadStates: cloudReadStates, channelsToPollExplicitely: channelsToPollExplicitely), initialPeers: peers, initialReferencedMessageIds: associatedMessageIds, initialStoredMessages: storedMessages, initialReadInboxMaxIds: readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: storedMessagesByPeerIdAndTimestamp)
var state = AccountMutableState(initialState: AccountInitialState(state: (transaction.getState() as? AuthorizedAccountState)!.state!, peerIds: peerIds, peerIdsRequiringLocalChatState: peerIdsRequiringLocalChatState, channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: locallyGeneratedMessageTimestamps, cloudReadStates: cloudReadStates, channelsToPollExplicitely: channelsToPollExplicitely), initialPeers: peers, initialReferencedMessageIds: associatedMessageIds, initialStoredMessages: storedMessages, initialReadInboxMaxIds: readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: storedMessagesByPeerIdAndTimestamp)
return state
}
func initialStateWithUpdateGroups(postbox: Postbox, groups: [UpdateGroup]) -> Signal<AccountMutableState, NoError> {

View file

@ -16,6 +16,36 @@ public enum ChatHistoryImport {
case generic
}
//messages.historyImportParsed flags:# pm:flags.0?true group:flags.1?true title:flags.1?string = messages.HistoryImportParsed;
public enum ParsedInfo {
case privateChat(title: String?)
case group(title: String?)
}
public enum GetInfoError {
case generic
case parseError
}
public static func getInfo(account: Account, header: String) -> Signal<ParsedInfo, GetInfoError> {
return account.network.request(Api.functions.messages.checkHistoryImport(importHead: header))
|> mapError { _ -> GetInfoError in
return .generic
}
|> mapToSignal { result -> Signal<ParsedInfo, GetInfoError> in
switch result {
case let .historyImportParsed(flags, title):
if (flags & (1 << 0)) != 0 {
return .single(.privateChat(title: title))
} else if (flags & (1 << 1)) != 0 {
return .single(.group(title: title))
} else {
return .fail(.parseError)
}
}
}
}
public static func initSession(account: Account, peerId: PeerId, file: TempBoxFile, mediaCount: Int32) -> Signal<Session, InitImportError> {
return multipartUpload(network: account.network, postbox: account.postbox, source: .tempFile(file), encrypt: false, tag: nil, hintFileSize: nil, hintFileIsLarge: false)
|> mapError { _ -> InitImportError in
@ -63,12 +93,12 @@ public enum ChatHistoryImport {
case generic
}
public static func uploadMedia(account: Account, session: Session, file: TempBoxFile, fileName: String, type: MediaType) -> Signal<Never, UploadMediaError> {
public static func uploadMedia(account: Account, session: Session, file: TempBoxFile, fileName: String, type: MediaType) -> Signal<Float, UploadMediaError> {
return multipartUpload(network: account.network, postbox: account.postbox, source: .tempFile(file), encrypt: false, tag: nil, hintFileSize: nil, hintFileIsLarge: false)
|> mapError { _ -> UploadMediaError in
return .generic
}
|> mapToSignal { result -> Signal<Never, UploadMediaError> in
|> mapToSignal { result -> Signal<Float, UploadMediaError> in
let inputMedia: Api.InputMedia
switch result {
case let .inputFile(inputFile):
@ -91,8 +121,8 @@ public enum ChatHistoryImport {
}
inputMedia = .inputMediaUploadedDocument(flags: 0, file: inputFile, thumb: nil, mimeType: mimeType, attributes: attributes, stickers: nil, ttlSeconds: nil)
}
case .progress:
return .complete()
case let .progress(value):
return .single(value)
case .inputSecretFile:
return .fail(.generic)
}
@ -100,8 +130,8 @@ public enum ChatHistoryImport {
|> mapError { _ -> UploadMediaError in
return .generic
}
|> mapToSignal { result -> Signal<Never, UploadMediaError> in
return .complete()
|> mapToSignal { result -> Signal<Float, UploadMediaError> in
return .single(1.0)
}
}
}

View file

@ -215,6 +215,7 @@ swift_library(
"//Telegram:GeneratedSources",
"//third-party/ZIPFoundation:ZIPFoundation",
"//submodules/ChatImportUI:ChatImportUI",
"//submodules/ChatHistoryImportTasks:ChatHistoryImportTasks",
],
visibility = [
"//visibility:public",

View file

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

View file

@ -371,6 +371,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private var isEmbeddedTitleContentHidden = false
private let chatLocationContextHolder: Atomic<ChatLocationContextHolder?>
private weak var currentImportMessageTooltip: UndoOverlayController?
public override var customData: Any? {
return self.chatLocation
@ -2208,6 +2210,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.present(MessageReactionListController(context: strongSelf.context, messageId: message.id, initialReactions: initialReactions), in: .window(.root))
}
})
}, displayImportedMessageTooltip: { [weak self] _ in
guard let strongSelf = self else {
return
}
if let _ = strongSelf.currentImportMessageTooltip {
} else {
let controller = UndoOverlayController(presentationData: strongSelf.presentationData, content: .importedMessage(text: strongSelf.presentationData.strings.Conversation_ImportedMessageHint), elevatedLayout: false, action: { _ in return false })
strongSelf.currentImportMessageTooltip = controller
strongSelf.present(controller, in: .current)
}
}, displaySwipeToReplyHint: { [weak self] in
if let strongSelf = self, let validLayout = strongSelf.validLayout, min(validLayout.size.width, validLayout.size.height) > 320.0 {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .swipeToReply(title: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintTitle, text: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintText), elevatedLayout: false, action: { _ in return false }), in: .current)

View file

@ -103,6 +103,7 @@ public final class ChatControllerInteraction {
let performTextSelectionAction: (UInt32, NSAttributedString, TextSelectionAction) -> Void
let updateMessageLike: (MessageId, Bool) -> Void
let openMessageReactions: (MessageId) -> Void
let displayImportedMessageTooltip: (ASDisplayNode) -> Void
let displaySwipeToReplyHint: () -> Void
let dismissReplyMarkupMessage: (Message) -> Void
let openMessagePollResults: (MessageId, Data) -> Void
@ -192,6 +193,7 @@ public final class ChatControllerInteraction {
performTextSelectionAction: @escaping (UInt32, NSAttributedString, TextSelectionAction) -> Void,
updateMessageLike: @escaping (MessageId, Bool) -> Void,
openMessageReactions: @escaping (MessageId) -> Void,
displayImportedMessageTooltip: @escaping (ASDisplayNode) -> Void,
displaySwipeToReplyHint: @escaping () -> Void,
dismissReplyMarkupMessage: @escaping (Message) -> Void,
openMessagePollResults: @escaping (MessageId, Data) -> Void,
@ -271,6 +273,7 @@ public final class ChatControllerInteraction {
self.performTextSelectionAction = performTextSelectionAction
self.updateMessageLike = updateMessageLike
self.openMessageReactions = openMessageReactions
self.displayImportedMessageTooltip = displayImportedMessageTooltip
self.displaySwipeToReplyHint = displaySwipeToReplyHint
self.dismissReplyMarkupMessage = dismissReplyMarkupMessage
self.openMessagePollResults = openMessagePollResults
@ -320,6 +323,7 @@ public final class ChatControllerInteraction {
}, performTextSelectionAction: { _, _, _ in
}, updateMessageLike: { _, _ in
}, openMessageReactions: { _ in
}, displayImportedMessageTooltip: { _ in
}, displaySwipeToReplyHint: {
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in

View file

@ -1168,6 +1168,17 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
actionButtonsNode.removeFromSupernode()
strongSelf.actionButtonsNode = nil
}
if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) {
strongSelf.dateAndStatusNode.pressed = {
guard let strongSelf = self else {
return
}
item.controllerInteraction.displayImportedMessageTooltip(strongSelf.dateAndStatusNode)
}
} else {
strongSelf.dateAndStatusNode.pressed = nil
}
}
})
}

View file

@ -319,6 +319,17 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
} else {
strongSelf.avatarNode.setCustomLetters(customLetters)
}
if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) {
strongSelf.dateAndStatusNode.pressed = {
guard let strongSelf = self else {
return
}
item.controllerInteraction.displayImportedMessageTooltip(strongSelf.dateAndStatusNode)
}
} else {
strongSelf.dateAndStatusNode.pressed = nil
}
}
})
})

View file

@ -167,8 +167,24 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
private var theme: ChatPresentationThemeData?
private var layoutSize: CGSize?
private var tapGestureRecognizer: UITapGestureRecognizer?
var openReactions: (() -> Void)?
var openReplies: (() -> Void)?
var pressed: (() -> Void)? {
didSet {
if self.pressed != nil {
if self.tapGestureRecognizer == nil {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
self.tapGestureRecognizer = tapGestureRecognizer
self.view.addGestureRecognizer(tapGestureRecognizer)
}
} else if let tapGestureRecognizer = self.tapGestureRecognizer{
self.tapGestureRecognizer = nil
self.view.removeGestureRecognizer(tapGestureRecognizer)
}
}
}
override init() {
self.dateNode = TextNode()
@ -180,6 +196,12 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
self.addSubnode(self.dateNode)
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.pressed?()
}
}
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ reactions: [MessageReaction], _ replies: Int, _ isPinned: Bool) -> (CGSize, (Bool) -> Void) {
let dateLayout = TextNode.asyncLayout(self.dateNode)
@ -868,6 +890,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
return reactionButtonNode.view
}
}
if self.pressed != nil {
if self.bounds.contains(point) {
return self.view
}
}
return nil
}
}

View file

@ -51,6 +51,12 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id)
}
}
self.interactiveFileNode.displayImportedTooltip = { [weak self] sourceNode in
if let strongSelf = self, let item = strongSelf.item {
let _ = item.controllerInteraction.displayImportedMessageTooltip(sourceNode)
}
}
}
required init?(coder aDecoder: NSCoder) {

View file

@ -84,6 +84,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
var toggleSelection: (Bool) -> Void = { _ in }
var activateLocalContent: () -> Void = { }
var requestUpdateLayout: (Bool) -> Void = { _ in }
var displayImportedTooltip: (ASDisplayNode) -> Void = { _ in }
private var context: AccountContext?
private var message: Message?
@ -729,6 +730,17 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
strongSelf.updateStatus(animated: isAnimated)
if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported) {
strongSelf.dateAndStatusNode.pressed = {
guard let strongSelf = self else {
return
}
strongSelf.displayImportedTooltip(strongSelf.dateAndStatusNode)
}
} else {
strongSelf.dateAndStatusNode.pressed = nil
}
}
})
})

View file

@ -469,6 +469,17 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
if let telegramFile = updatedFile, previousAutomaticDownload != automaticDownload, automaticDownload {
strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: item.context, message: item.message, file: telegramFile, userInitiated: false).start())
}
if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) {
strongSelf.dateAndStatusNode.pressed = {
guard let strongSelf = self else {
return
}
item.controllerInteraction.displayImportedMessageTooltip(strongSelf.dateAndStatusNode)
}
} else {
strongSelf.dateAndStatusNode.pressed = nil
}
}
})
}

View file

@ -426,6 +426,17 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.pinNode.frame = CGRect(origin: CGPoint(x: imageFrame.minX + floor((imageFrame.size.width - pinSize.width) / 2.0), y: imageFrame.minY + floor(imageFrame.size.height * 0.5 - 10.0 - pinSize.height / 2.0)), size: pinSize)
pinApply()
if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) {
strongSelf.dateAndStatusNode.pressed = {
guard let strongSelf = self else {
return
}
item.controllerInteraction.displayImportedMessageTooltip(strongSelf.dateAndStatusNode)
}
} else {
strongSelf.dateAndStatusNode.pressed = nil
}
}
})
})

View file

@ -308,6 +308,17 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
selectionNode.removeFromSupernode()
}
}
if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) {
strongSelf.dateAndStatusNode.pressed = {
guard let strongSelf = self else {
return
}
item.controllerInteraction.displayImportedMessageTooltip(strongSelf.dateAndStatusNode)
}
} else {
strongSelf.dateAndStatusNode.pressed = nil
}
}
})
})

View file

@ -750,6 +750,17 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
actionButtonsNode.removeFromSupernode()
strongSelf.actionButtonsNode = nil
}
if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) {
strongSelf.dateAndStatusNode.pressed = {
guard let strongSelf = self else {
return
}
item.controllerInteraction.displayImportedMessageTooltip(strongSelf.dateAndStatusNode)
}
} else {
strongSelf.dateAndStatusNode.pressed = nil
}
}
})
}

View file

@ -407,6 +407,17 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
strongSelf.textAccessibilityOverlayNode.frame = textFrame
strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout
if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) {
strongSelf.statusNode.pressed = {
guard let strongSelf = self else {
return
}
item.controllerInteraction.displayImportedMessageTooltip(strongSelf.statusNode)
}
} else {
strongSelf.statusNode.pressed = nil
}
}
})
})

View file

@ -442,6 +442,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, performTextSelectionAction: { _, _, _ in
}, updateMessageLike: { _, _ in
}, openMessageReactions: { _ in
}, displayImportedMessageTooltip: { _ in
}, displaySwipeToReplyHint: {
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in

View file

@ -134,6 +134,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
}, performTextSelectionAction: { _, _, _ in
}, updateMessageLike: { _, _ in
}, openMessageReactions: { _ in
}, displayImportedMessageTooltip: { _ in
}, displaySwipeToReplyHint: {
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in

View file

@ -127,6 +127,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, performTextSelectionAction: { _, _, _ in
}, updateMessageLike: { _, _ in
}, openMessageReactions: { _ in
}, displayImportedMessageTooltip: { _ in
}, displaySwipeToReplyHint: {
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in

View file

@ -2006,6 +2006,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}, performTextSelectionAction: { _, _, _ in
}, updateMessageLike: { _, _ in
}, openMessageReactions: { _ in
}, displayImportedMessageTooltip: { _ in
}, displaySwipeToReplyHint: {
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in

View file

@ -381,6 +381,12 @@ public class ShareRootControllerImpl {
context.account.resetStateManagement()
}
/*if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication {
let selector = NSSelectorFromString("openURL:")
let url = URL(string: "tg://open")!
application.perform(selector, with: url)
}*/
if let strongSelf = self, let inputItems = strongSelf.getExtensionContext()?.inputItems, inputItems.count == 1, let item = inputItems[0] as? NSExtensionItem, let attachments = item.attachments {
for attachment in attachments {
if attachment.hasItemConformingToTypeIdentifier(kUTTypeFileURL as String) {
@ -413,16 +419,6 @@ public class ShareRootControllerImpl {
let stickerRegex = try! NSRegularExpression(pattern: "[\\d]+-STICKER-.*?\\.webp")
let voiceRegex = try! NSRegularExpression(pattern: "[\\d]+-AUDIO-.*?\\.opus")
let groupVerificationRegexList = [
try! NSRegularExpression(pattern: "created this group"),
try! NSRegularExpression(pattern: "created group “(.*?)”"),
]
let groupCreationRegexList = [
try! NSRegularExpression(pattern: "created group “(.*?)”"),
try! NSRegularExpression(pattern: "] (.*?): Messages and calls are end-to-end encrypted")
]
var groupTitle: String?
var otherEntries: [(Entry, String, ChatHistoryImport.MediaType)] = []
var mainFile: TempBoxFile?
@ -435,29 +431,6 @@ public class ShareRootControllerImpl {
let tempFile = TempBox.shared.tempFile(fileName: entryPath)
if entryPath == "_chat.txt" {
let _ = try archive.extract(entry, to: URL(fileURLWithPath: tempFile.path))
if let fileContents = try? String(contentsOfFile: tempFile.path) {
let fullRange = NSRange(fileContents.startIndex ..< fileContents.endIndex, in: fileContents)
var isGroup = false
for regex in groupVerificationRegexList {
if let _ = regex.firstMatch(in: fileContents, options: [], range: fullRange) {
isGroup = true
break
}
}
if isGroup {
for regex in groupCreationRegexList {
if groupTitle != nil {
break
}
if let match = regex.firstMatch(in: fileContents, options: [], range: fullRange) {
let range = match.range(at: 1)
if let mappedRange = Range(range, in: fileContents) {
groupTitle = String(fileContents[mappedRange])
}
}
}
}
}
mainFile = tempFile
} else {
let entryFileName = (entryPath as NSString).lastPathComponent
@ -481,173 +454,205 @@ public class ShareRootControllerImpl {
}
} catch {
}
if let mainFile = mainFile {
if let groupTitle = groupTitle {
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
let navigationController = NavigationController(mode: .single, theme: NavigationControllerTheme(presentationTheme: presentationData.theme))
//TODO:localize
var attemptSelectionImpl: ((Peer) -> Void)?
var createNewGroupImpl: (() -> Void)?
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyGroups, .onlyManageable, .excludeDisabled, .doNotSearchMessages], hasContactSelector: false, hasGlobalSearch: false, title: "Import Chat", attemptSelection: { peer in
attemptSelectionImpl?(peer)
}, createNewGroup: {
createNewGroupImpl?()
}, pretendPresentedInModal: true))
controller.customDismiss = {
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
}
controller.peerSelected = { peer in
attemptSelectionImpl?(peer)
}
controller.navigationPresentation = .default
let beginWithPeer: (PeerId) -> Void = { peerId in
navigationController.pushViewController(ChatImportActivityScreen(context: context, cancel: {
if let mainFile = mainFile, let mainFileText = try? String(contentsOf: URL(fileURLWithPath: mainFile.path)) {
let mainFileHeader: String
if mainFileText.count < 1000 {
mainFileHeader = mainFileText
} else {
mainFileHeader = String(mainFileText[mainFileText.startIndex ..< mainFileText.index(mainFileText.startIndex, offsetBy: 1000)])
}
let _ = (ChatHistoryImport.getInfo(account: context.account, header: mainFileHeader)
|> deliverOnMainQueue).start(next: { parseInfo in
switch parseInfo {
case let .group(groupTitle):
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
let navigationController = NavigationController(mode: .single, theme: NavigationControllerTheme(presentationTheme: presentationData.theme))
//TODO:localize
var attemptSelectionImpl: ((Peer) -> Void)?
var createNewGroupImpl: (() -> Void)?
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyGroups, .onlyManageable, .excludeDisabled, .doNotSearchMessages], hasContactSelector: false, hasGlobalSearch: false, title: "Import Chat", attemptSelection: { peer in
attemptSelectionImpl?(peer)
}, createNewGroup: {
createNewGroupImpl?()
}, pretendPresentedInModal: true))
controller.customDismiss = {
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
}, peerId: peerId, archive: archive, mainEntry: mainFile, otherEntries: otherEntries))
}
attemptSelectionImpl = { peer in
var errorText: String?
if let channel = peer as? TelegramChannel {
if channel.flags.contains(.isCreator) || channel.adminRights != nil {
} else {
errorText = "You need to be an admin of the group to import messages into it."
}
} else if let group = peer as? TelegramGroup {
switch group.role {
case .creator:
break
default:
errorText = "You need to be an admin of the group to import messages into it."
}
} else {
errorText = "You can't import history into this group."
}
if let errorText = errorText {
controller.peerSelected = { peer in
attemptSelectionImpl?(peer)
}
controller.navigationPresentation = .default
let beginWithPeer: (PeerId) -> Void = { peerId in
navigationController.pushViewController(ChatImportActivityScreen(context: context, cancel: {
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
}, peerId: peerId, archive: archive, mainEntry: mainFile, otherEntries: otherEntries))
}
attemptSelectionImpl = { peer in
var errorText: String?
if let channel = peer as? TelegramChannel {
if channel.flags.contains(.isCreator) || channel.adminRights != nil {
} else {
errorText = "You need to be an admin of the group to import messages into it."
}
} else if let group = peer as? TelegramGroup {
switch group.role {
case .creator:
break
default:
errorText = "You need to be an admin of the group to import messages into it."
}
} else {
errorText = "You can't import history into this group."
}
if let errorText = errorText {
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
})])
strongSelf.mainWindow?.present(controller, on: .root)
} else {
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
let text: String
if let groupTitle = groupTitle {
text = "Are you sure you want to import messages from **\(groupTitle)** into **\(peer.debugDisplayTitle)**?"
} else {
text = "Are you sure you want to import messages into **\(peer.debugDisplayTitle)**?"
}
let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Import Messages", text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
}), TextAlertAction(type: .defaultAction, title: "Import", action: {
beginWithPeer(peer.id)
})], parseMarkdown: true)
strongSelf.mainWindow?.present(controller, on: .root)
}
}
createNewGroupImpl = {
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
})])
strongSelf.mainWindow?.present(controller, on: .root)
} else {
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Import Messages", text: "Are you sure you want to import messages from **\(groupTitle)** into **\(peer.debugDisplayTitle)**?", actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
}), TextAlertAction(type: .defaultAction, title: "Import", action: {
beginWithPeer(peer.id)
let resolvedGroupTitle: String
if let groupTitle = groupTitle {
resolvedGroupTitle = groupTitle
} else {
resolvedGroupTitle = "Group"
}
let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Create Group and Import Messages", text: "Are you sure you want to create group **\(resolvedGroupTitle)** and import messages from another messaging app?", actions: [TextAlertAction(type: .defaultAction, title: "Create and Import", action: {
var signal: Signal<PeerId?, NoError> = createSupergroup(account: context.account, title: resolvedGroupTitle, description: nil, isForHistoryImport: true)
|> map(Optional.init)
|> `catch` { _ -> Signal<PeerId?, NoError> in
return .single(nil)
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
if let strongSelf = self {
strongSelf.mainWindow?.present(controller, on: .root)
}
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
signal = signal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
let _ = (signal
|> deliverOnMainQueue).start(next: { peerId in
if let peerId = peerId {
beginWithPeer(peerId)
} else {
//TODO:localize
}
})
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
})], parseMarkdown: true)
strongSelf.mainWindow?.present(controller, on: .root)
}
}
createNewGroupImpl = {
navigationController.viewControllers = [controller]
strongSelf.mainWindow?.present(navigationController, on: .root)
case let .privateChat(title):
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Create Group and Import Messages", text: "Are you sure you want to create group **\(groupTitle)** and import messages from another messaging app?", actions: [TextAlertAction(type: .defaultAction, title: "Create and Import", action: {
var signal: Signal<PeerId?, NoError> = createSupergroup(account: context.account, title: groupTitle, description: nil, isForHistoryImport: true)
|> map(Optional.init)
|> `catch` { _ -> Signal<PeerId?, NoError> in
return .single(nil)
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
if let strongSelf = self {
strongSelf.mainWindow?.present(controller, on: .root)
}
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
signal = signal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
let _ = (signal
|> deliverOnMainQueue).start(next: { peerId in
if let peerId = peerId {
beginWithPeer(peerId)
} else {
//TODO:localize
}
})
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
})], parseMarkdown: true)
strongSelf.mainWindow?.present(controller, on: .root)
}
navigationController.viewControllers = [controller]
strongSelf.mainWindow?.present(navigationController, on: .root)
} else {
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
let navigationController = NavigationController(mode: .single, theme: NavigationControllerTheme(presentationTheme: presentationData.theme))
//TODO:localize
var attemptSelectionImpl: ((Peer) -> Void)?
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyPrivateChats, .excludeDisabled, .doNotSearchMessages], hasChatListSelector: false, hasContactSelector: true, hasGlobalSearch: false, title: "Import Chat", attemptSelection: { peer in
attemptSelectionImpl?(peer)
}, pretendPresentedInModal: true))
controller.customDismiss = {
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
}
controller.peerSelected = { peer in
attemptSelectionImpl?(peer)
}
controller.navigationPresentation = .default
let beginWithPeer: (PeerId) -> Void = { peerId in
navigationController.pushViewController(ChatImportActivityScreen(context: context, cancel: {
let navigationController = NavigationController(mode: .single, theme: NavigationControllerTheme(presentationTheme: presentationData.theme))
//TODO:localize
var attemptSelectionImpl: ((Peer) -> Void)?
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyPrivateChats, .excludeDisabled, .doNotSearchMessages], hasChatListSelector: false, hasContactSelector: true, hasGlobalSearch: false, title: "Import Chat", attemptSelection: { peer in
attemptSelectionImpl?(peer)
}, pretendPresentedInModal: true))
controller.customDismiss = {
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
}, peerId: peerId, archive: archive, mainEntry: mainFile, otherEntries: otherEntries))
}
controller.peerSelected = { peer in
attemptSelectionImpl?(peer)
}
controller.navigationPresentation = .default
let beginWithPeer: (PeerId) -> Void = { peerId in
navigationController.pushViewController(ChatImportActivityScreen(context: context, cancel: {
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
}, peerId: peerId, archive: archive, mainEntry: mainFile, otherEntries: otherEntries))
}
attemptSelectionImpl = { [weak controller] peer in
controller?.inProgress = true
let _ = (ChatHistoryImport.checkPeerImport(account: context.account, peerId: peer.id)
|> deliverOnMainQueue).start(error: { error in
controller?.inProgress = false
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
let errorText: String
switch error {
case .generic:
errorText = presentationData.strings.Login_UnknownError
case .userIsNotMutualContact:
errorText = "You can only import messages into private chats with users who added you in their contact list."
}
let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
})])
strongSelf.mainWindow?.present(controller, on: .root)
}, completed: {
controller?.inProgress = false
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
let text: String
if let title = title {
text = "Are you sure you want to import messages from **\(title)** into the chat with **\(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))**?"
} else {
text = "Are you sure you want to import messages into the chat with **\(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))**?"
}
let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Import Messages", text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
}), TextAlertAction(type: .defaultAction, title: "Import", action: {
beginWithPeer(peer.id)
})], parseMarkdown: true)
strongSelf.mainWindow?.present(controller, on: .root)
})
}
navigationController.viewControllers = [controller]
strongSelf.mainWindow?.present(navigationController, on: .root)
}
attemptSelectionImpl = { [weak controller] peer in
controller?.inProgress = true
let _ = (ChatHistoryImport.checkPeerImport(account: context.account, peerId: peer.id)
|> deliverOnMainQueue).start(error: { error in
controller?.inProgress = false
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
let errorText: String
switch error {
case .generic:
errorText = presentationData.strings.Login_UnknownError
case .userIsNotMutualContact:
errorText = "You can only import messages into private chats with users who added you in their contact list."
}
let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
})])
strongSelf.mainWindow?.present(controller, on: .root)
}, completed: {
controller?.inProgress = false
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Import Messages", text: "Are you sure you want to import messages into the chat with **\(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))**?", actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
}), TextAlertAction(type: .defaultAction, title: "Import", action: {
beginWithPeer(peer.id)
})], parseMarkdown: true)
strongSelf.mainWindow?.present(controller, on: .root)
})
}
navigationController.viewControllers = [controller]
strongSelf.mainWindow?.present(navigationController, on: .root)
}
}, error: { _ in
beginShare()
})
} else {
beginShare()
return

View file

@ -1245,6 +1245,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, performTextSelectionAction: { _, _, _ in
}, updateMessageLike: { _, _ in
}, openMessageReactions: { _ in
}, displayImportedMessageTooltip: { _ in
}, displaySwipeToReplyHint: {
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in

View file

@ -26,6 +26,7 @@ public enum UndoOverlayContent {
case invitedToVoiceChat(context: AccountContext, peer: Peer, text: String)
case linkCopied(text: String)
case banned(text: String)
case importedMessage(text: String)
}
public enum UndoOverlayAction {

View file

@ -210,6 +210,18 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.textNode.maximumNumberOfLines = 2
displayUndo = false
self.originalRemainingSeconds = 5
case let .importedMessage(text):
self.avatarNode = nil
self.iconNode = ASImageNode()
self.iconNode?.displayWithoutProcessing = true
self.iconNode?.displaysAsynchronously = false
self.iconNode?.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/ImportedMessageTooltipIcon"), color: .white)
self.iconCheckNode = nil
self.animationNode = nil
self.animatedStickerNode = nil
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white)
displayUndo = false
self.originalRemainingSeconds = 5
case let .chatAddedToFolder(chatTitle, folderTitle):
self.avatarNode = nil
self.iconNode = nil
@ -495,7 +507,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
switch content {
case .removedChat:
self.panelWrapperNode.addSubnode(self.timerTextNode)
case .archivedChat, .hidArchive, .revealedArchive, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned:
case .archivedChat, .hidArchive, .revealedArchive, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage:
break
case .dice:
self.panelWrapperNode.clipsToBounds = true