mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
6d49664e6b
@ -5957,7 +5957,7 @@ Sorry for the inconvenience.";
|
|||||||
"ChatImportActivity.InProgress" = "Please keep this window open\nduring the import.";
|
"ChatImportActivity.InProgress" = "Please keep this window open\nduring the import.";
|
||||||
"ChatImportActivity.ErrorNotAdmin" = "You need to be an admin.";
|
"ChatImportActivity.ErrorNotAdmin" = "You need to be an admin.";
|
||||||
"ChatImportActivity.ErrorInvalidChatType" = "You can't import this history in this type of chat.";
|
"ChatImportActivity.ErrorInvalidChatType" = "You can't import this history in this type of chat.";
|
||||||
"ChatImportActivity.ErrorUserBlocked" = "You need to be an admin.";
|
"ChatImportActivity.ErrorUserBlocked" = "This user is blocked.";
|
||||||
"ChatImportActivity.ErrorGeneric" = "An error occurred.";
|
"ChatImportActivity.ErrorGeneric" = "An error occurred.";
|
||||||
"ChatImportActivity.Success" = "Chat imported\nsuccessfully.";
|
"ChatImportActivity.Success" = "Chat imported\nsuccessfully.";
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ private final class ProgressEstimator {
|
|||||||
if let (lastTimestamp, lastProgress) = self.lastMeasurement {
|
if let (lastTimestamp, lastProgress) = self.lastMeasurement {
|
||||||
if abs(lastProgress - progress) >= 0.01 || abs(lastTimestamp - timestamp) > 1.0 {
|
if abs(lastProgress - progress) >= 0.01 || abs(lastTimestamp - timestamp) > 1.0 {
|
||||||
let immediateProgressPerSecond = Double(progress - lastProgress) / (timestamp - lastTimestamp)
|
let immediateProgressPerSecond = Double(progress - lastProgress) / (timestamp - lastTimestamp)
|
||||||
let alpha: Double = 0.05
|
let alpha: Double = 0.01
|
||||||
self.averageProgressPerSecond = alpha * immediateProgressPerSecond + (1.0 - alpha) * self.averageProgressPerSecond
|
self.averageProgressPerSecond = alpha * immediateProgressPerSecond + (1.0 - alpha) * self.averageProgressPerSecond
|
||||||
self.lastMeasurement = (timestamp, progress)
|
self.lastMeasurement = (timestamp, progress)
|
||||||
}
|
}
|
||||||
@ -45,24 +45,230 @@ private final class ProgressEstimator {
|
|||||||
} else {
|
} else {
|
||||||
let remainingProgress = Double(1.0 - progress)
|
let remainingProgress = Double(1.0 - progress)
|
||||||
let remainingTime = remainingProgress / self.averageProgressPerSecond
|
let remainingTime = remainingProgress / self.averageProgressPerSecond
|
||||||
|
//print("remainingTime \(remainingTime)")
|
||||||
return remainingTime
|
return remainingTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class ChatImportActivityScreen: ViewController {
|
private final class ImportManager {
|
||||||
enum ImportError {
|
enum ImportError {
|
||||||
case generic
|
case generic
|
||||||
case chatAdminRequired
|
case chatAdminRequired
|
||||||
case invalidChatType
|
case invalidChatType
|
||||||
|
case userBlocked
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum State {
|
enum State {
|
||||||
case progress(CGFloat)
|
case progress(totalBytes: Int, totalUploadedBytes: Int, totalMediaBytes: Int, totalUploadedMediaBytes: Int)
|
||||||
case error(ImportError)
|
case error(ImportError)
|
||||||
case done
|
case done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let account: Account
|
||||||
|
private let archivePath: String?
|
||||||
|
private let entries: [(SSZipEntry, String, ChatHistoryImport.MediaType)]
|
||||||
|
|
||||||
|
private var session: ChatHistoryImport.Session?
|
||||||
|
|
||||||
|
private let disposable = MetaDisposable()
|
||||||
|
|
||||||
|
private let totalBytes: Int
|
||||||
|
private let totalMediaBytes: Int
|
||||||
|
private let mainFileSize: Int
|
||||||
|
private var pendingEntries: [(SSZipEntry, String, ChatHistoryImport.MediaType)]
|
||||||
|
private var entryProgress: [String: (Int, Int)] = [:]
|
||||||
|
private var activeEntries: [String: Disposable] = [:]
|
||||||
|
|
||||||
|
private var stateValue: State {
|
||||||
|
didSet {
|
||||||
|
self.statePromise.set(.single(self.stateValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private let statePromise = Promise<State>()
|
||||||
|
var state: Signal<State, NoError> {
|
||||||
|
return self.statePromise.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
init(account: Account, peerId: PeerId, mainFile: TempBoxFile, archivePath: String?, entries: [(SSZipEntry, String, ChatHistoryImport.MediaType)]) {
|
||||||
|
self.account = account
|
||||||
|
self.archivePath = archivePath
|
||||||
|
self.entries = entries
|
||||||
|
self.pendingEntries = entries
|
||||||
|
|
||||||
|
self.mainFileSize = fileSize(mainFile.path) ?? 0
|
||||||
|
|
||||||
|
var totalMediaBytes = 0
|
||||||
|
for entry in self.entries {
|
||||||
|
self.entryProgress[entry.0.path] = (Int(entry.0.uncompressedSize), 0)
|
||||||
|
totalMediaBytes += Int(entry.0.uncompressedSize)
|
||||||
|
}
|
||||||
|
self.totalBytes = self.mainFileSize + totalMediaBytes
|
||||||
|
self.totalMediaBytes = totalMediaBytes
|
||||||
|
|
||||||
|
self.stateValue = .progress(totalBytes: self.totalBytes, totalUploadedBytes: 0, totalMediaBytes: self.totalMediaBytes, totalUploadedMediaBytes: 0)
|
||||||
|
|
||||||
|
self.disposable.set((ChatHistoryImport.initSession(account: self.account, peerId: peerId, file: mainFile, mediaCount: Int32(entries.count))
|
||||||
|
|> mapError { error -> ImportError in
|
||||||
|
switch error {
|
||||||
|
case .chatAdminRequired:
|
||||||
|
return .chatAdminRequired
|
||||||
|
case .invalidChatType:
|
||||||
|
return .invalidChatType
|
||||||
|
case .generic:
|
||||||
|
return .generic
|
||||||
|
case .userBlocked:
|
||||||
|
return .userBlocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] session in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.session = session
|
||||||
|
strongSelf.updateState()
|
||||||
|
}, error: { [weak self] error in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.failWithError(error)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.disposable.dispose()
|
||||||
|
for (_, disposable) in self.activeEntries {
|
||||||
|
disposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateProgress() {
|
||||||
|
if case .error = self.stateValue {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalUploadedMediaBytes = 0
|
||||||
|
for (_, entrySizes) in self.entryProgress {
|
||||||
|
totalUploadedMediaBytes += entrySizes.1
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalUploadedBytes = totalUploadedMediaBytes
|
||||||
|
if let _ = self.session {
|
||||||
|
totalUploadedBytes += self.mainFileSize
|
||||||
|
}
|
||||||
|
|
||||||
|
self.stateValue = .progress(totalBytes: self.totalBytes, totalUploadedBytes: totalUploadedBytes, totalMediaBytes: self.totalMediaBytes, totalUploadedMediaBytes: totalUploadedMediaBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func failWithError(_ error: ImportError) {
|
||||||
|
self.stateValue = .error(error)
|
||||||
|
for (_, disposable) in self.activeEntries {
|
||||||
|
disposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func complete() {
|
||||||
|
guard let session = self.session else {
|
||||||
|
self.failWithError(.generic)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.disposable.set((ChatHistoryImport.startImport(account: self.account, session: session)
|
||||||
|
|> deliverOnMainQueue).start(error: { [weak self] _ in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.failWithError(.generic)
|
||||||
|
}, completed: { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.stateValue = .done
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateState() {
|
||||||
|
guard let session = self.session else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.pendingEntries.isEmpty && self.activeEntries.isEmpty {
|
||||||
|
self.complete()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if case .error = self.stateValue {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let archivePath = self.archivePath else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
while true {
|
||||||
|
if self.activeEntries.count >= 3 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if self.pendingEntries.isEmpty {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry = self.pendingEntries.removeFirst()
|
||||||
|
let unpackedFile = Signal<TempBoxFile, ImportError> { subscriber in
|
||||||
|
let tempFile = TempBox.shared.tempFile(fileName: entry.0.path)
|
||||||
|
Logger.shared.log("ChatImportScreen", "Extracting \(entry.0.path) to \(tempFile.path)...")
|
||||||
|
let startTime = CACurrentMediaTime()
|
||||||
|
if SSZipArchive.extractFileFromArchive(atPath: archivePath, filePath: entry.0.path, toPath: tempFile.path) {
|
||||||
|
Logger.shared.log("ChatImportScreen", "[Done in \(CACurrentMediaTime() - startTime) s] Extract \(entry.0.path) to \(tempFile.path)")
|
||||||
|
subscriber.putNext(tempFile)
|
||||||
|
subscriber.putCompletion()
|
||||||
|
} else {
|
||||||
|
subscriber.putError(.generic)
|
||||||
|
}
|
||||||
|
|
||||||
|
return EmptyDisposable
|
||||||
|
}
|
||||||
|
|
||||||
|
let account = self.account
|
||||||
|
|
||||||
|
let uploadedEntrySignal: Signal<Float, ImportError> = unpackedFile
|
||||||
|
|> mapToSignal { tempFile -> Signal<Float, ImportError> in
|
||||||
|
return ChatHistoryImport.uploadMedia(account: account, session: session, file: tempFile, fileName: entry.0.path, mimeType: entry.1, type: entry.2)
|
||||||
|
|> mapError { error -> ImportError in
|
||||||
|
switch error {
|
||||||
|
case .chatAdminRequired:
|
||||||
|
return .chatAdminRequired
|
||||||
|
case .generic:
|
||||||
|
return .generic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let disposable = MetaDisposable()
|
||||||
|
self.activeEntries[entry.1] = disposable
|
||||||
|
|
||||||
|
disposable.set((uploadedEntrySignal
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] progress in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let (size, _) = strongSelf.entryProgress[entry.0.path] {
|
||||||
|
strongSelf.entryProgress[entry.0.path] = (size, Int(progress * Float(entry.0.uncompressedSize)))
|
||||||
|
strongSelf.updateProgress()
|
||||||
|
}
|
||||||
|
}, error: { [weak self] error in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.failWithError(error)
|
||||||
|
}, completed: { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.activeEntries.removeValue(forKey: entry.0.path)
|
||||||
|
strongSelf.updateState()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class ChatImportActivityScreen: ViewController {
|
||||||
private final class Node: ViewControllerTracingNode {
|
private final class Node: ViewControllerTracingNode {
|
||||||
private weak var controller: ChatImportActivityScreen?
|
private weak var controller: ChatImportActivityScreen?
|
||||||
|
|
||||||
@ -85,17 +291,18 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||||
|
|
||||||
private let totalBytes: Int
|
private let totalBytes: Int
|
||||||
private var state: State = .progress(0.0)
|
private var state: ImportManager.State
|
||||||
|
|
||||||
private var videoNode: UniversalVideoNode?
|
private var videoNode: UniversalVideoNode?
|
||||||
private var feedback: HapticFeedback?
|
private var feedback: HapticFeedback?
|
||||||
|
|
||||||
fileprivate var remainingAnimationSeconds: Double?
|
fileprivate var remainingAnimationSeconds: Double?
|
||||||
|
|
||||||
init(controller: ChatImportActivityScreen, context: AccountContext, totalBytes: Int) {
|
init(controller: ChatImportActivityScreen, context: AccountContext, totalBytes: Int, totalMediaBytes: Int) {
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.context = context
|
self.context = context
|
||||||
self.totalBytes = totalBytes
|
self.totalBytes = totalBytes
|
||||||
|
self.state = .progress(totalBytes: totalBytes, totalUploadedBytes: 0, totalMediaBytes: totalMediaBytes, totalUploadedMediaBytes: 0)
|
||||||
|
|
||||||
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
@ -245,24 +452,55 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
let isFirstLayout = self.validLayout == nil
|
let isFirstLayout = self.validLayout == nil
|
||||||
self.validLayout = (layout, navigationHeight)
|
self.validLayout = (layout, navigationHeight)
|
||||||
|
|
||||||
let iconSize = CGSize(width: 190.0, height: 190.0)
|
let availableHeight = layout.size.height - navigationHeight
|
||||||
let radialStatusSize = CGSize(width: 186.0, height: 186.0)
|
|
||||||
let maxIconStatusSpacing: CGFloat = 46.0
|
var iconSize = CGSize(width: 190.0, height: 190.0)
|
||||||
let maxProgressTextSpacing: CGFloat = 33.0
|
var radialStatusSize = CGSize(width: 186.0, height: 186.0)
|
||||||
let progressStatusSpacing: CGFloat = 14.0
|
var maxIconStatusSpacing: CGFloat = 46.0
|
||||||
let statusButtonSpacing: CGFloat = 19.0
|
var maxProgressTextSpacing: CGFloat = 33.0
|
||||||
|
var progressStatusSpacing: CGFloat = 14.0
|
||||||
|
var statusButtonSpacing: CGFloat = 19.0
|
||||||
|
|
||||||
|
var maxK: CGFloat = availableHeight / (iconSize.height + maxIconStatusSpacing + 30.0 + maxProgressTextSpacing + 320.0)
|
||||||
|
maxK = max(0.5, min(1.0, maxK))
|
||||||
|
|
||||||
|
iconSize.width = floor(iconSize.width * maxK)
|
||||||
|
iconSize.height = floor(iconSize.height * maxK)
|
||||||
|
radialStatusSize.width = floor(radialStatusSize.width * maxK)
|
||||||
|
radialStatusSize.height = floor(radialStatusSize.height * maxK)
|
||||||
|
maxIconStatusSpacing = floor(maxIconStatusSpacing * maxK)
|
||||||
|
maxProgressTextSpacing = floor(maxProgressTextSpacing * maxK)
|
||||||
|
progressStatusSpacing = floor(progressStatusSpacing * maxK)
|
||||||
|
statusButtonSpacing = floor(statusButtonSpacing * maxK)
|
||||||
|
|
||||||
|
var updateRadialBackround = false
|
||||||
|
if let width = self.radialStatusBackground.image?.size.width {
|
||||||
|
if abs(width - radialStatusSize.width) > 0.01 {
|
||||||
|
updateRadialBackround = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateRadialBackround = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if updateRadialBackround {
|
||||||
|
self.radialStatusBackground.image = generateCircleImage(diameter: radialStatusSize.width, lineWidth: 6.0, color: self.presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.2))
|
||||||
|
}
|
||||||
|
|
||||||
let effectiveProgress: CGFloat
|
let effectiveProgress: CGFloat
|
||||||
switch state {
|
switch state {
|
||||||
case let .progress(value):
|
case let .progress(totalBytes, totalUploadedBytes, _, _):
|
||||||
effectiveProgress = value
|
if totalBytes == 0 {
|
||||||
|
effectiveProgress = 1.0
|
||||||
|
} else {
|
||||||
|
effectiveProgress = CGFloat(totalUploadedBytes) / CGFloat(totalBytes)
|
||||||
|
}
|
||||||
case .error:
|
case .error:
|
||||||
effectiveProgress = 0.0
|
effectiveProgress = 0.0
|
||||||
case .done:
|
case .done:
|
||||||
effectiveProgress = 1.0
|
effectiveProgress = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
self.radialStatusText.attributedText = NSAttributedString(string: "\(Int(effectiveProgress * 100.0))%", font: Font.with(size: 36.0, design: .round, weight: .semibold), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
self.radialStatusText.attributedText = NSAttributedString(string: "\(Int(effectiveProgress * 100.0))%", font: Font.with(size: floor(36.0 * maxK), design: .round, weight: .semibold), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
||||||
let radialStatusTextSize = self.radialStatusText.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
|
let radialStatusTextSize = self.radialStatusText.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
|
||||||
|
|
||||||
self.progressText.attributedText = NSAttributedString(string: "\(dataSizeString(Int(effectiveProgress * CGFloat(self.totalBytes)))) of \(dataSizeString(Int(1.0 * CGFloat(self.totalBytes))))", font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
self.progressText.attributedText = NSAttributedString(string: "\(dataSizeString(Int(effectiveProgress * CGFloat(self.totalBytes)))) of \(dataSizeString(Int(1.0 * CGFloat(self.totalBytes))))", font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
||||||
@ -288,6 +526,8 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
errorText = self.presentationData.strings.ChatImportActivity_ErrorInvalidChatType
|
errorText = self.presentationData.strings.ChatImportActivity_ErrorInvalidChatType
|
||||||
case .generic:
|
case .generic:
|
||||||
errorText = self.presentationData.strings.ChatImportActivity_ErrorGeneric
|
errorText = self.presentationData.strings.ChatImportActivity_ErrorGeneric
|
||||||
|
case .userBlocked:
|
||||||
|
errorText = self.presentationData.strings.ChatImportActivity_ErrorUserBlocked
|
||||||
}
|
}
|
||||||
self.statusText.attributedText = NSAttributedString(string: errorText, font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemDestructiveColor)
|
self.statusText.attributedText = NSAttributedString(string: errorText, font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemDestructiveColor)
|
||||||
case .done:
|
case .done:
|
||||||
@ -302,7 +542,7 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
hideIcon = true
|
hideIcon = true
|
||||||
contentHeight = progressTextSize.height + progressStatusSpacing + 160.0
|
contentHeight = progressTextSize.height + progressStatusSpacing + 160.0
|
||||||
} else {
|
} else {
|
||||||
contentHeight = iconSize.height + maxIconStatusSpacing + radialStatusSize.height + maxProgressTextSpacing + progressTextSize.height + progressStatusSpacing + 100.0
|
contentHeight = iconSize.height + maxIconStatusSpacing + radialStatusSize.height + maxProgressTextSpacing + progressTextSize.height + progressStatusSpacing + 140.0
|
||||||
}
|
}
|
||||||
|
|
||||||
transition.updateAlpha(node: self.radialStatus, alpha: hideIcon ? 0.0 : 1.0)
|
transition.updateAlpha(node: self.radialStatus, alpha: hideIcon ? 0.0 : 1.0)
|
||||||
@ -380,7 +620,7 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
self.animationNode.stopAtNearestLoop = true
|
self.animationNode.stopAtNearestLoop = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateState(state: State, animated: Bool) {
|
func updateState(state: ImportManager.State, animated: Bool) {
|
||||||
var wasDone = false
|
var wasDone = false
|
||||||
if case .done = self.state {
|
if case .done = self.state {
|
||||||
wasDone = true
|
wasDone = true
|
||||||
@ -392,8 +632,12 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
|
|
||||||
let effectiveProgress: CGFloat
|
let effectiveProgress: CGFloat
|
||||||
switch state {
|
switch state {
|
||||||
case let .progress(value):
|
case let .progress(totalBytes, totalUploadedBytes, _, _):
|
||||||
effectiveProgress = value
|
if totalBytes == 0 {
|
||||||
|
effectiveProgress = 1.0
|
||||||
|
} else {
|
||||||
|
effectiveProgress = CGFloat(totalUploadedBytes) / CGFloat(totalBytes)
|
||||||
|
}
|
||||||
case .error:
|
case .error:
|
||||||
effectiveProgress = 0.0
|
effectiveProgress = 0.0
|
||||||
case .done:
|
case .done:
|
||||||
@ -451,20 +695,19 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
fileprivate let cancel: () -> Void
|
fileprivate let cancel: () -> Void
|
||||||
fileprivate var peerId: PeerId
|
fileprivate var peerId: PeerId
|
||||||
private let archivePath: String
|
private let archivePath: String?
|
||||||
private let mainEntry: TempBoxFile
|
private let mainEntry: TempBoxFile
|
||||||
private let mainEntrySize: Int
|
|
||||||
private let otherEntries: [(SSZipEntry, String, ChatHistoryImport.MediaType, Signal<TempBoxFile?, NoError>)]
|
|
||||||
private let totalBytes: Int
|
private let totalBytes: Int
|
||||||
private let totalMediaBytes: Int
|
private let totalMediaBytes: Int
|
||||||
|
private let otherEntries: [(SSZipEntry, String, ChatHistoryImport.MediaType)]
|
||||||
|
|
||||||
|
private var importManager: ImportManager?
|
||||||
private var progressEstimator: ProgressEstimator?
|
private var progressEstimator: ProgressEstimator?
|
||||||
private var totalMediaProgress: Float = 0.0
|
private var totalMediaProgress: Float = 0.0
|
||||||
private var beganCompletion: Bool = false
|
private var beganCompletion: Bool = false
|
||||||
|
|
||||||
private var pendingEntries: [String: (Int, Float)] = [:]
|
|
||||||
|
|
||||||
private let disposable = MetaDisposable()
|
private let disposable = MetaDisposable()
|
||||||
|
private let progressDisposable = MetaDisposable()
|
||||||
|
|
||||||
override public var _presentedInModal: Bool {
|
override public var _presentedInModal: Bool {
|
||||||
get {
|
get {
|
||||||
@ -473,50 +716,24 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(context: AccountContext, cancel: @escaping () -> Void, peerId: PeerId, archivePath: String, mainEntry: TempBoxFile, otherEntries: [(SSZipEntry, String, ChatHistoryImport.MediaType)]) {
|
public init(context: AccountContext, cancel: @escaping () -> Void, peerId: PeerId, archivePath: String?, mainEntry: TempBoxFile, otherEntries: [(SSZipEntry, String, ChatHistoryImport.MediaType)]) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.cancel = cancel
|
self.cancel = cancel
|
||||||
self.peerId = peerId
|
self.peerId = peerId
|
||||||
self.archivePath = archivePath
|
self.archivePath = archivePath
|
||||||
self.mainEntry = mainEntry
|
self.mainEntry = mainEntry
|
||||||
|
|
||||||
var isFirstFile = true
|
self.otherEntries = otherEntries.map { entry -> (SSZipEntry, String, ChatHistoryImport.MediaType) in
|
||||||
self.otherEntries = otherEntries.map { entry -> (SSZipEntry, String, ChatHistoryImport.MediaType, Signal<TempBoxFile?, NoError>) in
|
return (entry.0, entry.1, entry.2)
|
||||||
let signal = Signal<TempBoxFile?, NoError> { subscriber in
|
|
||||||
let tempFile = TempBox.shared.tempFile(fileName: entry.1)
|
|
||||||
print("Extracting \(entry.0.path) to \(tempFile.path)...")
|
|
||||||
let startTime = CACurrentMediaTime()
|
|
||||||
if SSZipArchive.extractFileFromArchive(atPath: archivePath, filePath: entry.0.path, toPath: tempFile.path) {
|
|
||||||
print("[Done in \(CACurrentMediaTime() - startTime) s] Extract \(entry.0.path) to \(tempFile.path)")
|
|
||||||
subscriber.putNext(tempFile)
|
|
||||||
subscriber.putCompletion()
|
|
||||||
} else {
|
|
||||||
subscriber.putNext(nil)
|
|
||||||
subscriber.putCompletion()
|
|
||||||
}
|
|
||||||
|
|
||||||
return EmptyDisposable
|
|
||||||
}
|
|
||||||
//let promise = Promise<TempBoxFile?>()
|
|
||||||
//promise.set(signal)
|
|
||||||
return (entry.0, entry.1, entry.2, signal)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let size = fileSize(self.mainEntry.path) {
|
let mainEntrySize = fileSize(self.mainEntry.path) ?? 0
|
||||||
self.mainEntrySize = size
|
|
||||||
} else {
|
|
||||||
self.mainEntrySize = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
for (entry, fileName, _) in otherEntries {
|
|
||||||
self.pendingEntries[fileName] = (Int(entry.uncompressedSize), 0.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalMediaBytes = 0
|
var totalMediaBytes = 0
|
||||||
for entry in self.otherEntries {
|
for entry in self.otherEntries {
|
||||||
totalMediaBytes += Int(entry.0.uncompressedSize)
|
totalMediaBytes += Int(entry.0.uncompressedSize)
|
||||||
}
|
}
|
||||||
self.totalBytes = self.mainEntrySize + totalMediaBytes
|
self.totalBytes = mainEntrySize + totalMediaBytes
|
||||||
self.totalMediaBytes = totalMediaBytes
|
self.totalMediaBytes = totalMediaBytes
|
||||||
|
|
||||||
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
@ -544,6 +761,7 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
self.disposable.dispose()
|
self.disposable.dispose()
|
||||||
|
self.progressDisposable.dispose()
|
||||||
|
|
||||||
if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication {
|
if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication {
|
||||||
application.isIdleTimerDisabled = false
|
application.isIdleTimerDisabled = false
|
||||||
@ -555,7 +773,7 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override public func loadDisplayNode() {
|
override public func loadDisplayNode() {
|
||||||
self.displayNode = Node(controller: self, context: self.context, totalBytes: self.totalBytes)
|
self.displayNode = Node(controller: self, context: self.context, totalBytes: self.totalBytes, totalMediaBytes: self.totalMediaBytes)
|
||||||
|
|
||||||
self.displayNodeDidLoad()
|
self.displayNodeDidLoad()
|
||||||
}
|
}
|
||||||
@ -567,23 +785,13 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func beginImport() {
|
private func beginImport() {
|
||||||
for (key, value) in self.pendingEntries {
|
|
||||||
self.pendingEntries[key] = (value.0, 0.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.progressEstimator = ProgressEstimator()
|
self.progressEstimator = ProgressEstimator()
|
||||||
self.beganCompletion = false
|
self.beganCompletion = false
|
||||||
|
|
||||||
self.controllerNode.updateState(state: .progress(0.0), animated: true)
|
let resolvedPeerId: Signal<PeerId, ImportManager.ImportError>
|
||||||
|
|
||||||
let context = self.context
|
|
||||||
let mainEntry = self.mainEntry
|
|
||||||
let otherEntries = self.otherEntries
|
|
||||||
|
|
||||||
let resolvedPeerId: Signal<PeerId, ImportError>
|
|
||||||
if self.peerId.namespace == Namespaces.Peer.CloudGroup {
|
if self.peerId.namespace == Namespaces.Peer.CloudGroup {
|
||||||
resolvedPeerId = convertGroupToSupergroup(account: self.context.account, peerId: self.peerId)
|
resolvedPeerId = convertGroupToSupergroup(account: self.context.account, peerId: self.peerId)
|
||||||
|> mapError { _ -> ImportError in
|
|> mapError { _ -> ImportManager.ImportError in
|
||||||
return .generic
|
return .generic
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -591,117 +799,28 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.disposable.set((resolvedPeerId
|
self.disposable.set((resolvedPeerId
|
||||||
|> mapToSignal { [weak self] peerId -> Signal<ChatHistoryImport.Session, ImportError> in
|
|> deliverOnMainQueue).start(next: { [weak self] peerId in
|
||||||
Queue.mainQueue().async {
|
|
||||||
self?.peerId = peerId
|
|
||||||
}
|
|
||||||
|
|
||||||
return ChatHistoryImport.initSession(account: context.account, peerId: peerId, file: mainEntry, mediaCount: Int32(otherEntries.count))
|
|
||||||
|> mapError { error -> ImportError in
|
|
||||||
switch error {
|
|
||||||
case .chatAdminRequired:
|
|
||||||
return .chatAdminRequired
|
|
||||||
case .invalidChatType:
|
|
||||||
return .invalidChatType
|
|
||||||
case .generic:
|
|
||||||
return .generic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|> mapToSignal { session -> Signal<[(String, Float)], ImportError> in
|
|
||||||
var mediaSignals: [Signal<(String, Float), ImportError>] = []
|
|
||||||
|
|
||||||
for (_, fileName, mediaType, fileData) in otherEntries {
|
|
||||||
let unpackedFile: Signal<TempBoxFile, ImportError> = fileData
|
|
||||||
|> take(1)
|
|
||||||
|> deliverOnMainQueue
|
|
||||||
|> castError(ImportError.self)
|
|
||||||
|> mapToSignal { file -> Signal<TempBoxFile, ImportError> in
|
|
||||||
if let file = file {
|
|
||||||
return .single(file)
|
|
||||||
} else {
|
|
||||||
return .fail(.generic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let uploadedMedia = unpackedFile
|
|
||||||
|> mapToSignal { tempFile -> Signal<(String, Float), ImportError> in
|
|
||||||
var mimeTypeValue = "application/binary"
|
|
||||||
let fileExtension = (tempFile.path as NSString).pathExtension
|
|
||||||
if !fileExtension.isEmpty {
|
|
||||||
if let value = TGMimeTypeMap.mimeType(forExtension: fileExtension.lowercased()) {
|
|
||||||
mimeTypeValue = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ChatHistoryImport.uploadMedia(account: context.account, session: session, file: tempFile, fileName: fileName, mimeType: mimeTypeValue, type: mediaType)
|
|
||||||
|> mapError { error -> ImportError in
|
|
||||||
switch error {
|
|
||||||
case .chatAdminRequired:
|
|
||||||
return .chatAdminRequired
|
|
||||||
case .generic:
|
|
||||||
return .generic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|> map { progress -> (String, Float) in
|
|
||||||
return (fileName, progress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaSignals.append(Signal<(String, Float), ImportError>.single((fileName, 0.0))
|
|
||||||
|> then(uploadedMedia))
|
|
||||||
}
|
|
||||||
|
|
||||||
return combineLatest(mediaSignals)
|
|
||||||
|> then(ChatHistoryImport.startImport(account: context.account, session: session)
|
|
||||||
|> mapError { _ -> ImportError in
|
|
||||||
return .generic
|
|
||||||
}
|
|
||||||
|> map { _ -> [(String, Float)] in
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] fileNameAndProgress in
|
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let importManager = ImportManager(account: strongSelf.context.account, peerId: peerId, mainFile: strongSelf.mainEntry, archivePath: strongSelf.archivePath, entries: strongSelf.otherEntries)
|
||||||
for (fileName, progress) in fileNameAndProgress {
|
strongSelf.importManager = importManager
|
||||||
if let (fileSize, _) = strongSelf.pendingEntries[fileName] {
|
strongSelf.progressDisposable.set((importManager.state
|
||||||
strongSelf.pendingEntries[fileName] = (fileSize, progress)
|
|> deliverOnMainQueue).start(next: { state in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
strongSelf.controllerNode.updateState(state: state, animated: true)
|
||||||
|
if case let .progress(_, _, totalMediaBytes, totalUploadedMediaBytes) = state {
|
||||||
var totalDoneMediaBytes = 0
|
let progress = Float(totalUploadedMediaBytes) / Float(totalMediaBytes)
|
||||||
for (_, sizeAndProgress) in strongSelf.pendingEntries {
|
strongSelf.totalMediaProgress = progress
|
||||||
totalDoneMediaBytes += Int(Float(sizeAndProgress.0) * sizeAndProgress.1)
|
}
|
||||||
}
|
}))
|
||||||
|
|
||||||
let totalDoneBytes = strongSelf.mainEntrySize + totalDoneMediaBytes
|
|
||||||
|
|
||||||
var totalProgress: CGFloat = 1.0
|
|
||||||
if !strongSelf.otherEntries.isEmpty {
|
|
||||||
totalProgress = CGFloat(totalDoneBytes) / CGFloat(strongSelf.totalBytes)
|
|
||||||
}
|
|
||||||
var totalMediaProgress: CGFloat = 1.0
|
|
||||||
if !strongSelf.otherEntries.isEmpty {
|
|
||||||
totalProgress = CGFloat(totalDoneBytes) / CGFloat(strongSelf.totalBytes)
|
|
||||||
totalMediaProgress = CGFloat(totalDoneMediaBytes) / CGFloat(strongSelf.totalMediaBytes)
|
|
||||||
}
|
|
||||||
strongSelf.controllerNode.updateState(state: .progress(totalProgress), animated: true)
|
|
||||||
strongSelf.totalMediaProgress = Float(totalMediaProgress)
|
|
||||||
}, error: { [weak self] error in
|
}, error: { [weak self] error in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.controllerNode.updateState(state: .error(error), animated: true)
|
strongSelf.controllerNode.updateState(state: .error(error), animated: true)
|
||||||
}, completed: { [weak self] in
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
strongSelf.controllerNode.updateState(state: .done, animated: true)
|
|
||||||
|
|
||||||
if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication {
|
|
||||||
application.isIdleTimerDisabled = false
|
|
||||||
}
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ public final class MediaPlayerNode: ASDisplayNode {
|
|||||||
self.currentRotationAngle = rotationAngle
|
self.currentRotationAngle = rotationAngle
|
||||||
self.currentAspect = aspect
|
self.currentAspect = aspect
|
||||||
var transform = CGAffineTransform(rotationAngle: CGFloat(rotationAngle))
|
var transform = CGAffineTransform(rotationAngle: CGFloat(rotationAngle))
|
||||||
if !rotationAngle.isZero {
|
if abs(rotationAngle).remainder(dividingBy: Double.pi) > 0.1 {
|
||||||
transform = transform.scaledBy(x: CGFloat(aspect), y: CGFloat(1.0 / aspect))
|
transform = transform.scaledBy(x: CGFloat(aspect), y: CGFloat(1.0 / aspect))
|
||||||
}
|
}
|
||||||
videoLayer.setAffineTransform(transform)
|
videoLayer.setAffineTransform(transform)
|
||||||
|
@ -5,6 +5,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
@interface MTGzip : NSObject
|
@interface MTGzip : NSObject
|
||||||
|
|
||||||
+ (NSData * _Nullable)decompress:(NSData *)data;
|
+ (NSData * _Nullable)decompress:(NSData *)data;
|
||||||
|
+ (NSData * _Nullable)compress:(NSData *)data;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
@ -50,4 +50,42 @@
|
|||||||
return (retCode == Z_STREAM_END ? result : nil);
|
return (retCode == Z_STREAM_END ? result : nil);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
+ (NSData * _Nullable)compress:(NSData *)data {
|
||||||
|
if (data.length == 0) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
z_stream stream;
|
||||||
|
stream.zalloc = Z_NULL;
|
||||||
|
stream.zfree = Z_NULL;
|
||||||
|
stream.opaque = Z_NULL;
|
||||||
|
stream.avail_in = (uint)data.length;
|
||||||
|
stream.next_in = (Bytef *)(void *)data.bytes;
|
||||||
|
stream.total_out = 0;
|
||||||
|
stream.avail_out = 0;
|
||||||
|
|
||||||
|
static const NSUInteger ChunkSize = 16384;
|
||||||
|
|
||||||
|
NSMutableData *output = nil;
|
||||||
|
int compression = Z_BEST_COMPRESSION;
|
||||||
|
if (deflateInit2(&stream, compression, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY) == Z_OK)
|
||||||
|
{
|
||||||
|
output = [NSMutableData dataWithLength:ChunkSize];
|
||||||
|
while (stream.avail_out == 0)
|
||||||
|
{
|
||||||
|
if (stream.total_out >= output.length)
|
||||||
|
{
|
||||||
|
output.length += ChunkSize;
|
||||||
|
}
|
||||||
|
stream.next_out = (uint8_t *)output.mutableBytes + stream.total_out;
|
||||||
|
stream.avail_out = (uInt)(output.length - stream.total_out);
|
||||||
|
deflate(&stream, Z_FINISH);
|
||||||
|
}
|
||||||
|
deflateEnd(&stream);
|
||||||
|
output.length = stream.total_out;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
import Postbox
|
||||||
import SyncCore
|
import SyncCore
|
||||||
import TelegramCore
|
|
||||||
import TelegramApi
|
import TelegramApi
|
||||||
|
|
||||||
public enum ChatHistoryImport {
|
public enum ChatHistoryImport {
|
||||||
@ -16,6 +15,7 @@ public enum ChatHistoryImport {
|
|||||||
case generic
|
case generic
|
||||||
case chatAdminRequired
|
case chatAdminRequired
|
||||||
case invalidChatType
|
case invalidChatType
|
||||||
|
case userBlocked
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ParsedInfo {
|
public enum ParsedInfo {
|
||||||
@ -49,7 +49,7 @@ public enum ChatHistoryImport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static func initSession(account: Account, peerId: PeerId, file: TempBoxFile, mediaCount: Int32) -> Signal<Session, InitImportError> {
|
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, forceNoBigParts: false)
|
return multipartUpload(network: account.network, postbox: account.postbox, source: .tempFile(file), encrypt: false, tag: nil, hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false, useLargerParts: true, increaseParallelParts: true, useMultiplexedRequests: false, useCompression: true)
|
||||||
|> mapError { _ -> InitImportError in
|
|> mapError { _ -> InitImportError in
|
||||||
return .generic
|
return .generic
|
||||||
}
|
}
|
||||||
@ -71,6 +71,8 @@ public enum ChatHistoryImport {
|
|||||||
return .chatAdminRequired
|
return .chatAdminRequired
|
||||||
case "IMPORT_PEER_TYPE_INVALID":
|
case "IMPORT_PEER_TYPE_INVALID":
|
||||||
return .invalidChatType
|
return .invalidChatType
|
||||||
|
case "USER_IS_BLOCKED":
|
||||||
|
return .userBlocked
|
||||||
default:
|
default:
|
||||||
return .generic
|
return .generic
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,20 @@ enum UploadPartError {
|
|||||||
case invalidMedia
|
case invalidMedia
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func wrapMethodBody(_ body: (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>), useCompression: Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||||
|
if useCompression {
|
||||||
|
if let compressed = MTGzip.compress(body.1.makeData()) {
|
||||||
|
if compressed.count < body.1.size {
|
||||||
|
let os = MTOutputStream()
|
||||||
|
os.write(0x3072cfa1 as Int32)
|
||||||
|
os.writeBytes(compressed)
|
||||||
|
return (body.0, Buffer(data: os.currentBytes()), body.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
class Download: NSObject, MTRequestMessageServiceDelegate {
|
class Download: NSObject, MTRequestMessageServiceDelegate {
|
||||||
let datacenterId: Int
|
let datacenterId: Int
|
||||||
@ -82,7 +96,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate {
|
|||||||
self.context.authTokenForDatacenter(withIdRequired: self.datacenterId, authToken:self.mtProto.requiredAuthToken, masterDatacenterId: self.mtProto.authTokenMasterDatacenterId)
|
self.context.authTokenForDatacenter(withIdRequired: self.datacenterId, authToken:self.mtProto.requiredAuthToken, masterDatacenterId: self.mtProto.authTokenMasterDatacenterId)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func uploadPart(multiplexedManager: MultiplexedRequestManager, datacenterId: Int, consumerId: Int64, tag: MediaResourceFetchTag?, fileId: Int64, index: Int, data: Data, asBigPart: Bool, bigTotalParts: Int? = nil) -> Signal<Void, UploadPartError> {
|
static func uploadPart(multiplexedManager: MultiplexedRequestManager, datacenterId: Int, consumerId: Int64, tag: MediaResourceFetchTag?, fileId: Int64, index: Int, data: Data, asBigPart: Bool, bigTotalParts: Int? = nil, useCompression: Bool = false) -> Signal<Void, UploadPartError> {
|
||||||
let saveFilePart: (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>)
|
let saveFilePart: (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>)
|
||||||
if asBigPart {
|
if asBigPart {
|
||||||
let totalParts: Int32
|
let totalParts: Int32
|
||||||
@ -96,7 +110,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate {
|
|||||||
saveFilePart = Api.functions.upload.saveFilePart(fileId: fileId, filePart: Int32(index), bytes: Buffer(data: data))
|
saveFilePart = Api.functions.upload.saveFilePart(fileId: fileId, filePart: Int32(index), bytes: Buffer(data: data))
|
||||||
}
|
}
|
||||||
|
|
||||||
return multiplexedManager.request(to: .main(datacenterId), consumerId: consumerId, data: saveFilePart, tag: tag, continueInBackground: true)
|
return multiplexedManager.request(to: .main(datacenterId), consumerId: consumerId, data: wrapMethodBody(saveFilePart, useCompression: useCompression), tag: tag, continueInBackground: true)
|
||||||
|> mapError { error -> UploadPartError in
|
|> mapError { error -> UploadPartError in
|
||||||
if error.errorCode == 400 {
|
if error.errorCode == 400 {
|
||||||
return .invalidMedia
|
return .invalidMedia
|
||||||
@ -109,11 +123,11 @@ class Download: NSObject, MTRequestMessageServiceDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func uploadPart(fileId: Int64, index: Int, data: Data, asBigPart: Bool, bigTotalParts: Int? = nil) -> Signal<Void, UploadPartError> {
|
func uploadPart(fileId: Int64, index: Int, data: Data, asBigPart: Bool, bigTotalParts: Int? = nil, useCompression: Bool = false) -> Signal<Void, UploadPartError> {
|
||||||
return Signal<Void, MTRpcError> { subscriber in
|
return Signal<Void, MTRpcError> { subscriber in
|
||||||
let request = MTRequest()
|
let request = MTRequest()
|
||||||
|
|
||||||
let saveFilePart: (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>)
|
var saveFilePart: (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>)
|
||||||
if asBigPart {
|
if asBigPart {
|
||||||
let totalParts: Int32
|
let totalParts: Int32
|
||||||
if let bigTotalParts = bigTotalParts {
|
if let bigTotalParts = bigTotalParts {
|
||||||
@ -126,6 +140,8 @@ class Download: NSObject, MTRequestMessageServiceDelegate {
|
|||||||
saveFilePart = Api.functions.upload.saveFilePart(fileId: fileId, filePart: Int32(index), bytes: Buffer(data: data))
|
saveFilePart = Api.functions.upload.saveFilePart(fileId: fileId, filePart: Int32(index), bytes: Buffer(data: data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveFilePart = wrapMethodBody(saveFilePart, useCompression: useCompression)
|
||||||
|
|
||||||
request.setPayload(saveFilePart.1.makeData() as Data, metadata: WrappedRequestMetadata(metadata: WrappedFunctionDescription(saveFilePart.0), tag: nil), shortMetadata: WrappedRequestShortMetadata(shortMetadata: WrappedShortFunctionDescription(saveFilePart.0)), responseParser: { response in
|
request.setPayload(saveFilePart.1.makeData() as Data, metadata: WrappedRequestMetadata(metadata: WrappedFunctionDescription(saveFilePart.0), tag: nil), shortMetadata: WrappedRequestShortMetadata(shortMetadata: WrappedShortFunctionDescription(saveFilePart.0)), responseParser: { response in
|
||||||
if let result = saveFilePart.2.parse(Buffer(data: response)) {
|
if let result = saveFilePart.2.parse(Buffer(data: response)) {
|
||||||
return BoxedMessage(result)
|
return BoxedMessage(result)
|
||||||
|
@ -113,7 +113,7 @@ private enum HeaderPartState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private final class MultipartUploadManager {
|
private final class MultipartUploadManager {
|
||||||
let parallelParts: Int = 3
|
let parallelParts: Int
|
||||||
var defaultPartSize: Int
|
var defaultPartSize: Int
|
||||||
var bigTotalParts: Int?
|
var bigTotalParts: Int?
|
||||||
var bigParts: Bool
|
var bigParts: Bool
|
||||||
@ -140,13 +140,19 @@ private final class MultipartUploadManager {
|
|||||||
|
|
||||||
let state: MultipartUploadState
|
let state: MultipartUploadState
|
||||||
|
|
||||||
init(headerSize: Int32, data: Signal<MultipartUploadData, NoError>, encryptionKey: SecretFileEncryptionKey?, hintFileSize: Int?, hintFileIsLarge: Bool, forceNoBigParts: Bool, useLargerParts: Bool, uploadPart: @escaping (UploadPart) -> Signal<Void, UploadPartError>, progress: @escaping (Float) -> Void, completed: @escaping (MultipartIntermediateResult?) -> Void) {
|
init(headerSize: Int32, data: Signal<MultipartUploadData, NoError>, encryptionKey: SecretFileEncryptionKey?, hintFileSize: Int?, hintFileIsLarge: Bool, forceNoBigParts: Bool, useLargerParts: Bool, increaseParallelParts: Bool, uploadPart: @escaping (UploadPart) -> Signal<Void, UploadPartError>, progress: @escaping (Float) -> Void, completed: @escaping (MultipartIntermediateResult?) -> Void) {
|
||||||
self.dataSignal = data
|
self.dataSignal = data
|
||||||
|
|
||||||
var fileId: Int64 = 0
|
var fileId: Int64 = 0
|
||||||
arc4random_buf(&fileId, 8)
|
arc4random_buf(&fileId, 8)
|
||||||
self.fileId = fileId
|
self.fileId = fileId
|
||||||
|
|
||||||
|
if increaseParallelParts {
|
||||||
|
self.parallelParts = 30
|
||||||
|
} else {
|
||||||
|
self.parallelParts = 3
|
||||||
|
}
|
||||||
|
|
||||||
self.forceNoBigParts = forceNoBigParts
|
self.forceNoBigParts = forceNoBigParts
|
||||||
self.useLargerParts = useLargerParts
|
self.useLargerParts = useLargerParts
|
||||||
|
|
||||||
@ -381,7 +387,7 @@ enum MultipartUploadError {
|
|||||||
case generic
|
case generic
|
||||||
}
|
}
|
||||||
|
|
||||||
func multipartUpload(network: Network, postbox: Postbox, source: MultipartUploadSource, encrypt: Bool, tag: MediaResourceFetchTag?, hintFileSize: Int?, hintFileIsLarge: Bool, forceNoBigParts: Bool, useLargerParts: Bool = false, useMultiplexedRequests: Bool = false) -> Signal<MultipartUploadResult, MultipartUploadError> {
|
func multipartUpload(network: Network, postbox: Postbox, source: MultipartUploadSource, encrypt: Bool, tag: MediaResourceFetchTag?, hintFileSize: Int?, hintFileIsLarge: Bool, forceNoBigParts: Bool, useLargerParts: Bool = false, increaseParallelParts: Bool = false, useMultiplexedRequests: Bool = false, useCompression: Bool = false) -> Signal<MultipartUploadResult, MultipartUploadError> {
|
||||||
enum UploadInterface {
|
enum UploadInterface {
|
||||||
case download(Download)
|
case download(Download)
|
||||||
case multiplexed(manager: MultiplexedRequestManager, datacenterId: Int, consumerId: Int64)
|
case multiplexed(manager: MultiplexedRequestManager, datacenterId: Int, consumerId: Int64)
|
||||||
@ -447,12 +453,12 @@ func multipartUpload(network: Network, postbox: Postbox, source: MultipartUpload
|
|||||||
fetchedResource = .complete()
|
fetchedResource = .complete()
|
||||||
}
|
}
|
||||||
|
|
||||||
let manager = MultipartUploadManager(headerSize: headerSize, data: dataSignal, encryptionKey: encryptionKey, hintFileSize: hintFileSize, hintFileIsLarge: hintFileIsLarge, forceNoBigParts: forceNoBigParts, useLargerParts: useLargerParts, uploadPart: { part in
|
let manager = MultipartUploadManager(headerSize: headerSize, data: dataSignal, encryptionKey: encryptionKey, hintFileSize: hintFileSize, hintFileIsLarge: hintFileIsLarge, forceNoBigParts: forceNoBigParts, useLargerParts: useLargerParts, increaseParallelParts: increaseParallelParts, uploadPart: { part in
|
||||||
switch uploadInterface {
|
switch uploadInterface {
|
||||||
case let .download(download):
|
case let .download(download):
|
||||||
return download.uploadPart(fileId: part.fileId, index: part.index, data: part.data, asBigPart: part.bigPart, bigTotalParts: part.bigTotalParts)
|
return download.uploadPart(fileId: part.fileId, index: part.index, data: part.data, asBigPart: part.bigPart, bigTotalParts: part.bigTotalParts, useCompression: useCompression)
|
||||||
case let .multiplexed(multiplexed, datacenterId, consumerId):
|
case let .multiplexed(multiplexed, datacenterId, consumerId):
|
||||||
return Download.uploadPart(multiplexedManager: multiplexed, datacenterId: datacenterId, consumerId: consumerId, tag: nil, fileId: part.fileId, index: part.index, data: part.data, asBigPart: part.bigPart, bigTotalParts: part.bigTotalParts)
|
return Download.uploadPart(multiplexedManager: multiplexed, datacenterId: datacenterId, consumerId: consumerId, tag: nil, fileId: part.fileId, index: part.index, data: part.data, asBigPart: part.bigPart, bigTotalParts: part.bigTotalParts, useCompression: useCompression)
|
||||||
}
|
}
|
||||||
}, progress: { progress in
|
}, progress: { progress in
|
||||||
subscriber.putNext(.progress(progress))
|
subscriber.putNext(.progress(progress))
|
||||||
|
@ -319,6 +319,9 @@ final class AuthorizedApplicationContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let forwardInfo = firstMessage.forwardInfo, forwardInfo.flags.contains(.isImported) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if chatIsVisible {
|
if chatIsVisible {
|
||||||
return
|
return
|
||||||
|
@ -401,81 +401,153 @@ public class ShareRootControllerImpl {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
let fileExtension = (fileName as NSString).pathExtension
|
let fileExtension = (fileName as NSString).pathExtension
|
||||||
guard fileExtension.lowercased() == "zip" else {
|
|
||||||
beginShare()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let archivePath = url.path
|
|
||||||
|
|
||||||
guard let entries = SSZipArchive.getEntriesForFile(atPath: archivePath) else {
|
|
||||||
beginShare()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let mainFileNames: [NSRegularExpression] = [
|
|
||||||
try! NSRegularExpression(pattern: "_chat\\.txt"),
|
|
||||||
try! NSRegularExpression(pattern: "KakaoTalkChats\\.txt"),
|
|
||||||
try! NSRegularExpression(pattern: "Talk_.*?\\.txt"),
|
|
||||||
]
|
|
||||||
|
|
||||||
var maybeMainFileName: String?
|
|
||||||
mainFileLoop: for entry in entries {
|
|
||||||
let entryFileName = entry.path.replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "..", with: "_")
|
|
||||||
let fullRange = NSRange(entryFileName.startIndex ..< entryFileName.endIndex, in: entryFileName)
|
|
||||||
for expression in mainFileNames {
|
|
||||||
if expression.firstMatch(in: entryFileName, options: [], range: fullRange) != nil {
|
|
||||||
maybeMainFileName = entryFileName
|
|
||||||
break mainFileLoop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let mainFileName = maybeMainFileName else {
|
|
||||||
beginShare()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let photoRegex = try! NSRegularExpression(pattern: ".*?\\.jpg")
|
|
||||||
let videoRegex = try! NSRegularExpression(pattern: "[\\d]+-VIDEO-.*?\\.mp4")
|
|
||||||
let stickerRegex = try! NSRegularExpression(pattern: "[\\d]+-STICKER-.*?\\.webp")
|
|
||||||
let voiceRegex = try! NSRegularExpression(pattern: "[\\d]+-AUDIO-.*?\\.opus")
|
|
||||||
|
|
||||||
|
var archivePathValue: String?
|
||||||
var otherEntries: [(SSZipEntry, String, ChatHistoryImport.MediaType)] = []
|
var otherEntries: [(SSZipEntry, String, ChatHistoryImport.MediaType)] = []
|
||||||
|
|
||||||
var mainFile: TempBoxFile?
|
var mainFile: TempBoxFile?
|
||||||
do {
|
|
||||||
for entry in entries {
|
let appConfiguration = context.currentAppConfiguration.with({ $0 })
|
||||||
let entryPath = entry.path.replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "..", with: "_")
|
|
||||||
if entryPath.isEmpty {
|
/*
|
||||||
continue
|
history_import_filters: {
|
||||||
}
|
"zip": {
|
||||||
let tempFile = TempBox.shared.tempFile(fileName: entryPath)
|
"main_file_patterns": [
|
||||||
if entryPath == mainFileName {
|
"_chat\\.txt",
|
||||||
if SSZipArchive.extractFileFromArchive(atPath: archivePath, filePath: entry.path, toPath: tempFile.path) {
|
"KakaoTalkChats\\.txt",
|
||||||
mainFile = tempFile
|
"Talk_.*?\\.txt"
|
||||||
}
|
]
|
||||||
} else {
|
},
|
||||||
let entryFileName = (entryPath as NSString).lastPathComponent
|
"txt": {
|
||||||
if !entryFileName.isEmpty {
|
"patterns": [
|
||||||
let mediaType: ChatHistoryImport.MediaType
|
"^\\[LINE\\]"
|
||||||
let fullRange = NSRange(entryFileName.startIndex ..< entryFileName.endIndex, in: entryFileName)
|
]
|
||||||
if photoRegex.firstMatch(in: entryFileName, options: [], range: fullRange) != nil {
|
}
|
||||||
mediaType = .photo
|
}
|
||||||
} else if videoRegex.firstMatch(in: entryFileName, options: [], range: fullRange) != nil {
|
*/
|
||||||
mediaType = .video
|
|
||||||
} else if stickerRegex.firstMatch(in: entryFileName, options: [], range: fullRange) != nil {
|
if fileExtension.lowercased() == "zip" {
|
||||||
mediaType = .sticker
|
let archivePath = url.path
|
||||||
} else if voiceRegex.firstMatch(in: entryFileName, options: [], range: fullRange) != nil {
|
archivePathValue = archivePath
|
||||||
mediaType = .voice
|
|
||||||
} else {
|
guard let entries = SSZipArchive.getEntriesForFile(atPath: archivePath) else {
|
||||||
mediaType = .file
|
beginShare()
|
||||||
}
|
return
|
||||||
otherEntries.append((entry, entryFileName, mediaType))
|
}
|
||||||
|
|
||||||
|
var mainFileNameExpressions: [String] = [
|
||||||
|
"_chat\\.txt",
|
||||||
|
"KakaoTalkChats\\.txt",
|
||||||
|
"Talk_.*?\\.txt",
|
||||||
|
]
|
||||||
|
|
||||||
|
if let data = appConfiguration.data, let dict = data["history_import_filters"] as? [String: Any] {
|
||||||
|
if let zip = dict["zip"] as? [String: Any] {
|
||||||
|
if let patterns = zip["main_file_patterns"] as? [String] {
|
||||||
|
mainFileNameExpressions = patterns
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
|
let mainFileNames: [NSRegularExpression] = mainFileNameExpressions.compactMap { string -> NSRegularExpression? in
|
||||||
|
return try? NSRegularExpression(pattern: string)
|
||||||
|
}
|
||||||
|
|
||||||
|
var maybeMainFileName: String?
|
||||||
|
mainFileLoop: for entry in entries {
|
||||||
|
let entryFileName = entry.path.replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "..", with: "_")
|
||||||
|
let fullRange = NSRange(entryFileName.startIndex ..< entryFileName.endIndex, in: entryFileName)
|
||||||
|
for expression in mainFileNames {
|
||||||
|
if expression.firstMatch(in: entryFileName, options: [], range: fullRange) != nil {
|
||||||
|
maybeMainFileName = entryFileName
|
||||||
|
break mainFileLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let mainFileName = maybeMainFileName else {
|
||||||
|
beginShare()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let photoRegex = try! NSRegularExpression(pattern: ".*?\\.jpg")
|
||||||
|
let videoRegex = try! NSRegularExpression(pattern: "[\\d]+-VIDEO-.*?\\.mp4")
|
||||||
|
let stickerRegex = try! NSRegularExpression(pattern: "[\\d]+-STICKER-.*?\\.webp")
|
||||||
|
let voiceRegex = try! NSRegularExpression(pattern: "[\\d]+-AUDIO-.*?\\.opus")
|
||||||
|
|
||||||
|
do {
|
||||||
|
for entry in entries {
|
||||||
|
let entryPath = entry.path.replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "..", with: "_")
|
||||||
|
if entryPath.isEmpty {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let tempFile = TempBox.shared.tempFile(fileName: entryPath)
|
||||||
|
if entryPath == mainFileName {
|
||||||
|
if SSZipArchive.extractFileFromArchive(atPath: archivePath, filePath: entry.path, toPath: tempFile.path) {
|
||||||
|
mainFile = tempFile
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let entryFileName = (entryPath as NSString).lastPathComponent
|
||||||
|
if !entryFileName.isEmpty {
|
||||||
|
let mediaType: ChatHistoryImport.MediaType
|
||||||
|
let fullRange = NSRange(entryFileName.startIndex ..< entryFileName.endIndex, in: entryFileName)
|
||||||
|
if photoRegex.firstMatch(in: entryFileName, options: [], range: fullRange) != nil {
|
||||||
|
mediaType = .photo
|
||||||
|
} else if videoRegex.firstMatch(in: entryFileName, options: [], range: fullRange) != nil {
|
||||||
|
mediaType = .video
|
||||||
|
} else if stickerRegex.firstMatch(in: entryFileName, options: [], range: fullRange) != nil {
|
||||||
|
mediaType = .sticker
|
||||||
|
} else if voiceRegex.firstMatch(in: entryFileName, options: [], range: fullRange) != nil {
|
||||||
|
mediaType = .voice
|
||||||
|
} else {
|
||||||
|
mediaType = .file
|
||||||
|
}
|
||||||
|
otherEntries.append((entry, entryFileName, mediaType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if fileExtension.lowercased() == "txt" {
|
||||||
|
var fileScanExpressions: [String] = [
|
||||||
|
"^\\[LINE\\]",
|
||||||
|
]
|
||||||
|
|
||||||
|
if let data = appConfiguration.data, let dict = data["history_import_filters"] as? [String: Any] {
|
||||||
|
if let zip = dict["txt"] as? [String: Any] {
|
||||||
|
if let patterns = zip["patterns"] as? [String] {
|
||||||
|
fileScanExpressions = patterns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let filePatterns: [NSRegularExpression] = fileScanExpressions.compactMap { string -> NSRegularExpression? in
|
||||||
|
return try? NSRegularExpression(pattern: string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let mainFileText = try? String(contentsOf: URL(fileURLWithPath: url.path)) {
|
||||||
|
let fullRange = NSRange(mainFileText.startIndex ..< mainFileText.endIndex, in: mainFileText)
|
||||||
|
var foundMatch = false
|
||||||
|
for pattern in filePatterns {
|
||||||
|
if pattern.firstMatch(in: mainFileText, options: [], range: fullRange) != nil {
|
||||||
|
foundMatch = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundMatch {
|
||||||
|
beginShare()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
beginShare()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let tempFile = TempBox.shared.tempFile(fileName: "History.txt")
|
||||||
|
if let _ = try? FileManager.default.copyItem(atPath: url.path, toPath: tempFile.path) {
|
||||||
|
mainFile = tempFile
|
||||||
|
} else {
|
||||||
|
beginShare()
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let mainFile = mainFile, let mainFileText = try? String(contentsOf: URL(fileURLWithPath: mainFile.path)) {
|
if let mainFile = mainFile, let mainFileText = try? String(contentsOf: URL(fileURLWithPath: mainFile.path)) {
|
||||||
@ -525,7 +597,7 @@ public class ShareRootControllerImpl {
|
|||||||
super.containerLayoutUpdated(layout, transition: transition)
|
super.containerLayoutUpdated(layout, transition: transition)
|
||||||
|
|
||||||
let indicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0))
|
let indicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0))
|
||||||
transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - indicatorSize.width) / 2.0), y: floor((layout.size.height - indicatorSize.height - 50.0) / 2.0)), size: indicatorSize))
|
transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - indicatorSize.width) / 2.0), y: self.navigationHeight + floor((layout.size.height - self.navigationHeight - indicatorSize.height) / 2.0)), size: indicatorSize))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -561,7 +633,7 @@ public class ShareRootControllerImpl {
|
|||||||
navigationController.view.endEditing(true)
|
navigationController.view.endEditing(true)
|
||||||
navigationController.pushViewController(ChatImportActivityScreen(context: context, cancel: {
|
navigationController.pushViewController(ChatImportActivityScreen(context: context, cancel: {
|
||||||
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
|
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||||
}, peerId: peerId, archivePath: archivePath, mainEntry: mainFile, otherEntries: otherEntries))
|
}, peerId: peerId, archivePath: archivePathValue, mainEntry: mainFile, otherEntries: otherEntries))
|
||||||
}
|
}
|
||||||
|
|
||||||
attemptSelectionImpl = { peer in
|
attemptSelectionImpl = { peer in
|
||||||
@ -675,7 +747,7 @@ public class ShareRootControllerImpl {
|
|||||||
navigationController.view.endEditing(true)
|
navigationController.view.endEditing(true)
|
||||||
navigationController.pushViewController(ChatImportActivityScreen(context: context, cancel: {
|
navigationController.pushViewController(ChatImportActivityScreen(context: context, cancel: {
|
||||||
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
|
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||||
}, peerId: peerId, archivePath: archivePath, mainEntry: mainFile, otherEntries: otherEntries))
|
}, peerId: peerId, archivePath: archivePathValue, mainEntry: mainFile, otherEntries: otherEntries))
|
||||||
}
|
}
|
||||||
|
|
||||||
attemptSelectionImpl = { [weak controller] peer in
|
attemptSelectionImpl = { [weak controller] peer in
|
||||||
@ -737,7 +809,7 @@ public class ShareRootControllerImpl {
|
|||||||
navigationController.view.endEditing(true)
|
navigationController.view.endEditing(true)
|
||||||
navigationController.pushViewController(ChatImportActivityScreen(context: context, cancel: {
|
navigationController.pushViewController(ChatImportActivityScreen(context: context, cancel: {
|
||||||
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
|
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||||
}, peerId: peerId, archivePath: archivePath, mainEntry: mainFile, otherEntries: otherEntries))
|
}, peerId: peerId, archivePath: archivePathValue, mainEntry: mainFile, otherEntries: otherEntries))
|
||||||
}
|
}
|
||||||
|
|
||||||
attemptSelectionImpl = { [weak controller] peer in
|
attemptSelectionImpl = { [weak controller] peer in
|
||||||
|
Loading…
x
Reference in New Issue
Block a user