mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-06-20 18:24:43 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
@@ -568,6 +568,7 @@
|
||||
|
||||
"ConversationProfile.LeaveDeleteAndExit" = "Delete and Exit";
|
||||
"Group.LeaveGroup" = "Leave Group";
|
||||
"Group.DeleteGroup" = "Delete Group";
|
||||
|
||||
"Conversation.Megabytes" = "%.1f MB";
|
||||
"Conversation.Kilobytes" = "%d KB";
|
||||
@@ -1129,6 +1130,7 @@
|
||||
"ShareFileTip.CloseTip" = "Close Tip";
|
||||
|
||||
"DialogList.SearchSectionDialogs" = "Chats and Contacts";
|
||||
"DialogList.SearchSectionChats" = "Chats";
|
||||
"DialogList.SearchSectionGlobal" = "Global Search";
|
||||
"DialogList.SearchSectionMessages" = "Messages";
|
||||
|
||||
@@ -4272,6 +4274,8 @@ Unused sets are archived when you add more.";
|
||||
"ChatList.DeleteForEveryoneConfirmationTitle" = "Warning!";
|
||||
"ChatList.DeleteForEveryoneConfirmationText" = "This will **delete all messages** in this chat for **both participants**.";
|
||||
"ChatList.DeleteForEveryoneConfirmationAction" = "Delete All";
|
||||
"ChatList.DeleteForAllMembers" = "Delete for all members";
|
||||
"ChatList.DeleteForAllMembersConfirmationText" = "This will **delete all messages** in this chat for **all participants**.";
|
||||
|
||||
"ChatList.DeleteSavedMessagesConfirmationTitle" = "Warning!";
|
||||
"ChatList.DeleteSavedMessagesConfirmationText" = "This will **delete all messages** in this chat.";
|
||||
@@ -5907,3 +5911,6 @@ Sorry for the inconvenience.";
|
||||
"ChannelInfo.FakeChannelWarning" = "⚠️ Warning: Many users reported that this account impersonates a famous person or organization.";
|
||||
|
||||
"ReportPeer.ReasonFake" = "Fake Account";
|
||||
|
||||
"ChatList.HeaderImportIntoAnExistingGroup" = "OR IMPORT INTO AN EXISTING GROUP";
|
||||
|
||||
|
||||
@@ -14,14 +14,6 @@ def generate(build_environment: BuildEnvironment, disable_extensions, configurat
|
||||
project_path = os.path.join(build_environment.base_path, 'build-input/gen/project')
|
||||
app_target = 'Telegram'
|
||||
|
||||
'''
|
||||
TULSI_APP="build-input/gen/project/Tulsi.app"
|
||||
TULSI="$TULSI_APP/Contents/MacOS/Tulsi"
|
||||
|
||||
rm -rf "$GEN_DIRECTORY/${APP_TARGET}.tulsiproj"
|
||||
rm -rf "$TULSI_APP"
|
||||
'''
|
||||
|
||||
os.makedirs(project_path, exist_ok=True)
|
||||
remove_directory('{}/Tulsi.app'.format(project_path))
|
||||
remove_directory('{project}/{target}.tulsiproj'.format(project=project_path, target=app_target))
|
||||
|
||||
@@ -6,14 +6,21 @@ import Postbox
|
||||
import TelegramCore
|
||||
|
||||
public struct ChatListNodeAdditionalCategory {
|
||||
public enum Appearance {
|
||||
case option
|
||||
case action
|
||||
}
|
||||
|
||||
public var id: Int
|
||||
public var icon: UIImage?
|
||||
public var title: String
|
||||
public var appearance: Appearance
|
||||
|
||||
public init(id: Int, icon: UIImage?, title: String) {
|
||||
public init(id: Int, icon: UIImage?, title: String, appearance: Appearance = .option) {
|
||||
self.id = id
|
||||
self.icon = icon
|
||||
self.title = title
|
||||
self.appearance = appearance
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,20 +32,29 @@ public struct ChatListNodePeersFilter: OptionSet {
|
||||
public final class PeerSelectionControllerParams {
|
||||
public let context: AccountContext
|
||||
public let filter: ChatListNodePeersFilter
|
||||
public let hasChatListSelector: Bool
|
||||
public let hasContactSelector: Bool
|
||||
public let hasGlobalSearch: Bool
|
||||
public let title: String?
|
||||
public let attemptSelection: ((Peer) -> Void)?
|
||||
public let createNewGroup: (() -> Void)?
|
||||
public let pretendPresentedInModal: Bool
|
||||
|
||||
public init(context: AccountContext, filter: ChatListNodePeersFilter = [.onlyWriteable], hasContactSelector: Bool = true, title: String? = nil, attemptSelection: ((Peer) -> Void)? = nil) {
|
||||
public init(context: AccountContext, filter: ChatListNodePeersFilter = [.onlyWriteable], hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false) {
|
||||
self.context = context
|
||||
self.filter = filter
|
||||
self.hasChatListSelector = hasChatListSelector
|
||||
self.hasContactSelector = hasContactSelector
|
||||
self.hasGlobalSearch = hasGlobalSearch
|
||||
self.title = title
|
||||
self.attemptSelection = attemptSelection
|
||||
self.createNewGroup = createNewGroup
|
||||
self.pretendPresentedInModal = pretendPresentedInModal
|
||||
}
|
||||
}
|
||||
|
||||
public protocol PeerSelectionController: ViewController {
|
||||
var peerSelected: ((PeerId) -> Void)? { get set }
|
||||
var peerSelected: ((Peer) -> Void)? { get set }
|
||||
var inProgress: Bool { get set }
|
||||
var customDismiss: (() -> Void)? { get set }
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import AccountContext
|
||||
import Emoji
|
||||
|
||||
private let deletedIcon = UIImage(bundleImageName: "Avatar/DeletedIcon")?.precomposed()
|
||||
private let phoneIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/PhoneIcon"), color: .white)
|
||||
private let savedMessagesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/SavedMessagesIcon"), color: .white)
|
||||
private let archivedChatsIcon = UIImage(bundleImageName: "Avatar/ArchiveAvatarIcon")?.precomposed()
|
||||
private let repliesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/RepliesMessagesIcon"), color: .white)
|
||||
@@ -79,10 +80,14 @@ private let savedMessagesColors: NSArray = [
|
||||
UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor
|
||||
]
|
||||
|
||||
public enum AvatarNodeExplicitIcon {
|
||||
case phone
|
||||
}
|
||||
|
||||
private enum AvatarNodeState: Equatable {
|
||||
case empty
|
||||
case peerAvatar(PeerId, [String], TelegramMediaImageRepresentation?)
|
||||
case custom(letter: [String], explicitColorIndex: Int?)
|
||||
case custom(letter: [String], explicitColorIndex: Int?, explicitIcon: AvatarNodeExplicitIcon?)
|
||||
}
|
||||
|
||||
private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool {
|
||||
@@ -91,8 +96,8 @@ private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool {
|
||||
return true
|
||||
case let (.peerAvatar(lhsPeerId, lhsLetters, lhsPhotoRepresentations), .peerAvatar(rhsPeerId, rhsLetters, rhsPhotoRepresentations)):
|
||||
return lhsPeerId == rhsPeerId && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations
|
||||
case let (.custom(lhsLetters, lhsIndex), .custom(rhsLetters, rhsIndex)):
|
||||
return lhsLetters == rhsLetters && lhsIndex == rhsIndex
|
||||
case let (.custom(lhsLetters, lhsIndex, lhsIcon), .custom(rhsLetters, rhsIndex, rhsIcon)):
|
||||
return lhsLetters == rhsLetters && lhsIndex == rhsIndex && lhsIcon == rhsIcon
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -105,6 +110,7 @@ private enum AvatarNodeIcon: Equatable {
|
||||
case archivedChatsIcon(hiddenByDefault: Bool)
|
||||
case editAvatarIcon
|
||||
case deletedIcon
|
||||
case phoneIcon
|
||||
}
|
||||
|
||||
public enum AvatarNodeImageOverride: Equatable {
|
||||
@@ -115,6 +121,7 @@ public enum AvatarNodeImageOverride: Equatable {
|
||||
case archivedChatsIcon(hiddenByDefault: Bool)
|
||||
case editAvatarIcon
|
||||
case deletedIcon
|
||||
case phoneIcon
|
||||
}
|
||||
|
||||
public enum AvatarNodeColorOverride {
|
||||
@@ -323,6 +330,9 @@ public final class AvatarNode: ASDisplayNode {
|
||||
case .deletedIcon:
|
||||
representation = nil
|
||||
icon = .deletedIcon
|
||||
case .phoneIcon:
|
||||
representation = nil
|
||||
icon = .phoneIcon
|
||||
}
|
||||
} else if peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) == nil {
|
||||
representation = peer?.smallProfileImage
|
||||
@@ -383,7 +393,7 @@ public final class AvatarNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
public func setCustomLetters(_ letters: [String], explicitColor: AvatarNodeColorOverride? = nil) {
|
||||
public func setCustomLetters(_ letters: [String], explicitColor: AvatarNodeColorOverride? = nil, icon: AvatarNodeExplicitIcon? = nil) {
|
||||
var explicitIndex: Int?
|
||||
if let explicitColor = explicitColor {
|
||||
switch explicitColor {
|
||||
@@ -391,11 +401,16 @@ public final class AvatarNode: ASDisplayNode {
|
||||
explicitIndex = 5
|
||||
}
|
||||
}
|
||||
let updatedState: AvatarNodeState = .custom(letter: letters, explicitColorIndex: explicitIndex)
|
||||
let updatedState: AvatarNodeState = .custom(letter: letters, explicitColorIndex: explicitIndex, explicitIcon: icon)
|
||||
if updatedState != self.state {
|
||||
self.state = updatedState
|
||||
|
||||
let parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
|
||||
let parameters: AvatarNodeParameters
|
||||
if let icon = icon, case .phone = icon {
|
||||
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
|
||||
} else {
|
||||
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
|
||||
}
|
||||
|
||||
self.displaySuspended = true
|
||||
self.contents = nil
|
||||
@@ -456,6 +471,8 @@ public final class AvatarNode: ASDisplayNode {
|
||||
if let parameters = parameters as? AvatarNodeParameters, parameters.icon != .none {
|
||||
if case .deletedIcon = parameters.icon {
|
||||
colorsArray = grayscaleColors
|
||||
} else if case .phoneIcon = parameters.icon {
|
||||
colorsArray = grayscaleColors
|
||||
} else if case .savedMessagesIcon = parameters.icon {
|
||||
colorsArray = savedMessagesColors
|
||||
} else if case .repliesIcon = parameters.icon {
|
||||
@@ -505,6 +522,15 @@ public final class AvatarNode: ASDisplayNode {
|
||||
if let deletedIcon = deletedIcon {
|
||||
context.draw(deletedIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - deletedIcon.size.width) / 2.0), y: floor((bounds.size.height - deletedIcon.size.height) / 2.0)), size: deletedIcon.size))
|
||||
}
|
||||
} else if case .phoneIcon = parameters.icon {
|
||||
let factor: CGFloat = 1.0
|
||||
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
||||
context.scaleBy(x: factor, y: -factor)
|
||||
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
|
||||
|
||||
if let phoneIcon = phoneIcon {
|
||||
context.draw(phoneIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - phoneIcon.size.width) / 2.0), y: floor((bounds.size.height - phoneIcon.size.height) / 2.0)), size: phoneIcon.size))
|
||||
}
|
||||
} else if case .savedMessagesIcon = parameters.icon {
|
||||
let factor = bounds.size.width / 60.0
|
||||
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
||||
|
||||
@@ -120,8 +120,8 @@ private func mappedInsertEntries(context: AccountContext, presentationData: Item
|
||||
}), directionHint: entry.directionHint)
|
||||
case let .displayTabInfo(_, text):
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
|
||||
case let .groupCall(peer, editing, isActive):
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: editing, interaction: nodeInteraction), directionHint: entry.directionHint)
|
||||
case let .groupCall(peer, _, isActive):
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: false, interaction: nodeInteraction), directionHint: entry.directionHint)
|
||||
case let .messageEntry(topMessage, messages, _, _, dateTimeFormat, editing, hasActiveRevealControls, displayHeader):
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, context: context, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, displayHeader: displayHeader, interaction: nodeInteraction), directionHint: entry.directionHint)
|
||||
case let .holeEntry(_, theme):
|
||||
@@ -139,8 +139,8 @@ private func mappedUpdateEntries(context: AccountContext, presentationData: Item
|
||||
}), directionHint: entry.directionHint)
|
||||
case let .displayTabInfo(_, text):
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
|
||||
case let .groupCall(peer, editing, isActive):
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: editing, interaction: nodeInteraction), directionHint: entry.directionHint)
|
||||
case let .groupCall(peer, _, isActive):
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: false, interaction: nodeInteraction), directionHint: entry.directionHint)
|
||||
case let .messageEntry(topMessage, messages, _, _, dateTimeFormat, editing, hasActiveRevealControls, displayHeader):
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, context: context, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, displayHeader: displayHeader, interaction: nodeInteraction), directionHint: entry.directionHint)
|
||||
case let .holeEntry(_, theme):
|
||||
@@ -263,9 +263,49 @@ final class CallListControllerNode: ASDisplayNode {
|
||||
}, openInfo: { [weak self] peerId, messages in
|
||||
self?.openInfo(peerId, messages)
|
||||
}, delete: { [weak self] messageIds in
|
||||
if let strongSelf = self {
|
||||
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: messageIds, type: .forLocalPeer).start()
|
||||
guard let strongSelf = self, let peerId = messageIds.first?.peerId else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Peer? in
|
||||
return transaction.getPeer(peerId)
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
guard let strongSelf = self, let peer = peer else {
|
||||
return
|
||||
}
|
||||
|
||||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||||
var items: [ActionSheetItem] = []
|
||||
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_DeleteMessagesFor(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0, color: .destructive, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: messageIds, type: .forEveryone).start()
|
||||
}))
|
||||
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe, color: .destructive, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: messageIds, type: .forLocalPeer).start()
|
||||
}))
|
||||
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: items),
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])
|
||||
])
|
||||
strongSelf.controller?.present(actionSheet, in: .window(.root))
|
||||
})
|
||||
}, updateShowCallsTab: { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
let _ = updateCallListSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, {
|
||||
|
||||
@@ -432,7 +432,7 @@ class CallListGroupCallItemNode: ItemListRevealOptionsItemNode {
|
||||
transition.updateFrameAdditive(node: strongSelf.joinBackgroundNode, frame: CGRect(origin: CGPoint(), size: joinButtonFrame.size))
|
||||
|
||||
let _ = joinTitleApply()
|
||||
transition.updateFrameAdditive(node: strongSelf.joinTitleNode, frame: CGRect(origin: CGPoint(x: floor((joinButtonSize.width - joinTitleLayout.size.width) / 2.0), y: floor((joinButtonSize.height - joinTitleLayout.size.height) / 2.0) + 1.0), size: titleLayout.size))
|
||||
transition.updateFrameAdditive(node: strongSelf.joinTitleNode, frame: CGRect(origin: CGPoint(x: floor((joinButtonSize.width - joinTitleLayout.size.width) / 2.0), y: floor((joinButtonSize.height - joinTitleLayout.size.height) / 2.0) + 1.0), size: joinTitleLayout.size))
|
||||
|
||||
let topHighlightInset: CGFloat = (first || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height))
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatImportUI",
|
||||
module_name = "ChatImportUI",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/SyncCore:SyncCore",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//third-party/ZIPFoundation:ZIPFoundation",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/RadialStatusNode:RadialStatusNode",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,364 @@
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import PresentationDataUtils
|
||||
import RadialStatusNode
|
||||
import AnimatedStickerNode
|
||||
import AppBundle
|
||||
import ZIPFoundation
|
||||
|
||||
public final class ChatImportActivityScreen: ViewController {
|
||||
private final class Node: ViewControllerTracingNode {
|
||||
private weak var controller: ChatImportActivityScreen?
|
||||
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
|
||||
private let animationNode: AnimatedStickerNode
|
||||
private let radialStatus: RadialStatusNode
|
||||
private let radialStatusBackground: ASImageNode
|
||||
private let radialStatusText: ImmediateTextNode
|
||||
private let progressText: ImmediateTextNode
|
||||
private let statusText: ImmediateTextNode
|
||||
|
||||
private let statusButtonText: ImmediateTextNode
|
||||
private let statusButton: HighlightableButtonNode
|
||||
|
||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
private var totalProgress: CGFloat = 0.0
|
||||
private let totalBytes: Int
|
||||
private var isDone: Bool = false
|
||||
|
||||
init(controller: ChatImportActivityScreen, context: AccountContext, totalBytes: Int) {
|
||||
self.controller = controller
|
||||
self.context = context
|
||||
self.totalBytes = totalBytes
|
||||
|
||||
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
self.animationNode = AnimatedStickerNode()
|
||||
|
||||
self.radialStatus = RadialStatusNode(backgroundNodeColor: .clear)
|
||||
self.radialStatusBackground = ASImageNode()
|
||||
self.radialStatusBackground.isUserInteractionEnabled = false
|
||||
self.radialStatusBackground.displaysAsynchronously = false
|
||||
self.radialStatusBackground.image = generateCircleImage(diameter: 180.0, lineWidth: 6.0, color: self.presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.2))
|
||||
|
||||
self.radialStatusText = ImmediateTextNode()
|
||||
self.radialStatusText.isUserInteractionEnabled = false
|
||||
self.radialStatusText.displaysAsynchronously = false
|
||||
self.radialStatusText.maximumNumberOfLines = 1
|
||||
self.radialStatusText.isAccessibilityElement = false
|
||||
|
||||
self.progressText = ImmediateTextNode()
|
||||
self.progressText.isUserInteractionEnabled = false
|
||||
self.progressText.displaysAsynchronously = false
|
||||
self.progressText.maximumNumberOfLines = 1
|
||||
self.progressText.isAccessibilityElement = false
|
||||
|
||||
self.statusText = ImmediateTextNode()
|
||||
self.statusText.textAlignment = .center
|
||||
self.statusText.isUserInteractionEnabled = false
|
||||
self.statusText.displaysAsynchronously = false
|
||||
self.statusText.maximumNumberOfLines = 0
|
||||
self.statusText.isAccessibilityElement = false
|
||||
|
||||
self.statusButtonText = ImmediateTextNode()
|
||||
self.statusButtonText.isUserInteractionEnabled = false
|
||||
self.statusButtonText.displaysAsynchronously = false
|
||||
self.statusButtonText.maximumNumberOfLines = 1
|
||||
self.statusButtonText.isAccessibilityElement = false
|
||||
|
||||
self.statusButton = HighlightableButtonNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||
|
||||
if let path = getAppBundle().path(forResource: "HistoryImport", ofType: "tgs") {
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 170 * 2, height: 170 * 2, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
|
||||
self.animationNode.visibility = true
|
||||
}
|
||||
|
||||
self.addSubnode(self.animationNode)
|
||||
self.addSubnode(self.radialStatusBackground)
|
||||
self.addSubnode(self.radialStatus)
|
||||
self.addSubnode(self.radialStatusText)
|
||||
self.addSubnode(self.progressText)
|
||||
self.addSubnode(self.statusText)
|
||||
self.addSubnode(self.statusButtonText)
|
||||
self.addSubnode(self.statusButton)
|
||||
|
||||
self.statusButton.addTarget(self, action: #selector(self.statusButtonPressed), forControlEvents: .touchUpInside)
|
||||
self.statusButton.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.statusButtonText.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.statusButtonText.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.statusButtonText.alpha = 1.0
|
||||
strongSelf.statusButtonText.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func statusButtonPressed() {
|
||||
self.controller?.cancel()
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (layout, navigationHeight)
|
||||
|
||||
//TODO:localize
|
||||
|
||||
let iconSize = CGSize(width: 170.0, height: 170.0)
|
||||
let radialStatusSize = CGSize(width: 186.0, height: 186.0)
|
||||
let maxIconStatusSpacing: CGFloat = 62.0
|
||||
let maxProgressTextSpacing: CGFloat = 33.0
|
||||
let progressStatusSpacing: CGFloat = 14.0
|
||||
let statusButtonSpacing: CGFloat = 19.0
|
||||
|
||||
self.radialStatusText.attributedText = NSAttributedString(string: "\(Int(self.totalProgress * 100.0))%", font: Font.with(size: 42.0, design: .round, weight: .semibold), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
||||
let radialStatusTextSize = self.radialStatusText.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
|
||||
|
||||
self.progressText.attributedText = NSAttributedString(string: "\(dataSizeString(Int(self.totalProgress * CGFloat(self.totalBytes)))) of \(dataSizeString(Int(1.0 * CGFloat(self.totalBytes))))", font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
||||
let progressTextSize = self.progressText.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: .greatestFiniteMagnitude))
|
||||
|
||||
self.statusButtonText.attributedText = NSAttributedString(string: "Done", font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemAccentColor)
|
||||
let statusButtonTextSize = self.statusButtonText.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: .greatestFiniteMagnitude))
|
||||
|
||||
if !self.isDone {
|
||||
self.statusText.attributedText = NSAttributedString(string: "Please keep this window open\nduring the import.", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor)
|
||||
} else {
|
||||
self.statusText.attributedText = NSAttributedString(string: "This chat has been imported\nsuccessfully.", font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
||||
}
|
||||
let statusTextSize = self.statusText.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: .greatestFiniteMagnitude))
|
||||
|
||||
let contentHeight: CGFloat
|
||||
var hideIcon = false
|
||||
if case .compact = layout.metrics.heightClass, layout.size.width > layout.size.height {
|
||||
hideIcon = true
|
||||
contentHeight = radialStatusSize.height + maxProgressTextSpacing + progressTextSize.height + progressStatusSpacing + 100.0
|
||||
} else {
|
||||
contentHeight = iconSize.height + maxIconStatusSpacing + radialStatusSize.height + maxProgressTextSpacing + progressTextSize.height + progressStatusSpacing + 100.0
|
||||
}
|
||||
|
||||
transition.updateAlpha(node: self.animationNode, alpha: hideIcon ? 0.0 : 1.0)
|
||||
|
||||
let contentOriginY = navigationHeight + floor((layout.size.height - contentHeight) / 2.0)
|
||||
|
||||
self.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: contentOriginY), size: iconSize)
|
||||
self.animationNode.updateLayout(size: iconSize)
|
||||
|
||||
self.radialStatus.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - radialStatusSize.width) / 2.0), y: hideIcon ? contentOriginY : (contentOriginY + iconSize.height + maxIconStatusSpacing)), size: radialStatusSize)
|
||||
self.radialStatusBackground.frame = self.radialStatus.frame
|
||||
|
||||
self.radialStatusText.frame = CGRect(origin: CGPoint(x: self.radialStatus.frame.minX + floor((self.radialStatus.frame.width - radialStatusTextSize.width) / 2.0), y: self.radialStatus.frame.minY + floor((self.radialStatus.frame.height - radialStatusTextSize.height) / 2.0)), size: radialStatusTextSize)
|
||||
|
||||
self.progressText.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - progressTextSize.width) / 2.0), y: self.radialStatus.frame.maxY + maxProgressTextSpacing), size: progressTextSize)
|
||||
|
||||
if self.isDone {
|
||||
self.statusText.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusTextSize.width) / 2.0), y: self.progressText.frame.minY), size: statusTextSize)
|
||||
} else {
|
||||
self.statusText.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusTextSize.width) / 2.0), y: self.progressText.frame.maxY + progressStatusSpacing), size: statusTextSize)
|
||||
}
|
||||
|
||||
let statusButtonTextFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusButtonTextSize.width) / 2.0), y: self.statusText.frame.maxY + statusButtonSpacing), size: statusButtonTextSize)
|
||||
self.statusButtonText.frame = statusButtonTextFrame
|
||||
self.statusButton.frame = statusButtonTextFrame.insetBy(dx: -10.0, dy: -10.0)
|
||||
|
||||
self.statusButtonText.isHidden = !self.isDone
|
||||
self.statusButton.isHidden = !self.isDone
|
||||
self.progressText.isHidden = self.isDone
|
||||
}
|
||||
|
||||
func updateProgress(totalProgress: CGFloat, isDone: Bool, animated: Bool) {
|
||||
self.totalProgress = totalProgress
|
||||
self.isDone = isDone
|
||||
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate)
|
||||
self.radialStatus.transitionToState(.progress(color: self.presentationData.theme.list.itemAccentColor, lineWidth: 6.0, value: self.totalProgress, cancelEnabled: false), animated: animated, synchronous: true, completion: {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var controllerNode: Node {
|
||||
return self.displayNode as! Node
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
fileprivate let cancel: () -> Void
|
||||
private let peerId: PeerId
|
||||
private let archive: Archive
|
||||
private let mainEntry: TempBoxFile
|
||||
private let otherEntries: [(Entry, String, ChatHistoryImport.MediaType)]
|
||||
|
||||
private var pendingEntries = Set<String>()
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
override public var _presentedInModal: Bool {
|
||||
get {
|
||||
return true
|
||||
} set(value) {
|
||||
}
|
||||
}
|
||||
|
||||
public init(context: AccountContext, cancel: @escaping () -> Void, peerId: PeerId, archive: Archive, mainEntry: TempBoxFile, otherEntries: [(Entry, String, ChatHistoryImport.MediaType)]) {
|
||||
self.context = context
|
||||
self.cancel = cancel
|
||||
self.peerId = peerId
|
||||
self.archive = archive
|
||||
self.mainEntry = mainEntry
|
||||
self.otherEntries = otherEntries
|
||||
|
||||
self.pendingEntries = Set(otherEntries.map { $0.1 })
|
||||
|
||||
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, hideBackground: true, hideBadge: true))
|
||||
|
||||
//TODO:localize
|
||||
self.title = "Importing Chat"
|
||||
|
||||
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)), animated: false)
|
||||
|
||||
self.attemptNavigation = { _ in
|
||||
return false
|
||||
}
|
||||
|
||||
self.beginImport()
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
self.cancel()
|
||||
}
|
||||
|
||||
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.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationHeight, transition: transition)
|
||||
}
|
||||
|
||||
private func beginImport() {
|
||||
enum ImportError {
|
||||
case generic
|
||||
}
|
||||
|
||||
let context = self.context
|
||||
let archive = self.archive
|
||||
let mainEntry = self.mainEntry
|
||||
let otherEntries = self.otherEntries
|
||||
|
||||
let resolvedPeerId: Signal<PeerId, ImportError>
|
||||
if self.peerId.namespace == Namespaces.Peer.CloudGroup {
|
||||
resolvedPeerId = convertGroupToSupergroup(account: self.context.account, peerId: self.peerId)
|
||||
|> mapError { _ -> ImportError in
|
||||
return .generic
|
||||
}
|
||||
} else {
|
||||
resolvedPeerId = .single(self.peerId)
|
||||
}
|
||||
|
||||
self.disposable.set((resolvedPeerId
|
||||
|> mapToSignal { peerId -> Signal<ChatHistoryImport.Session, ImportError> in
|
||||
return ChatHistoryImport.initSession(account: context.account, peerId: peerId, file: mainEntry, mediaCount: Int32(otherEntries.count))
|
||||
|> mapError { _ -> ImportError in
|
||||
return .generic
|
||||
}
|
||||
}
|
||||
|> mapToSignal { session -> Signal<String, ImportError> in
|
||||
var importSignal: Signal<String, ImportError> = .single("")
|
||||
|
||||
for (entry, fileName, mediaType) in otherEntries {
|
||||
let unpackedFile = Signal<TempBoxFile, ImportError> { subscriber in
|
||||
let tempFile = TempBox.shared.tempFile(fileName: fileName)
|
||||
do {
|
||||
let _ = try archive.extract(entry, to: URL(fileURLWithPath: tempFile.path))
|
||||
subscriber.putNext(tempFile)
|
||||
subscriber.putCompletion()
|
||||
} catch {
|
||||
subscriber.putError(.generic)
|
||||
}
|
||||
|
||||
return EmptyDisposable
|
||||
}
|
||||
let uploadedMedia = unpackedFile
|
||||
|> mapToSignal { tempFile -> Signal<String, ImportError> in
|
||||
return ChatHistoryImport.uploadMedia(account: context.account, session: session, file: tempFile, fileName: fileName, type: mediaType)
|
||||
|> mapError { _ -> ImportError in
|
||||
return .generic
|
||||
}
|
||||
|> map { _ -> String in
|
||||
}
|
||||
|> then(.single(fileName))
|
||||
}
|
||||
|
||||
importSignal = importSignal
|
||||
|> then(uploadedMedia)
|
||||
}
|
||||
|
||||
importSignal = importSignal
|
||||
|> then(ChatHistoryImport.startImport(account: context.account, session: session)
|
||||
|> mapError { _ -> ImportError in
|
||||
return .generic
|
||||
}
|
||||
|> map { _ -> String in
|
||||
})
|
||||
|
||||
return importSignal
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] fileName in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.pendingEntries.remove(fileName)
|
||||
var totalProgress: CGFloat = 1.0
|
||||
if !strongSelf.otherEntries.isEmpty {
|
||||
totalProgress = CGFloat(strongSelf.otherEntries.count - strongSelf.pendingEntries.count) / CGFloat(strongSelf.otherEntries.count)
|
||||
}
|
||||
strongSelf.controllerNode.updateProgress(totalProgress: totalProgress, isDone: false, animated: true)
|
||||
}, error: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.controllerNode.updateProgress(totalProgress: 0.0, isDone: false, animated: true)
|
||||
}, completed: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.controllerNode.updateProgress(totalProgress: 1.0, isDone: true, animated: true)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ public enum ChatListSearchItemHeaderType {
|
||||
case groupMembers
|
||||
case activeVoiceChats
|
||||
case recentCalls
|
||||
case orImportIntoAnExistingGroup
|
||||
|
||||
fileprivate func title(strings: PresentationStrings) -> String {
|
||||
switch self {
|
||||
@@ -68,6 +69,8 @@ public enum ChatListSearchItemHeaderType {
|
||||
return strings.CallList_ActiveVoiceChatsHeader
|
||||
case .recentCalls:
|
||||
return strings.CallList_RecentCallsHeader
|
||||
case .orImportIntoAnExistingGroup:
|
||||
return strings.ChatList_HeaderImportIntoAnExistingGroup
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +116,8 @@ public enum ChatListSearchItemHeaderType {
|
||||
return .activeVoiceChats
|
||||
case .recentCalls:
|
||||
return .recentCalls
|
||||
case .orImportIntoAnExistingGroup:
|
||||
return .orImportIntoAnExistingGroup
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,6 +147,7 @@ private enum ChatListSearchItemHeaderId: Int32 {
|
||||
case groupMembers
|
||||
case activeVoiceChats
|
||||
case recentCalls
|
||||
case orImportIntoAnExistingGroup
|
||||
}
|
||||
|
||||
public final class ChatListSearchItemHeader: ListViewItemHeader {
|
||||
|
||||
@@ -16,6 +16,7 @@ public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeade
|
||||
let context: AccountContext
|
||||
let title: String
|
||||
let image: UIImage?
|
||||
let appearance: ChatListNodeAdditionalCategory.Appearance
|
||||
let isSelected: Bool
|
||||
let action: () -> Void
|
||||
|
||||
@@ -29,6 +30,7 @@ public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeade
|
||||
context: AccountContext,
|
||||
title: String,
|
||||
image: UIImage?,
|
||||
appearance: ChatListNodeAdditionalCategory.Appearance,
|
||||
isSelected: Bool,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
@@ -37,10 +39,16 @@ public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeade
|
||||
self.context = context
|
||||
self.title = title
|
||||
self.image = image
|
||||
self.appearance = appearance
|
||||
self.isSelected = isSelected
|
||||
self.action = action
|
||||
|
||||
self.header = ChatListSearchItemHeader(type: .chatTypes, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
switch appearance {
|
||||
case .option:
|
||||
self.header = ChatListSearchItemHeader(type: .chatTypes, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
case .action:
|
||||
self.header = nil
|
||||
}
|
||||
}
|
||||
|
||||
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
@@ -81,6 +89,9 @@ public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeade
|
||||
}
|
||||
|
||||
public func selected(listView: ListView) {
|
||||
if case .action = self.appearance {
|
||||
listView.clearHighlightAnimated(true)
|
||||
}
|
||||
self.action()
|
||||
}
|
||||
|
||||
@@ -107,6 +118,9 @@ public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeade
|
||||
} else {
|
||||
last = true
|
||||
}
|
||||
} else if let _ = nextItem as? ChatListAdditionalCategoryItem {
|
||||
} else {
|
||||
last = true
|
||||
}
|
||||
} else {
|
||||
last = true
|
||||
@@ -172,16 +186,37 @@ public class ChatListAdditionalCategoryItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
|
||||
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
return
|
||||
|
||||
/*super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
self.isHighlighted = highlighted
|
||||
self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)*/
|
||||
if let item = self.item, case .action = item.appearance {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
self.isHighlighted = highlighted
|
||||
self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
|
||||
let reallyHighlighted = self.isHighlighted
|
||||
let highlightProgress: CGFloat = 1.0
|
||||
|
||||
if reallyHighlighted {
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
}
|
||||
self.highlightedBackgroundNode.layer.removeAllAnimations()
|
||||
transition.updateAlpha(layer: self.highlightedBackgroundNode.layer, alpha: highlightProgress)
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
transition.updateAlpha(layer: self.highlightedBackgroundNode.layer, alpha: 1.0 - highlightProgress, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func asyncLayout() -> (_ item: ChatListAdditionalCategoryItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (Bool, Bool) -> Void)) {
|
||||
@@ -206,20 +241,29 @@ public class ChatListAdditionalCategoryItemNode: ItemListRevealOptionsItemNode {
|
||||
let updatedSelectionNode: CheckNode?
|
||||
let isSelected = item.isSelected
|
||||
|
||||
rightInset += 28.0
|
||||
|
||||
let selectionNode: CheckNode
|
||||
if let current = currentSelectionNode {
|
||||
selectionNode = current
|
||||
updatedSelectionNode = selectionNode
|
||||
if case .option = item.appearance {
|
||||
rightInset += 28.0
|
||||
|
||||
let selectionNode: CheckNode
|
||||
if let current = currentSelectionNode {
|
||||
selectionNode = current
|
||||
updatedSelectionNode = selectionNode
|
||||
} else {
|
||||
selectionNode = CheckNode(strokeColor: item.presentationData.theme.list.itemCheckColors.strokeColor, fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, style: .plain)
|
||||
selectionNode.isUserInteractionEnabled = false
|
||||
updatedSelectionNode = selectionNode
|
||||
}
|
||||
} else {
|
||||
selectionNode = CheckNode(strokeColor: item.presentationData.theme.list.itemCheckColors.strokeColor, fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, style: .plain)
|
||||
selectionNode.isUserInteractionEnabled = false
|
||||
updatedSelectionNode = selectionNode
|
||||
updatedSelectionNode = nil
|
||||
}
|
||||
|
||||
var titleAttributedString: NSAttributedString?
|
||||
let textColor = item.presentationData.theme.list.itemPrimaryTextColor
|
||||
let textColor: UIColor
|
||||
if case .action = item.appearance {
|
||||
textColor = item.presentationData.theme.list.itemAccentColor
|
||||
} else {
|
||||
textColor = item.presentationData.theme.list.itemPrimaryTextColor
|
||||
}
|
||||
titleAttributedString = NSAttributedString(string: item.title, font: titleFont, textColor: textColor)
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
@@ -261,11 +305,12 @@ public class ChatListAdditionalCategoryItemNode: ItemListRevealOptionsItemNode {
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
|
||||
}
|
||||
|
||||
strongSelf.avatarNode.image = item.image
|
||||
|
||||
strongSelf.topSeparatorNode.isHidden = true
|
||||
|
||||
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0, y: floor((nodeLayout.contentSize.height - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)))
|
||||
if let image = item.image {
|
||||
strongSelf.avatarNode.image = item.image
|
||||
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0 + floor((avatarDiameter - image.size.width) / 2.0), y: floor((nodeLayout.contentSize.height - image.size.width) / 2.0)), size: image.size))
|
||||
}
|
||||
|
||||
let _ = titleApply()
|
||||
transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame.offsetBy(dx: revealOffset, dy: 0.0))
|
||||
|
||||
@@ -2126,15 +2126,20 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
if limitsConfiguration.maxMessageRevokeIntervalInPrivateChats == LimitsConfiguration.timeIntervalForever {
|
||||
canRemoveGlobally = true
|
||||
}
|
||||
} else if peer.peerId.namespace == Namespaces.Peer.SecretChat {
|
||||
canRemoveGlobally = true
|
||||
}
|
||||
|
||||
if let user = chatPeer as? TelegramUser, user.botInfo == nil, canRemoveGlobally {
|
||||
strongSelf.maybeAskForPeerChatRemoval(peer: peer, joined: joined, completion: { _ in }, removed: {})
|
||||
} else if let _ = chatPeer as? TelegramSecretChat, canRemoveGlobally {
|
||||
strongSelf.maybeAskForPeerChatRemoval(peer: peer, joined: joined, completion: { _ in }, removed: {})
|
||||
} else {
|
||||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||||
var items: [ActionSheetItem] = []
|
||||
var canClear = true
|
||||
var canStop = false
|
||||
var canRemoveGlobally = false
|
||||
|
||||
var deleteTitle = strongSelf.presentationData.strings.Common_Delete
|
||||
if let channel = chatPeer as? TelegramChannel {
|
||||
@@ -2142,11 +2147,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
canClear = false
|
||||
deleteTitle = strongSelf.presentationData.strings.Channel_LeaveChannel
|
||||
} else {
|
||||
deleteTitle = strongSelf.presentationData.strings.Group_LeaveGroup
|
||||
deleteTitle = strongSelf.presentationData.strings.Group_DeleteGroup
|
||||
}
|
||||
if let addressName = channel.addressName, !addressName.isEmpty {
|
||||
canClear = false
|
||||
}
|
||||
if channel.flags.contains(.isCreator) {
|
||||
canRemoveGlobally = true
|
||||
}
|
||||
} else if let group = chatPeer as? TelegramGroup {
|
||||
if case .creator = group.role {
|
||||
canRemoveGlobally = true
|
||||
}
|
||||
} else if let user = chatPeer as? TelegramUser, user.botInfo != nil {
|
||||
canStop = !user.flags.contains(.isSupport)
|
||||
canClear = user.botInfo == nil
|
||||
@@ -2155,12 +2167,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
deleteTitle = strongSelf.presentationData.strings.ChatList_DeleteChat
|
||||
}
|
||||
|
||||
var canRemoveGlobally = false
|
||||
let limitsConfiguration = strongSelf.context.currentLimitsConfiguration.with { $0 }
|
||||
if chatPeer is TelegramUser && chatPeer.id != strongSelf.context.account.peerId {
|
||||
if limitsConfiguration.maxMessageRevokeIntervalInPrivateChats == LimitsConfiguration.timeIntervalForever {
|
||||
canRemoveGlobally = true
|
||||
}
|
||||
} else if chatPeer is TelegramSecretChat {
|
||||
canRemoveGlobally = true
|
||||
}
|
||||
|
||||
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .delete, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
|
||||
@@ -2265,7 +2278,43 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.maybeAskForPeerChatRemoval(peer: peer, completion: { _ in }, removed: {})
|
||||
if canRemoveGlobally, (mainPeer is TelegramGroup || mainPeer is TelegramChannel) {
|
||||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||||
var items: [ActionSheetItem] = []
|
||||
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
self?.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: false, completion: {
|
||||
})
|
||||
}))
|
||||
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForAllMembers, color: .destructive, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteForAllMembersConfirmationText, actions: [
|
||||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||||
}),
|
||||
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: {
|
||||
self?.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: true, completion: {
|
||||
})
|
||||
})
|
||||
], parseMarkdown: true), in: .window(.root))
|
||||
}))
|
||||
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: items),
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])
|
||||
])
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
} else {
|
||||
strongSelf.maybeAskForPeerChatRemoval(peer: peer, completion: { _ in }, removed: {})
|
||||
}
|
||||
}))
|
||||
|
||||
if canStop {
|
||||
@@ -2311,6 +2360,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
if let user = chatPeer as? TelegramUser, user.botInfo != nil {
|
||||
canRemoveGlobally = false
|
||||
}
|
||||
if let _ = chatPeer as? TelegramSecretChat {
|
||||
canRemoveGlobally = true
|
||||
}
|
||||
|
||||
if canRemoveGlobally {
|
||||
let actionSheet = ActionSheetController(presentationData: self.presentationData)
|
||||
|
||||
@@ -895,7 +895,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
}).start()
|
||||
|
||||
let peerSelectionController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.onlyWriteable, .excludeDisabled]))
|
||||
peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peerId in
|
||||
peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peer in
|
||||
let peerId = peer.id
|
||||
if let strongSelf = self, let _ = peerSelectionController {
|
||||
if peerId == strongSelf.context.account.peerId {
|
||||
let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messageIds.map { id -> EnqueueMessage in
|
||||
|
||||
@@ -429,7 +429,13 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
case .collapse:
|
||||
actionTitle = strings.ChatList_Search_ShowLess
|
||||
}
|
||||
header = ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings, actionTitle: actionTitle, action: actionTitle == nil ? nil : {
|
||||
let headerType: ChatListSearchItemHeaderType
|
||||
if filter.contains(.onlyGroups) {
|
||||
headerType = .chats
|
||||
} else {
|
||||
headerType = .localPeers
|
||||
}
|
||||
header = ChatListSearchItemHeader(type: headerType, theme: theme, strings: strings, actionTitle: actionTitle, action: actionTitle == nil ? nil : {
|
||||
toggleExpandLocalResults()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -165,12 +165,13 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
switch entry.entry {
|
||||
case .HeaderEntry:
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint)
|
||||
case let .AdditionalCategory(_, id, title, image, selected, presentationData):
|
||||
case let .AdditionalCategory(_, id, title, image, appearance, selected, presentationData):
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem(
|
||||
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings),
|
||||
context: context,
|
||||
title: title,
|
||||
image: image,
|
||||
appearance: appearance,
|
||||
isSelected: selected,
|
||||
action: {
|
||||
nodeInteraction.additionalCategorySelected(id)
|
||||
@@ -249,7 +250,14 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
switch mode {
|
||||
case let .peers(_, _, additionalCategories, _):
|
||||
if !additionalCategories.isEmpty {
|
||||
header = ChatListSearchItemHeader(type: .chats, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
let headerType: ChatListSearchItemHeaderType
|
||||
if case .action = additionalCategories[0].appearance {
|
||||
// TODO: hack, generalize
|
||||
headerType = .orImportIntoAnExistingGroup
|
||||
} else {
|
||||
headerType = .chats
|
||||
}
|
||||
header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
}
|
||||
default:
|
||||
break
|
||||
@@ -319,7 +327,14 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
switch mode {
|
||||
case let .peers(_, _, additionalCategories, _):
|
||||
if !additionalCategories.isEmpty {
|
||||
header = ChatListSearchItemHeader(type: .chats, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
let headerType: ChatListSearchItemHeaderType
|
||||
if case .action = additionalCategories[0].appearance {
|
||||
// TODO: hack, generalize
|
||||
headerType = .orImportIntoAnExistingGroup
|
||||
} else {
|
||||
headerType = .chats
|
||||
}
|
||||
header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
}
|
||||
default:
|
||||
break
|
||||
@@ -355,12 +370,13 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint)
|
||||
case .HeaderEntry:
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint)
|
||||
case let .AdditionalCategory(index: _, id, title, image, selected, presentationData):
|
||||
case let .AdditionalCategory(index: _, id, title, image, appearance, selected, presentationData):
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem(
|
||||
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings),
|
||||
context: context,
|
||||
title: title,
|
||||
image: image,
|
||||
appearance: appearance,
|
||||
isSelected: selected,
|
||||
action: {
|
||||
nodeInteraction.additionalCategorySelected(id)
|
||||
|
||||
@@ -5,6 +5,7 @@ import TelegramCore
|
||||
import SyncCore
|
||||
import TelegramPresentationData
|
||||
import MergeLists
|
||||
import AccountContext
|
||||
|
||||
enum ChatListNodeEntryId: Hashable {
|
||||
case Header
|
||||
@@ -50,7 +51,7 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
||||
case HoleEntry(ChatListHole, theme: PresentationTheme)
|
||||
case GroupReferenceEntry(index: ChatListIndex, presentationData: ChatListPresentationData, groupId: PeerGroupId, peers: [ChatListGroupReferencePeer], message: Message?, editing: Bool, unreadState: PeerGroupUnreadCountersCombinedSummary, revealed: Bool, hiddenByDefault: Bool)
|
||||
case ArchiveIntro(presentationData: ChatListPresentationData)
|
||||
case AdditionalCategory(index: Int, id: Int, title: String, image: UIImage?, selected: Bool, presentationData: ChatListPresentationData)
|
||||
case AdditionalCategory(index: Int, id: Int, title: String, image: UIImage?, appearance: ChatListNodeAdditionalCategory.Appearance, selected: Bool, presentationData: ChatListPresentationData)
|
||||
|
||||
var sortIndex: ChatListNodeEntrySortIndex {
|
||||
switch self {
|
||||
@@ -242,8 +243,8 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .AdditionalCategory(lhsIndex, lhsId, lhsTitle, lhsImage, lhsSelected, lhsPresentationData):
|
||||
if case let .AdditionalCategory(rhsIndex, rhsId, rhsTitle, rhsImage, rhsSelected, rhsPresentationData) = rhs {
|
||||
case let .AdditionalCategory(lhsIndex, lhsId, lhsTitle, lhsImage, lhsAppearance, lhsSelected, lhsPresentationData):
|
||||
if case let .AdditionalCategory(rhsIndex, rhsId, rhsTitle, rhsImage, rhsAppearance, rhsSelected, rhsPresentationData) = rhs {
|
||||
if lhsIndex != rhsIndex {
|
||||
return false
|
||||
}
|
||||
@@ -256,6 +257,9 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
||||
if lhsImage !== rhsImage {
|
||||
return false
|
||||
}
|
||||
if lhsAppearance != rhsAppearance {
|
||||
return false
|
||||
}
|
||||
if lhsSelected != rhsSelected {
|
||||
return false
|
||||
}
|
||||
@@ -374,7 +378,7 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState,
|
||||
_) = mode {
|
||||
var index = 0
|
||||
for category in additionalCategories.reversed(){
|
||||
result.append(.AdditionalCategory(index: index, id: category.id, title: category.title, image: category.icon, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData))
|
||||
result.append(.AdditionalCategory(index: index, id: category.id, title: category.title, image: category.icon, appearance: category.appearance, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ public struct Font {
|
||||
case bold
|
||||
}
|
||||
|
||||
public static func with(size: CGFloat, design: Design = .regular, traits: Traits = []) -> UIFont {
|
||||
public static func with(size: CGFloat, design: Design = .regular, weight: Weight = .regular, traits: Traits = []) -> UIFont {
|
||||
if #available(iOS 13.0, *) {
|
||||
let descriptor = UIFont.systemFont(ofSize: size).fontDescriptor
|
||||
var symbolicTraits = descriptor.symbolicTraits
|
||||
@@ -63,6 +63,15 @@ public struct Font {
|
||||
default:
|
||||
updatedDescriptor = updatedDescriptor?.withDesign(.default)
|
||||
}
|
||||
switch weight {
|
||||
case .semibold:
|
||||
let fontTraits = [UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold]
|
||||
updatedDescriptor = updatedDescriptor?.addingAttributes([
|
||||
UIFontDescriptor.AttributeName.traits: fontTraits
|
||||
])
|
||||
default:
|
||||
break
|
||||
}
|
||||
if let updatedDescriptor = updatedDescriptor {
|
||||
return UIFont(descriptor: updatedDescriptor, size: size)
|
||||
} else {
|
||||
|
||||
@@ -139,7 +139,7 @@ public enum TabBarItemContextActionType {
|
||||
}
|
||||
|
||||
open var navigationPresentation: ViewControllerNavigationPresentation = .default
|
||||
var _presentedInModal: Bool = false
|
||||
open var _presentedInModal: Bool = false
|
||||
|
||||
public var presentedOverCoveringView: Bool = false
|
||||
|
||||
|
||||
@@ -3,9 +3,6 @@ import UIKit
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
public func qewfqewfq() {
|
||||
}
|
||||
|
||||
private struct WindowLayout: Equatable {
|
||||
let size: CGSize
|
||||
let metrics: LayoutMetrics
|
||||
@@ -294,7 +291,7 @@ public class Window1 {
|
||||
self.systemUserInterfaceStyle = hostView.systemUserInterfaceStyle
|
||||
|
||||
let boundsSize = self.hostView.eventView.bounds.size
|
||||
self.deviceMetrics = DeviceMetrics(screenSize: UIScreen.main.bounds.size, scale: UIScreen.main.scale, statusBarHeight: statusBarHost?.statusBarFrame.height ?? defaultStatusBarHeight, onScreenNavigationHeight: self.hostView.onScreenNavigationHeight)
|
||||
self.deviceMetrics = DeviceMetrics(screenSize: UIScreen.main.bounds.size, scale: UIScreen.main.scale, statusBarHeight: statusBarHost?.statusBarFrame.height ?? 0.0, onScreenNavigationHeight: self.hostView.onScreenNavigationHeight)
|
||||
|
||||
self.statusBarHost = statusBarHost
|
||||
let statusBarHeight: CGFloat
|
||||
@@ -303,7 +300,7 @@ public class Window1 {
|
||||
self.keyboardManager = KeyboardManager(host: statusBarHost)
|
||||
self.keyboardViewManager = KeyboardViewManager(host: statusBarHost)
|
||||
} else {
|
||||
statusBarHeight = self.deviceMetrics.statusBarHeight
|
||||
statusBarHeight = 0.0
|
||||
self.keyboardManager = nil
|
||||
self.keyboardViewManager = nil
|
||||
}
|
||||
@@ -406,7 +403,7 @@ public class Window1 {
|
||||
self.overlayPresentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout, deviceMetrics: self.deviceMetrics), transition: .immediate)
|
||||
|
||||
self.statusBarChangeObserver = NotificationCenter.default.addObserver(forName: UIApplication.willChangeStatusBarFrameNotification, object: nil, queue: OperationQueue.main, using: { [weak self] notification in
|
||||
if let strongSelf = self {
|
||||
if let strongSelf = self, strongSelf.statusBarHost != nil {
|
||||
let statusBarHeight: CGFloat = max(defaultStatusBarHeight, (notification.userInfo?[UIApplication.statusBarFrameUserInfoKey] as? NSValue)?.cgRectValue.height ?? defaultStatusBarHeight)
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .easeInOut)
|
||||
@@ -981,10 +978,12 @@ public class Window1 {
|
||||
var statusBarHeight: CGFloat? = self.deviceMetrics.statusBarHeight(for: boundsSize)
|
||||
if let statusBarHeightValue = statusBarHeight, let statusBarHost = self.statusBarHost {
|
||||
statusBarHeight = max(statusBarHeightValue, statusBarHost.statusBarFrame.size.height)
|
||||
} else {
|
||||
statusBarHeight = nil
|
||||
}
|
||||
|
||||
if self.deviceMetrics.type == .tablet, let onScreenNavigationHeight = self.hostView.onScreenNavigationHeight, onScreenNavigationHeight != self.deviceMetrics.onScreenNavigationHeight(inLandscape: false, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight) {
|
||||
self.deviceMetrics = DeviceMetrics(screenSize: UIScreen.main.bounds.size, scale: UIScreen.main.scale, statusBarHeight: statusBarHeight ?? defaultStatusBarHeight, onScreenNavigationHeight: onScreenNavigationHeight)
|
||||
self.deviceMetrics = DeviceMetrics(screenSize: UIScreen.main.bounds.size, scale: UIScreen.main.scale, statusBarHeight: statusBarHeight ?? 0.0, onScreenNavigationHeight: onScreenNavigationHeight)
|
||||
}
|
||||
|
||||
let statusBarWasHidden = self.statusBarHidden
|
||||
|
||||
@@ -258,7 +258,7 @@ public final class LiveLocationManagerImpl: LiveLocationManager {
|
||||
transaction.updateMessage(id, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
var updatedMedia = currentMessage.media
|
||||
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
|
||||
@@ -7,14 +7,16 @@ struct IntermediateMessageForwardInfo {
|
||||
let date: Int32
|
||||
let authorSignature: String?
|
||||
let psaType: String?
|
||||
let flags: MessageForwardInfo.Flags
|
||||
|
||||
init(authorId: PeerId?, sourceId: PeerId?, sourceMessageId: MessageId?, date: Int32, authorSignature: String?, psaType: String?) {
|
||||
init(authorId: PeerId?, sourceId: PeerId?, sourceMessageId: MessageId?, date: Int32, authorSignature: String?, psaType: String?, flags: MessageForwardInfo.Flags) {
|
||||
self.authorId = authorId
|
||||
self.sourceId = sourceId
|
||||
self.sourceMessageId = sourceMessageId
|
||||
self.date = date
|
||||
self.authorSignature = authorSignature
|
||||
self.psaType = psaType
|
||||
self.flags = flags
|
||||
}
|
||||
|
||||
init(_ storeInfo: StoreMessageForwardInfo) {
|
||||
@@ -24,6 +26,7 @@ struct IntermediateMessageForwardInfo {
|
||||
self.date = storeInfo.date
|
||||
self.authorSignature = storeInfo.authorSignature
|
||||
self.psaType = storeInfo.psaType
|
||||
self.flags = storeInfo.flags
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -413,36 +413,50 @@ public struct StoreMessageForwardInfo {
|
||||
public let date: Int32
|
||||
public let authorSignature: String?
|
||||
public let psaType: String?
|
||||
public let flags: MessageForwardInfo.Flags
|
||||
|
||||
public init(authorId: PeerId?, sourceId: PeerId?, sourceMessageId: MessageId?, date: Int32, authorSignature: String?, psaType: String?) {
|
||||
public init(authorId: PeerId?, sourceId: PeerId?, sourceMessageId: MessageId?, date: Int32, authorSignature: String?, psaType: String?, flags: MessageForwardInfo.Flags) {
|
||||
self.authorId = authorId
|
||||
self.sourceId = sourceId
|
||||
self.sourceMessageId = sourceMessageId
|
||||
self.date = date
|
||||
self.authorSignature = authorSignature
|
||||
self.psaType = psaType
|
||||
self.flags = flags
|
||||
}
|
||||
|
||||
public init(_ info: MessageForwardInfo) {
|
||||
self.init(authorId: info.author?.id, sourceId: info.source?.id, sourceMessageId: info.sourceMessageId, date: info.date, authorSignature: info.authorSignature, psaType: info.psaType)
|
||||
self.init(authorId: info.author?.id, sourceId: info.source?.id, sourceMessageId: info.sourceMessageId, date: info.date, authorSignature: info.authorSignature, psaType: info.psaType, flags: info.flags)
|
||||
}
|
||||
}
|
||||
|
||||
public struct MessageForwardInfo: Equatable {
|
||||
public struct Flags: OptionSet {
|
||||
public var rawValue: Int32
|
||||
|
||||
public init(rawValue: Int32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public static let isImported = Flags(rawValue: 1 << 0)
|
||||
}
|
||||
|
||||
public let author: Peer?
|
||||
public let source: Peer?
|
||||
public let sourceMessageId: MessageId?
|
||||
public let date: Int32
|
||||
public let authorSignature: String?
|
||||
public let psaType: String?
|
||||
public let flags: MessageForwardInfo.Flags
|
||||
|
||||
public init(author: Peer?, source: Peer?, sourceMessageId: MessageId?, date: Int32, authorSignature: String?, psaType: String?) {
|
||||
public init(author: Peer?, source: Peer?, sourceMessageId: MessageId?, date: Int32, authorSignature: String?, psaType: String?, flags: MessageForwardInfo.Flags) {
|
||||
self.author = author
|
||||
self.source = source
|
||||
self.sourceMessageId = sourceMessageId
|
||||
self.date = date
|
||||
self.authorSignature = authorSignature
|
||||
self.psaType = psaType
|
||||
self.flags = flags
|
||||
}
|
||||
|
||||
public static func ==(lhs: MessageForwardInfo, rhs: MessageForwardInfo) -> Bool {
|
||||
@@ -468,6 +482,9 @@ public struct MessageForwardInfo: Equatable {
|
||||
if lhs.psaType != rhs.psaType {
|
||||
return false
|
||||
}
|
||||
if lhs.flags != rhs.flags {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1061,6 +1061,9 @@ final class MessageHistoryTable: Table {
|
||||
if forwardInfo.psaType != nil {
|
||||
forwardInfoFlags |= 1 << 4
|
||||
}
|
||||
if !forwardInfo.flags.isEmpty {
|
||||
forwardInfoFlags |= 1 << 5
|
||||
}
|
||||
sharedBuffer.write(&forwardInfoFlags, offset: 0, length: 1)
|
||||
var forwardAuthorId: Int64 = forwardInfo.authorId?.toInt64() ?? 0
|
||||
var forwardDate: Int32 = forwardInfo.date
|
||||
@@ -1102,6 +1105,11 @@ final class MessageHistoryTable: Table {
|
||||
sharedBuffer.write(&length, offset: 0, length: 4)
|
||||
}
|
||||
}
|
||||
|
||||
if !forwardInfo.flags.isEmpty {
|
||||
var value: Int32 = forwardInfo.flags.rawValue
|
||||
sharedBuffer.write(&value, offset: 0, length: 4)
|
||||
}
|
||||
} else {
|
||||
var forwardInfoFlags: Int8 = 0
|
||||
sharedBuffer.write(&forwardInfoFlags, offset: 0, length: 1)
|
||||
@@ -1630,6 +1638,9 @@ final class MessageHistoryTable: Table {
|
||||
if forwardInfo.psaType != nil {
|
||||
forwardInfoFlags |= 1 << 4
|
||||
}
|
||||
if !forwardInfo.flags.isEmpty {
|
||||
forwardInfoFlags |= 1 << 5
|
||||
}
|
||||
sharedBuffer.write(&forwardInfoFlags, offset: 0, length: 1)
|
||||
var forwardAuthorId: Int64 = forwardInfo.authorId?.toInt64() ?? 0
|
||||
var forwardDate: Int32 = forwardInfo.date
|
||||
@@ -1671,6 +1682,11 @@ final class MessageHistoryTable: Table {
|
||||
sharedBuffer.write(&length, offset: 0, length: 4)
|
||||
}
|
||||
}
|
||||
|
||||
if !forwardInfo.flags.isEmpty {
|
||||
var value: Int32 = forwardInfo.flags.rawValue
|
||||
sharedBuffer.write(&value, offset: 0, length: 4)
|
||||
}
|
||||
} else {
|
||||
var forwardInfoFlags: Int8 = 0
|
||||
sharedBuffer.write(&forwardInfoFlags, offset: 0, length: 1)
|
||||
@@ -1784,7 +1800,7 @@ final class MessageHistoryTable: Table {
|
||||
if let previousMessage = self.getMessage(index) {
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = previousMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.authorId, sourceId: forwardInfo.sourceId, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.authorId, sourceId: forwardInfo.sourceId, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
|
||||
var parsedAttributes: [MessageAttribute] = []
|
||||
@@ -2187,6 +2203,7 @@ final class MessageHistoryTable: Table {
|
||||
var forwardSourceMessageId: MessageId?
|
||||
var authorSignature: String? = nil
|
||||
var psaType: String? = nil
|
||||
var flags: MessageForwardInfo.Flags = []
|
||||
|
||||
value.read(&forwardAuthorId, offset: 0, length: 8)
|
||||
value.read(&forwardDate, offset: 0, length: 4)
|
||||
@@ -2221,7 +2238,13 @@ final class MessageHistoryTable: Table {
|
||||
value.skip(Int(psaTypeLength))
|
||||
}
|
||||
|
||||
forwardInfo = IntermediateMessageForwardInfo(authorId: forwardAuthorId == 0 ? nil : PeerId(forwardAuthorId), sourceId: forwardSourceId, sourceMessageId: forwardSourceMessageId, date: forwardDate, authorSignature: authorSignature, psaType: psaType)
|
||||
if (forwardInfoFlags & (1 << 5)) != 0 {
|
||||
var rawValue: Int32 = 0
|
||||
value.read(&rawValue, offset: 0, length: 4)
|
||||
flags = MessageForwardInfo.Flags(rawValue: rawValue)
|
||||
}
|
||||
|
||||
forwardInfo = IntermediateMessageForwardInfo(authorId: forwardAuthorId == 0 ? nil : PeerId(forwardAuthorId), sourceId: forwardSourceId, sourceMessageId: forwardSourceMessageId, date: forwardDate, authorSignature: authorSignature, psaType: psaType, flags: flags)
|
||||
}
|
||||
|
||||
var hasAuthor: Int8 = 0
|
||||
@@ -2377,7 +2400,7 @@ final class MessageHistoryTable: Table {
|
||||
if let sourceId = internalForwardInfo.sourceId {
|
||||
source = peerTable.get(sourceId)
|
||||
}
|
||||
forwardInfo = MessageForwardInfo(author: forwardAuthor, source: source, sourceMessageId: internalForwardInfo.sourceMessageId, date: internalForwardInfo.date, authorSignature: internalForwardInfo.authorSignature, psaType: internalForwardInfo.psaType)
|
||||
forwardInfo = MessageForwardInfo(author: forwardAuthor, source: source, sourceMessageId: internalForwardInfo.sourceMessageId, date: internalForwardInfo.date, authorSignature: internalForwardInfo.authorSignature, psaType: internalForwardInfo.psaType, flags: internalForwardInfo.flags)
|
||||
}
|
||||
|
||||
var author: Peer?
|
||||
|
||||
@@ -1549,7 +1549,7 @@ public final class Postbox {
|
||||
flags.insert(.Failed)
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = message.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
return .update(StoreMessage(id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, threadId: message.threadId, timestamp: message.timestamp, flags: flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: storeForwardInfo, authorId: message.author?.id, text: message.text, attributes: message.attributes, media: message.media))
|
||||
} else {
|
||||
|
||||
@@ -199,7 +199,9 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
actionSheet?.dismissAnimated()
|
||||
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled]))
|
||||
controller.peerSelected = { [weak controller] peerId in
|
||||
controller.peerSelected = { [weak controller] peer in
|
||||
let peerId = peer.id
|
||||
|
||||
if let strongController = controller {
|
||||
strongController.dismiss()
|
||||
|
||||
@@ -267,7 +269,9 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
actionSheet?.dismissAnimated()
|
||||
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled]))
|
||||
controller.peerSelected = { [weak controller] peerId in
|
||||
controller.peerSelected = { [weak controller] peer in
|
||||
let peerId = peer.id
|
||||
|
||||
if let strongController = controller {
|
||||
strongController.dismiss()
|
||||
|
||||
@@ -347,7 +351,9 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
actionSheet?.dismissAnimated()
|
||||
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled]))
|
||||
controller.peerSelected = { [weak controller] peerId in
|
||||
controller.peerSelected = { [weak controller] peer in
|
||||
let peerId = peer.id
|
||||
|
||||
if let strongController = controller {
|
||||
strongController.dismiss()
|
||||
|
||||
@@ -409,7 +415,9 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return
|
||||
}
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled]))
|
||||
controller.peerSelected = { [weak controller] peerId in
|
||||
controller.peerSelected = { [weak controller] peer in
|
||||
let peerId = peer.id
|
||||
|
||||
if let strongController = controller {
|
||||
strongController.dismiss()
|
||||
|
||||
@@ -438,7 +446,9 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
actionSheet?.dismissAnimated()
|
||||
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled]))
|
||||
controller.peerSelected = { [weak controller] peerId in
|
||||
controller.peerSelected = { [weak controller] peer in
|
||||
let peerId = peer.id
|
||||
|
||||
if let strongController = controller {
|
||||
strongController.dismiss()
|
||||
|
||||
|
||||
+3
-1
@@ -891,7 +891,9 @@ final class NotificationExceptionsControllerNode: ViewControllerTracingNode {
|
||||
filter.insert(.onlyChannels)
|
||||
}
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: filter, hasContactSelector: false, title: presentationData.strings.Notifications_AddExceptionTitle))
|
||||
controller.peerSelected = { [weak controller] peerId in
|
||||
controller.peerSelected = { [weak controller] peer in
|
||||
let peerId = peer.id
|
||||
|
||||
presentPeerSettings(peerId, {
|
||||
controller?.dismiss()
|
||||
})
|
||||
|
||||
@@ -230,7 +230,9 @@ public func blockedPeersController(context: AccountContext, blockedPeersContext:
|
||||
}, addPeer: {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyPrivateChats, .excludeSavedMessages, .removeSearchHeader, .excludeRecent, .doNotSearchMessages], title: presentationData.strings.BlockedUsers_SelectUserTitle))
|
||||
controller.peerSelected = { [weak controller] peerId in
|
||||
controller.peerSelected = { [weak controller] peer in
|
||||
let peerId = peer.id
|
||||
|
||||
guard let strongController = controller else {
|
||||
return
|
||||
}
|
||||
|
||||
+1
-1
@@ -157,7 +157,7 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode {
|
||||
|
||||
peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: item.peerName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [])
|
||||
|
||||
let forwardInfo = MessageForwardInfo(author: item.linkEnabled ? peers[peerId] : nil, source: nil, sourceMessageId: nil, date: 0, authorSignature: item.linkEnabled ? nil : item.peerName, psaType: nil)
|
||||
let forwardInfo = MessageForwardInfo(author: item.linkEnabled ? peers[peerId] : nil, source: nil, sourceMessageId: nil, date: 0, authorSignature: item.linkEnabled ? nil : item.peerName, psaType: nil, flags: [])
|
||||
|
||||
let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: forwardInfo, author: nil, text: item.strings.Privacy_Forwards_PreviewMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil)
|
||||
|
||||
|
||||
@@ -295,6 +295,7 @@ public final class ShareController: ViewController {
|
||||
private let immediatePeerId: PeerId?
|
||||
private let openStats: (() -> Void)?
|
||||
private let shares: Int?
|
||||
private let fromForeignApp: Bool
|
||||
|
||||
private let peers = Promise<([(RenderedPeer, PeerPresence?)], Peer)>()
|
||||
private let peersDisposable = MetaDisposable()
|
||||
@@ -305,11 +306,11 @@ public final class ShareController: ViewController {
|
||||
|
||||
public var dismissed: ((Bool) -> Void)?
|
||||
|
||||
public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, openStats: (() -> Void)? = nil, shares: Int? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, forcedTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil) {
|
||||
self.init(sharedContext: context.sharedContext, currentContext: context, subject: subject, presetText: presetText, preferredAction: preferredAction, showInChat: showInChat, openStats: openStats, shares: shares, externalShare: externalShare, immediateExternalShare: immediateExternalShare, switchableAccounts: switchableAccounts, immediatePeerId: immediatePeerId, forcedTheme: forcedTheme, forcedActionTitle: forcedActionTitle)
|
||||
public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, openStats: (() -> Void)? = nil, fromForeignApp: Bool = false, shares: Int? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, forcedTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil) {
|
||||
self.init(sharedContext: context.sharedContext, currentContext: context, subject: subject, presetText: presetText, preferredAction: preferredAction, showInChat: showInChat, openStats: openStats, fromForeignApp: fromForeignApp, shares: shares, externalShare: externalShare, immediateExternalShare: immediateExternalShare, switchableAccounts: switchableAccounts, immediatePeerId: immediatePeerId, forcedTheme: forcedTheme, forcedActionTitle: forcedActionTitle)
|
||||
}
|
||||
|
||||
public init(sharedContext: SharedAccountContext, currentContext: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, openStats: (() -> Void)? = nil, shares: Int? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, forcedTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil) {
|
||||
public init(sharedContext: SharedAccountContext, currentContext: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, openStats: (() -> Void)? = nil, fromForeignApp: Bool = false, shares: Int? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, forcedTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil) {
|
||||
self.sharedContext = sharedContext
|
||||
self.currentContext = currentContext
|
||||
self.currentAccount = currentContext.account
|
||||
@@ -320,6 +321,7 @@ public final class ShareController: ViewController {
|
||||
self.switchableAccounts = switchableAccounts
|
||||
self.immediatePeerId = immediatePeerId
|
||||
self.openStats = openStats
|
||||
self.fromForeignApp = fromForeignApp
|
||||
self.shares = shares
|
||||
self.forcedTheme = forcedTheme
|
||||
|
||||
@@ -448,7 +450,7 @@ public final class ShareController: ViewController {
|
||||
return
|
||||
}
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, shares: self.shares, forcedTheme: self.forcedTheme)
|
||||
}, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, shares: self.shares, fromForeignApp: self.fromForeignApp, forcedTheme: self.forcedTheme)
|
||||
self.controllerNode.dismiss = { [weak self] shared in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
self?.dismissed?(shared)
|
||||
|
||||
@@ -34,6 +34,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
private let immediateExternalShare: Bool
|
||||
private var immediatePeerId: PeerId?
|
||||
private let shares: Int?
|
||||
private let fromForeignApp: Bool
|
||||
|
||||
private let defaultAction: ShareControllerAction?
|
||||
private let requestLayout: (ContainedViewLayoutTransition) -> Void
|
||||
@@ -81,7 +82,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
|
||||
private let presetText: String?
|
||||
|
||||
init(sharedContext: SharedAccountContext, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, shares: Int?, forcedTheme: PresentationTheme?) {
|
||||
init(sharedContext: SharedAccountContext, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, shares: Int?, fromForeignApp: Bool, forcedTheme: PresentationTheme?) {
|
||||
self.sharedContext = sharedContext
|
||||
self.presentationData = sharedContext.currentPresentationData.with { $0 }
|
||||
self.forcedTheme = forcedTheme
|
||||
@@ -89,6 +90,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
self.immediateExternalShare = immediateExternalShare
|
||||
self.immediatePeerId = immediatePeerId
|
||||
self.shares = shares
|
||||
self.fromForeignApp = fromForeignApp
|
||||
self.presentError = presentError
|
||||
|
||||
self.presetText = presetText
|
||||
@@ -124,7 +126,11 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
self.wrappingScrollNode.view.canCancelContentTouches = true
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
if self.fromForeignApp {
|
||||
self.dimNode.backgroundColor = .clear
|
||||
} else {
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
}
|
||||
|
||||
self.cancelButtonNode = ASButtonNode()
|
||||
self.cancelButtonNode.displaysAsynchronously = false
|
||||
|
||||
@@ -122,7 +122,7 @@ public enum SecretChatOutgoingOperationContents: PostboxCoding {
|
||||
case pfsCommitKey(layer: SecretChatSequenceBasedLayer, actionGloballyUniqueId: Int64, rekeySessionId: Int64, keyFingerprint: Int64)
|
||||
case noop(layer: SecretChatSequenceBasedLayer, actionGloballyUniqueId: Int64)
|
||||
case setMessageAutoremoveTimeout(layer: SecretChatLayer, actionGloballyUniqueId: Int64, timeout: Int32, messageId: MessageId)
|
||||
case terminate(reportSpam: Bool)
|
||||
case terminate(reportSpam: Bool, requestRemoteHistoryRemoval: Bool)
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
switch decoder.decodeInt32ForKey("r", orElse: 0) {
|
||||
@@ -155,7 +155,7 @@ public enum SecretChatOutgoingOperationContents: PostboxCoding {
|
||||
case SecretChatOutgoingOperationValue.setMessageAutoremoveTimeout.rawValue:
|
||||
self = .setMessageAutoremoveTimeout(layer: SecretChatLayer(rawValue: decoder.decodeInt32ForKey("l", orElse: 0))!, actionGloballyUniqueId: decoder.decodeInt64ForKey("i", orElse: 0), timeout: decoder.decodeInt32ForKey("t", orElse: 0), messageId: MessageId(peerId: PeerId(decoder.decodeInt64ForKey("m.p", orElse: 0)), namespace: decoder.decodeInt32ForKey("m.n", orElse: 0), id: decoder.decodeInt32ForKey("m.i", orElse: 0)))
|
||||
case SecretChatOutgoingOperationValue.terminate.rawValue:
|
||||
self = .terminate(reportSpam: decoder.decodeInt32ForKey("rs", orElse: 0) != 0)
|
||||
self = .terminate(reportSpam: decoder.decodeInt32ForKey("rs", orElse: 0) != 0, requestRemoteHistoryRemoval: decoder.decodeInt32ForKey("requestRemoteHistoryRemoval", orElse: 0) != 0)
|
||||
default:
|
||||
self = .noop(layer: SecretChatSequenceBasedLayer(rawValue: decoder.decodeInt32ForKey("l", orElse: 0))!, actionGloballyUniqueId: 0)
|
||||
assertionFailure()
|
||||
@@ -249,9 +249,10 @@ public enum SecretChatOutgoingOperationContents: PostboxCoding {
|
||||
encoder.encodeInt64(messageId.peerId.toInt64(), forKey: "m.p")
|
||||
encoder.encodeInt32(messageId.namespace, forKey: "m.n")
|
||||
encoder.encodeInt32(messageId.id, forKey: "m.i")
|
||||
case let .terminate(reportSpam):
|
||||
case let .terminate(reportSpam, requestRemoteHistoryRemoval):
|
||||
encoder.encodeInt32(SecretChatOutgoingOperationValue.terminate.rawValue, forKey: "r")
|
||||
encoder.encodeInt32(reportSpam ? 1 : 0, forKey: "rs")
|
||||
encoder.encodeInt32(requestRemoteHistoryRemoval ? 1 : 0, forKey: "requestRemoteHistoryRemoval")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-457104426] = { return Api.InputGeoPoint.parse_inputGeoPointEmpty($0) }
|
||||
dict[1210199983] = { return Api.InputGeoPoint.parse_inputGeoPoint($0) }
|
||||
dict[-784000893] = { return Api.payments.ValidatedRequestedInfo.parse_validatedRequestedInfo($0) }
|
||||
dict[-213431562] = { return Api.ChatFull.parse_chatFull($0) }
|
||||
dict[2055070967] = { return Api.ChatFull.parse_channelFull($0) }
|
||||
dict[-213431562] = { return Api.ChatFull.parse_chatFull($0) }
|
||||
dict[-1159937629] = { return Api.PollResults.parse_pollResults($0) }
|
||||
dict[-925415106] = { return Api.ChatParticipant.parse_chatParticipant($0) }
|
||||
dict[-636267638] = { return Api.ChatParticipant.parse_chatParticipantCreator($0) }
|
||||
@@ -175,6 +175,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-1997373508] = { return Api.SendMessageAction.parse_sendMessageRecordRoundAction($0) }
|
||||
dict[608050278] = { return Api.SendMessageAction.parse_sendMessageUploadRoundAction($0) }
|
||||
dict[-651419003] = { return Api.SendMessageAction.parse_speakingInGroupCallAction($0) }
|
||||
dict[-606432698] = { return Api.SendMessageAction.parse_sendMessageHistoryImportAction($0) }
|
||||
dict[-1137792208] = { return Api.PrivacyKey.parse_privacyKeyStatusTimestamp($0) }
|
||||
dict[1343122938] = { return Api.PrivacyKey.parse_privacyKeyChatInvite($0) }
|
||||
dict[1030105979] = { return Api.PrivacyKey.parse_privacyKeyPhoneCall($0) }
|
||||
@@ -574,6 +575,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-1991004873] = { return Api.InputChatPhoto.parse_inputChatPhoto($0) }
|
||||
dict[-968723890] = { return Api.InputChatPhoto.parse_inputChatUploadedPhoto($0) }
|
||||
dict[-1228606141] = { return Api.messages.MessageViews.parse_messageViews($0) }
|
||||
dict[375566091] = { return Api.messages.HistoryImport.parse_historyImport($0) }
|
||||
dict[-368917890] = { return Api.PaymentCharge.parse_paymentCharge($0) }
|
||||
dict[-1387279939] = { return Api.MessageInteractionCounters.parse_messageInteractionCounters($0) }
|
||||
dict[-1107852396] = { return Api.stats.BroadcastStats.parse_broadcastStats($0) }
|
||||
@@ -656,7 +658,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-1056001329] = { return Api.InputPaymentCredentials.parse_inputPaymentCredentialsSaved($0) }
|
||||
dict[873977640] = { return Api.InputPaymentCredentials.parse_inputPaymentCredentials($0) }
|
||||
dict[178373535] = { return Api.InputPaymentCredentials.parse_inputPaymentCredentialsApplePay($0) }
|
||||
dict[-905587442] = { return Api.InputPaymentCredentials.parse_inputPaymentCredentialsAndroidPay($0) }
|
||||
dict[-1966921727] = { return Api.InputPaymentCredentials.parse_inputPaymentCredentialsGooglePay($0) }
|
||||
dict[-1239335713] = { return Api.ShippingOption.parse_shippingOption($0) }
|
||||
dict[859091184] = { return Api.InputSecureFile.parse_inputSecureFileUploaded($0) }
|
||||
dict[1399317950] = { return Api.InputSecureFile.parse_inputSecureFile($0) }
|
||||
@@ -736,7 +738,6 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-1673717362] = { return Api.InputPeerNotifySettings.parse_inputPeerNotifySettings($0) }
|
||||
dict[-1634752813] = { return Api.messages.FavedStickers.parse_favedStickersNotModified($0) }
|
||||
dict[-209768682] = { return Api.messages.FavedStickers.parse_favedStickers($0) }
|
||||
dict[1776236393] = { return Api.ExportedChatInvite.parse_chatInviteEmpty($0) }
|
||||
dict[1847917725] = { return Api.ExportedChatInvite.parse_chatInviteExported($0) }
|
||||
dict[-1389486888] = { return Api.account.AuthorizationForm.parse_authorizationForm($0) }
|
||||
dict[-1392388579] = { return Api.Authorization.parse_authorization($0) }
|
||||
@@ -1324,6 +1325,8 @@ public struct Api {
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.messages.MessageViews:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.messages.HistoryImport:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.PaymentCharge:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.MessageInteractionCounters:
|
||||
|
||||
@@ -1507,6 +1507,40 @@ public struct messages {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
public enum HistoryImport: TypeConstructorDescription {
|
||||
case historyImport(id: Int64)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .historyImport(let id):
|
||||
if boxed {
|
||||
buffer.appendInt32(375566091)
|
||||
}
|
||||
serializeInt64(id, buffer: buffer, boxed: false)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .historyImport(let id):
|
||||
return ("historyImport", [("id", id)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_historyImport(_ reader: BufferReader) -> HistoryImport? {
|
||||
var _1: Int64?
|
||||
_1 = reader.readInt64()
|
||||
let _c1 = _1 != nil
|
||||
if _c1 {
|
||||
return Api.messages.HistoryImport.historyImport(id: _1!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
public enum PeerDialogs: TypeConstructorDescription {
|
||||
case peerDialogs(dialogs: [Api.Dialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User], state: Api.updates.State)
|
||||
@@ -2206,31 +2240,11 @@ public extension Api {
|
||||
|
||||
}
|
||||
public enum ChatFull: TypeConstructorDescription {
|
||||
case chatFull(flags: Int32, id: Int32, about: String, participants: Api.ChatParticipants, chatPhoto: Api.Photo?, notifySettings: Api.PeerNotifySettings, exportedInvite: Api.ExportedChatInvite?, botInfo: [Api.BotInfo]?, pinnedMsgId: Int32?, folderId: Int32?, call: Api.InputGroupCall?)
|
||||
case channelFull(flags: Int32, id: Int32, about: String, participantsCount: Int32?, adminsCount: Int32?, kickedCount: Int32?, bannedCount: Int32?, onlineCount: Int32?, readInboxMaxId: Int32, readOutboxMaxId: Int32, unreadCount: Int32, chatPhoto: Api.Photo, notifySettings: Api.PeerNotifySettings, exportedInvite: Api.ExportedChatInvite?, botInfo: [Api.BotInfo], migratedFromChatId: Int32?, migratedFromMaxId: Int32?, pinnedMsgId: Int32?, stickerset: Api.StickerSet?, availableMinId: Int32?, folderId: Int32?, linkedChatId: Int32?, location: Api.ChannelLocation?, slowmodeSeconds: Int32?, slowmodeNextSendDate: Int32?, statsDc: Int32?, pts: Int32, call: Api.InputGroupCall?)
|
||||
case chatFull(flags: Int32, id: Int32, about: String, participants: Api.ChatParticipants, chatPhoto: Api.Photo?, notifySettings: Api.PeerNotifySettings, exportedInvite: Api.ExportedChatInvite?, botInfo: [Api.BotInfo]?, pinnedMsgId: Int32?, folderId: Int32?, call: Api.InputGroupCall?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .chatFull(let flags, let id, let about, let participants, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let pinnedMsgId, let folderId, let call):
|
||||
if boxed {
|
||||
buffer.appendInt32(-213431562)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeInt32(id, buffer: buffer, boxed: false)
|
||||
serializeString(about, buffer: buffer, boxed: false)
|
||||
participants.serialize(buffer, true)
|
||||
if Int(flags) & Int(1 << 2) != 0 {chatPhoto!.serialize(buffer, true)}
|
||||
notifySettings.serialize(buffer, true)
|
||||
if Int(flags) & Int(1 << 13) != 0 {exportedInvite!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(botInfo!.count))
|
||||
for item in botInfo! {
|
||||
item.serialize(buffer, true)
|
||||
}}
|
||||
if Int(flags) & Int(1 << 6) != 0 {serializeInt32(pinnedMsgId!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 11) != 0 {serializeInt32(folderId!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 12) != 0 {call!.serialize(buffer, true)}
|
||||
break
|
||||
case .channelFull(let flags, let id, let about, let participantsCount, let adminsCount, let kickedCount, let bannedCount, let onlineCount, let readInboxMaxId, let readOutboxMaxId, let unreadCount, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let migratedFromChatId, let migratedFromMaxId, let pinnedMsgId, let stickerset, let availableMinId, let folderId, let linkedChatId, let location, let slowmodeSeconds, let slowmodeNextSendDate, let statsDc, let pts, let call):
|
||||
if boxed {
|
||||
buffer.appendInt32(2055070967)
|
||||
@@ -2268,71 +2282,38 @@ public extension Api {
|
||||
serializeInt32(pts, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 21) != 0 {call!.serialize(buffer, true)}
|
||||
break
|
||||
case .chatFull(let flags, let id, let about, let participants, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let pinnedMsgId, let folderId, let call):
|
||||
if boxed {
|
||||
buffer.appendInt32(-213431562)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeInt32(id, buffer: buffer, boxed: false)
|
||||
serializeString(about, buffer: buffer, boxed: false)
|
||||
participants.serialize(buffer, true)
|
||||
if Int(flags) & Int(1 << 2) != 0 {chatPhoto!.serialize(buffer, true)}
|
||||
notifySettings.serialize(buffer, true)
|
||||
if Int(flags) & Int(1 << 13) != 0 {exportedInvite!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(botInfo!.count))
|
||||
for item in botInfo! {
|
||||
item.serialize(buffer, true)
|
||||
}}
|
||||
if Int(flags) & Int(1 << 6) != 0 {serializeInt32(pinnedMsgId!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 11) != 0 {serializeInt32(folderId!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 12) != 0 {call!.serialize(buffer, true)}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .chatFull(let flags, let id, let about, let participants, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let pinnedMsgId, let folderId, let call):
|
||||
return ("chatFull", [("flags", flags), ("id", id), ("about", about), ("participants", participants), ("chatPhoto", chatPhoto), ("notifySettings", notifySettings), ("exportedInvite", exportedInvite), ("botInfo", botInfo), ("pinnedMsgId", pinnedMsgId), ("folderId", folderId), ("call", call)])
|
||||
case .channelFull(let flags, let id, let about, let participantsCount, let adminsCount, let kickedCount, let bannedCount, let onlineCount, let readInboxMaxId, let readOutboxMaxId, let unreadCount, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let migratedFromChatId, let migratedFromMaxId, let pinnedMsgId, let stickerset, let availableMinId, let folderId, let linkedChatId, let location, let slowmodeSeconds, let slowmodeNextSendDate, let statsDc, let pts, let call):
|
||||
return ("channelFull", [("flags", flags), ("id", id), ("about", about), ("participantsCount", participantsCount), ("adminsCount", adminsCount), ("kickedCount", kickedCount), ("bannedCount", bannedCount), ("onlineCount", onlineCount), ("readInboxMaxId", readInboxMaxId), ("readOutboxMaxId", readOutboxMaxId), ("unreadCount", unreadCount), ("chatPhoto", chatPhoto), ("notifySettings", notifySettings), ("exportedInvite", exportedInvite), ("botInfo", botInfo), ("migratedFromChatId", migratedFromChatId), ("migratedFromMaxId", migratedFromMaxId), ("pinnedMsgId", pinnedMsgId), ("stickerset", stickerset), ("availableMinId", availableMinId), ("folderId", folderId), ("linkedChatId", linkedChatId), ("location", location), ("slowmodeSeconds", slowmodeSeconds), ("slowmodeNextSendDate", slowmodeNextSendDate), ("statsDc", statsDc), ("pts", pts), ("call", call)])
|
||||
case .chatFull(let flags, let id, let about, let participants, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let pinnedMsgId, let folderId, let call):
|
||||
return ("chatFull", [("flags", flags), ("id", id), ("about", about), ("participants", participants), ("chatPhoto", chatPhoto), ("notifySettings", notifySettings), ("exportedInvite", exportedInvite), ("botInfo", botInfo), ("pinnedMsgId", pinnedMsgId), ("folderId", folderId), ("call", call)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_chatFull(_ reader: BufferReader) -> ChatFull? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Int32?
|
||||
_2 = reader.readInt32()
|
||||
var _3: String?
|
||||
_3 = parseString(reader)
|
||||
var _4: Api.ChatParticipants?
|
||||
if let signature = reader.readInt32() {
|
||||
_4 = Api.parse(reader, signature: signature) as? Api.ChatParticipants
|
||||
}
|
||||
var _5: Api.Photo?
|
||||
if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() {
|
||||
_5 = Api.parse(reader, signature: signature) as? Api.Photo
|
||||
} }
|
||||
var _6: Api.PeerNotifySettings?
|
||||
if let signature = reader.readInt32() {
|
||||
_6 = Api.parse(reader, signature: signature) as? Api.PeerNotifySettings
|
||||
}
|
||||
var _7: Api.ExportedChatInvite?
|
||||
if Int(_1!) & Int(1 << 13) != 0 {if let signature = reader.readInt32() {
|
||||
_7 = Api.parse(reader, signature: signature) as? Api.ExportedChatInvite
|
||||
} }
|
||||
var _8: [Api.BotInfo]?
|
||||
if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() {
|
||||
_8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotInfo.self)
|
||||
} }
|
||||
var _9: Int32?
|
||||
if Int(_1!) & Int(1 << 6) != 0 {_9 = reader.readInt32() }
|
||||
var _10: Int32?
|
||||
if Int(_1!) & Int(1 << 11) != 0 {_10 = reader.readInt32() }
|
||||
var _11: Api.InputGroupCall?
|
||||
if Int(_1!) & Int(1 << 12) != 0 {if let signature = reader.readInt32() {
|
||||
_11 = Api.parse(reader, signature: signature) as? Api.InputGroupCall
|
||||
} }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = _4 != nil
|
||||
let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil
|
||||
let _c6 = _6 != nil
|
||||
let _c7 = (Int(_1!) & Int(1 << 13) == 0) || _7 != nil
|
||||
let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil
|
||||
let _c9 = (Int(_1!) & Int(1 << 6) == 0) || _9 != nil
|
||||
let _c10 = (Int(_1!) & Int(1 << 11) == 0) || _10 != nil
|
||||
let _c11 = (Int(_1!) & Int(1 << 12) == 0) || _11 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 {
|
||||
return Api.ChatFull.chatFull(flags: _1!, id: _2!, about: _3!, participants: _4!, chatPhoto: _5, notifySettings: _6!, exportedInvite: _7, botInfo: _8, pinnedMsgId: _9, folderId: _10, call: _11)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_channelFull(_ reader: BufferReader) -> ChatFull? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
@@ -2439,6 +2420,59 @@ public extension Api {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_chatFull(_ reader: BufferReader) -> ChatFull? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Int32?
|
||||
_2 = reader.readInt32()
|
||||
var _3: String?
|
||||
_3 = parseString(reader)
|
||||
var _4: Api.ChatParticipants?
|
||||
if let signature = reader.readInt32() {
|
||||
_4 = Api.parse(reader, signature: signature) as? Api.ChatParticipants
|
||||
}
|
||||
var _5: Api.Photo?
|
||||
if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() {
|
||||
_5 = Api.parse(reader, signature: signature) as? Api.Photo
|
||||
} }
|
||||
var _6: Api.PeerNotifySettings?
|
||||
if let signature = reader.readInt32() {
|
||||
_6 = Api.parse(reader, signature: signature) as? Api.PeerNotifySettings
|
||||
}
|
||||
var _7: Api.ExportedChatInvite?
|
||||
if Int(_1!) & Int(1 << 13) != 0 {if let signature = reader.readInt32() {
|
||||
_7 = Api.parse(reader, signature: signature) as? Api.ExportedChatInvite
|
||||
} }
|
||||
var _8: [Api.BotInfo]?
|
||||
if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() {
|
||||
_8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotInfo.self)
|
||||
} }
|
||||
var _9: Int32?
|
||||
if Int(_1!) & Int(1 << 6) != 0 {_9 = reader.readInt32() }
|
||||
var _10: Int32?
|
||||
if Int(_1!) & Int(1 << 11) != 0 {_10 = reader.readInt32() }
|
||||
var _11: Api.InputGroupCall?
|
||||
if Int(_1!) & Int(1 << 12) != 0 {if let signature = reader.readInt32() {
|
||||
_11 = Api.parse(reader, signature: signature) as? Api.InputGroupCall
|
||||
} }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = _4 != nil
|
||||
let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil
|
||||
let _c6 = _6 != nil
|
||||
let _c7 = (Int(_1!) & Int(1 << 13) == 0) || _7 != nil
|
||||
let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil
|
||||
let _c9 = (Int(_1!) & Int(1 << 6) == 0) || _9 != nil
|
||||
let _c10 = (Int(_1!) & Int(1 << 11) == 0) || _10 != nil
|
||||
let _c11 = (Int(_1!) & Int(1 << 12) == 0) || _11 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 {
|
||||
return Api.ChatFull.chatFull(flags: _1!, id: _2!, about: _3!, participants: _4!, chatPhoto: _5, notifySettings: _6!, exportedInvite: _7, botInfo: _8, pinnedMsgId: _9, folderId: _10, call: _11)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
public enum PollResults: TypeConstructorDescription {
|
||||
@@ -6184,6 +6218,7 @@ public extension Api {
|
||||
case sendMessageRecordRoundAction
|
||||
case sendMessageUploadRoundAction(progress: Int32)
|
||||
case speakingInGroupCallAction
|
||||
case sendMessageHistoryImportAction(progress: Int32)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
@@ -6270,6 +6305,12 @@ public extension Api {
|
||||
buffer.appendInt32(-651419003)
|
||||
}
|
||||
|
||||
break
|
||||
case .sendMessageHistoryImportAction(let progress):
|
||||
if boxed {
|
||||
buffer.appendInt32(-606432698)
|
||||
}
|
||||
serializeInt32(progress, buffer: buffer, boxed: false)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -6304,6 +6345,8 @@ public extension Api {
|
||||
return ("sendMessageUploadRoundAction", [("progress", progress)])
|
||||
case .speakingInGroupCallAction:
|
||||
return ("speakingInGroupCallAction", [])
|
||||
case .sendMessageHistoryImportAction(let progress):
|
||||
return ("sendMessageHistoryImportAction", [("progress", progress)])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6389,6 +6432,17 @@ public extension Api {
|
||||
public static func parse_speakingInGroupCallAction(_ reader: BufferReader) -> SendMessageAction? {
|
||||
return Api.SendMessageAction.speakingInGroupCallAction
|
||||
}
|
||||
public static func parse_sendMessageHistoryImportAction(_ reader: BufferReader) -> SendMessageAction? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
let _c1 = _1 != nil
|
||||
if _c1 {
|
||||
return Api.SendMessageAction.sendMessageHistoryImportAction(progress: _1!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
public enum PrivacyKey: TypeConstructorDescription {
|
||||
@@ -19113,7 +19167,7 @@ public extension Api {
|
||||
case inputPaymentCredentialsSaved(id: String, tmpPassword: Buffer)
|
||||
case inputPaymentCredentials(flags: Int32, data: Api.DataJSON)
|
||||
case inputPaymentCredentialsApplePay(paymentData: Api.DataJSON)
|
||||
case inputPaymentCredentialsAndroidPay(paymentToken: Api.DataJSON, googleTransactionId: String)
|
||||
case inputPaymentCredentialsGooglePay(paymentToken: Api.DataJSON)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
@@ -19137,12 +19191,11 @@ public extension Api {
|
||||
}
|
||||
paymentData.serialize(buffer, true)
|
||||
break
|
||||
case .inputPaymentCredentialsAndroidPay(let paymentToken, let googleTransactionId):
|
||||
case .inputPaymentCredentialsGooglePay(let paymentToken):
|
||||
if boxed {
|
||||
buffer.appendInt32(-905587442)
|
||||
buffer.appendInt32(-1966921727)
|
||||
}
|
||||
paymentToken.serialize(buffer, true)
|
||||
serializeString(googleTransactionId, buffer: buffer, boxed: false)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -19155,8 +19208,8 @@ public extension Api {
|
||||
return ("inputPaymentCredentials", [("flags", flags), ("data", data)])
|
||||
case .inputPaymentCredentialsApplePay(let paymentData):
|
||||
return ("inputPaymentCredentialsApplePay", [("paymentData", paymentData)])
|
||||
case .inputPaymentCredentialsAndroidPay(let paymentToken, let googleTransactionId):
|
||||
return ("inputPaymentCredentialsAndroidPay", [("paymentToken", paymentToken), ("googleTransactionId", googleTransactionId)])
|
||||
case .inputPaymentCredentialsGooglePay(let paymentToken):
|
||||
return ("inputPaymentCredentialsGooglePay", [("paymentToken", paymentToken)])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19203,17 +19256,14 @@ public extension Api {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_inputPaymentCredentialsAndroidPay(_ reader: BufferReader) -> InputPaymentCredentials? {
|
||||
public static func parse_inputPaymentCredentialsGooglePay(_ reader: BufferReader) -> InputPaymentCredentials? {
|
||||
var _1: Api.DataJSON?
|
||||
if let signature = reader.readInt32() {
|
||||
_1 = Api.parse(reader, signature: signature) as? Api.DataJSON
|
||||
}
|
||||
var _2: String?
|
||||
_2 = parseString(reader)
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
if _c1 && _c2 {
|
||||
return Api.InputPaymentCredentials.inputPaymentCredentialsAndroidPay(paymentToken: _1!, googleTransactionId: _2!)
|
||||
if _c1 {
|
||||
return Api.InputPaymentCredentials.inputPaymentCredentialsGooglePay(paymentToken: _1!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
@@ -20978,17 +21028,10 @@ public extension Api {
|
||||
|
||||
}
|
||||
public enum ExportedChatInvite: TypeConstructorDescription {
|
||||
case chatInviteEmpty
|
||||
case chatInviteExported(flags: Int32, link: String, adminId: Int32, date: Int32, startDate: Int32?, expireDate: Int32?, usageLimit: Int32?, usage: Int32?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .chatInviteEmpty:
|
||||
if boxed {
|
||||
buffer.appendInt32(1776236393)
|
||||
}
|
||||
|
||||
break
|
||||
case .chatInviteExported(let flags, let link, let adminId, let date, let startDate, let expireDate, let usageLimit, let usage):
|
||||
if boxed {
|
||||
buffer.appendInt32(1847917725)
|
||||
@@ -21007,16 +21050,11 @@ public extension Api {
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .chatInviteEmpty:
|
||||
return ("chatInviteEmpty", [])
|
||||
case .chatInviteExported(let flags, let link, let adminId, let date, let startDate, let expireDate, let usageLimit, let usage):
|
||||
return ("chatInviteExported", [("flags", flags), ("link", link), ("adminId", adminId), ("date", date), ("startDate", startDate), ("expireDate", expireDate), ("usageLimit", usageLimit), ("usage", usage)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_chatInviteEmpty(_ reader: BufferReader) -> ExportedChatInvite? {
|
||||
return Api.ExportedChatInvite.chatInviteEmpty
|
||||
}
|
||||
public static func parse_chatInviteExported(_ reader: BufferReader) -> ExportedChatInvite? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
|
||||
@@ -3889,6 +3889,25 @@ public extension Api {
|
||||
})
|
||||
}
|
||||
|
||||
public static func getExportedChatInvites(flags: Int32, peer: Api.InputPeer, adminId: Api.InputUser?, offsetDate: Int32?, offsetLink: String?, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.ExportedChatInvites>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(1785900140)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
peer.serialize(buffer, true)
|
||||
if Int(flags) & Int(1 << 0) != 0 {adminId!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 2) != 0 {serializeInt32(offsetDate!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 2) != 0 {serializeString(offsetLink!, buffer: buffer, boxed: false)}
|
||||
serializeInt32(limit, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "messages.getExportedChatInvites", parameters: [("flags", flags), ("peer", peer), ("adminId", adminId), ("offsetDate", offsetDate), ("offsetLink", offsetLink), ("limit", limit)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.ExportedChatInvites? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.messages.ExportedChatInvites?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.messages.ExportedChatInvites
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
public static func exportChatInvite(flags: Int32, peer: Api.InputPeer, expireDate: Int32?, usageLimit: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.ExportedChatInvite>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(347716823)
|
||||
@@ -3924,43 +3943,6 @@ public extension Api {
|
||||
})
|
||||
}
|
||||
|
||||
public static func getChatInviteImporters(peer: Api.InputPeer, link: String, offsetDate: Int32, offsetUser: Api.InputUser, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.ChatInviteImporters>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(654013065)
|
||||
peer.serialize(buffer, true)
|
||||
serializeString(link, buffer: buffer, boxed: false)
|
||||
serializeInt32(offsetDate, buffer: buffer, boxed: false)
|
||||
offsetUser.serialize(buffer, true)
|
||||
serializeInt32(limit, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "messages.getChatInviteImporters", parameters: [("peer", peer), ("link", link), ("offsetDate", offsetDate), ("offsetUser", offsetUser), ("limit", limit)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.ChatInviteImporters? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.messages.ChatInviteImporters?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.messages.ChatInviteImporters
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
public static func getExportedChatInvites(flags: Int32, peer: Api.InputPeer, adminId: Api.InputUser?, offsetDate: Int32?, offsetLink: String?, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.ExportedChatInvites>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(1785900140)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
peer.serialize(buffer, true)
|
||||
if Int(flags) & Int(1 << 0) != 0 {adminId!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 2) != 0 {serializeInt32(offsetDate!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 2) != 0 {serializeString(offsetLink!, buffer: buffer, boxed: false)}
|
||||
serializeInt32(limit, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "messages.getExportedChatInvites", parameters: [("flags", flags), ("peer", peer), ("adminId", adminId), ("offsetDate", offsetDate), ("offsetLink", offsetLink), ("limit", limit)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.ExportedChatInvites? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.messages.ExportedChatInvites?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.messages.ExportedChatInvites
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
public static func deleteRevokedExportedChatInvites(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(1375999075)
|
||||
@@ -3990,6 +3972,24 @@ public extension Api {
|
||||
})
|
||||
}
|
||||
|
||||
public static func getChatInviteImporters(peer: Api.InputPeer, link: String, offsetDate: Int32, offsetUser: Api.InputUser, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.ChatInviteImporters>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(654013065)
|
||||
peer.serialize(buffer, true)
|
||||
serializeString(link, buffer: buffer, boxed: false)
|
||||
serializeInt32(offsetDate, buffer: buffer, boxed: false)
|
||||
offsetUser.serialize(buffer, true)
|
||||
serializeInt32(limit, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "messages.getChatInviteImporters", parameters: [("peer", peer), ("link", link), ("offsetDate", offsetDate), ("offsetUser", offsetUser), ("limit", limit)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.ChatInviteImporters? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.messages.ChatInviteImporters?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.messages.ChatInviteImporters
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
public static func discardEncryption(flags: Int32, chatId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-208425312)
|
||||
@@ -4032,6 +4032,54 @@ public extension Api {
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
public static func initHistoryImport(peer: Api.InputPeer, file: Api.InputFile, mediaCount: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.HistoryImport>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(873008187)
|
||||
peer.serialize(buffer, true)
|
||||
file.serialize(buffer, true)
|
||||
serializeInt32(mediaCount, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "messages.initHistoryImport", parameters: [("peer", peer), ("file", file), ("mediaCount", mediaCount)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.HistoryImport? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.messages.HistoryImport?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.messages.HistoryImport
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
public static func uploadImportedMedia(peer: Api.InputPeer, importId: Int64, fileName: String, media: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.MessageMedia>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(713433234)
|
||||
peer.serialize(buffer, true)
|
||||
serializeInt64(importId, buffer: buffer, boxed: false)
|
||||
serializeString(fileName, buffer: buffer, boxed: false)
|
||||
media.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "messages.uploadImportedMedia", parameters: [("peer", peer), ("importId", importId), ("fileName", fileName), ("media", media)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.MessageMedia? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.MessageMedia?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.MessageMedia
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
public static func startHistoryImport(peer: Api.InputPeer, importId: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-1271008444)
|
||||
peer.serialize(buffer, true)
|
||||
serializeInt64(importId, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "messages.startHistoryImport", parameters: [("peer", peer), ("importId", importId)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Bool?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.Bool
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public struct channels {
|
||||
public static func readHistory(channel: Api.InputChannel, maxId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
|
||||
@@ -2554,7 +2554,7 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP
|
||||
transaction.updateMessage(messageId, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
var attributes = currentMessage.attributes
|
||||
var found = false
|
||||
@@ -2848,7 +2848,7 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP
|
||||
}
|
||||
updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: presences)
|
||||
case let .UpdateSecretChat(chat, _):
|
||||
updateSecretChat(encryptionProvider: encryptionProvider, accountPeerId: accountPeerId, transaction: transaction, chat: chat, requestData: nil)
|
||||
updateSecretChat(encryptionProvider: encryptionProvider, accountPeerId: accountPeerId, transaction: transaction, mediaBox: mediaBox, chat: chat, requestData: nil)
|
||||
case let .AddSecretMessages(messages):
|
||||
for message in messages {
|
||||
let peerId = message.peerId
|
||||
@@ -2917,7 +2917,7 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP
|
||||
transaction.updateMessage(id, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
var attributes = currentMessage.attributes
|
||||
loop: for j in 0 ..< attributes.count {
|
||||
@@ -2932,7 +2932,7 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP
|
||||
transaction.updateMessage(id, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
var attributes = currentMessage.attributes
|
||||
loop: for j in 0 ..< attributes.count {
|
||||
|
||||
@@ -106,7 +106,7 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? {
|
||||
if (flags & Int32(1 << 25)) != 0 {
|
||||
channelFlags.insert(.isFake)
|
||||
}
|
||||
|
||||
|
||||
let restrictionInfo: PeerAccessRestrictionInfo?
|
||||
if let restrictionReason = restrictionReason {
|
||||
restrictionInfo = PeerAccessRestrictionInfo(apiReasons: restrictionReason)
|
||||
|
||||
@@ -23,7 +23,7 @@ func applyMaxReadIndexInteractively(transaction: Transaction, stateManager: Acco
|
||||
transaction.updateMessage(message.id, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
let updatedAttributes = currentMessage.attributes.map({ currentAttribute -> MessageAttribute in
|
||||
if let currentAttribute = currentAttribute as? AutoremoveTimeoutMessageAttribute {
|
||||
@@ -92,7 +92,7 @@ func applySecretOutgoingMessageReadActions(transaction: Transaction, id: Message
|
||||
transaction.updateMessage(message.id, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
let updatedAttributes = currentMessage.attributes.map({ currentAttribute -> MessageAttribute in
|
||||
if let currentAttribute = currentAttribute as? AutoremoveTimeoutMessageAttribute {
|
||||
|
||||
@@ -339,7 +339,7 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage
|
||||
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
|
||||
if let fromMedia = currentMessage.media.first, let toMedia = media.first {
|
||||
|
||||
@@ -12,7 +12,7 @@ public enum CreateChannelError {
|
||||
case serverProvided(String)
|
||||
}
|
||||
|
||||
private func createChannel(account: Account, title: String, description: String?, isSupergroup:Bool, location: (latitude: Double, longitude: Double, address: String)? = nil) -> Signal<PeerId, CreateChannelError> {
|
||||
private func createChannel(account: Account, title: String, description: String?, isSupergroup:Bool, location: (latitude: Double, longitude: Double, address: String)? = nil, isForHistoryImport: Bool = false) -> Signal<PeerId, CreateChannelError> {
|
||||
return account.postbox.transaction { transaction -> Signal<PeerId, CreateChannelError> in
|
||||
var flags: Int32 = 0
|
||||
if isSupergroup {
|
||||
@@ -20,6 +20,9 @@ private func createChannel(account: Account, title: String, description: String?
|
||||
} else {
|
||||
flags |= (1 << 0)
|
||||
}
|
||||
if isForHistoryImport {
|
||||
flags |= (1 << 3)
|
||||
}
|
||||
|
||||
var geoPoint: Api.InputGeoPoint?
|
||||
var address: String?
|
||||
@@ -69,8 +72,8 @@ public func createChannel(account: Account, title: String, description: String?)
|
||||
return createChannel(account: account, title: title, description: description, isSupergroup: false)
|
||||
}
|
||||
|
||||
public func createSupergroup(account: Account, title: String, description: String?, location: (latitude: Double, longitude: Double, address: String)? = nil) -> Signal<PeerId, CreateChannelError> {
|
||||
return createChannel(account: account, title: title, description: description, isSupergroup: true, location: location)
|
||||
public func createSupergroup(account: Account, title: String, description: String?, location: (latitude: Double, longitude: Double, address: String)? = nil, isForHistoryImport: Bool = false) -> Signal<PeerId, CreateChannelError> {
|
||||
return createChannel(account: account, title: title, description: description, isSupergroup: true, location: location, isForHistoryImport: isForHistoryImport)
|
||||
}
|
||||
|
||||
public enum DeleteChannelError {
|
||||
@@ -81,7 +84,7 @@ public func deleteChannel(account: Account, peerId: PeerId) -> Signal<Void, Dele
|
||||
return account.postbox.transaction { transaction -> Api.InputChannel? in
|
||||
return transaction.getPeer(peerId).flatMap(apiInputChannel)
|
||||
}
|
||||
|> mapError { _ -> DeleteChannelError in return .generic }
|
||||
|> mapError { _ -> DeleteChannelError in }
|
||||
|> mapToSignal { inputChannel -> Signal<Void, DeleteChannelError> in
|
||||
if let inputChannel = inputChannel {
|
||||
return account.network.request(Api.functions.channels.deleteChannel(channel: inputChannel))
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import SyncCore
|
||||
import TelegramCore
|
||||
import TelegramApi
|
||||
|
||||
public enum ChatHistoryImport {
|
||||
public struct Session {
|
||||
fileprivate var peerId: PeerId
|
||||
fileprivate var inputPeer: Api.InputPeer
|
||||
fileprivate var id: Int64
|
||||
}
|
||||
|
||||
public enum InitImportError {
|
||||
case generic
|
||||
}
|
||||
|
||||
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
|
||||
return .generic
|
||||
}
|
||||
|> mapToSignal { result -> Signal<Session, InitImportError> in
|
||||
switch result {
|
||||
case let .inputFile(inputFile):
|
||||
return account.postbox.transaction { transaction -> Api.InputPeer? in
|
||||
return transaction.getPeer(peerId).flatMap(apiInputPeer)
|
||||
}
|
||||
|> castError(InitImportError.self)
|
||||
|> mapToSignal { inputPeer -> Signal<Session, InitImportError> in
|
||||
guard let inputPeer = inputPeer else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
return account.network.request(Api.functions.messages.initHistoryImport(peer: inputPeer, file: inputFile, mediaCount: mediaCount))
|
||||
|> mapError { _ -> InitImportError in
|
||||
return .generic
|
||||
}
|
||||
|> map { result -> Session in
|
||||
switch result {
|
||||
case let .historyImport(id):
|
||||
return Session(peerId: peerId, inputPeer: inputPeer, id: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
case .progress:
|
||||
return .complete()
|
||||
case .inputSecretFile:
|
||||
return .fail(.generic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum MediaType {
|
||||
case photo
|
||||
case file
|
||||
case video
|
||||
case sticker
|
||||
case voice
|
||||
}
|
||||
|
||||
public enum UploadMediaError {
|
||||
case generic
|
||||
}
|
||||
|
||||
public static func uploadMedia(account: Account, session: Session, file: TempBoxFile, fileName: String, type: MediaType) -> Signal<Never, 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
|
||||
let inputMedia: Api.InputMedia
|
||||
switch result {
|
||||
case let .inputFile(inputFile):
|
||||
switch type {
|
||||
case .photo:
|
||||
inputMedia = .inputMediaUploadedPhoto(flags: 0, file: inputFile, stickers: nil, ttlSeconds: nil)
|
||||
case .file, .video, .sticker, .voice:
|
||||
var attributes: [Api.DocumentAttribute] = []
|
||||
attributes.append(.documentAttributeFilename(fileName: fileName))
|
||||
var mimeType = "application/octet-stream"
|
||||
switch type {
|
||||
case .video:
|
||||
mimeType = "video/mp4"
|
||||
case .sticker:
|
||||
mimeType = "image/webp"
|
||||
case .voice:
|
||||
mimeType = "audio/ogg"
|
||||
default:
|
||||
break
|
||||
}
|
||||
inputMedia = .inputMediaUploadedDocument(flags: 0, file: inputFile, thumb: nil, mimeType: mimeType, attributes: attributes, stickers: nil, ttlSeconds: nil)
|
||||
}
|
||||
case .progress:
|
||||
return .complete()
|
||||
case .inputSecretFile:
|
||||
return .fail(.generic)
|
||||
}
|
||||
return account.network.request(Api.functions.messages.uploadImportedMedia(peer: session.inputPeer, importId: session.id, fileName: fileName, media: inputMedia))
|
||||
|> mapError { _ -> UploadMediaError in
|
||||
return .generic
|
||||
}
|
||||
|> mapToSignal { result -> Signal<Never, UploadMediaError> in
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum StartImportError {
|
||||
case generic
|
||||
}
|
||||
|
||||
public static func startImport(account: Account, session: Session) -> Signal<Never, StartImportError> {
|
||||
return account.network.request(Api.functions.messages.startHistoryImport(peer: session.inputPeer, importId: session.id))
|
||||
|> mapError { _ -> StartImportError in
|
||||
return .generic
|
||||
}
|
||||
|> mapToSignal { result -> Signal<Never, StartImportError> in
|
||||
if case .boolTrue = result {
|
||||
return .complete()
|
||||
} else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum CheckPeerImportError {
|
||||
case generic
|
||||
case userIsNotMutualContact
|
||||
}
|
||||
|
||||
public static func checkPeerImport(account: Account, peerId: PeerId) -> Signal<Never, CheckPeerImportError> {
|
||||
return account.postbox.transaction { transaction -> Peer? in
|
||||
return transaction.getPeer(peerId)
|
||||
}
|
||||
|> castError(CheckPeerImportError.self)
|
||||
|> mapToSignal { peer -> Signal<Never, CheckPeerImportError> in
|
||||
guard let peer = peer else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
if let inputUser = apiInputUser(peer) {
|
||||
return account.network.request(Api.functions.users.getUsers(id: [inputUser]))
|
||||
|> mapError { _ -> CheckPeerImportError in
|
||||
return .generic
|
||||
}
|
||||
|> mapToSignal { result -> Signal<Never, CheckPeerImportError> in
|
||||
guard let apiUser = result.first else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
switch apiUser {
|
||||
case let .user(flags, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
if (flags & (1 << 12)) == 0 {
|
||||
// not mutual contact
|
||||
return .fail(.userIsNotMutualContact)
|
||||
}
|
||||
return .complete()
|
||||
case.userEmpty:
|
||||
return .fail(.generic)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ public func createSecretChat(account: Account, peerId: PeerId) -> Signal<PeerId,
|
||||
}
|
||||
|> mapToSignal { result -> Signal<PeerId, CreateSecretChatError> in
|
||||
return account.postbox.transaction { transaction -> PeerId in
|
||||
updateSecretChat(encryptionProvider: account.network.encryptionProvider, accountPeerId: account.peerId, transaction: transaction, chat: result, requestData: SecretChatRequestData(g: config.g, p: config.p, a: a))
|
||||
updateSecretChat(encryptionProvider: account.network.encryptionProvider, accountPeerId: account.peerId, transaction: transaction, mediaBox: account.postbox.mediaBox, chat: result, requestData: SecretChatRequestData(g: config.g, p: config.p, a: a))
|
||||
|
||||
return result.peerId
|
||||
} |> mapError { _ -> CreateSecretChatError in return .generic }
|
||||
|
||||
@@ -515,7 +515,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId,
|
||||
}
|
||||
|
||||
if let sourceForwardInfo = sourceMessage.forwardInfo {
|
||||
forwardInfo = StoreMessageForwardInfo(authorId: sourceForwardInfo.author?.id, sourceId: sourceForwardInfo.source?.id, sourceMessageId: sourceForwardInfo.sourceMessageId, date: sourceForwardInfo.date, authorSignature: sourceForwardInfo.authorSignature, psaType: nil)
|
||||
forwardInfo = StoreMessageForwardInfo(authorId: sourceForwardInfo.author?.id, sourceId: sourceForwardInfo.source?.id, sourceMessageId: sourceForwardInfo.sourceMessageId, date: sourceForwardInfo.date, authorSignature: sourceForwardInfo.authorSignature, psaType: nil, flags: [])
|
||||
} else {
|
||||
if sourceMessage.id.peerId != account.peerId {
|
||||
var hasHiddenForwardMedia = false
|
||||
@@ -545,7 +545,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId,
|
||||
|
||||
let psaType: String? = nil
|
||||
|
||||
forwardInfo = StoreMessageForwardInfo(authorId: author.id, sourceId: sourceId, sourceMessageId: sourceMessageId, date: sourceMessage.timestamp, authorSignature: authorSignature, psaType: psaType)
|
||||
forwardInfo = StoreMessageForwardInfo(authorId: author.id, sourceId: sourceId, sourceMessageId: sourceMessageId, date: sourceMessage.timestamp, authorSignature: authorSignature, psaType: psaType, flags: [])
|
||||
}
|
||||
} else {
|
||||
forwardInfo = nil
|
||||
|
||||
@@ -7,8 +7,6 @@ import SyncCore
|
||||
extension ExportedInvitation {
|
||||
init?(apiExportedInvite: Api.ExportedChatInvite) {
|
||||
switch apiExportedInvite {
|
||||
case .chatInviteEmpty:
|
||||
return nil
|
||||
case let .chatInviteExported(flags, link, adminId, date, startDate, expireDate, usageLimit, usage):
|
||||
self = ExportedInvitation(link: link, isPermanent: (flags & (1 << 5)) != 0, isRevoked: (flags & (1 << 0)) != 0, adminId: PeerId(namespace: Namespaces.Peer.CloudUser, id: adminId), date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: usage)
|
||||
}
|
||||
|
||||
@@ -742,7 +742,7 @@ private func validateBatch(postbox: Postbox, network: Network, transaction: Tran
|
||||
} else {
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
var attributes = currentMessage.attributes
|
||||
if let channelPts = channelPts {
|
||||
@@ -777,7 +777,7 @@ private func validateBatch(postbox: Postbox, network: Network, transaction: Tran
|
||||
updatedTags.remove(tag)
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
var attributes = currentMessage.attributes
|
||||
for i in (0 ..< attributes.count).reversed() {
|
||||
@@ -810,7 +810,7 @@ private func validateBatch(postbox: Postbox, network: Network, transaction: Tran
|
||||
updatedTags.remove(tag)
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
var attributes = currentMessage.attributes
|
||||
for i in (0 ..< attributes.count).reversed() {
|
||||
@@ -845,7 +845,7 @@ private func validateBatch(postbox: Postbox, network: Network, transaction: Tran
|
||||
transaction.updateMessage(id, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
var attributes = currentMessage.attributes
|
||||
for i in (0 ..< attributes.count).reversed() {
|
||||
@@ -982,7 +982,7 @@ private func validateReplyThreadBatch(postbox: Postbox, network: Network, transa
|
||||
} else {
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
var attributes = currentMessage.attributes
|
||||
if let channelPts = channelPts {
|
||||
|
||||
@@ -164,6 +164,59 @@ public func revokePeerExportedInvitation(account: Account, peerId: PeerId, link:
|
||||
|> switchToLatest
|
||||
}
|
||||
|
||||
public struct ExportedInvitations : Equatable {
|
||||
public let list: [ExportedInvitation]?
|
||||
public let totalCount: Int32
|
||||
}
|
||||
|
||||
public func peerExportedInvitations(account: Account, peerId: PeerId, revoked: Bool, offsetLink: ExportedInvitation? = nil) -> Signal<ExportedInvitations?, NoError> {
|
||||
return account.postbox.transaction { transaction -> Signal<ExportedInvitations?, NoError> in
|
||||
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
|
||||
var flags: Int32 = 0
|
||||
if let _ = offsetLink {
|
||||
flags |= (1 << 2)
|
||||
}
|
||||
if revoked {
|
||||
flags |= (1 << 3)
|
||||
}
|
||||
return account.network.request(Api.functions.messages.getExportedChatInvites(flags: flags, peer: inputPeer, adminId: nil, offsetDate: offsetLink?.date, offsetLink: offsetLink?.link, limit: 50))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.messages.ExportedChatInvites?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<ExportedInvitations?, NoError> in
|
||||
return account.postbox.transaction { transaction -> ExportedInvitations? in
|
||||
if let result = result, case let .exportedChatInvites(count, apiInvites, users) = result {
|
||||
var peers: [Peer] = []
|
||||
var peersMap: [PeerId: Peer] = [:]
|
||||
for user in users {
|
||||
let telegramUser = TelegramUser(user: user)
|
||||
peers.append(telegramUser)
|
||||
peersMap[telegramUser.id] = telegramUser
|
||||
}
|
||||
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
|
||||
return updated
|
||||
})
|
||||
|
||||
var invites: [ExportedInvitation] = []
|
||||
for apiInvite in apiInvites {
|
||||
if let invite = ExportedInvitation(apiExportedInvite: apiInvite) {
|
||||
invites.append(invite)
|
||||
}
|
||||
}
|
||||
return ExportedInvitations(list: invites, totalCount: count)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
} |> switchToLatest
|
||||
}
|
||||
|
||||
|
||||
public enum DeletePeerExportedInvitationError {
|
||||
case generic
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func managedAutoremoveMessageOperations(postbox: Postbox) -> Signal<Void, NoErro
|
||||
transaction.updateMessage(message.id, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
var updatedMedia = currentMessage.media
|
||||
for i in 0 ..< updatedMedia.count {
|
||||
|
||||
@@ -265,18 +265,29 @@ private func removeChat(transaction: Transaction, postbox: Postbox, network: Net
|
||||
return .complete()
|
||||
}
|
||||
} else if peer.id.namespace == Namespaces.Peer.CloudGroup {
|
||||
let deleteUser: Signal<Void, NoError> = network.request(Api.functions.messages.deleteChatUser(chatId: peer.id.id, userId: Api.InputUser.inputUserSelf))
|
||||
|> map { result -> Api.Updates? in
|
||||
return result
|
||||
}
|
||||
|> `catch` { _ in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { updates in
|
||||
if let updates = updates {
|
||||
stateManager.addUpdates(updates)
|
||||
let deleteUser: Signal<Void, NoError>
|
||||
if operation.deleteGloballyIfPossible {
|
||||
deleteUser = network.request(Api.functions.messages.deleteChat(chatId: peer.id.id))
|
||||
|> `catch` { _ in
|
||||
return .single(.boolFalse)
|
||||
}
|
||||
return .complete()
|
||||
|> mapToSignal { _ in
|
||||
return .complete()
|
||||
}
|
||||
} else {
|
||||
deleteUser = network.request(Api.functions.messages.deleteChatUser(chatId: peer.id.id, userId: Api.InputUser.inputUserSelf))
|
||||
|> map { result -> Api.Updates? in
|
||||
return result
|
||||
}
|
||||
|> `catch` { _ in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { updates in
|
||||
if let updates = updates {
|
||||
stateManager.addUpdates(updates)
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
let reportSignal: Signal<Void, NoError>
|
||||
if let inputPeer = apiInputPeer(peer), operation.reportChatSpam {
|
||||
|
||||
@@ -171,7 +171,7 @@ private func synchronizeConsumeMessageContents(transaction: Transaction, postbox
|
||||
transaction.updateMessage(id, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
var attributes = currentMessage.attributes
|
||||
loop: for j in 0 ..< attributes.count {
|
||||
@@ -197,7 +197,7 @@ private func synchronizeConsumeMessageContents(transaction: Transaction, postbox
|
||||
transaction.updateMessage(id, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
var attributes = currentMessage.attributes
|
||||
loop: for j in 0 ..< attributes.count {
|
||||
|
||||
@@ -153,8 +153,8 @@ func managedSecretChatOutgoingOperations(auxiliaryMethods: AccountAuxiliaryMetho
|
||||
return sendServiceActionMessage(postbox: postbox, network: network, peerId: entry.peerId, action: .resendOperations(layer: layer, actionGloballyUniqueId: actionGloballyUniqueId, fromSeqNo: fromSeqNo, toSeqNo: toSeqNo), tagLocalIndex: entry.tagLocalIndex, wasDelivered: operation.delivered)
|
||||
case let .screenshotMessages(layer, actionGloballyUniqueId, globallyUniqueIds, messageId):
|
||||
return sendServiceActionMessage(postbox: postbox, network: network, peerId: entry.peerId, action: .screenshotMessages(layer: layer, actionGloballyUniqueId: actionGloballyUniqueId, globallyUniqueIds: globallyUniqueIds, messageId: messageId), tagLocalIndex: entry.tagLocalIndex, wasDelivered: operation.delivered)
|
||||
case let .terminate(reportSpam):
|
||||
return requestTerminateSecretChat(postbox: postbox, network: network, peerId: entry.peerId, tagLocalIndex: entry.tagLocalIndex, reportSpam: reportSpam)
|
||||
case let .terminate(reportSpam, requestRemoteHistoryRemoval):
|
||||
return requestTerminateSecretChat(postbox: postbox, network: network, peerId: entry.peerId, tagLocalIndex: entry.tagLocalIndex, reportSpam: reportSpam, requestRemoteHistoryRemoval: requestRemoteHistoryRemoval)
|
||||
}
|
||||
} else {
|
||||
assertionFailure()
|
||||
@@ -1479,7 +1479,7 @@ private func sendMessage(auxiliaryMethods: AccountAuxiliaryMethods, postbox: Pos
|
||||
}
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
|
||||
var updatedMedia = currentMessage.media
|
||||
@@ -1567,7 +1567,7 @@ private func sendServiceActionMessage(postbox: Postbox, network: Network, peerId
|
||||
resultTimestamp = timestamp
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: timestamp, flags: flags, tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: currentMessage.media))
|
||||
})
|
||||
@@ -1673,8 +1673,12 @@ private func sendBoxedDecryptedMessage(postbox: Postbox, network: Network, peer:
|
||||
}
|
||||
}
|
||||
|
||||
private func requestTerminateSecretChat(postbox: Postbox, network: Network, peerId: PeerId, tagLocalIndex: Int32, reportSpam: Bool) -> Signal<Void, NoError> {
|
||||
return network.request(Api.functions.messages.discardEncryption(flags: 0, chatId: peerId.id))
|
||||
private func requestTerminateSecretChat(postbox: Postbox, network: Network, peerId: PeerId, tagLocalIndex: Int32, reportSpam: Bool, requestRemoteHistoryRemoval: Bool) -> Signal<Void, NoError> {
|
||||
var flags: Int32 = 0
|
||||
if requestRemoteHistoryRemoval {
|
||||
flags |= 1 << 0
|
||||
}
|
||||
return network.request(Api.functions.messages.discardEncryption(flags: flags, chatId: peerId.id))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ in
|
||||
return .single(nil)
|
||||
|
||||
@@ -92,7 +92,7 @@ public func markMessageContentAsConsumedInteractively(postbox: Postbox, messageI
|
||||
transaction.updateMessage(message.id, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: currentMessage.media))
|
||||
})
|
||||
@@ -151,7 +151,7 @@ func markMessageContentAsConsumedRemotely(transaction: Transaction, messageId: M
|
||||
transaction.updateMessage(message.id, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: updatedTags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: updatedMedia))
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ public func updateMessageReactionsInteractively(postbox: Postbox, messageId: Mes
|
||||
transaction.updateMessage(messageId, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
var attributes = currentMessage.attributes
|
||||
loop: for j in 0 ..< attributes.count {
|
||||
@@ -71,7 +71,7 @@ private func requestUpdateMessageReaction(postbox: Postbox, network: Network, st
|
||||
transaction.updateMessage(messageId, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
let reactions = mergedMessageReactions(attributes: currentMessage.attributes)
|
||||
var attributes = currentMessage.attributes
|
||||
@@ -211,7 +211,7 @@ private func synchronizeMessageReactions(transaction: Transaction, postbox: Post
|
||||
transaction.updateMessage(id, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
var attributes = currentMessage.attributes
|
||||
loop: for j in 0 ..< attributes.count {
|
||||
|
||||
@@ -174,7 +174,7 @@ func locallyRenderedMessage(message: StoreMessage, peers: [PeerId: Peer]) -> Mes
|
||||
|
||||
var forwardInfo: MessageForwardInfo?
|
||||
if let info = message.forwardInfo {
|
||||
forwardInfo = MessageForwardInfo(author: info.authorId.flatMap({ peers[$0] }), source: info.sourceId.flatMap({ peers[$0] }), sourceMessageId: info.sourceMessageId, date: info.date, authorSignature: info.authorSignature, psaType: info.psaType)
|
||||
forwardInfo = MessageForwardInfo(author: info.authorId.flatMap({ peers[$0] }), source: info.sourceId.flatMap({ peers[$0] }), sourceMessageId: info.sourceMessageId, date: info.date, authorSignature: info.authorSignature, psaType: info.psaType, flags: info.flags)
|
||||
if let author = forwardInfo?.author {
|
||||
messagePeers[author.id] = author
|
||||
}
|
||||
|
||||
@@ -361,6 +361,7 @@ public enum MultipartUploadSource {
|
||||
case resource(MediaResourceReference)
|
||||
case data(Data)
|
||||
case custom(Signal<MediaResourceData, NoError>)
|
||||
case tempFile(TempBoxFile)
|
||||
}
|
||||
|
||||
enum MultipartUploadError {
|
||||
@@ -395,6 +396,15 @@ func multipartUpload(network: Network, postbox: Postbox, source: MultipartUpload
|
||||
headerSize = resource.resource.headerSize
|
||||
fetchedResource = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: resource)
|
||||
|> map { _ in }
|
||||
case let .tempFile(file):
|
||||
if let size = fileSize(file.path) {
|
||||
dataSignal = .single(.resourceData(MediaResourceData(path: file.path, offset: 0, size: size, complete: true)))
|
||||
headerSize = 0
|
||||
fetchedResource = .complete()
|
||||
} else {
|
||||
subscriber.putError(.generic)
|
||||
return EmptyDisposable
|
||||
}
|
||||
case let .data(data):
|
||||
dataSignal = .single(.data(data))
|
||||
headerSize = 0
|
||||
|
||||
@@ -63,6 +63,8 @@ extension PeerInputActivity {
|
||||
self = .uploadingInstantVideo(progress: progress)
|
||||
case .speakingInGroupCallAction:
|
||||
self = .speakingInGroupCall(timestamp: timestamp)
|
||||
case let .sendMessageHistoryImportAction(progress):
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ public extension Peer {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var isVerified: Bool {
|
||||
switch self {
|
||||
case let user as TelegramUser:
|
||||
@@ -265,4 +265,13 @@ public extension PeerId {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var isImport: Bool {
|
||||
if self.namespace == Namespaces.Peer.CloudUser {
|
||||
if self.id == 225079 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ private func failMessages(postbox: Postbox, ids: [MessageId]) -> Signal<Void, No
|
||||
transaction.updateMessage(id, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
return .update(StoreMessage(id: id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: [.Failed], tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: currentMessage.media))
|
||||
})
|
||||
@@ -572,7 +572,7 @@ public final class PendingMessageManager {
|
||||
transaction.updateMessage(id, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
return .update(StoreMessage(id: id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: [.Failed], tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: currentMessage.media))
|
||||
})
|
||||
@@ -920,7 +920,7 @@ public final class PendingMessageManager {
|
||||
}
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: flags, tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: currentMessage.media))
|
||||
})
|
||||
@@ -936,7 +936,7 @@ public final class PendingMessageManager {
|
||||
}
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: flags, tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: currentMessage.media))
|
||||
})
|
||||
@@ -945,7 +945,7 @@ public final class PendingMessageManager {
|
||||
transaction.updateMessage(message.id, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
return .update(StoreMessage(id: message.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: [.Failed], tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: currentMessage.media))
|
||||
})
|
||||
@@ -1086,7 +1086,7 @@ public final class PendingMessageManager {
|
||||
transaction.updateMessage(message.id, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
return .update(StoreMessage(id: message.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: [.Failed], tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: currentMessage.media))
|
||||
})
|
||||
@@ -1100,7 +1100,7 @@ public final class PendingMessageManager {
|
||||
transaction.updateMessage(message.id, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
return .update(StoreMessage(id: message.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: [.Failed], tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: currentMessage.media))
|
||||
})
|
||||
@@ -1128,7 +1128,7 @@ public final class PendingMessageManager {
|
||||
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
||||
})
|
||||
|
||||
@@ -364,7 +364,7 @@ private func uploadedMediaImageContent(network: Network, postbox: Postbox, trans
|
||||
transaction.updateMessage(messageId, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: nil, psaType: nil)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: nil, psaType: nil, flags: [])
|
||||
}
|
||||
var updatedAttributes = currentMessage.attributes
|
||||
if let index = updatedAttributes.firstIndex(where: { $0 is OutgoingMessageInfoAttribute }){
|
||||
@@ -654,7 +654,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili
|
||||
transaction.updateMessage(messageId, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: nil, psaType: nil)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: nil, psaType: nil, flags: [])
|
||||
}
|
||||
var updatedAttributes = currentMessage.attributes
|
||||
if let index = updatedAttributes.firstIndex(where: { $0 is OutgoingMessageInfoAttribute }){
|
||||
|
||||
@@ -10,9 +10,9 @@ public func removePeerChat(account: Account, peerId: PeerId, reportChatSpam: Boo
|
||||
}
|
||||
}
|
||||
|
||||
public func terminateSecretChat(transaction: Transaction, peerId: PeerId) {
|
||||
public func terminateSecretChat(transaction: Transaction, peerId: PeerId, requestRemoteHistoryRemoval: Bool) {
|
||||
if let state = transaction.getPeerChatState(peerId) as? SecretChatState, state.embeddedState != .terminated {
|
||||
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.terminate(reportSpam: false), state: state).withUpdatedEmbeddedState(.terminated)
|
||||
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.terminate(reportSpam: false, requestRemoteHistoryRemoval: requestRemoteHistoryRemoval), state: state).withUpdatedEmbeddedState(.terminated)
|
||||
if updatedState != state {
|
||||
transaction.setPeerChatState(peerId, state: updatedState)
|
||||
if let peer = transaction.getPeer(peerId) as? TelegramSecretChat {
|
||||
@@ -48,7 +48,7 @@ public func removePeerChat(account: Account, transaction: Transaction, mediaBox:
|
||||
})
|
||||
if peerId.namespace == Namespaces.Peer.SecretChat {
|
||||
if let state = transaction.getPeerChatState(peerId) as? SecretChatState, state.embeddedState != .terminated {
|
||||
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.terminate(reportSpam: reportChatSpam), state: state).withUpdatedEmbeddedState(.terminated)
|
||||
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.terminate(reportSpam: reportChatSpam, requestRemoteHistoryRemoval: deleteGloballyIfPossible), state: state).withUpdatedEmbeddedState(.terminated)
|
||||
if updatedState != state {
|
||||
transaction.setPeerChatState(peerId, state: updatedState)
|
||||
if let peer = transaction.getPeer(peerId) as? TelegramSecretChat {
|
||||
|
||||
@@ -316,7 +316,7 @@ public func requestEditLiveLocation(postbox: Postbox, network: Network, stateMan
|
||||
transaction.updateMessage(messageId, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
var updatedLocalTags = currentMessage.localTags
|
||||
updatedLocalTags.remove(.OutgoingLiveLocation)
|
||||
|
||||
@@ -14,6 +14,14 @@ public struct TelegramPeerPhoto {
|
||||
public let index: Int
|
||||
public let totalCount: Int
|
||||
public let messageId: MessageId?
|
||||
public init(image: TelegramMediaImage, reference: TelegramMediaImageReference?, date: Int32, index: Int, totalCount: Int, messageId: MessageId?) {
|
||||
self.image = image
|
||||
self.reference = reference
|
||||
self.date = date
|
||||
self.index = index
|
||||
self.totalCount = totalCount
|
||||
self.messageId = messageId
|
||||
}
|
||||
}
|
||||
|
||||
public func requestPeerPhotos(postbox: Postbox, network: Network, peerId: PeerId) -> Signal<[TelegramPeerPhoto], NoError> {
|
||||
|
||||
@@ -414,7 +414,13 @@ extension StoreMessage {
|
||||
var forwardInfo: StoreMessageForwardInfo?
|
||||
if let fwdFrom = fwdFrom {
|
||||
switch fwdFrom {
|
||||
case let .messageFwdHeader(_, fromId, fromName, date, channelPost, postAuthor, savedFromPeer, savedFromMsgId, psaType):
|
||||
case let .messageFwdHeader(flags, fromId, fromName, date, channelPost, postAuthor, savedFromPeer, savedFromMsgId, psaType):
|
||||
var forwardInfoFlags: MessageForwardInfo.Flags = []
|
||||
let isImported = (flags & (1 << 7)) != 0
|
||||
if isImported {
|
||||
forwardInfoFlags.insert(.isImported)
|
||||
}
|
||||
|
||||
var authorId: PeerId?
|
||||
var sourceId: PeerId?
|
||||
var sourceMessageId: MessageId?
|
||||
@@ -448,11 +454,11 @@ extension StoreMessage {
|
||||
}
|
||||
|
||||
if let authorId = authorId {
|
||||
forwardInfo = StoreMessageForwardInfo(authorId: authorId, sourceId: sourceId, sourceMessageId: sourceMessageId, date: date, authorSignature: postAuthor, psaType: psaType)
|
||||
forwardInfo = StoreMessageForwardInfo(authorId: authorId, sourceId: sourceId, sourceMessageId: sourceMessageId, date: date, authorSignature: postAuthor, psaType: psaType, flags: forwardInfoFlags)
|
||||
} else if let sourceId = sourceId {
|
||||
forwardInfo = StoreMessageForwardInfo(authorId: sourceId, sourceId: sourceId, sourceMessageId: sourceMessageId, date: date, authorSignature: postAuthor, psaType: psaType)
|
||||
forwardInfo = StoreMessageForwardInfo(authorId: sourceId, sourceId: sourceId, sourceMessageId: sourceMessageId, date: date, authorSignature: postAuthor, psaType: psaType, flags: forwardInfoFlags)
|
||||
} else if let postAuthor = postAuthor ?? fromName {
|
||||
forwardInfo = StoreMessageForwardInfo(authorId: nil, sourceId: nil, sourceMessageId: sourceMessageId, date: date, authorSignature: postAuthor, psaType: psaType)
|
||||
forwardInfo = StoreMessageForwardInfo(authorId: nil, sourceId: nil, sourceMessageId: sourceMessageId, date: date, authorSignature: postAuthor, psaType: psaType, flags: forwardInfoFlags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,10 +53,10 @@ extension TelegramUser {
|
||||
if (flags & (1 << 24)) != 0 {
|
||||
userFlags.insert(.isScam)
|
||||
}
|
||||
if (flags & Int32(1 << 26)) != 0 {
|
||||
if (flags & (1 << 26)) != 0 {
|
||||
userFlags.insert(.isFake)
|
||||
}
|
||||
|
||||
|
||||
var botInfo: BotUserInfo?
|
||||
if (flags & (1 << 14)) != 0 {
|
||||
var botFlags = BotUserInfoFlags()
|
||||
@@ -165,7 +165,7 @@ extension TelegramUser {
|
||||
if rhs.flags.contains(.isFake) {
|
||||
userFlags.insert(.isFake)
|
||||
}
|
||||
|
||||
|
||||
let botInfo: BotUserInfo? = rhs.botInfo
|
||||
|
||||
let restrictionInfo: PeerAccessRestrictionInfo? = rhs.restrictionInfo
|
||||
|
||||
@@ -22,7 +22,7 @@ func updateMessageMedia(transaction: Transaction, id: MediaId, media: Media?) {
|
||||
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: currentMessage.media))
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ struct SecretChatRequestData {
|
||||
let a: MemoryBuffer
|
||||
}
|
||||
|
||||
func updateSecretChat(encryptionProvider: EncryptionProvider, accountPeerId: PeerId, transaction: Transaction, chat: Api.EncryptedChat, requestData: SecretChatRequestData?) {
|
||||
func updateSecretChat(encryptionProvider: EncryptionProvider, accountPeerId: PeerId, transaction: Transaction, mediaBox: MediaBox, chat: Api.EncryptedChat, requestData: SecretChatRequestData?) {
|
||||
let currentPeer = transaction.getPeer(chat.peerId) as? TelegramSecretChat
|
||||
let currentState = transaction.getPeerChatState(chat.peerId) as? SecretChatState
|
||||
let settings = transaction.getPreferencesEntry(key: PreferencesKeys.secretChatSettings) as? SecretChatSettings ?? SecretChatSettings.defaultSettings
|
||||
@@ -68,11 +68,20 @@ func updateSecretChat(encryptionProvider: EncryptionProvider, accountPeerId: Pee
|
||||
}
|
||||
case let .encryptedChatDiscarded(flags, _):
|
||||
if let currentPeer = currentPeer, let currentState = currentState {
|
||||
let isRemoved = (flags & (1 << 0)) != 0
|
||||
|
||||
let state = currentState.withUpdatedEmbeddedState(.terminated)
|
||||
let peer = currentPeer.withUpdatedEmbeddedState(state.embeddedState.peerState)
|
||||
updatePeers(transaction: transaction, peers: [peer], update: { _, updated in return updated })
|
||||
transaction.setPeerChatState(peer.id, state: state)
|
||||
transaction.operationLogRemoveAllEntries(peerId: peer.id, tag: OperationLogTags.SecretOutgoing)
|
||||
|
||||
if isRemoved {
|
||||
let peerId = currentPeer.id
|
||||
clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, namespaces: .all)
|
||||
transaction.updatePeerChatListInclusion(peerId, inclusion: .notIncluded)
|
||||
transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentlySearchedPeerIds, itemId: RecentPeerItemId(peerId).rawValue)
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("State", "got encryptedChatDiscarded, but peer doesn't exist")
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -213,6 +213,8 @@ swift_library(
|
||||
"//submodules/AnimatedNavigationStripeNode:AnimatedNavigationStripeNode",
|
||||
"//submodules/AudioBlob:AudioBlob",
|
||||
"//Telegram:GeneratedSources",
|
||||
"//third-party/ZIPFoundation:ZIPFoundation",
|
||||
"//submodules/ChatImportUI:ChatImportUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_phoneavatar.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -9944,7 +9944,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
controller.present(textAlertController(context: context, title: nil, text: presentationData.strings.Forward_ErrorDisabledForChat, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
controller.peerSelected = { [weak self, weak controller] peerId in
|
||||
controller.peerSelected = { [weak self, weak controller] peer in
|
||||
let peerId = peer.id
|
||||
|
||||
guard let strongSelf = self, let strongController = controller else {
|
||||
return
|
||||
}
|
||||
@@ -10137,7 +10139,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
case let .chat(textInputState, _, _):
|
||||
if let textInputState = textInputState {
|
||||
let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context))
|
||||
controller.peerSelected = { [weak self, weak controller] peerId in
|
||||
controller.peerSelected = { [weak self, weak controller] peer in
|
||||
let peerId = peer.id
|
||||
|
||||
if let strongSelf = self, let strongController = controller {
|
||||
if case let .peer(currentPeerId) = strongSelf.chatLocation, peerId == currentPeerId {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
@@ -10337,7 +10341,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
let _ = requestUpdatePeerIsBlocked(account: strongSelf.context.account, peerId: peer.id, isBlocked: true).start()
|
||||
if let _ = chatPeer as? TelegramSecretChat {
|
||||
let _ = (strongSelf.context.account.postbox.transaction { transaction in
|
||||
terminateSecretChat(transaction: transaction, peerId: chatPeer.id)
|
||||
terminateSecretChat(transaction: transaction, peerId: chatPeer.id, requestRemoteHistoryRemoval: true)
|
||||
}).start()
|
||||
}
|
||||
if deleteChat {
|
||||
|
||||
@@ -454,7 +454,9 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled]))
|
||||
controller.peerSelected = { [weak controller] peerId in
|
||||
controller.peerSelected = { [weak controller] peer in
|
||||
let peerId = peer.id
|
||||
|
||||
if let strongController = controller {
|
||||
strongController.dismiss()
|
||||
|
||||
|
||||
@@ -17,17 +17,19 @@ final class ChatMessageAvatarAccessoryItem: ListViewAccessoryItem {
|
||||
private let peer: Peer?
|
||||
private let messageReference: MessageReference?
|
||||
private let messageTimestamp: Int32
|
||||
private let forwardInfo: MessageForwardInfo?
|
||||
private let emptyColor: UIColor
|
||||
private let controllerInteraction: ChatControllerInteraction
|
||||
|
||||
private let day: Int32
|
||||
|
||||
init(context: AccountContext, peerId: PeerId, peer: Peer?, messageReference: MessageReference?, messageTimestamp: Int32, emptyColor: UIColor, controllerInteraction: ChatControllerInteraction) {
|
||||
init(context: AccountContext, peerId: PeerId, peer: Peer?, messageReference: MessageReference?, messageTimestamp: Int32, forwardInfo: MessageForwardInfo?, emptyColor: UIColor, controllerInteraction: ChatControllerInteraction) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.peer = peer
|
||||
self.messageReference = messageReference
|
||||
self.messageTimestamp = messageTimestamp
|
||||
self.forwardInfo = forwardInfo
|
||||
self.emptyColor = emptyColor
|
||||
self.controllerInteraction = controllerInteraction
|
||||
|
||||
@@ -40,16 +42,56 @@ final class ChatMessageAvatarAccessoryItem: ListViewAccessoryItem {
|
||||
|
||||
func isEqualToItem(_ other: ListViewAccessoryItem) -> Bool {
|
||||
if case let other as ChatMessageAvatarAccessoryItem = other {
|
||||
return other.peerId == self.peerId && self.day == other.day && abs(other.messageTimestamp - self.messageTimestamp) < 10 * 60
|
||||
if other.peerId != self.peerId {
|
||||
return false
|
||||
}
|
||||
if self.day != other.day {
|
||||
return false
|
||||
}
|
||||
if abs(other.messageTimestamp - self.messageTimestamp) >= 10 * 60 {
|
||||
return false
|
||||
}
|
||||
if let forwardInfo = self.forwardInfo, let otherForwardInfo = other.forwardInfo {
|
||||
if forwardInfo.flags.contains(.isImported) == forwardInfo.flags.contains(.isImported) {
|
||||
if forwardInfo.authorSignature != otherForwardInfo.authorSignature {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else if let forwardInfo = self.forwardInfo, forwardInfo.flags.contains(.isImported) {
|
||||
return false
|
||||
} else if let otherForwardInfo = other.forwardInfo, otherForwardInfo.flags.contains(.isImported) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func node(synchronous: Bool) -> ListViewAccessoryItemNode {
|
||||
let node = ChatMessageAvatarAccessoryItemNode()
|
||||
node.frame = CGRect(origin: CGPoint(), size: CGSize(width: 38.0, height: 38.0))
|
||||
if let peer = self.peer {
|
||||
if let forwardInfo = self.forwardInfo, forwardInfo.flags.contains(.isImported) {
|
||||
if let authorSignature = forwardInfo.authorSignature, !authorSignature.isEmpty {
|
||||
let components = authorSignature.components(separatedBy: " ")
|
||||
if !components.isEmpty, !components[0].hasPrefix("+") {
|
||||
var letters: [String] = []
|
||||
|
||||
letters.append(String(components[0][components[0].startIndex]))
|
||||
if components.count > 1 {
|
||||
letters.append(String(components[1][components[1].startIndex]))
|
||||
}
|
||||
|
||||
node.setCustomLetters(context: self.context, theme: self.context.sharedContext.currentPresentationData.with({ $0 }).theme, synchronousLoad: synchronous, letters: letters, emptyColor: self.emptyColor, controllerInteraction: self.controllerInteraction)
|
||||
} else {
|
||||
node.setCustomLetters(context: self.context, theme: self.context.sharedContext.currentPresentationData.with({ $0 }).theme, synchronousLoad: synchronous, letters: [], emptyColor: self.emptyColor, controllerInteraction: self.controllerInteraction)
|
||||
}
|
||||
} else {
|
||||
node.setCustomLetters(context: self.context, theme: self.context.sharedContext.currentPresentationData.with({ $0 }).theme, synchronousLoad: synchronous, letters: [], emptyColor: self.emptyColor, controllerInteraction: self.controllerInteraction)
|
||||
}
|
||||
} else if let peer = self.peer {
|
||||
node.setPeer(context: self.context, theme: self.context.sharedContext.currentPresentationData.with({ $0 }).theme, synchronousLoad: synchronous, peer: peer, authorOfMessage: self.messageReference, emptyColor: self.emptyColor, controllerInteraction: self.controllerInteraction)
|
||||
}
|
||||
return node
|
||||
@@ -91,11 +133,20 @@ final class ChatMessageAvatarAccessoryItemNode: ListViewAccessoryItemNode {
|
||||
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction, let peer = strongSelf.peer else {
|
||||
return
|
||||
}
|
||||
strongSelf.controllerInteraction?.openPeerContextMenu(peer, strongSelf.containerNode, strongSelf.containerNode.bounds, gesture)
|
||||
controllerInteraction.openPeerContextMenu(peer, strongSelf.containerNode, strongSelf.containerNode.bounds, gesture)
|
||||
}
|
||||
}
|
||||
|
||||
func setPeer(context: AccountContext, theme: PresentationTheme, synchronousLoad:Bool, peer: Peer, authorOfMessage: MessageReference?, emptyColor: UIColor, controllerInteraction: ChatControllerInteraction) {
|
||||
func setCustomLetters(context: AccountContext, theme: PresentationTheme, synchronousLoad: Bool, letters: [String], emptyColor: UIColor, controllerInteraction: ChatControllerInteraction) {
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.peer = nil
|
||||
|
||||
self.contextActionIsEnabled = false
|
||||
|
||||
self.avatarNode.setCustomLetters(letters, icon: !letters.isEmpty ? nil : .phone)
|
||||
}
|
||||
|
||||
func setPeer(context: AccountContext, theme: PresentationTheme, synchronousLoad: Bool, peer: Peer, authorOfMessage: MessageReference?, emptyColor: UIColor, controllerInteraction: ChatControllerInteraction) {
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.peer = peer
|
||||
|
||||
|
||||
@@ -1036,6 +1036,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
effectiveAuthor = source
|
||||
displayAuthorInfo = !mergedTop.merged && incoming && effectiveAuthor != nil
|
||||
} else if let forwardInfo = item.content.firstMessage.forwardInfo, forwardInfo.flags.contains(.isImported), let authorSignature = forwardInfo.authorSignature {
|
||||
ignoreForward = true
|
||||
effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: Int32(clamping: authorSignature.persistentHashValue)), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: UserInfoFlags())
|
||||
displayAuthorInfo = !mergedTop.merged && incoming
|
||||
} else {
|
||||
effectiveAuthor = firstMessage.author
|
||||
|
||||
|
||||
@@ -119,6 +119,21 @@ private func messagesShouldBeMerged(accountPeerId: PeerId, _ lhs: Message, _ rhs
|
||||
}
|
||||
}
|
||||
|
||||
var sameAuthor = false
|
||||
if lhsEffectiveAuthor?.id == rhsEffectiveAuthor?.id && lhs.effectivelyIncoming(accountPeerId) == rhs.effectivelyIncoming(accountPeerId) {
|
||||
sameAuthor = true
|
||||
}
|
||||
|
||||
var lhsEffectiveTimestamp = lhs.timestamp
|
||||
var rhsEffectiveTimestamp = rhs.timestamp
|
||||
|
||||
if let lhsForwardInfo = lhs.forwardInfo, lhsForwardInfo.flags.contains(.isImported), let rhsForwardInfo = rhs.forwardInfo, rhsForwardInfo.flags.contains(.isImported) {
|
||||
lhsEffectiveTimestamp = lhsForwardInfo.date
|
||||
rhsEffectiveTimestamp = rhsForwardInfo.date
|
||||
|
||||
sameAuthor = lhsForwardInfo.authorSignature == rhsForwardInfo.authorSignature
|
||||
}
|
||||
|
||||
if lhs.id.peerId.isRepliesOrSavedMessages(accountPeerId: accountPeerId) {
|
||||
if let forwardInfo = lhs.forwardInfo {
|
||||
lhsEffectiveAuthor = forwardInfo.author
|
||||
@@ -130,12 +145,7 @@ private func messagesShouldBeMerged(accountPeerId: PeerId, _ lhs: Message, _ rhs
|
||||
}
|
||||
}
|
||||
|
||||
var sameAuthor = false
|
||||
if lhsEffectiveAuthor?.id == rhsEffectiveAuthor?.id && lhs.effectivelyIncoming(accountPeerId) == rhs.effectivelyIncoming(accountPeerId) {
|
||||
sameAuthor = true
|
||||
}
|
||||
|
||||
if abs(lhs.timestamp - rhs.timestamp) < Int32(10 * 60) && sameAuthor {
|
||||
if abs(lhsEffectiveTimestamp - rhsEffectiveTimestamp) < Int32(10 * 60) && sameAuthor {
|
||||
if let channel = lhs.peers[lhs.id.peerId] as? TelegramChannel, case .group = channel.info, lhsEffectiveAuthor?.id == channel.id, !lhs.effectivelyIncoming(accountPeerId) {
|
||||
return .none
|
||||
}
|
||||
@@ -337,7 +347,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
|
||||
}
|
||||
if !hasActionMedia && !isBroadcastChannel {
|
||||
if let effectiveAuthor = effectiveAuthor {
|
||||
accessoryItem = ChatMessageAvatarAccessoryItem(context: context, peerId: effectiveAuthor.id, peer: effectiveAuthor, messageReference: MessageReference(message), messageTimestamp: content.index.timestamp, emptyColor: presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper.fill, controllerInteraction: controllerInteraction)
|
||||
accessoryItem = ChatMessageAvatarAccessoryItem(context: context, peerId: effectiveAuthor.id, peer: effectiveAuthor, messageReference: MessageReference(message), messageTimestamp: content.index.timestamp, forwardInfo: message.forwardInfo, emptyColor: presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper.fill, controllerInteraction: controllerInteraction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,9 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
||||
openPeer(peerId, .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive)))
|
||||
case let .groupBotStart(botPeerId, payload):
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .onlyGroups, .onlyManageable], title: presentationData.strings.UserInfo_InviteBotToGroup))
|
||||
controller.peerSelected = { [weak controller] peerId in
|
||||
controller.peerSelected = { [weak controller] peer in
|
||||
let peerId = peer.id
|
||||
|
||||
if payload.isEmpty {
|
||||
if peerId.namespace == Namespaces.Peer.CloudGroup {
|
||||
let _ = (addGroupMember(account: context.account, peerId: peerId, memberId: botPeerId)
|
||||
@@ -263,7 +265,9 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
||||
context.sharedContext.applicationBindings.dismissNativeController()
|
||||
} else {
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled]))
|
||||
controller.peerSelected = { [weak controller] peerId in
|
||||
controller.peerSelected = { [weak controller] peer in
|
||||
let peerId = peer.id
|
||||
|
||||
if let strongController = controller {
|
||||
strongController.dismiss()
|
||||
continueWithPeer(peerId)
|
||||
|
||||
@@ -4927,7 +4927,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
func forwardMessages(messageIds: Set<MessageId>?) {
|
||||
if let messageIds = messageIds ?? self.state.selectedMessageIds, !messageIds.isEmpty {
|
||||
let peerSelectionController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.onlyWriteable, .excludeDisabled]))
|
||||
peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peerId in
|
||||
peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peer in
|
||||
let peerId = peer.id
|
||||
|
||||
if let strongSelf = self, let _ = peerSelectionController {
|
||||
if peerId == strongSelf.context.account.peerId {
|
||||
strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone)
|
||||
|
||||
@@ -19,10 +19,11 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
|
||||
|
||||
private var customTitle: String?
|
||||
|
||||
public var peerSelected: ((PeerId) -> Void)?
|
||||
public var peerSelected: ((Peer) -> Void)?
|
||||
private let filter: ChatListNodePeersFilter
|
||||
|
||||
private let attemptSelection: ((Peer) -> Void)?
|
||||
private let createNewGroup: (() -> Void)?
|
||||
|
||||
public var inProgress: Bool = false {
|
||||
didSet {
|
||||
@@ -40,6 +41,8 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
|
||||
}
|
||||
}
|
||||
|
||||
public var customDismiss: (() -> Void)?
|
||||
|
||||
private var peerSelectionNode: PeerSelectionControllerNode {
|
||||
return super.displayNode as! PeerSelectionControllerNode
|
||||
}
|
||||
@@ -51,16 +54,37 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
|
||||
return self._ready
|
||||
}
|
||||
|
||||
private let hasChatListSelector: Bool
|
||||
private let hasContactSelector: Bool
|
||||
private let hasGlobalSearch: Bool
|
||||
private let pretendPresentedInModal: Bool
|
||||
|
||||
override public var _presentedInModal: Bool {
|
||||
get {
|
||||
if self.pretendPresentedInModal {
|
||||
return true
|
||||
} else {
|
||||
return super._presentedInModal
|
||||
}
|
||||
} set(value) {
|
||||
if !self.pretendPresentedInModal {
|
||||
super._presentedInModal = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var searchContentNode: NavigationBarSearchContentNode?
|
||||
|
||||
public init(_ params: PeerSelectionControllerParams) {
|
||||
self.context = params.context
|
||||
self.filter = params.filter
|
||||
self.hasChatListSelector = params.hasChatListSelector
|
||||
self.hasContactSelector = params.hasContactSelector
|
||||
self.hasGlobalSearch = params.hasGlobalSearch
|
||||
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.attemptSelection = params.attemptSelection
|
||||
self.createNewGroup = params.createNewGroup
|
||||
self.pretendPresentedInModal = params.pretendPresentedInModal
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
|
||||
|
||||
@@ -120,7 +144,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = PeerSelectionControllerNode(context: self.context, filter: self.filter, hasContactSelector: hasContactSelector, present: { [weak self] c, a in
|
||||
self.displayNode = PeerSelectionControllerNode(context: self.context, filter: self.filter, hasChatListSelector: self.hasChatListSelector, hasContactSelector: self.hasContactSelector, hasGlobalSearch: self.hasGlobalSearch, createNewGroup: self.createNewGroup, present: { [weak self] c, a in
|
||||
self?.present(c, in: .window(.root), with: a)
|
||||
}, dismiss: { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
@@ -136,9 +160,9 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
|
||||
self?.activateSearch()
|
||||
}
|
||||
|
||||
self.peerSelectionNode.requestOpenPeer = { [weak self] peerId in
|
||||
self.peerSelectionNode.requestOpenPeer = { [weak self] peer in
|
||||
if let strongSelf = self, let peerSelected = strongSelf.peerSelected {
|
||||
peerSelected(peerId)
|
||||
peerSelected(peer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +183,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
|
||||
}
|
||||
strongSelf.openMessageFromSearchDisposable.set((storedPeer |> deliverOnMainQueue).start(completed: { [weak strongSelf] in
|
||||
if let strongSelf = strongSelf, let peerSelected = strongSelf.peerSelected {
|
||||
peerSelected(peer.id)
|
||||
peerSelected(peer)
|
||||
}
|
||||
}))
|
||||
}
|
||||
@@ -197,7 +221,11 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
|
||||
}
|
||||
|
||||
@objc func cancelPressed() {
|
||||
self.dismiss()
|
||||
if let customDismiss = self.customDismiss {
|
||||
customDismiss()
|
||||
} else {
|
||||
self.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private func activateSearch() {
|
||||
|
||||
@@ -19,6 +19,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
private let present: (ViewController, Any?) -> Void
|
||||
private let dismiss: () -> Void
|
||||
private let filter: ChatListNodePeersFilter
|
||||
private let hasGlobalSearch: Bool
|
||||
|
||||
var inProgress: Bool = false {
|
||||
didSet {
|
||||
@@ -46,7 +47,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
|
||||
var requestActivateSearch: (() -> Void)?
|
||||
var requestDeactivateSearch: (() -> Void)?
|
||||
var requestOpenPeer: ((PeerId) -> Void)?
|
||||
var requestOpenPeer: ((Peer) -> Void)?
|
||||
var requestOpenDisabledPeer: ((Peer) -> Void)?
|
||||
var requestOpenPeerFromSearch: ((Peer) -> Void)?
|
||||
var requestOpenMessageFromSearch: ((Peer, MessageId) -> Void)?
|
||||
@@ -59,15 +60,16 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
return self.readyValue.get()
|
||||
}
|
||||
|
||||
init(context: AccountContext, filter: ChatListNodePeersFilter, hasContactSelector: Bool, present: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) {
|
||||
init(context: AccountContext, filter: ChatListNodePeersFilter, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.present = present
|
||||
self.dismiss = dismiss
|
||||
self.filter = filter
|
||||
self.hasGlobalSearch = hasGlobalSearch
|
||||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
if hasContactSelector {
|
||||
if hasChatListSelector && hasContactSelector {
|
||||
self.toolbarBackgroundNode = ASDisplayNode()
|
||||
self.toolbarBackgroundNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor
|
||||
|
||||
@@ -84,8 +86,15 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
self.toolbarSeparatorNode = nil
|
||||
self.segmentedControlNode = nil
|
||||
}
|
||||
|
||||
var chatListcategories: [ChatListNodeAdditionalCategory] = []
|
||||
|
||||
if let _ = createNewGroup {
|
||||
//TODO:localize
|
||||
chatListcategories.append(ChatListNodeAdditionalCategory(id: 0, icon: PresentationResourcesItemList.createGroupIcon(self.presentationData.theme), title: "Create a New Group", appearance: .action))
|
||||
}
|
||||
|
||||
self.chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, fillPreloadItems: false, mode: .peers(filter: filter, isSelecting: false, additionalCategories: [], chatListFilters: nil), theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations)
|
||||
self.chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, fillPreloadItems: false, mode: .peers(filter: filter, isSelecting: false, additionalCategories: chatListcategories, chatListFilters: nil), theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations)
|
||||
|
||||
super.init()
|
||||
|
||||
@@ -93,6 +102,10 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
return UITracingLayerView()
|
||||
})
|
||||
|
||||
self.chatListNode.additionalCategorySelected = { _ in
|
||||
createNewGroup?()
|
||||
}
|
||||
|
||||
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
|
||||
|
||||
self.chatListNode.activateSearch = { [weak self] in
|
||||
@@ -100,7 +113,8 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
self.chatListNode.peerSelected = { [weak self] peer, _, _ in
|
||||
self?.requestOpenPeer?(peer.id)
|
||||
self?.chatListNode.clearHighlightAnimated(true)
|
||||
self?.requestOpenPeer?(peer)
|
||||
}
|
||||
|
||||
self.chatListNode.disabledPeerSelected = { [weak self] peer in
|
||||
@@ -133,7 +147,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
}
|
||||
})
|
||||
|
||||
if hasContactSelector {
|
||||
if hasChatListSelector && hasContactSelector {
|
||||
self.segmentedControlNode!.selectedIndexChanged = { [weak self] index in
|
||||
self?.indexChanged(index)
|
||||
}
|
||||
@@ -143,6 +157,9 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
self.addSubnode(self.segmentedControlNode!)
|
||||
}
|
||||
|
||||
if !hasChatListSelector && hasContactSelector {
|
||||
self.indexChanged(1)
|
||||
}
|
||||
|
||||
self.readyValue.set(self.chatListNode.ready)
|
||||
}
|
||||
@@ -249,7 +266,11 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
}, placeholder: placeholderNode)
|
||||
|
||||
} else if let contactListNode = self.contactListNode, contactListNode.supernode != nil {
|
||||
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, onlyWriteable: true, categories: [.cloudContacts, .global], addContact: nil, openPeer: { [weak self] peer in
|
||||
var categories: ContactsSearchCategories = [.cloudContacts]
|
||||
if self.hasGlobalSearch {
|
||||
categories.insert(.global)
|
||||
}
|
||||
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, onlyWriteable: true, categories: categories, addContact: nil, openPeer: { [weak self] peer in
|
||||
if let strongSelf = self {
|
||||
switch peer {
|
||||
case let .peer(peer, _, _):
|
||||
@@ -304,10 +325,6 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
private func indexChanged(_ index: Int) {
|
||||
guard let (layout, navigationHeight, actualNavigationHeight) = self.containerLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let contactListActive = index == 1
|
||||
if contactListActive != self.contactListActive {
|
||||
self.contactListActive = contactListActive
|
||||
@@ -326,7 +343,8 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
}
|
||||
contactListNode.openPeer = { [weak self] peer, _ in
|
||||
if case let .peer(peer, _, _) = peer {
|
||||
self?.requestOpenPeer?(peer.id)
|
||||
self?.contactListNode?.listNode.clearHighlightAnimated(true)
|
||||
self?.requestOpenPeer?(peer)
|
||||
}
|
||||
}
|
||||
contactListNode.suppressPermissionWarning = { [weak self] in
|
||||
@@ -348,17 +366,26 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
contactListNode.contentScrollingEnded = { [weak self] listView in
|
||||
return self?.contentScrollingEnded?(listView) ?? false
|
||||
}
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, actualNavigationBarHeight: actualNavigationHeight, transition: .immediate)
|
||||
|
||||
let _ = (contactListNode.ready |> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
if let contactListNode = strongSelf.contactListNode {
|
||||
strongSelf.insertSubnode(contactListNode, aboveSubnode: strongSelf.chatListNode)
|
||||
if let (layout, navigationHeight, actualNavigationHeight) = self.containerLayout {
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, actualNavigationBarHeight: actualNavigationHeight, transition: .immediate)
|
||||
|
||||
let _ = (contactListNode.ready |> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
if let contactListNode = strongSelf.contactListNode {
|
||||
strongSelf.insertSubnode(contactListNode, aboveSubnode: strongSelf.chatListNode)
|
||||
}
|
||||
strongSelf.chatListNode.removeFromSupernode()
|
||||
strongSelf.recursivelyEnsureDisplaySynchronously(true)
|
||||
}
|
||||
strongSelf.chatListNode.removeFromSupernode()
|
||||
strongSelf.recursivelyEnsureDisplaySynchronously(true)
|
||||
})
|
||||
} else {
|
||||
if let contactListNode = self.contactListNode {
|
||||
self.insertSubnode(contactListNode, aboveSubnode: self.chatListNode)
|
||||
}
|
||||
})
|
||||
self.chatListNode.removeFromSupernode()
|
||||
self.recursivelyEnsureDisplaySynchronously(true)
|
||||
}
|
||||
}
|
||||
} else if let contactListNode = self.contactListNode {
|
||||
contactListNode.enableUpdates = false
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
@@ -16,6 +17,11 @@ import SettingsUI
|
||||
import OpenSSLEncryptionProvider
|
||||
import AppLock
|
||||
import Intents
|
||||
import MobileCoreServices
|
||||
import OverlayStatusController
|
||||
import PresentationDataUtils
|
||||
import ChatImportUI
|
||||
import ZIPFoundation
|
||||
|
||||
private let inForeground = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
|
||||
@@ -285,93 +291,376 @@ public class ShareRootControllerImpl {
|
||||
let displayShare: () -> Void = {
|
||||
var cancelImpl: (() -> Void)?
|
||||
|
||||
let requestUserInteraction: ([UnpreparedShareItemContent]) -> Signal<[PreparedShareItemContent], NoError> = { content in
|
||||
return Signal { [weak self] subscriber in
|
||||
switch content[0] {
|
||||
case let .contact(data):
|
||||
let controller = deviceContactInfoController(context: context, subject: .filter(peer: nil, contactId: nil, contactData: data, completion: { peer, contactData in
|
||||
let phone = contactData.basicData.phoneNumbers[0].value
|
||||
if let vCardData = contactData.serializedVCard() {
|
||||
subscriber.putNext([.media(.media(.standalone(media: TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: vCardData))))])
|
||||
let beginShare: () -> Void = {
|
||||
let requestUserInteraction: ([UnpreparedShareItemContent]) -> Signal<[PreparedShareItemContent], NoError> = { content in
|
||||
return Signal { [weak self] subscriber in
|
||||
switch content[0] {
|
||||
case let .contact(data):
|
||||
let controller = deviceContactInfoController(context: context, subject: .filter(peer: nil, contactId: nil, contactData: data, completion: { peer, contactData in
|
||||
let phone = contactData.basicData.phoneNumbers[0].value
|
||||
if let vCardData = contactData.serializedVCard() {
|
||||
subscriber.putNext([.media(.media(.standalone(media: TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: vCardData))))])
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
}), completed: nil, cancelled: {
|
||||
cancelImpl?()
|
||||
})
|
||||
|
||||
if let strongSelf = self, let window = strongSelf.mainWindow {
|
||||
controller.presentationArguments = ViewControllerPresentationArguments(presentationAnimation: .modalSheet)
|
||||
window.present(controller, on: .root)
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
}), completed: nil, cancelled: {
|
||||
cancelImpl?()
|
||||
})
|
||||
|
||||
if let strongSelf = self, let window = strongSelf.mainWindow {
|
||||
controller.presentationArguments = ViewControllerPresentationArguments(presentationAnimation: .modalSheet)
|
||||
window.present(controller, on: .root)
|
||||
break
|
||||
}
|
||||
return EmptyDisposable
|
||||
} |> runOn(Queue.mainQueue())
|
||||
}
|
||||
|
||||
let sentItems: ([PeerId], [PreparedShareItemContent], Account) -> Signal<ShareControllerExternalStatus, NoError> = { peerIds, contents, account in
|
||||
let sentItems = sentShareItems(account: account, to: peerIds, items: contents)
|
||||
|> `catch` { _ -> Signal<
|
||||
Float, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
return sentItems
|
||||
|> map { value -> ShareControllerExternalStatus in
|
||||
return .progress(value)
|
||||
}
|
||||
|> then(.single(.done))
|
||||
}
|
||||
|
||||
let shareController = ShareController(context: context, subject: .fromExternal({ peerIds, additionalText, account in
|
||||
if let strongSelf = self, let inputItems = strongSelf.getExtensionContext()?.inputItems, !inputItems.isEmpty, !peerIds.isEmpty {
|
||||
let rawSignals = TGItemProviderSignals.itemSignals(forInputItems: inputItems)!
|
||||
return preparedShareItems(account: account, to: peerIds[0], dataItems: rawSignals, additionalText: additionalText)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<PreparedShareItems?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { state -> Signal<ShareControllerExternalStatus, NoError> in
|
||||
guard let state = state else {
|
||||
return .single(.done)
|
||||
}
|
||||
break
|
||||
}
|
||||
return EmptyDisposable
|
||||
} |> runOn(Queue.mainQueue())
|
||||
}
|
||||
|
||||
let sentItems: ([PeerId], [PreparedShareItemContent], Account) -> Signal<ShareControllerExternalStatus, NoError> = { peerIds, contents, account in
|
||||
let sentItems = sentShareItems(account: account, to: peerIds, items: contents)
|
||||
|> `catch` { _ -> Signal<
|
||||
Float, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
return sentItems
|
||||
|> map { value -> ShareControllerExternalStatus in
|
||||
return .progress(value)
|
||||
}
|
||||
|> then(.single(.done))
|
||||
}
|
||||
|
||||
let shareController = ShareController(context: context, subject: .fromExternal({ peerIds, additionalText, account in
|
||||
if let strongSelf = self, let inputItems = strongSelf.getExtensionContext()?.inputItems, !inputItems.isEmpty, !peerIds.isEmpty {
|
||||
let rawSignals = TGItemProviderSignals.itemSignals(forInputItems: inputItems)!
|
||||
return preparedShareItems(account: account, to: peerIds[0], dataItems: rawSignals, additionalText: additionalText)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<PreparedShareItems?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { state -> Signal<ShareControllerExternalStatus, NoError> in
|
||||
guard let state = state else {
|
||||
return .single(.done)
|
||||
}
|
||||
switch state {
|
||||
case .preparing:
|
||||
return .single(.preparing)
|
||||
case let .progress(value):
|
||||
return .single(.progress(value))
|
||||
case let .userInteractionRequired(value):
|
||||
return requestUserInteraction(value)
|
||||
|> mapToSignal { contents -> Signal<ShareControllerExternalStatus, NoError> in
|
||||
switch state {
|
||||
case .preparing:
|
||||
return .single(.preparing)
|
||||
case let .progress(value):
|
||||
return .single(.progress(value))
|
||||
case let .userInteractionRequired(value):
|
||||
return requestUserInteraction(value)
|
||||
|> mapToSignal { contents -> Signal<ShareControllerExternalStatus, NoError> in
|
||||
return sentItems(peerIds, contents, account)
|
||||
}
|
||||
case let .done(contents):
|
||||
return sentItems(peerIds, contents, account)
|
||||
}
|
||||
case let .done(contents):
|
||||
return sentItems(peerIds, contents, account)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .single(.done)
|
||||
}
|
||||
} else {
|
||||
return .single(.done)
|
||||
}
|
||||
}), externalShare: false, switchableAccounts: otherAccounts, immediatePeerId: immediatePeerId)
|
||||
shareController.presentationArguments = ViewControllerPresentationArguments(presentationAnimation: .modalSheet)
|
||||
shareController.dismissed = { _ in
|
||||
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||
}
|
||||
|
||||
cancelImpl = { [weak shareController] in
|
||||
shareController?.dismiss(completion: { [weak self] in
|
||||
}), fromForeignApp: true, externalShare: false, switchableAccounts: otherAccounts, immediatePeerId: immediatePeerId)
|
||||
shareController.presentationArguments = ViewControllerPresentationArguments(presentationAnimation: .modalSheet)
|
||||
shareController.dismissed = { _ in
|
||||
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||
})
|
||||
}
|
||||
|
||||
cancelImpl = { [weak shareController] in
|
||||
shareController?.dismiss(completion: { [weak self] in
|
||||
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||
})
|
||||
}
|
||||
|
||||
if let strongSelf = self {
|
||||
if let currentShareController = strongSelf.currentShareController {
|
||||
currentShareController.dismiss()
|
||||
}
|
||||
strongSelf.currentShareController = shareController
|
||||
strongSelf.mainWindow?.present(shareController, on: .root)
|
||||
}
|
||||
|
||||
context.account.resetStateManagement()
|
||||
}
|
||||
|
||||
if let strongSelf = self {
|
||||
if let currentShareController = strongSelf.currentShareController {
|
||||
currentShareController.dismiss()
|
||||
}
|
||||
strongSelf.currentShareController = shareController
|
||||
strongSelf.mainWindow?.present(shareController, on: .root)
|
||||
}
|
||||
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) {
|
||||
attachment.loadItem(forTypeIdentifier: kUTTypeFileURL as String, completionHandler: { result, error in
|
||||
Queue.mainQueue().async {
|
||||
guard let url = result as? URL else {
|
||||
beginShare()
|
||||
return
|
||||
}
|
||||
guard let fileName = url.pathComponents.last else {
|
||||
beginShare()
|
||||
return
|
||||
}
|
||||
let fileExtension = (fileName as NSString).pathExtension
|
||||
guard fileExtension.lowercased() == "zip" else {
|
||||
beginShare()
|
||||
return
|
||||
}
|
||||
guard let archive = Archive(url: url, accessMode: .read) else {
|
||||
beginShare()
|
||||
return
|
||||
}
|
||||
guard let _ = archive["_chat.txt"] else {
|
||||
beginShare()
|
||||
return
|
||||
}
|
||||
|
||||
context.account.resetStateManagement()
|
||||
let photoRegex = try! NSRegularExpression(pattern: "[\\d]+-PHOTO-.*?\\.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")
|
||||
|
||||
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?
|
||||
do {
|
||||
for entry in archive {
|
||||
let entryPath = entry.path(using: .utf8).replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "..", with: "_")
|
||||
if entryPath.isEmpty {
|
||||
continue
|
||||
}
|
||||
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
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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: {
|
||||
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 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)
|
||||
})], parseMarkdown: true)
|
||||
strongSelf.mainWindow?.present(controller, on: .root)
|
||||
}
|
||||
}
|
||||
|
||||
createNewGroupImpl = {
|
||||
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: {
|
||||
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 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)
|
||||
}
|
||||
} else {
|
||||
beginShare()
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
beginShare()
|
||||
} else {
|
||||
beginShare()
|
||||
}
|
||||
}
|
||||
|
||||
let modalPresentation: Bool
|
||||
|
||||
@@ -12,6 +12,23 @@ enum MessageTimestampStatusFormat {
|
||||
case minimal
|
||||
}
|
||||
|
||||
private func dateStringForDay(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, timestamp: Int32) -> String {
|
||||
var t: time_t = time_t(timestamp)
|
||||
var timeinfo: tm = tm()
|
||||
localtime_r(&t, &timeinfo)
|
||||
|
||||
let timestampNow = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||
var now: time_t = time_t(timestampNow)
|
||||
var timeinfoNow: tm = tm()
|
||||
localtime_r(&now, &timeinfoNow)
|
||||
|
||||
if timeinfo.tm_year != timeinfoNow.tm_year {
|
||||
return "\(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat))"
|
||||
} else {
|
||||
return "\(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, dateTimeFormat: dateTimeFormat))"
|
||||
}
|
||||
}
|
||||
|
||||
func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Message, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, strings: PresentationStrings, format: MessageTimestampStatusFormat = .regular, reactionCount: Int) -> String {
|
||||
let timestamp: Int32
|
||||
if let scheduleTime = message.scheduleTime {
|
||||
@@ -24,6 +41,12 @@ func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Message, da
|
||||
dateText = " "
|
||||
}
|
||||
|
||||
if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported) {
|
||||
//TODO:localize
|
||||
|
||||
dateText = dateStringForDay(strings: strings, dateTimeFormat: dateTimeFormat, timestamp: forwardInfo.date) + ", " + stringForMessageTimestamp(timestamp: forwardInfo.date, dateTimeFormat: dateTimeFormat) + " Imported " + dateText
|
||||
}
|
||||
|
||||
var authorTitle: String?
|
||||
if let author = message.author as? TelegramUser {
|
||||
if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
|
||||
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ZIPFoundation",
|
||||
module_name = "ZIPFoundation",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,178 @@
|
||||
//
|
||||
// Archive+MemoryFile.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2020 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors.
|
||||
// Released under the MIT License.
|
||||
//
|
||||
// See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if swift(>=5.0)
|
||||
|
||||
extension Archive {
|
||||
/// Returns a `Data` object containing a representation of the receiver.
|
||||
public var data: Data? { return memoryFile?.data }
|
||||
|
||||
static func configureMemoryBacking(for data: Data, mode: AccessMode)
|
||||
-> (UnsafeMutablePointer<FILE>, MemoryFile)? {
|
||||
let posixMode: String
|
||||
switch mode {
|
||||
case .read: posixMode = "rb"
|
||||
case .create: posixMode = "wb+"
|
||||
case .update: posixMode = "rb+"
|
||||
}
|
||||
let memoryFile = MemoryFile(data: data)
|
||||
guard let archiveFile = memoryFile.open(mode: posixMode) else { return nil }
|
||||
|
||||
if mode == .create {
|
||||
let endOfCentralDirectoryRecord = EndOfCentralDirectoryRecord(numberOfDisk: 0, numberOfDiskStart: 0,
|
||||
totalNumberOfEntriesOnDisk: 0,
|
||||
totalNumberOfEntriesInCentralDirectory: 0,
|
||||
sizeOfCentralDirectory: 0,
|
||||
offsetToStartOfCentralDirectory: 0,
|
||||
zipFileCommentLength: 0,
|
||||
zipFileCommentData: Data())
|
||||
_ = endOfCentralDirectoryRecord.data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) in
|
||||
fwrite(buffer.baseAddress, buffer.count, 1, archiveFile) // Errors handled during read
|
||||
}
|
||||
}
|
||||
return (archiveFile, memoryFile)
|
||||
}
|
||||
}
|
||||
|
||||
class MemoryFile {
|
||||
private(set) var data: Data
|
||||
private var offset = 0
|
||||
|
||||
init(data: Data = Data()) {
|
||||
self.data = data
|
||||
}
|
||||
|
||||
func open(mode: String) -> UnsafeMutablePointer<FILE>? {
|
||||
let cookie = Unmanaged.passRetained(self)
|
||||
let writable = mode.count > 0 && (mode.first! != "r" || mode.last! == "+")
|
||||
let append = mode.count > 0 && mode.first! == "a"
|
||||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
|
||||
let result = writable
|
||||
? funopen(cookie.toOpaque(), readStub, writeStub, seekStub, closeStub)
|
||||
: funopen(cookie.toOpaque(), readStub, nil, seekStub, closeStub)
|
||||
#else
|
||||
let stubs = cookie_io_functions_t(read: readStub, write: writeStub, seek: seekStub, close: closeStub)
|
||||
let result = fopencookie(cookie.toOpaque(), mode, stubs)
|
||||
#endif
|
||||
if append {
|
||||
fseek(result, 0, SEEK_END)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private extension MemoryFile {
|
||||
func readData(buffer: UnsafeMutableRawBufferPointer) -> Int {
|
||||
let size = min(buffer.count, data.count-offset)
|
||||
let start = data.startIndex
|
||||
data.copyBytes(to: buffer.bindMemory(to: UInt8.self), from: start+offset..<start+offset+size)
|
||||
offset += size
|
||||
return size
|
||||
}
|
||||
|
||||
func writeData(buffer: UnsafeRawBufferPointer) -> Int {
|
||||
let start = data.startIndex
|
||||
if offset < data.count && offset+buffer.count > data.count {
|
||||
data.removeSubrange(start+offset..<start+data.count)
|
||||
} else if offset > data.count {
|
||||
data.append(Data(count: offset-data.count))
|
||||
}
|
||||
if offset == data.count {
|
||||
data.append(buffer.bindMemory(to: UInt8.self))
|
||||
} else {
|
||||
let start = data.startIndex // May have changed in earlier mutation
|
||||
data.replaceSubrange(start+offset..<start+offset+buffer.count, with: buffer.bindMemory(to: UInt8.self))
|
||||
}
|
||||
offset += buffer.count
|
||||
return buffer.count
|
||||
}
|
||||
|
||||
func seek(offset: Int, whence: Int32) -> Int {
|
||||
var result = -1
|
||||
if whence == SEEK_SET {
|
||||
result = offset
|
||||
} else if whence == SEEK_CUR {
|
||||
result = self.offset + offset
|
||||
} else if whence == SEEK_END {
|
||||
result = data.count + offset
|
||||
}
|
||||
self.offset = result
|
||||
return self.offset
|
||||
}
|
||||
}
|
||||
|
||||
private func fileFromCookie(cookie: UnsafeRawPointer) -> MemoryFile {
|
||||
return Unmanaged<MemoryFile>.fromOpaque(cookie).takeUnretainedValue()
|
||||
}
|
||||
|
||||
private func closeStub(_ cookie: UnsafeMutableRawPointer?) -> Int32 {
|
||||
if let cookie = cookie {
|
||||
Unmanaged<MemoryFile>.fromOpaque(cookie).release()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
|
||||
private func readStub(_ cookie: UnsafeMutableRawPointer?,
|
||||
_ bytePtr: UnsafeMutablePointer<Int8>?,
|
||||
_ count: Int32) -> Int32 {
|
||||
guard let cookie = cookie, let bytePtr = bytePtr else { return 0 }
|
||||
return Int32(fileFromCookie(cookie: cookie).readData(
|
||||
buffer: UnsafeMutableRawBufferPointer(start: bytePtr, count: Int(count))))
|
||||
}
|
||||
|
||||
private func writeStub(_ cookie: UnsafeMutableRawPointer?,
|
||||
_ bytePtr: UnsafePointer<Int8>?,
|
||||
_ count: Int32) -> Int32 {
|
||||
guard let cookie = cookie, let bytePtr = bytePtr else { return 0 }
|
||||
return Int32(fileFromCookie(cookie: cookie).writeData(
|
||||
buffer: UnsafeRawBufferPointer(start: bytePtr, count: Int(count))))
|
||||
}
|
||||
|
||||
private func seekStub(_ cookie: UnsafeMutableRawPointer?,
|
||||
_ offset: fpos_t,
|
||||
_ whence: Int32) -> fpos_t {
|
||||
guard let cookie = cookie else { return 0 }
|
||||
return fpos_t(fileFromCookie(cookie: cookie).seek(offset: Int(offset), whence: whence))
|
||||
}
|
||||
|
||||
#else
|
||||
private func readStub(_ cookie: UnsafeMutableRawPointer?,
|
||||
_ bytePtr: UnsafeMutablePointer<Int8>?,
|
||||
_ count: Int) -> Int {
|
||||
guard let cookie = cookie, let bytePtr = bytePtr else { return 0 }
|
||||
return fileFromCookie(cookie: cookie).readData(
|
||||
buffer: UnsafeMutableRawBufferPointer(start: bytePtr, count: count))
|
||||
}
|
||||
|
||||
private func writeStub(_ cookie: UnsafeMutableRawPointer?,
|
||||
_ bytePtr: UnsafePointer<Int8>?,
|
||||
_ count: Int) -> Int {
|
||||
guard let cookie = cookie, let bytePtr = bytePtr else { return 0 }
|
||||
return fileFromCookie(cookie: cookie).writeData(
|
||||
buffer: UnsafeRawBufferPointer(start: bytePtr, count: count))
|
||||
}
|
||||
|
||||
private func seekStub(_ cookie: UnsafeMutableRawPointer?,
|
||||
_ offset: UnsafeMutablePointer<Int>?,
|
||||
_ whence: Int32) -> Int32 {
|
||||
guard let cookie = cookie, let offset = offset else { return 0 }
|
||||
let result = fileFromCookie(cookie: cookie).seek(offset: Int(offset.pointee), whence: whence)
|
||||
if result >= 0 {
|
||||
offset.pointee = result
|
||||
return 0
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,133 @@
|
||||
//
|
||||
// Archive+Reading.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2020 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors.
|
||||
// Released under the MIT License.
|
||||
//
|
||||
// See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Archive {
|
||||
/// Read a ZIP `Entry` from the receiver and write it to `url`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - entry: The ZIP `Entry` to read.
|
||||
/// - url: The destination file URL.
|
||||
/// - bufferSize: The maximum size of the read buffer and the decompression buffer (if needed).
|
||||
/// - skipCRC32: Optional flag to skip calculation of the CRC32 checksum to improve performance.
|
||||
/// - progress: A progress object that can be used to track or cancel the extract operation.
|
||||
/// - Returns: The checksum of the processed content or 0 if the `skipCRC32` flag was set to `true`.
|
||||
/// - Throws: An error if the destination file cannot be written or the entry contains malformed content.
|
||||
public func extract(_ entry: Entry, to url: URL, bufferSize: UInt32 = defaultReadChunkSize, skipCRC32: Bool = false,
|
||||
progress: Progress? = nil) throws -> CRC32 {
|
||||
let fileManager = FileManager()
|
||||
var checksum = CRC32(0)
|
||||
switch entry.type {
|
||||
case .file:
|
||||
guard !fileManager.itemExists(at: url) else {
|
||||
throw CocoaError(.fileWriteFileExists, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
try fileManager.createParentDirectoryStructure(for: url)
|
||||
let destinationRepresentation = fileManager.fileSystemRepresentation(withPath: url.path)
|
||||
guard let destinationFile: UnsafeMutablePointer<FILE> = fopen(destinationRepresentation, "wb+") else {
|
||||
throw CocoaError(.fileNoSuchFile)
|
||||
}
|
||||
defer { fclose(destinationFile) }
|
||||
let consumer = { _ = try Data.write(chunk: $0, to: destinationFile) }
|
||||
checksum = try self.extract(entry, bufferSize: bufferSize, skipCRC32: skipCRC32,
|
||||
progress: progress, consumer: consumer)
|
||||
case .directory:
|
||||
let consumer = { (_: Data) in
|
||||
try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
checksum = try self.extract(entry, bufferSize: bufferSize, skipCRC32: skipCRC32,
|
||||
progress: progress, consumer: consumer)
|
||||
case .symlink:
|
||||
guard !fileManager.itemExists(at: url) else {
|
||||
throw CocoaError(.fileWriteFileExists, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
let consumer = { (data: Data) in
|
||||
guard let linkPath = String(data: data, encoding: .utf8) else { throw ArchiveError.invalidEntryPath }
|
||||
try fileManager.createParentDirectoryStructure(for: url)
|
||||
try fileManager.createSymbolicLink(atPath: url.path, withDestinationPath: linkPath)
|
||||
}
|
||||
checksum = try self.extract(entry, bufferSize: bufferSize, skipCRC32: skipCRC32,
|
||||
progress: progress, consumer: consumer)
|
||||
}
|
||||
let attributes = FileManager.attributes(from: entry)
|
||||
try fileManager.setAttributes(attributes, ofItemAtPath: url.path)
|
||||
return checksum
|
||||
}
|
||||
|
||||
/// Read a ZIP `Entry` from the receiver and forward its contents to a `Consumer` closure.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - entry: The ZIP `Entry` to read.
|
||||
/// - bufferSize: The maximum size of the read buffer and the decompression buffer (if needed).
|
||||
/// - skipCRC32: Optional flag to skip calculation of the CRC32 checksum to improve performance.
|
||||
/// - progress: A progress object that can be used to track or cancel the extract operation.
|
||||
/// - consumer: A closure that consumes contents of `Entry` as `Data` chunks.
|
||||
/// - Returns: The checksum of the processed content or 0 if the `skipCRC32` flag was set to `true`..
|
||||
/// - Throws: An error if the destination file cannot be written or the entry contains malformed content.
|
||||
public func extract(_ entry: Entry, bufferSize: UInt32 = defaultReadChunkSize, skipCRC32: Bool = false,
|
||||
progress: Progress? = nil, consumer: Consumer) throws -> CRC32 {
|
||||
var checksum = CRC32(0)
|
||||
let localFileHeader = entry.localFileHeader
|
||||
fseek(self.archiveFile, entry.dataOffset, SEEK_SET)
|
||||
progress?.totalUnitCount = self.totalUnitCountForReading(entry)
|
||||
switch entry.type {
|
||||
case .file:
|
||||
guard let compressionMethod = CompressionMethod(rawValue: localFileHeader.compressionMethod) else {
|
||||
throw ArchiveError.invalidCompressionMethod
|
||||
}
|
||||
switch compressionMethod {
|
||||
case .none: checksum = try self.readUncompressed(entry: entry, bufferSize: bufferSize,
|
||||
skipCRC32: skipCRC32, progress: progress, with: consumer)
|
||||
case .deflate: checksum = try self.readCompressed(entry: entry, bufferSize: bufferSize,
|
||||
skipCRC32: skipCRC32, progress: progress, with: consumer)
|
||||
}
|
||||
case .directory:
|
||||
try consumer(Data())
|
||||
progress?.completedUnitCount = self.totalUnitCountForReading(entry)
|
||||
case .symlink:
|
||||
let localFileHeader = entry.localFileHeader
|
||||
let size = Int(localFileHeader.compressedSize)
|
||||
let data = try Data.readChunk(of: size, from: self.archiveFile)
|
||||
checksum = data.crc32(checksum: 0)
|
||||
try consumer(data)
|
||||
progress?.completedUnitCount = self.totalUnitCountForReading(entry)
|
||||
}
|
||||
return checksum
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func readUncompressed(entry: Entry, bufferSize: UInt32, skipCRC32: Bool,
|
||||
progress: Progress? = nil, with consumer: Consumer) throws -> CRC32 {
|
||||
let size = Int(entry.centralDirectoryStructure.uncompressedSize)
|
||||
return try Data.consumePart(of: size, chunkSize: Int(bufferSize), skipCRC32: skipCRC32,
|
||||
provider: { (_, chunkSize) -> Data in
|
||||
return try Data.readChunk(of: Int(chunkSize), from: self.archiveFile)
|
||||
}, consumer: { (data) in
|
||||
if progress?.isCancelled == true { throw ArchiveError.cancelledOperation }
|
||||
try consumer(data)
|
||||
progress?.completedUnitCount += Int64(data.count)
|
||||
})
|
||||
}
|
||||
|
||||
private func readCompressed(entry: Entry, bufferSize: UInt32, skipCRC32: Bool,
|
||||
progress: Progress? = nil, with consumer: Consumer) throws -> CRC32 {
|
||||
let size = Int(entry.centralDirectoryStructure.compressedSize)
|
||||
return try Data.decompress(size: size, bufferSize: Int(bufferSize), skipCRC32: skipCRC32,
|
||||
provider: { (_, chunkSize) -> Data in
|
||||
return try Data.readChunk(of: chunkSize, from: self.archiveFile)
|
||||
}, consumer: { (data) in
|
||||
if progress?.isCancelled == true { throw ArchiveError.cancelledOperation }
|
||||
try consumer(data)
|
||||
progress?.completedUnitCount += Int64(data.count)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
//
|
||||
// Archive+Writing.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2020 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors.
|
||||
// Released under the MIT License.
|
||||
//
|
||||
// See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Archive {
|
||||
private enum ModifyOperation: Int {
|
||||
case remove = -1
|
||||
case add = 1
|
||||
}
|
||||
|
||||
/// Write files, directories or symlinks to the receiver.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: The path that is used to identify an `Entry` within the `Archive` file.
|
||||
/// - baseURL: The base URL of the `Entry` to add.
|
||||
/// The `baseURL` combined with `path` must form a fully qualified file URL.
|
||||
/// - compressionMethod: Indicates the `CompressionMethod` that should be applied to `Entry`.
|
||||
/// By default, no compression will be applied.
|
||||
/// - bufferSize: The maximum size of the write buffer and the compression buffer (if needed).
|
||||
/// - progress: A progress object that can be used to track or cancel the add operation.
|
||||
/// - Throws: An error if the source file cannot be read or the receiver is not writable.
|
||||
public func addEntry(with path: String, relativeTo baseURL: URL, compressionMethod: CompressionMethod = .none,
|
||||
bufferSize: UInt32 = defaultWriteChunkSize, progress: Progress? = nil) throws {
|
||||
let fileManager = FileManager()
|
||||
let entryURL = baseURL.appendingPathComponent(path)
|
||||
guard fileManager.itemExists(at: entryURL) else {
|
||||
throw CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: entryURL.path])
|
||||
}
|
||||
let type = try FileManager.typeForItem(at: entryURL)
|
||||
// symlinks do not need to be readable
|
||||
guard type == .symlink || fileManager.isReadableFile(atPath: entryURL.path) else {
|
||||
throw CocoaError(.fileReadNoPermission, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
let modDate = try FileManager.fileModificationDateTimeForItem(at: entryURL)
|
||||
let uncompressedSize = type == .directory ? 0 : try FileManager.fileSizeForItem(at: entryURL)
|
||||
let permissions = try FileManager.permissionsForItem(at: entryURL)
|
||||
var provider: Provider
|
||||
switch type {
|
||||
case .file:
|
||||
let entryFileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: entryURL.path)
|
||||
guard let entryFile: UnsafeMutablePointer<FILE> = fopen(entryFileSystemRepresentation, "rb") else {
|
||||
throw CocoaError(.fileNoSuchFile)
|
||||
}
|
||||
defer { fclose(entryFile) }
|
||||
provider = { _, _ in return try Data.readChunk(of: Int(bufferSize), from: entryFile) }
|
||||
try self.addEntry(with: path, type: type, uncompressedSize: uncompressedSize,
|
||||
modificationDate: modDate, permissions: permissions,
|
||||
compressionMethod: compressionMethod, bufferSize: bufferSize,
|
||||
progress: progress, provider: provider)
|
||||
case .directory:
|
||||
provider = { _, _ in return Data() }
|
||||
try self.addEntry(with: path.hasSuffix("/") ? path : path + "/",
|
||||
type: type, uncompressedSize: uncompressedSize,
|
||||
modificationDate: modDate, permissions: permissions,
|
||||
compressionMethod: compressionMethod, bufferSize: bufferSize,
|
||||
progress: progress, provider: provider)
|
||||
case .symlink:
|
||||
provider = { _, _ -> Data in
|
||||
let linkDestination = try fileManager.destinationOfSymbolicLink(atPath: entryURL.path)
|
||||
let linkFileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: linkDestination)
|
||||
let linkLength = Int(strlen(linkFileSystemRepresentation))
|
||||
let linkBuffer = UnsafeBufferPointer(start: linkFileSystemRepresentation, count: linkLength)
|
||||
return Data(buffer: linkBuffer)
|
||||
}
|
||||
try self.addEntry(with: path, type: type, uncompressedSize: uncompressedSize,
|
||||
modificationDate: modDate, permissions: permissions,
|
||||
compressionMethod: compressionMethod, bufferSize: bufferSize,
|
||||
progress: progress, provider: provider)
|
||||
}
|
||||
}
|
||||
|
||||
/// Write files, directories or symlinks to the receiver.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: The path that is used to identify an `Entry` within the `Archive` file.
|
||||
/// - type: Indicates the `Entry.EntryType` of the added content.
|
||||
/// - uncompressedSize: The uncompressed size of the data that is going to be added with `provider`.
|
||||
/// - modificationDate: A `Date` describing the file modification date of the `Entry`.
|
||||
/// Default is the current `Date`.
|
||||
/// - permissions: POSIX file permissions for the `Entry`.
|
||||
/// Default is `0`o`644` for files and symlinks and `0`o`755` for directories.
|
||||
/// - compressionMethod: Indicates the `CompressionMethod` that should be applied to `Entry`.
|
||||
/// By default, no compression will be applied.
|
||||
/// - bufferSize: The maximum size of the write buffer and the compression buffer (if needed).
|
||||
/// - progress: A progress object that can be used to track or cancel the add operation.
|
||||
/// - provider: A closure that accepts a position and a chunk size. Returns a `Data` chunk.
|
||||
/// - Throws: An error if the source data is invalid or the receiver is not writable.
|
||||
public func addEntry(with path: String, type: Entry.EntryType, uncompressedSize: UInt32,
|
||||
modificationDate: Date = Date(), permissions: UInt16? = nil,
|
||||
compressionMethod: CompressionMethod = .none, bufferSize: UInt32 = defaultWriteChunkSize,
|
||||
progress: Progress? = nil, provider: Provider) throws {
|
||||
guard self.accessMode != .read else { throw ArchiveError.unwritableArchive }
|
||||
// Directories and symlinks cannot be compressed
|
||||
let compressionMethod = type == .file ? compressionMethod : .none
|
||||
progress?.totalUnitCount = type == .directory ? defaultDirectoryUnitCount : Int64(uncompressedSize)
|
||||
var endOfCentralDirRecord = self.endOfCentralDirectoryRecord
|
||||
var startOfCD = Int(endOfCentralDirRecord.offsetToStartOfCentralDirectory)
|
||||
fseek(self.archiveFile, startOfCD, SEEK_SET)
|
||||
let existingCentralDirData = try Data.readChunk(of: Int(endOfCentralDirRecord.sizeOfCentralDirectory),
|
||||
from: self.archiveFile)
|
||||
fseek(self.archiveFile, startOfCD, SEEK_SET)
|
||||
let localFileHeaderStart = ftell(self.archiveFile)
|
||||
let modDateTime = modificationDate.fileModificationDateTime
|
||||
defer { fflush(self.archiveFile) }
|
||||
do {
|
||||
var localFileHeader = try self.writeLocalFileHeader(path: path, compressionMethod: compressionMethod,
|
||||
size: (uncompressedSize, 0), checksum: 0,
|
||||
modificationDateTime: modDateTime)
|
||||
let (written, checksum) = try self.writeEntry(localFileHeader: localFileHeader, type: type,
|
||||
compressionMethod: compressionMethod, bufferSize: bufferSize,
|
||||
progress: progress, provider: provider)
|
||||
startOfCD = ftell(self.archiveFile)
|
||||
fseek(self.archiveFile, localFileHeaderStart, SEEK_SET)
|
||||
// Write the local file header a second time. Now with compressedSize (if applicable) and a valid checksum.
|
||||
localFileHeader = try self.writeLocalFileHeader(path: path, compressionMethod: compressionMethod,
|
||||
size: (uncompressedSize, written),
|
||||
checksum: checksum, modificationDateTime: modDateTime)
|
||||
fseek(self.archiveFile, startOfCD, SEEK_SET)
|
||||
_ = try Data.write(chunk: existingCentralDirData, to: self.archiveFile)
|
||||
let permissions = permissions ?? (type == .directory ? defaultDirectoryPermissions :defaultFilePermissions)
|
||||
let externalAttributes = FileManager.externalFileAttributesForEntry(of: type, permissions: permissions)
|
||||
let offset = UInt32(localFileHeaderStart)
|
||||
let centralDir = try self.writeCentralDirectoryStructure(localFileHeader: localFileHeader,
|
||||
relativeOffset: offset,
|
||||
externalFileAttributes: externalAttributes)
|
||||
if startOfCD > UINT32_MAX { throw ArchiveError.invalidStartOfCentralDirectoryOffset }
|
||||
endOfCentralDirRecord = try self.writeEndOfCentralDirectory(centralDirectoryStructure: centralDir,
|
||||
startOfCentralDirectory: UInt32(startOfCD),
|
||||
operation: .add)
|
||||
self.endOfCentralDirectoryRecord = endOfCentralDirRecord
|
||||
} catch ArchiveError.cancelledOperation {
|
||||
try rollback(localFileHeaderStart, existingCentralDirData, endOfCentralDirRecord)
|
||||
throw ArchiveError.cancelledOperation
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a ZIP `Entry` from the receiver.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - entry: The `Entry` to remove.
|
||||
/// - bufferSize: The maximum size for the read and write buffers used during removal.
|
||||
/// - progress: A progress object that can be used to track or cancel the remove operation.
|
||||
/// - Throws: An error if the `Entry` is malformed or the receiver is not writable.
|
||||
public func remove(_ entry: Entry, bufferSize: UInt32 = defaultReadChunkSize, progress: Progress? = nil) throws {
|
||||
let manager = FileManager()
|
||||
let tempDir = self.uniqueTemporaryDirectoryURL()
|
||||
defer { try? manager.removeItem(at: tempDir) }
|
||||
let uniqueString = ProcessInfo.processInfo.globallyUniqueString
|
||||
let tempArchiveURL = tempDir.appendingPathComponent(uniqueString)
|
||||
do { try manager.createParentDirectoryStructure(for: tempArchiveURL) } catch {
|
||||
throw ArchiveError.unwritableArchive }
|
||||
guard let tempArchive = Archive(url: tempArchiveURL, accessMode: .create) else {
|
||||
throw ArchiveError.unwritableArchive
|
||||
}
|
||||
progress?.totalUnitCount = self.totalUnitCountForRemoving(entry)
|
||||
var centralDirectoryData = Data()
|
||||
var offset = 0
|
||||
for currentEntry in self {
|
||||
let centralDirectoryStructure = currentEntry.centralDirectoryStructure
|
||||
if currentEntry != entry {
|
||||
let entryStart = Int(currentEntry.centralDirectoryStructure.relativeOffsetOfLocalHeader)
|
||||
fseek(self.archiveFile, entryStart, SEEK_SET)
|
||||
let provider: Provider = { (_, chunkSize) -> Data in
|
||||
return try Data.readChunk(of: Int(chunkSize), from: self.archiveFile)
|
||||
}
|
||||
let consumer: Consumer = {
|
||||
if progress?.isCancelled == true { throw ArchiveError.cancelledOperation }
|
||||
_ = try Data.write(chunk: $0, to: tempArchive.archiveFile)
|
||||
progress?.completedUnitCount += Int64($0.count)
|
||||
}
|
||||
_ = try Data.consumePart(of: Int(currentEntry.localSize), chunkSize: Int(bufferSize),
|
||||
provider: provider, consumer: consumer)
|
||||
let centralDir = CentralDirectoryStructure(centralDirectoryStructure: centralDirectoryStructure,
|
||||
offset: UInt32(offset))
|
||||
centralDirectoryData.append(centralDir.data)
|
||||
} else { offset = currentEntry.localSize }
|
||||
}
|
||||
let startOfCentralDirectory = ftell(tempArchive.archiveFile)
|
||||
_ = try Data.write(chunk: centralDirectoryData, to: tempArchive.archiveFile)
|
||||
tempArchive.endOfCentralDirectoryRecord = self.endOfCentralDirectoryRecord
|
||||
let endOfCentralDirectoryRecord = try
|
||||
tempArchive.writeEndOfCentralDirectory(centralDirectoryStructure: entry.centralDirectoryStructure,
|
||||
startOfCentralDirectory: UInt32(startOfCentralDirectory),
|
||||
operation: .remove)
|
||||
tempArchive.endOfCentralDirectoryRecord = endOfCentralDirectoryRecord
|
||||
self.endOfCentralDirectoryRecord = endOfCentralDirectoryRecord
|
||||
fflush(tempArchive.archiveFile)
|
||||
try self.replaceCurrentArchiveWithArchive(at: tempArchive.url)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
func uniqueTemporaryDirectoryURL() -> URL {
|
||||
#if swift(>=5.0) || os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
|
||||
if let tempDir = try? FileManager().url(for: .itemReplacementDirectory, in: .userDomainMask,
|
||||
appropriateFor: self.url, create: true) {
|
||||
return tempDir
|
||||
}
|
||||
#endif
|
||||
|
||||
return URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(
|
||||
ProcessInfo.processInfo.globallyUniqueString)
|
||||
}
|
||||
|
||||
func replaceCurrentArchiveWithArchive(at URL: URL) throws {
|
||||
fclose(self.archiveFile)
|
||||
let fileManager = FileManager()
|
||||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
|
||||
do {
|
||||
_ = try fileManager.replaceItemAt(self.url, withItemAt: URL)
|
||||
} catch {
|
||||
_ = try fileManager.removeItem(at: self.url)
|
||||
_ = try fileManager.moveItem(at: URL, to: self.url)
|
||||
}
|
||||
#else
|
||||
_ = try fileManager.removeItem(at: self.url)
|
||||
_ = try fileManager.moveItem(at: URL, to: self.url)
|
||||
#endif
|
||||
let fileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: self.url.path)
|
||||
self.archiveFile = fopen(fileSystemRepresentation, "rb+")
|
||||
}
|
||||
|
||||
private func writeLocalFileHeader(path: String, compressionMethod: CompressionMethod,
|
||||
size: (uncompressed: UInt32, compressed: UInt32),
|
||||
checksum: CRC32,
|
||||
modificationDateTime: (UInt16, UInt16)) throws -> LocalFileHeader {
|
||||
// We always set Bit 11 in generalPurposeBitFlag, which indicates an UTF-8 encoded path.
|
||||
guard let fileNameData = path.data(using: .utf8) else { throw ArchiveError.invalidEntryPath }
|
||||
|
||||
let localFileHeader = LocalFileHeader(versionNeededToExtract: UInt16(20), generalPurposeBitFlag: UInt16(2048),
|
||||
compressionMethod: compressionMethod.rawValue,
|
||||
lastModFileTime: modificationDateTime.1,
|
||||
lastModFileDate: modificationDateTime.0, crc32: checksum,
|
||||
compressedSize: size.compressed, uncompressedSize: size.uncompressed,
|
||||
fileNameLength: UInt16(fileNameData.count), extraFieldLength: UInt16(0),
|
||||
fileNameData: fileNameData, extraFieldData: Data())
|
||||
_ = try Data.write(chunk: localFileHeader.data, to: self.archiveFile)
|
||||
return localFileHeader
|
||||
}
|
||||
|
||||
private func writeEntry(localFileHeader: LocalFileHeader, type: Entry.EntryType,
|
||||
compressionMethod: CompressionMethod, bufferSize: UInt32, progress: Progress? = nil,
|
||||
provider: Provider) throws -> (sizeWritten: UInt32, crc32: CRC32) {
|
||||
var checksum = CRC32(0)
|
||||
var sizeWritten = UInt32(0)
|
||||
switch type {
|
||||
case .file:
|
||||
switch compressionMethod {
|
||||
case .none:
|
||||
(sizeWritten, checksum) = try self.writeUncompressed(size: localFileHeader.uncompressedSize,
|
||||
bufferSize: bufferSize,
|
||||
progress: progress, provider: provider)
|
||||
case .deflate:
|
||||
(sizeWritten, checksum) = try self.writeCompressed(size: localFileHeader.uncompressedSize,
|
||||
bufferSize: bufferSize,
|
||||
progress: progress, provider: provider)
|
||||
}
|
||||
case .directory:
|
||||
_ = try provider(0, 0)
|
||||
if let progress = progress { progress.completedUnitCount = progress.totalUnitCount }
|
||||
case .symlink:
|
||||
(sizeWritten, checksum) = try self.writeSymbolicLink(size: localFileHeader.uncompressedSize,
|
||||
provider: provider)
|
||||
if let progress = progress { progress.completedUnitCount = progress.totalUnitCount }
|
||||
}
|
||||
return (sizeWritten, checksum)
|
||||
}
|
||||
|
||||
private func writeUncompressed(size: UInt32, bufferSize: UInt32, progress: Progress? = nil,
|
||||
provider: Provider) throws -> (sizeWritten: UInt32, checksum: CRC32) {
|
||||
var position = 0
|
||||
var sizeWritten = 0
|
||||
var checksum = CRC32(0)
|
||||
while position < size {
|
||||
if progress?.isCancelled == true { throw ArchiveError.cancelledOperation }
|
||||
let readSize = (Int(size) - position) >= bufferSize ? Int(bufferSize) : (Int(size) - position)
|
||||
let entryChunk = try provider(Int(position), Int(readSize))
|
||||
checksum = entryChunk.crc32(checksum: checksum)
|
||||
sizeWritten += try Data.write(chunk: entryChunk, to: self.archiveFile)
|
||||
position += Int(bufferSize)
|
||||
progress?.completedUnitCount = Int64(sizeWritten)
|
||||
}
|
||||
return (UInt32(sizeWritten), checksum)
|
||||
}
|
||||
|
||||
private func writeCompressed(size: UInt32, bufferSize: UInt32, progress: Progress? = nil,
|
||||
provider: Provider) throws -> (sizeWritten: UInt32, checksum: CRC32) {
|
||||
var sizeWritten = 0
|
||||
let consumer: Consumer = { data in sizeWritten += try Data.write(chunk: data, to: self.archiveFile) }
|
||||
let checksum = try Data.compress(size: Int(size), bufferSize: Int(bufferSize),
|
||||
provider: { (position, size) -> Data in
|
||||
if progress?.isCancelled == true { throw ArchiveError.cancelledOperation }
|
||||
let data = try provider(position, size)
|
||||
progress?.completedUnitCount += Int64(data.count)
|
||||
return data
|
||||
}, consumer: consumer)
|
||||
return(UInt32(sizeWritten), checksum)
|
||||
}
|
||||
|
||||
private func writeSymbolicLink(size: UInt32, provider: Provider) throws -> (sizeWritten: UInt32, checksum: CRC32) {
|
||||
let linkData = try provider(0, Int(size))
|
||||
let checksum = linkData.crc32(checksum: 0)
|
||||
let sizeWritten = try Data.write(chunk: linkData, to: self.archiveFile)
|
||||
return (UInt32(sizeWritten), checksum)
|
||||
}
|
||||
|
||||
private func writeCentralDirectoryStructure(localFileHeader: LocalFileHeader, relativeOffset: UInt32,
|
||||
externalFileAttributes: UInt32) throws -> CentralDirectoryStructure {
|
||||
let centralDirectory = CentralDirectoryStructure(localFileHeader: localFileHeader,
|
||||
fileAttributes: externalFileAttributes,
|
||||
relativeOffset: relativeOffset)
|
||||
_ = try Data.write(chunk: centralDirectory.data, to: self.archiveFile)
|
||||
return centralDirectory
|
||||
}
|
||||
|
||||
private func writeEndOfCentralDirectory(centralDirectoryStructure: CentralDirectoryStructure,
|
||||
startOfCentralDirectory: UInt32,
|
||||
operation: ModifyOperation) throws -> EndOfCentralDirectoryRecord {
|
||||
var record = self.endOfCentralDirectoryRecord
|
||||
let countChange = operation.rawValue
|
||||
var dataLength = Int(centralDirectoryStructure.extraFieldLength)
|
||||
dataLength += Int(centralDirectoryStructure.fileNameLength)
|
||||
dataLength += Int(centralDirectoryStructure.fileCommentLength)
|
||||
let centralDirectoryDataLengthChange = operation.rawValue * (dataLength + CentralDirectoryStructure.size)
|
||||
var updatedSizeOfCentralDirectory = Int(record.sizeOfCentralDirectory)
|
||||
updatedSizeOfCentralDirectory += centralDirectoryDataLengthChange
|
||||
let numberOfEntriesOnDisk = UInt16(Int(record.totalNumberOfEntriesOnDisk) + countChange)
|
||||
let numberOfEntriesInCentralDirectory = UInt16(Int(record.totalNumberOfEntriesInCentralDirectory) + countChange)
|
||||
record = EndOfCentralDirectoryRecord(record: record, numberOfEntriesOnDisk: numberOfEntriesOnDisk,
|
||||
numberOfEntriesInCentralDirectory: numberOfEntriesInCentralDirectory,
|
||||
updatedSizeOfCentralDirectory: UInt32(updatedSizeOfCentralDirectory),
|
||||
startOfCentralDirectory: startOfCentralDirectory)
|
||||
_ = try Data.write(chunk: record.data, to: self.archiveFile)
|
||||
return record
|
||||
}
|
||||
|
||||
private func rollback(_ localFileHeaderStart: Int,
|
||||
_ existingCentralDirectoryData: Data,
|
||||
_ endOfCentralDirRecord: EndOfCentralDirectoryRecord) throws {
|
||||
fflush(self.archiveFile)
|
||||
ftruncate(fileno(self.archiveFile), off_t(localFileHeaderStart))
|
||||
fseek(self.archiveFile, localFileHeaderStart, SEEK_SET)
|
||||
_ = try Data.write(chunk: existingCentralDirectoryData, to: self.archiveFile)
|
||||
_ = try Data.write(chunk: endOfCentralDirRecord.data, to: self.archiveFile)
|
||||
}
|
||||
}
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
//
|
||||
// Archive.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2020 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors.
|
||||
// Released under the MIT License.
|
||||
//
|
||||
// See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// The default chunk size when reading entry data from an archive.
|
||||
public let defaultReadChunkSize = UInt32(16*1024)
|
||||
/// The default chunk size when writing entry data to an archive.
|
||||
public let defaultWriteChunkSize = defaultReadChunkSize
|
||||
/// The default permissions for newly added entries.
|
||||
public let defaultFilePermissions = UInt16(0o644)
|
||||
public let defaultDirectoryPermissions = UInt16(0o755)
|
||||
let defaultPOSIXBufferSize = defaultReadChunkSize
|
||||
let defaultDirectoryUnitCount = Int64(1)
|
||||
let minDirectoryEndOffset = 22
|
||||
let maxDirectoryEndOffset = 66000
|
||||
let endOfCentralDirectoryStructSignature = 0x06054b50
|
||||
let localFileHeaderStructSignature = 0x04034b50
|
||||
let dataDescriptorStructSignature = 0x08074b50
|
||||
let centralDirectoryStructSignature = 0x02014b50
|
||||
|
||||
/// The compression method of an `Entry` in a ZIP `Archive`.
|
||||
public enum CompressionMethod: UInt16 {
|
||||
/// Indicates that an `Entry` has no compression applied to its contents.
|
||||
case none = 0
|
||||
/// Indicates that contents of an `Entry` have been compressed with a zlib compatible Deflate algorithm.
|
||||
case deflate = 8
|
||||
}
|
||||
|
||||
/// A sequence of uncompressed or compressed ZIP entries.
|
||||
///
|
||||
/// You use an `Archive` to create, read or update ZIP files.
|
||||
/// To read an existing ZIP file, you have to pass in an existing file `URL` and `AccessMode.read`:
|
||||
///
|
||||
/// var archiveURL = URL(fileURLWithPath: "/path/file.zip")
|
||||
/// var archive = Archive(url: archiveURL, accessMode: .read)
|
||||
///
|
||||
/// An `Archive` is a sequence of entries. You can
|
||||
/// iterate over an archive using a `for`-`in` loop to get access to individual `Entry` objects:
|
||||
///
|
||||
/// for entry in archive {
|
||||
/// print(entry.path)
|
||||
/// }
|
||||
///
|
||||
/// Each `Entry` in an `Archive` is represented by its `path`. You can
|
||||
/// use `path` to retrieve the corresponding `Entry` from an `Archive` via subscripting:
|
||||
///
|
||||
/// let entry = archive['/path/file.txt']
|
||||
///
|
||||
/// To create a new `Archive`, pass in a non-existing file URL and `AccessMode.create`. To modify an
|
||||
/// existing `Archive` use `AccessMode.update`:
|
||||
///
|
||||
/// var archiveURL = URL(fileURLWithPath: "/path/file.zip")
|
||||
/// var archive = Archive(url: archiveURL, accessMode: .update)
|
||||
/// try archive?.addEntry("test.txt", relativeTo: baseURL, compressionMethod: .deflate)
|
||||
public final class Archive: Sequence {
|
||||
typealias LocalFileHeader = Entry.LocalFileHeader
|
||||
typealias DataDescriptor = Entry.DataDescriptor
|
||||
typealias CentralDirectoryStructure = Entry.CentralDirectoryStructure
|
||||
|
||||
/// An error that occurs during reading, creating or updating a ZIP file.
|
||||
public enum ArchiveError: Error {
|
||||
/// Thrown when an archive file is either damaged or inaccessible.
|
||||
case unreadableArchive
|
||||
/// Thrown when an archive is either opened with AccessMode.read or the destination file is unwritable.
|
||||
case unwritableArchive
|
||||
/// Thrown when the path of an `Entry` cannot be stored in an archive.
|
||||
case invalidEntryPath
|
||||
/// Thrown when an `Entry` can't be stored in the archive with the proposed compression method.
|
||||
case invalidCompressionMethod
|
||||
/// Thrown when the start of the central directory exceeds `UINT32_MAX`
|
||||
case invalidStartOfCentralDirectoryOffset
|
||||
/// Thrown when an archive does not contain the required End of Central Directory Record.
|
||||
case missingEndOfCentralDirectoryRecord
|
||||
/// Thrown when an extract, add or remove operation was canceled.
|
||||
case cancelledOperation
|
||||
}
|
||||
|
||||
/// The access mode for an `Archive`.
|
||||
public enum AccessMode: UInt {
|
||||
/// Indicates that a newly instantiated `Archive` should create its backing file.
|
||||
case create
|
||||
/// Indicates that a newly instantiated `Archive` should read from an existing backing file.
|
||||
case read
|
||||
/// Indicates that a newly instantiated `Archive` should update an existing backing file.
|
||||
case update
|
||||
}
|
||||
|
||||
struct EndOfCentralDirectoryRecord: DataSerializable {
|
||||
let endOfCentralDirectorySignature = UInt32(endOfCentralDirectoryStructSignature)
|
||||
let numberOfDisk: UInt16
|
||||
let numberOfDiskStart: UInt16
|
||||
let totalNumberOfEntriesOnDisk: UInt16
|
||||
let totalNumberOfEntriesInCentralDirectory: UInt16
|
||||
let sizeOfCentralDirectory: UInt32
|
||||
let offsetToStartOfCentralDirectory: UInt32
|
||||
let zipFileCommentLength: UInt16
|
||||
let zipFileCommentData: Data
|
||||
static let size = 22
|
||||
}
|
||||
|
||||
private var preferredEncoding: String.Encoding?
|
||||
/// URL of an Archive's backing file.
|
||||
public let url: URL
|
||||
/// Access mode for an archive file.
|
||||
public let accessMode: AccessMode
|
||||
var archiveFile: UnsafeMutablePointer<FILE>
|
||||
var endOfCentralDirectoryRecord: EndOfCentralDirectoryRecord
|
||||
|
||||
/// Initializes a new ZIP `Archive`.
|
||||
///
|
||||
/// You can use this initalizer to create new archive files or to read and update existing ones.
|
||||
/// The `mode` parameter indicates the intended usage of the archive: `.read`, `.create` or `.update`.
|
||||
/// - Parameters:
|
||||
/// - url: File URL to the receivers backing file.
|
||||
/// - mode: Access mode of the receiver.
|
||||
/// - preferredEncoding: Encoding for entry paths. Overrides the encoding specified in the archive.
|
||||
/// This encoding is only used when _decoding_ paths from the receiver.
|
||||
/// Paths of entries added with `addEntry` are always UTF-8 encoded.
|
||||
/// - Returns: An archive initialized with a backing file at the passed in file URL and the given access mode
|
||||
/// or `nil` if the following criteria are not met:
|
||||
/// - Note:
|
||||
/// - The file URL _must_ point to an existing file for `AccessMode.read`.
|
||||
/// - The file URL _must_ point to a non-existing file for `AccessMode.create`.
|
||||
/// - The file URL _must_ point to an existing file for `AccessMode.update`.
|
||||
public init?(url: URL, accessMode mode: AccessMode, preferredEncoding: String.Encoding? = nil) {
|
||||
self.url = url
|
||||
self.accessMode = mode
|
||||
self.preferredEncoding = preferredEncoding
|
||||
guard let (archiveFile, endOfCentralDirectoryRecord) = Archive.configureFileBacking(for: url, mode: mode) else {
|
||||
return nil
|
||||
}
|
||||
self.archiveFile = archiveFile
|
||||
self.endOfCentralDirectoryRecord = endOfCentralDirectoryRecord
|
||||
setvbuf(self.archiveFile, nil, _IOFBF, Int(defaultPOSIXBufferSize))
|
||||
}
|
||||
|
||||
#if swift(>=5.0)
|
||||
var memoryFile: MemoryFile?
|
||||
|
||||
/// Initializes a new in-memory ZIP `Archive`.
|
||||
///
|
||||
/// You can use this initalizer to create new in-memory archive files or to read and update existing ones.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - data: `Data` object used as backing for in-memory archives.
|
||||
/// - mode: Access mode of the receiver.
|
||||
/// - preferredEncoding: Encoding for entry paths. Overrides the encoding specified in the archive.
|
||||
/// This encoding is only used when _decoding_ paths from the receiver.
|
||||
/// Paths of entries added with `addEntry` are always UTF-8 encoded.
|
||||
/// - Returns: An in-memory archive initialized with passed in backing data.
|
||||
/// - Note:
|
||||
/// - The backing `data` _must_ contain a valid ZIP archive for `AccessMode.read` and `AccessMode.update`.
|
||||
/// - The backing `data` _must_ be empty (or omitted) for `AccessMode.create`.
|
||||
public init?(data: Data = Data(), accessMode mode: AccessMode, preferredEncoding: String.Encoding? = nil) {
|
||||
guard let url = URL(string: "memory:"),
|
||||
let (archiveFile, memoryFile) = Archive.configureMemoryBacking(for: data, mode: mode) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.url = url
|
||||
self.accessMode = mode
|
||||
self.preferredEncoding = preferredEncoding
|
||||
self.archiveFile = archiveFile
|
||||
self.memoryFile = memoryFile
|
||||
guard let endOfCentralDirectoryRecord = Archive.scanForEndOfCentralDirectoryRecord(in: archiveFile)
|
||||
else {
|
||||
fclose(self.archiveFile)
|
||||
return nil
|
||||
}
|
||||
self.endOfCentralDirectoryRecord = endOfCentralDirectoryRecord
|
||||
}
|
||||
#endif
|
||||
|
||||
deinit {
|
||||
fclose(self.archiveFile)
|
||||
}
|
||||
|
||||
public func makeIterator() -> AnyIterator<Entry> {
|
||||
let endOfCentralDirectoryRecord = self.endOfCentralDirectoryRecord
|
||||
var directoryIndex = Int(endOfCentralDirectoryRecord.offsetToStartOfCentralDirectory)
|
||||
var index = 0
|
||||
return AnyIterator {
|
||||
guard index < Int(endOfCentralDirectoryRecord.totalNumberOfEntriesInCentralDirectory) else { return nil }
|
||||
guard let centralDirStruct: CentralDirectoryStructure = Data.readStruct(from: self.archiveFile,
|
||||
at: directoryIndex) else {
|
||||
return nil
|
||||
}
|
||||
let offset = Int(centralDirStruct.relativeOffsetOfLocalHeader)
|
||||
guard let localFileHeader: LocalFileHeader = Data.readStruct(from: self.archiveFile,
|
||||
at: offset) else { return nil }
|
||||
var dataDescriptor: DataDescriptor?
|
||||
if centralDirStruct.usesDataDescriptor {
|
||||
let additionalSize = Int(localFileHeader.fileNameLength) + Int(localFileHeader.extraFieldLength)
|
||||
let isCompressed = centralDirStruct.compressionMethod != CompressionMethod.none.rawValue
|
||||
let dataSize = isCompressed ? centralDirStruct.compressedSize : centralDirStruct.uncompressedSize
|
||||
let descriptorPosition = offset + LocalFileHeader.size + additionalSize + Int(dataSize)
|
||||
dataDescriptor = Data.readStruct(from: self.archiveFile, at: descriptorPosition)
|
||||
}
|
||||
defer {
|
||||
directoryIndex += CentralDirectoryStructure.size
|
||||
directoryIndex += Int(centralDirStruct.fileNameLength)
|
||||
directoryIndex += Int(centralDirStruct.extraFieldLength)
|
||||
directoryIndex += Int(centralDirStruct.fileCommentLength)
|
||||
index += 1
|
||||
}
|
||||
return Entry(centralDirectoryStructure: centralDirStruct,
|
||||
localFileHeader: localFileHeader, dataDescriptor: dataDescriptor)
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve the ZIP `Entry` with the given `path` from the receiver.
|
||||
///
|
||||
/// - Note: The ZIP file format specification does not enforce unique paths for entries.
|
||||
/// Therefore an archive can contain multiple entries with the same path. This method
|
||||
/// always returns the first `Entry` with the given `path`.
|
||||
///
|
||||
/// - Parameter path: A relative file path identifiying the corresponding `Entry`.
|
||||
/// - Returns: An `Entry` with the given `path`. Otherwise, `nil`.
|
||||
public subscript(path: String) -> Entry? {
|
||||
if let encoding = preferredEncoding {
|
||||
return self.first { $0.path(using: encoding) == path }
|
||||
}
|
||||
return self.first { $0.path == path }
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private static func configureFileBacking(for url: URL, mode: AccessMode)
|
||||
-> (UnsafeMutablePointer<FILE>, EndOfCentralDirectoryRecord)? {
|
||||
let fileManager = FileManager()
|
||||
switch mode {
|
||||
case .read:
|
||||
let fileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: url.path)
|
||||
guard let archiveFile = fopen(fileSystemRepresentation, "rb"),
|
||||
let endOfCentralDirectoryRecord = Archive.scanForEndOfCentralDirectoryRecord(in: archiveFile) else {
|
||||
return nil
|
||||
}
|
||||
return (archiveFile, endOfCentralDirectoryRecord)
|
||||
case .create:
|
||||
let endOfCentralDirectoryRecord = EndOfCentralDirectoryRecord(numberOfDisk: 0, numberOfDiskStart: 0,
|
||||
totalNumberOfEntriesOnDisk: 0,
|
||||
totalNumberOfEntriesInCentralDirectory: 0,
|
||||
sizeOfCentralDirectory: 0,
|
||||
offsetToStartOfCentralDirectory: 0,
|
||||
zipFileCommentLength: 0,
|
||||
zipFileCommentData: Data())
|
||||
do {
|
||||
try endOfCentralDirectoryRecord.data.write(to: url, options: .withoutOverwriting)
|
||||
} catch { return nil }
|
||||
fallthrough
|
||||
case .update:
|
||||
let fileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: url.path)
|
||||
guard let archiveFile = fopen(fileSystemRepresentation, "rb+"),
|
||||
let endOfCentralDirectoryRecord = Archive.scanForEndOfCentralDirectoryRecord(in: archiveFile) else {
|
||||
return nil
|
||||
}
|
||||
fseek(archiveFile, 0, SEEK_SET)
|
||||
return (archiveFile, endOfCentralDirectoryRecord)
|
||||
}
|
||||
}
|
||||
|
||||
private static func scanForEndOfCentralDirectoryRecord(in file: UnsafeMutablePointer<FILE>)
|
||||
-> EndOfCentralDirectoryRecord? {
|
||||
var directoryEnd = 0
|
||||
var index = minDirectoryEndOffset
|
||||
fseek(file, 0, SEEK_END)
|
||||
let archiveLength = ftell(file)
|
||||
while directoryEnd == 0 && index < maxDirectoryEndOffset && index <= archiveLength {
|
||||
fseek(file, archiveLength - index, SEEK_SET)
|
||||
var potentialDirectoryEndTag: UInt32 = UInt32()
|
||||
fread(&potentialDirectoryEndTag, 1, MemoryLayout<UInt32>.size, file)
|
||||
if potentialDirectoryEndTag == UInt32(endOfCentralDirectoryStructSignature) {
|
||||
directoryEnd = archiveLength - index
|
||||
return Data.readStruct(from: file, at: directoryEnd)
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Archive {
|
||||
/// The number of the work units that have to be performed when
|
||||
/// removing `entry` from the receiver.
|
||||
///
|
||||
/// - Parameter entry: The entry that will be removed.
|
||||
/// - Returns: The number of the work units.
|
||||
public func totalUnitCountForRemoving(_ entry: Entry) -> Int64 {
|
||||
return Int64(self.endOfCentralDirectoryRecord.offsetToStartOfCentralDirectory
|
||||
- UInt32(entry.localSize))
|
||||
}
|
||||
|
||||
func makeProgressForRemoving(_ entry: Entry) -> Progress {
|
||||
return Progress(totalUnitCount: self.totalUnitCountForRemoving(entry))
|
||||
}
|
||||
|
||||
/// The number of the work units that have to be performed when
|
||||
/// reading `entry` from the receiver.
|
||||
///
|
||||
/// - Parameter entry: The entry that will be read.
|
||||
/// - Returns: The number of the work units.
|
||||
public func totalUnitCountForReading(_ entry: Entry) -> Int64 {
|
||||
switch entry.type {
|
||||
case .file, .symlink:
|
||||
return Int64(entry.uncompressedSize)
|
||||
case .directory:
|
||||
return defaultDirectoryUnitCount
|
||||
}
|
||||
}
|
||||
|
||||
func makeProgressForReading(_ entry: Entry) -> Progress {
|
||||
return Progress(totalUnitCount: self.totalUnitCountForReading(entry))
|
||||
}
|
||||
|
||||
/// The number of the work units that have to be performed when
|
||||
/// adding the file at `url` to the receiver.
|
||||
/// - Parameter entry: The entry that will be removed.
|
||||
/// - Returns: The number of the work units.
|
||||
public func totalUnitCountForAddingItem(at url: URL) -> Int64 {
|
||||
var count = Int64(0)
|
||||
do {
|
||||
let type = try FileManager.typeForItem(at: url)
|
||||
switch type {
|
||||
case .file, .symlink:
|
||||
count = Int64(try FileManager.fileSizeForItem(at: url))
|
||||
case .directory:
|
||||
count = defaultDirectoryUnitCount
|
||||
}
|
||||
} catch { count = -1 }
|
||||
return count
|
||||
}
|
||||
|
||||
func makeProgressForAddingItem(at url: URL) -> Progress {
|
||||
return Progress(totalUnitCount: self.totalUnitCountForAddingItem(at: url))
|
||||
}
|
||||
}
|
||||
|
||||
extension Archive.EndOfCentralDirectoryRecord {
|
||||
var data: Data {
|
||||
var endOfCDSignature = self.endOfCentralDirectorySignature
|
||||
var numberOfDisk = self.numberOfDisk
|
||||
var numberOfDiskStart = self.numberOfDiskStart
|
||||
var totalNumberOfEntriesOnDisk = self.totalNumberOfEntriesOnDisk
|
||||
var totalNumberOfEntriesInCD = self.totalNumberOfEntriesInCentralDirectory
|
||||
var sizeOfCentralDirectory = self.sizeOfCentralDirectory
|
||||
var offsetToStartOfCD = self.offsetToStartOfCentralDirectory
|
||||
var zipFileCommentLength = self.zipFileCommentLength
|
||||
var data = Data()
|
||||
withUnsafePointer(to: &endOfCDSignature, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &numberOfDisk, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &numberOfDiskStart, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &totalNumberOfEntriesOnDisk, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &totalNumberOfEntriesInCD, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &sizeOfCentralDirectory, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &offsetToStartOfCD, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &zipFileCommentLength, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
data.append(self.zipFileCommentData)
|
||||
return data
|
||||
}
|
||||
|
||||
init?(data: Data, additionalDataProvider provider: (Int) throws -> Data) {
|
||||
guard data.count == Archive.EndOfCentralDirectoryRecord.size else { return nil }
|
||||
guard data.scanValue(start: 0) == endOfCentralDirectorySignature else { return nil }
|
||||
self.numberOfDisk = data.scanValue(start: 4)
|
||||
self.numberOfDiskStart = data.scanValue(start: 6)
|
||||
self.totalNumberOfEntriesOnDisk = data.scanValue(start: 8)
|
||||
self.totalNumberOfEntriesInCentralDirectory = data.scanValue(start: 10)
|
||||
self.sizeOfCentralDirectory = data.scanValue(start: 12)
|
||||
self.offsetToStartOfCentralDirectory = data.scanValue(start: 16)
|
||||
self.zipFileCommentLength = data.scanValue(start: 20)
|
||||
guard let commentData = try? provider(Int(self.zipFileCommentLength)) else { return nil }
|
||||
guard commentData.count == Int(self.zipFileCommentLength) else { return nil }
|
||||
self.zipFileCommentData = commentData
|
||||
}
|
||||
|
||||
init(record: Archive.EndOfCentralDirectoryRecord,
|
||||
numberOfEntriesOnDisk: UInt16,
|
||||
numberOfEntriesInCentralDirectory: UInt16,
|
||||
updatedSizeOfCentralDirectory: UInt32,
|
||||
startOfCentralDirectory: UInt32) {
|
||||
numberOfDisk = record.numberOfDisk
|
||||
numberOfDiskStart = record.numberOfDiskStart
|
||||
totalNumberOfEntriesOnDisk = numberOfEntriesOnDisk
|
||||
totalNumberOfEntriesInCentralDirectory = numberOfEntriesInCentralDirectory
|
||||
sizeOfCentralDirectory = updatedSizeOfCentralDirectory
|
||||
offsetToStartOfCentralDirectory = startOfCentralDirectory
|
||||
zipFileCommentLength = record.zipFileCommentLength
|
||||
zipFileCommentData = record.zipFileCommentData
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
//
|
||||
// Data+Compression.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2020 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors.
|
||||
// Released under the MIT License.
|
||||
//
|
||||
// See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// An unsigned 32-Bit Integer representing a checksum.
|
||||
public typealias CRC32 = UInt32
|
||||
/// A custom handler that consumes a `Data` object containing partial entry data.
|
||||
/// - Parameters:
|
||||
/// - data: A chunk of `Data` to consume.
|
||||
/// - Throws: Can throw to indicate errors during data consumption.
|
||||
public typealias Consumer = (_ data: Data) throws -> Void
|
||||
/// A custom handler that receives a position and a size that can be used to provide data from an arbitrary source.
|
||||
/// - Parameters:
|
||||
/// - position: The current read position.
|
||||
/// - size: The size of the chunk to provide.
|
||||
/// - Returns: A chunk of `Data`.
|
||||
/// - Throws: Can throw to indicate errors in the data source.
|
||||
public typealias Provider = (_ position: Int, _ size: Int) throws -> Data
|
||||
|
||||
/// The lookup table used to calculate `CRC32` checksums.
|
||||
public let crcTable: [UInt32] = [
|
||||
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419,
|
||||
0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4,
|
||||
0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07,
|
||||
0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,
|
||||
0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856,
|
||||
0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
|
||||
0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4,
|
||||
0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
|
||||
0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3,
|
||||
0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, 0x51de003a,
|
||||
0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599,
|
||||
0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
|
||||
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190,
|
||||
0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f,
|
||||
0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e,
|
||||
0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
|
||||
0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed,
|
||||
0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
|
||||
0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3,
|
||||
0xfbd44c65, 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
|
||||
0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a,
|
||||
0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5,
|
||||
0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010,
|
||||
0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
|
||||
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17,
|
||||
0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6,
|
||||
0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615,
|
||||
0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
|
||||
0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344,
|
||||
0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
|
||||
0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a,
|
||||
0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
|
||||
0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1,
|
||||
0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xd80d2bda, 0xaf0a1b4c,
|
||||
0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef,
|
||||
0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
|
||||
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe,
|
||||
0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31,
|
||||
0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c,
|
||||
0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
|
||||
0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b,
|
||||
0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
|
||||
0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1,
|
||||
0x18b74777, 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
|
||||
0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278,
|
||||
0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7,
|
||||
0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66,
|
||||
0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
|
||||
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605,
|
||||
0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8,
|
||||
0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b,
|
||||
0x2d02ef8d]
|
||||
|
||||
extension Data {
|
||||
enum CompressionError: Error {
|
||||
case invalidStream
|
||||
case corruptedData
|
||||
}
|
||||
|
||||
/// Calculate the `CRC32` checksum of the receiver.
|
||||
///
|
||||
/// - Parameter checksum: The starting seed.
|
||||
/// - Returns: The checksum calcualted from the bytes of the receiver and the starting seed.
|
||||
public func crc32(checksum: CRC32) -> CRC32 {
|
||||
// The typecast is necessary on 32-bit platforms because of
|
||||
// https://bugs.swift.org/browse/SR-1774
|
||||
let mask = 0xffffffff as UInt32
|
||||
let bufferSize = self.count/MemoryLayout<UInt8>.size
|
||||
var result = checksum ^ mask
|
||||
#if swift(>=5.0)
|
||||
crcTable.withUnsafeBufferPointer { crcTablePointer in
|
||||
self.withUnsafeBytes { bufferPointer in
|
||||
let bytePointer = bufferPointer.bindMemory(to: UInt8.self)
|
||||
for bufferIndex in 0..<bufferSize {
|
||||
let byte = bytePointer[bufferIndex]
|
||||
let index = Int((result ^ UInt32(byte)) & 0xff)
|
||||
result = (result >> 8) ^ crcTablePointer[index]
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
self.withUnsafeBytes { (bytes) in
|
||||
let bins = stride(from: 0, to: bufferSize, by: 256)
|
||||
for bin in bins {
|
||||
for binIndex in 0..<256 {
|
||||
let byteIndex = bin + binIndex
|
||||
guard byteIndex < bufferSize else { break }
|
||||
|
||||
let byte = bytes[byteIndex]
|
||||
let index = Int((result ^ UInt32(byte)) & 0xff)
|
||||
result = (result >> 8) ^ crcTable[index]
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return result ^ mask
|
||||
}
|
||||
|
||||
/// Compress the output of `provider` and pass it to `consumer`.
|
||||
/// - Parameters:
|
||||
/// - size: The uncompressed size of the data to be compressed.
|
||||
/// - bufferSize: The maximum size of the compression buffer.
|
||||
/// - provider: A closure that accepts a position and a chunk size. Returns a `Data` chunk.
|
||||
/// - consumer: A closure that processes the result of the compress operation.
|
||||
/// - Returns: The checksum of the processed content.
|
||||
public static func compress(size: Int, bufferSize: Int, provider: Provider, consumer: Consumer) throws -> CRC32 {
|
||||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
|
||||
return try self.process(operation: COMPRESSION_STREAM_ENCODE, size: size, bufferSize: bufferSize,
|
||||
provider: provider, consumer: consumer)
|
||||
#else
|
||||
return try self.encode(size: size, bufferSize: bufferSize, provider: provider, consumer: consumer)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Decompress the output of `provider` and pass it to `consumer`.
|
||||
/// - Parameters:
|
||||
/// - size: The compressed size of the data to be decompressed.
|
||||
/// - bufferSize: The maximum size of the decompression buffer.
|
||||
/// - skipCRC32: Optional flag to skip calculation of the CRC32 checksum to improve performance.
|
||||
/// - provider: A closure that accepts a position and a chunk size. Returns a `Data` chunk.
|
||||
/// - consumer: A closure that processes the result of the decompress operation.
|
||||
/// - Returns: The checksum of the processed content.
|
||||
public static func decompress(size: Int, bufferSize: Int, skipCRC32: Bool,
|
||||
provider: Provider, consumer: Consumer) throws -> CRC32 {
|
||||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
|
||||
return try self.process(operation: COMPRESSION_STREAM_DECODE, size: size, bufferSize: bufferSize,
|
||||
skipCRC32: skipCRC32, provider: provider, consumer: consumer)
|
||||
#else
|
||||
return try self.decode(bufferSize: bufferSize, skipCRC32: skipCRC32, provider: provider, consumer: consumer)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Apple Platforms
|
||||
|
||||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
|
||||
import Compression
|
||||
|
||||
extension Data {
|
||||
static func process(operation: compression_stream_operation, size: Int, bufferSize: Int, skipCRC32: Bool = false,
|
||||
provider: Provider, consumer: Consumer) throws -> CRC32 {
|
||||
var crc32 = CRC32(0)
|
||||
let destPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
|
||||
defer { destPointer.deallocate() }
|
||||
let streamPointer = UnsafeMutablePointer<compression_stream>.allocate(capacity: 1)
|
||||
defer { streamPointer.deallocate() }
|
||||
var stream = streamPointer.pointee
|
||||
var status = compression_stream_init(&stream, operation, COMPRESSION_ZLIB)
|
||||
guard status != COMPRESSION_STATUS_ERROR else { throw CompressionError.invalidStream }
|
||||
defer { compression_stream_destroy(&stream) }
|
||||
stream.src_size = 0
|
||||
stream.dst_ptr = destPointer
|
||||
stream.dst_size = bufferSize
|
||||
var position = 0
|
||||
var sourceData: Data?
|
||||
repeat {
|
||||
if stream.src_size == 0 {
|
||||
do {
|
||||
sourceData = try provider(position, Swift.min((size - position), bufferSize))
|
||||
if let sourceData = sourceData {
|
||||
position += sourceData.count
|
||||
stream.src_size = sourceData.count
|
||||
}
|
||||
} catch { throw error }
|
||||
}
|
||||
if let sourceData = sourceData {
|
||||
sourceData.withUnsafeBytes { (rawBufferPointer) in
|
||||
if let baseAddress = rawBufferPointer.baseAddress {
|
||||
let pointer = baseAddress.assumingMemoryBound(to: UInt8.self)
|
||||
stream.src_ptr = pointer.advanced(by: sourceData.count - stream.src_size)
|
||||
let flags = sourceData.count < bufferSize ? Int32(COMPRESSION_STREAM_FINALIZE.rawValue) : 0
|
||||
status = compression_stream_process(&stream, flags)
|
||||
}
|
||||
}
|
||||
if operation == COMPRESSION_STREAM_ENCODE && !skipCRC32 { crc32 = sourceData.crc32(checksum: crc32) }
|
||||
}
|
||||
switch status {
|
||||
case COMPRESSION_STATUS_OK, COMPRESSION_STATUS_END:
|
||||
let outputData = Data(bytesNoCopy: destPointer, count: bufferSize - stream.dst_size, deallocator: .none)
|
||||
try consumer(outputData)
|
||||
if operation == COMPRESSION_STREAM_DECODE && !skipCRC32 { crc32 = outputData.crc32(checksum: crc32) }
|
||||
stream.dst_ptr = destPointer
|
||||
stream.dst_size = bufferSize
|
||||
default: throw CompressionError.corruptedData
|
||||
}
|
||||
} while status == COMPRESSION_STATUS_OK
|
||||
return crc32
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Linux
|
||||
|
||||
#else
|
||||
import CZlib
|
||||
|
||||
extension Data {
|
||||
static func encode(size: Int, bufferSize: Int, provider: Provider, consumer: Consumer) throws -> CRC32 {
|
||||
var stream = z_stream()
|
||||
let streamSize = Int32(MemoryLayout<z_stream>.size)
|
||||
var result = deflateInit2_(&stream, Z_DEFAULT_COMPRESSION,
|
||||
Z_DEFLATED, -MAX_WBITS, 9, Z_DEFAULT_STRATEGY, ZLIB_VERSION, streamSize)
|
||||
defer { deflateEnd(&stream) }
|
||||
guard result == Z_OK else { throw CompressionError.invalidStream }
|
||||
var flush = Z_NO_FLUSH
|
||||
var position = 0
|
||||
var zipCRC32 = CRC32(0)
|
||||
repeat {
|
||||
let readSize = Swift.min((size - position), bufferSize)
|
||||
var inputChunk = try provider(position, readSize)
|
||||
zipCRC32 = inputChunk.crc32(checksum: zipCRC32)
|
||||
stream.avail_in = UInt32(inputChunk.count)
|
||||
try inputChunk.withUnsafeMutableBytes { (rawBufferPointer) in
|
||||
if let baseAddress = rawBufferPointer.baseAddress {
|
||||
let pointer = baseAddress.assumingMemoryBound(to: UInt8.self)
|
||||
stream.next_in = pointer
|
||||
flush = position + bufferSize >= size ? Z_FINISH : Z_NO_FLUSH
|
||||
} else if rawBufferPointer.count > 0 {
|
||||
throw CompressionError.corruptedData
|
||||
} else {
|
||||
stream.next_in = nil
|
||||
flush = Z_FINISH
|
||||
}
|
||||
var outputChunk = Data(count: bufferSize)
|
||||
repeat {
|
||||
stream.avail_out = UInt32(bufferSize)
|
||||
try outputChunk.withUnsafeMutableBytes { (rawBufferPointer) in
|
||||
guard let baseAddress = rawBufferPointer.baseAddress, rawBufferPointer.count > 0 else {
|
||||
throw CompressionError.corruptedData
|
||||
}
|
||||
let pointer = baseAddress.assumingMemoryBound(to: UInt8.self)
|
||||
stream.next_out = pointer
|
||||
result = deflate(&stream, flush)
|
||||
}
|
||||
guard result >= Z_OK else { throw CompressionError.corruptedData }
|
||||
|
||||
outputChunk.count = bufferSize - Int(stream.avail_out)
|
||||
try consumer(outputChunk)
|
||||
} while stream.avail_out == 0
|
||||
}
|
||||
position += readSize
|
||||
} while flush != Z_FINISH
|
||||
return zipCRC32
|
||||
}
|
||||
|
||||
static func decode(bufferSize: Int, skipCRC32: Bool, provider: Provider, consumer: Consumer) throws -> CRC32 {
|
||||
var stream = z_stream()
|
||||
let streamSize = Int32(MemoryLayout<z_stream>.size)
|
||||
var result = inflateInit2_(&stream, -MAX_WBITS, ZLIB_VERSION, streamSize)
|
||||
defer { inflateEnd(&stream) }
|
||||
guard result == Z_OK else { throw CompressionError.invalidStream }
|
||||
var unzipCRC32 = CRC32(0)
|
||||
var position = 0
|
||||
repeat {
|
||||
stream.avail_in = UInt32(bufferSize)
|
||||
var chunk = try provider(position, bufferSize)
|
||||
position += chunk.count
|
||||
try chunk.withUnsafeMutableBytes { (rawBufferPointer) in
|
||||
if let baseAddress = rawBufferPointer.baseAddress, rawBufferPointer.count > 0 {
|
||||
let pointer = baseAddress.assumingMemoryBound(to: UInt8.self)
|
||||
stream.next_in = pointer
|
||||
repeat {
|
||||
var outputData = Data(count: bufferSize)
|
||||
stream.avail_out = UInt32(bufferSize)
|
||||
try outputData.withUnsafeMutableBytes { (rawBufferPointer) in
|
||||
if let baseAddress = rawBufferPointer.baseAddress, rawBufferPointer.count > 0 {
|
||||
let pointer = baseAddress.assumingMemoryBound(to: UInt8.self)
|
||||
stream.next_out = pointer
|
||||
} else {
|
||||
throw CompressionError.corruptedData
|
||||
}
|
||||
result = inflate(&stream, Z_NO_FLUSH)
|
||||
guard result != Z_NEED_DICT &&
|
||||
result != Z_DATA_ERROR &&
|
||||
result != Z_MEM_ERROR else {
|
||||
throw CompressionError.corruptedData
|
||||
}
|
||||
}
|
||||
let remainingLength = UInt32(bufferSize) - stream.avail_out
|
||||
outputData.count = Int(remainingLength)
|
||||
try consumer(outputData)
|
||||
if !skipCRC32 { unzipCRC32 = outputData.crc32(checksum: unzipCRC32) }
|
||||
} while stream.avail_out == 0
|
||||
}
|
||||
}
|
||||
} while result != Z_STREAM_END
|
||||
return unzipCRC32
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if !swift(>=5.0)
|
||||
|
||||
// Since Swift 5.0, `Data.withUnsafeBytes()` passes an `UnsafeRawBufferPointer` instead of an `UnsafePointer<UInt8>`
|
||||
// into `body`.
|
||||
// We provide a compatible method for targets that use Swift 4.x so that we can use the new version
|
||||
// across all language versions.
|
||||
|
||||
internal extension Data {
|
||||
func withUnsafeBytes<T>(_ body: (UnsafeRawBufferPointer) throws -> T) rethrows -> T {
|
||||
let count = self.count
|
||||
return try withUnsafeBytes { (pointer: UnsafePointer<UInt8>) throws -> T in
|
||||
try body(UnsafeRawBufferPointer(start: pointer, count: count))
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
|
||||
#else
|
||||
mutating func withUnsafeMutableBytes<T>(_ body: (UnsafeMutableRawBufferPointer) throws -> T) rethrows -> T {
|
||||
let count = self.count
|
||||
guard count > 0 else {
|
||||
return try body(UnsafeMutableRawBufferPointer(start: nil, count: count))
|
||||
}
|
||||
return try withUnsafeMutableBytes { (pointer: UnsafeMutablePointer<UInt8>) throws -> T in
|
||||
try body(UnsafeMutableRawBufferPointer(start: pointer, count: count))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,103 @@
|
||||
//
|
||||
// Data+Serialization.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2020 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors.
|
||||
// Released under the MIT License.
|
||||
//
|
||||
// See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol DataSerializable {
|
||||
static var size: Int { get }
|
||||
init?(data: Data, additionalDataProvider: (Int) throws -> Data)
|
||||
var data: Data { get }
|
||||
}
|
||||
|
||||
extension Data {
|
||||
enum DataError: Error {
|
||||
case unreadableFile
|
||||
case unwritableFile
|
||||
}
|
||||
|
||||
func scanValue<T>(start: Int) -> T {
|
||||
let subdata = self.subdata(in: start..<start+MemoryLayout<T>.size)
|
||||
#if swift(>=5.0)
|
||||
return subdata.withUnsafeBytes { $0.load(as: T.self) }
|
||||
#else
|
||||
return subdata.withUnsafeBytes { $0.pointee }
|
||||
#endif
|
||||
}
|
||||
|
||||
static func readStruct<T>(from file: UnsafeMutablePointer<FILE>, at offset: Int) -> T? where T: DataSerializable {
|
||||
fseek(file, offset, SEEK_SET)
|
||||
guard let data = try? self.readChunk(of: T.size, from: file) else {
|
||||
return nil
|
||||
}
|
||||
let structure = T(data: data, additionalDataProvider: { (additionalDataSize) -> Data in
|
||||
return try self.readChunk(of: additionalDataSize, from: file)
|
||||
})
|
||||
return structure
|
||||
}
|
||||
|
||||
static func consumePart(of size: Int, chunkSize: Int, skipCRC32: Bool = false,
|
||||
provider: Provider, consumer: Consumer) throws -> CRC32 {
|
||||
var checksum = CRC32(0)
|
||||
guard size > 0 else {
|
||||
try consumer(Data())
|
||||
return checksum
|
||||
}
|
||||
|
||||
let readInOneChunk = (size < chunkSize)
|
||||
var chunkSize = readInOneChunk ? size : chunkSize
|
||||
var bytesRead = 0
|
||||
while bytesRead < size {
|
||||
let remainingSize = size - bytesRead
|
||||
chunkSize = remainingSize < chunkSize ? remainingSize : chunkSize
|
||||
let data = try provider(bytesRead, chunkSize)
|
||||
try consumer(data)
|
||||
if !skipCRC32 {
|
||||
checksum = data.crc32(checksum: checksum)
|
||||
}
|
||||
bytesRead += chunkSize
|
||||
}
|
||||
return checksum
|
||||
}
|
||||
|
||||
static func readChunk(of size: Int, from file: UnsafeMutablePointer<FILE>) throws -> Data {
|
||||
let alignment = MemoryLayout<UInt>.alignment
|
||||
#if swift(>=4.1)
|
||||
let bytes = UnsafeMutableRawPointer.allocate(byteCount: size, alignment: alignment)
|
||||
#else
|
||||
let bytes = UnsafeMutableRawPointer.allocate(bytes: size, alignedTo: alignment)
|
||||
#endif
|
||||
let bytesRead = fread(bytes, 1, size, file)
|
||||
let error = ferror(file)
|
||||
if error > 0 {
|
||||
throw DataError.unreadableFile
|
||||
}
|
||||
#if swift(>=4.1)
|
||||
return Data(bytesNoCopy: bytes, count: bytesRead, deallocator: .custom({ buf, _ in buf.deallocate() }))
|
||||
#else
|
||||
let deallocator = Deallocator.custom({ buf, _ in buf.deallocate(bytes: size, alignedTo: 1) })
|
||||
return Data(bytesNoCopy: bytes, count: bytesRead, deallocator: deallocator)
|
||||
#endif
|
||||
}
|
||||
|
||||
static func write(chunk: Data, to file: UnsafeMutablePointer<FILE>) throws -> Int {
|
||||
var sizeWritten = 0
|
||||
chunk.withUnsafeBytes { (rawBufferPointer) in
|
||||
if let baseAddress = rawBufferPointer.baseAddress, rawBufferPointer.count > 0 {
|
||||
let pointer = baseAddress.assumingMemoryBound(to: UInt8.self)
|
||||
sizeWritten = fwrite(pointer, 1, chunk.count, file)
|
||||
}
|
||||
}
|
||||
let error = ferror(file)
|
||||
if error > 0 {
|
||||
throw DataError.unwritableFile
|
||||
}
|
||||
return sizeWritten
|
||||
}
|
||||
}
|
||||
+400
@@ -0,0 +1,400 @@
|
||||
//
|
||||
// Entry.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2020 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors.
|
||||
// Released under the MIT License.
|
||||
//
|
||||
// See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreFoundation
|
||||
|
||||
/// A value that represents a file, a directory or a symbolic link within a ZIP `Archive`.
|
||||
///
|
||||
/// You can retrieve instances of `Entry` from an `Archive` via subscripting or iteration.
|
||||
/// Entries are identified by their `path`.
|
||||
public struct Entry: Equatable {
|
||||
/// The type of an `Entry` in a ZIP `Archive`.
|
||||
public enum EntryType: Int {
|
||||
/// Indicates a regular file.
|
||||
case file
|
||||
/// Indicates a directory.
|
||||
case directory
|
||||
/// Indicates a symbolic link.
|
||||
case symlink
|
||||
|
||||
init(mode: mode_t) {
|
||||
switch mode & S_IFMT {
|
||||
case S_IFDIR:
|
||||
self = .directory
|
||||
case S_IFLNK:
|
||||
self = .symlink
|
||||
default:
|
||||
self = .file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum OSType: UInt {
|
||||
case msdos = 0
|
||||
case unix = 3
|
||||
case osx = 19
|
||||
case unused = 20
|
||||
}
|
||||
|
||||
struct LocalFileHeader: DataSerializable {
|
||||
let localFileHeaderSignature = UInt32(localFileHeaderStructSignature)
|
||||
let versionNeededToExtract: UInt16
|
||||
let generalPurposeBitFlag: UInt16
|
||||
let compressionMethod: UInt16
|
||||
let lastModFileTime: UInt16
|
||||
let lastModFileDate: UInt16
|
||||
let crc32: UInt32
|
||||
let compressedSize: UInt32
|
||||
let uncompressedSize: UInt32
|
||||
let fileNameLength: UInt16
|
||||
let extraFieldLength: UInt16
|
||||
static let size = 30
|
||||
let fileNameData: Data
|
||||
let extraFieldData: Data
|
||||
}
|
||||
|
||||
struct DataDescriptor: DataSerializable {
|
||||
let data: Data
|
||||
let dataDescriptorSignature = UInt32(dataDescriptorStructSignature)
|
||||
let crc32: UInt32
|
||||
let compressedSize: UInt32
|
||||
let uncompressedSize: UInt32
|
||||
static let size = 16
|
||||
}
|
||||
|
||||
struct CentralDirectoryStructure: DataSerializable {
|
||||
let centralDirectorySignature = UInt32(centralDirectoryStructSignature)
|
||||
let versionMadeBy: UInt16
|
||||
let versionNeededToExtract: UInt16
|
||||
let generalPurposeBitFlag: UInt16
|
||||
let compressionMethod: UInt16
|
||||
let lastModFileTime: UInt16
|
||||
let lastModFileDate: UInt16
|
||||
let crc32: UInt32
|
||||
let compressedSize: UInt32
|
||||
let uncompressedSize: UInt32
|
||||
let fileNameLength: UInt16
|
||||
let extraFieldLength: UInt16
|
||||
let fileCommentLength: UInt16
|
||||
let diskNumberStart: UInt16
|
||||
let internalFileAttributes: UInt16
|
||||
let externalFileAttributes: UInt32
|
||||
let relativeOffsetOfLocalHeader: UInt32
|
||||
static let size = 46
|
||||
let fileNameData: Data
|
||||
let extraFieldData: Data
|
||||
let fileCommentData: Data
|
||||
var usesDataDescriptor: Bool { return (self.generalPurposeBitFlag & (1 << 3 )) != 0 }
|
||||
var usesUTF8PathEncoding: Bool { return (self.generalPurposeBitFlag & (1 << 11 )) != 0 }
|
||||
var isEncrypted: Bool { return (self.generalPurposeBitFlag & (1 << 0)) != 0 }
|
||||
var isZIP64: Bool { return self.versionNeededToExtract >= 45 }
|
||||
}
|
||||
/// Returns the `path` of the receiver within a ZIP `Archive` using a given encoding.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - encoding: `String.Encoding`
|
||||
public func path(using encoding: String.Encoding) -> String {
|
||||
return String(data: self.centralDirectoryStructure.fileNameData, encoding: encoding) ?? ""
|
||||
}
|
||||
/// The `path` of the receiver within a ZIP `Archive`.
|
||||
public var path: String {
|
||||
let dosLatinUS = 0x400
|
||||
let dosLatinUSEncoding = CFStringEncoding(dosLatinUS)
|
||||
let dosLatinUSStringEncoding = CFStringConvertEncodingToNSStringEncoding(dosLatinUSEncoding)
|
||||
let codepage437 = String.Encoding(rawValue: dosLatinUSStringEncoding)
|
||||
let encoding = self.centralDirectoryStructure.usesUTF8PathEncoding ? .utf8 : codepage437
|
||||
return self.path(using: encoding)
|
||||
}
|
||||
/// The file attributes of the receiver as key/value pairs.
|
||||
///
|
||||
/// Contains the modification date and file permissions.
|
||||
public var fileAttributes: [FileAttributeKey: Any] {
|
||||
return FileManager.attributes(from: self)
|
||||
}
|
||||
/// The `CRC32` checksum of the receiver.
|
||||
///
|
||||
/// - Note: Always returns `0` for entries of type `EntryType.directory`.
|
||||
public var checksum: CRC32 {
|
||||
var checksum = self.centralDirectoryStructure.crc32
|
||||
if self.centralDirectoryStructure.usesDataDescriptor {
|
||||
guard let dataDescriptor = self.dataDescriptor else { return 0 }
|
||||
checksum = dataDescriptor.crc32
|
||||
}
|
||||
return checksum
|
||||
}
|
||||
/// The `EntryType` of the receiver.
|
||||
public var type: EntryType {
|
||||
// OS Type is stored in the upper byte of versionMadeBy
|
||||
let osTypeRaw = self.centralDirectoryStructure.versionMadeBy >> 8
|
||||
let osType = OSType(rawValue: UInt(osTypeRaw)) ?? .unused
|
||||
var isDirectory = self.path.hasSuffix("/")
|
||||
switch osType {
|
||||
case .unix, .osx:
|
||||
let mode = mode_t(self.centralDirectoryStructure.externalFileAttributes >> 16) & S_IFMT
|
||||
switch mode {
|
||||
case S_IFREG:
|
||||
return .file
|
||||
case S_IFDIR:
|
||||
return .directory
|
||||
case S_IFLNK:
|
||||
return .symlink
|
||||
default:
|
||||
return isDirectory ? .directory : .file
|
||||
}
|
||||
case .msdos:
|
||||
isDirectory = isDirectory || ((centralDirectoryStructure.externalFileAttributes >> 4) == 0x01)
|
||||
fallthrough // For all other OSes we can only guess based on the directory suffix char
|
||||
default: return isDirectory ? .directory : .file
|
||||
}
|
||||
}
|
||||
/// The size of the receiver's compressed data.
|
||||
public var compressedSize: Int {
|
||||
return Int(dataDescriptor?.compressedSize ?? localFileHeader.compressedSize)
|
||||
}
|
||||
/// The size of the receiver's uncompressed data.
|
||||
public var uncompressedSize: Int {
|
||||
return Int(dataDescriptor?.uncompressedSize ?? localFileHeader.uncompressedSize)
|
||||
}
|
||||
/// The combined size of the local header, the data and the optional data descriptor.
|
||||
var localSize: Int {
|
||||
let localFileHeader = self.localFileHeader
|
||||
var extraDataLength = Int(localFileHeader.fileNameLength)
|
||||
extraDataLength += Int(localFileHeader.extraFieldLength)
|
||||
var size = LocalFileHeader.size + extraDataLength
|
||||
let isCompressed = localFileHeader.compressionMethod != CompressionMethod.none.rawValue
|
||||
size += isCompressed ? self.compressedSize : self.uncompressedSize
|
||||
size += self.dataDescriptor != nil ? DataDescriptor.size : 0
|
||||
return size
|
||||
}
|
||||
var dataOffset: Int {
|
||||
var dataOffset = Int(self.centralDirectoryStructure.relativeOffsetOfLocalHeader)
|
||||
dataOffset += LocalFileHeader.size
|
||||
dataOffset += Int(self.localFileHeader.fileNameLength)
|
||||
dataOffset += Int(self.localFileHeader.extraFieldLength)
|
||||
return dataOffset
|
||||
}
|
||||
let centralDirectoryStructure: CentralDirectoryStructure
|
||||
let localFileHeader: LocalFileHeader
|
||||
let dataDescriptor: DataDescriptor?
|
||||
|
||||
public static func == (lhs: Entry, rhs: Entry) -> Bool {
|
||||
return lhs.path == rhs.path
|
||||
&& lhs.localFileHeader.crc32 == rhs.localFileHeader.crc32
|
||||
&& lhs.centralDirectoryStructure.relativeOffsetOfLocalHeader
|
||||
== rhs.centralDirectoryStructure.relativeOffsetOfLocalHeader
|
||||
}
|
||||
|
||||
init?(centralDirectoryStructure: CentralDirectoryStructure,
|
||||
localFileHeader: LocalFileHeader,
|
||||
dataDescriptor: DataDescriptor?) {
|
||||
// We currently don't support ZIP64 or encrypted archives
|
||||
guard !centralDirectoryStructure.isZIP64 else { return nil }
|
||||
guard !centralDirectoryStructure.isEncrypted else { return nil }
|
||||
self.centralDirectoryStructure = centralDirectoryStructure
|
||||
self.localFileHeader = localFileHeader
|
||||
self.dataDescriptor = dataDescriptor
|
||||
}
|
||||
}
|
||||
|
||||
extension Entry.LocalFileHeader {
|
||||
var data: Data {
|
||||
var localFileHeaderSignature = self.localFileHeaderSignature
|
||||
var versionNeededToExtract = self.versionNeededToExtract
|
||||
var generalPurposeBitFlag = self.generalPurposeBitFlag
|
||||
var compressionMethod = self.compressionMethod
|
||||
var lastModFileTime = self.lastModFileTime
|
||||
var lastModFileDate = self.lastModFileDate
|
||||
var crc32 = self.crc32
|
||||
var compressedSize = self.compressedSize
|
||||
var uncompressedSize = self.uncompressedSize
|
||||
var fileNameLength = self.fileNameLength
|
||||
var extraFieldLength = self.extraFieldLength
|
||||
var data = Data()
|
||||
withUnsafePointer(to: &localFileHeaderSignature, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &versionNeededToExtract, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &generalPurposeBitFlag, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &compressionMethod, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &lastModFileTime, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &lastModFileDate, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &crc32, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &compressedSize, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &uncompressedSize, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &fileNameLength, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &extraFieldLength, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
data.append(self.fileNameData)
|
||||
data.append(self.extraFieldData)
|
||||
return data
|
||||
}
|
||||
|
||||
init?(data: Data, additionalDataProvider provider: (Int) throws -> Data) {
|
||||
guard data.count == Entry.LocalFileHeader.size else { return nil }
|
||||
guard data.scanValue(start: 0) == localFileHeaderSignature else { return nil }
|
||||
self.versionNeededToExtract = data.scanValue(start: 4)
|
||||
self.generalPurposeBitFlag = data.scanValue(start: 6)
|
||||
self.compressionMethod = data.scanValue(start: 8)
|
||||
self.lastModFileTime = data.scanValue(start: 10)
|
||||
self.lastModFileDate = data.scanValue(start: 12)
|
||||
self.crc32 = data.scanValue(start: 14)
|
||||
self.compressedSize = data.scanValue(start: 18)
|
||||
self.uncompressedSize = data.scanValue(start: 22)
|
||||
self.fileNameLength = data.scanValue(start: 26)
|
||||
self.extraFieldLength = data.scanValue(start: 28)
|
||||
let additionalDataLength = Int(self.fileNameLength) + Int(self.extraFieldLength)
|
||||
guard let additionalData = try? provider(additionalDataLength) else { return nil }
|
||||
guard additionalData.count == additionalDataLength else { return nil }
|
||||
var subRangeStart = 0
|
||||
var subRangeEnd = Int(self.fileNameLength)
|
||||
self.fileNameData = additionalData.subdata(in: subRangeStart..<subRangeEnd)
|
||||
subRangeStart += Int(self.fileNameLength)
|
||||
subRangeEnd = subRangeStart + Int(self.extraFieldLength)
|
||||
self.extraFieldData = additionalData.subdata(in: subRangeStart..<subRangeEnd)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entry.CentralDirectoryStructure {
|
||||
var data: Data {
|
||||
var centralDirectorySignature = self.centralDirectorySignature
|
||||
var versionMadeBy = self.versionMadeBy
|
||||
var versionNeededToExtract = self.versionNeededToExtract
|
||||
var generalPurposeBitFlag = self.generalPurposeBitFlag
|
||||
var compressionMethod = self.compressionMethod
|
||||
var lastModFileTime = self.lastModFileTime
|
||||
var lastModFileDate = self.lastModFileDate
|
||||
var crc32 = self.crc32
|
||||
var compressedSize = self.compressedSize
|
||||
var uncompressedSize = self.uncompressedSize
|
||||
var fileNameLength = self.fileNameLength
|
||||
var extraFieldLength = self.extraFieldLength
|
||||
var fileCommentLength = self.fileCommentLength
|
||||
var diskNumberStart = self.diskNumberStart
|
||||
var internalFileAttributes = self.internalFileAttributes
|
||||
var externalFileAttributes = self.externalFileAttributes
|
||||
var relativeOffsetOfLocalHeader = self.relativeOffsetOfLocalHeader
|
||||
var data = Data()
|
||||
withUnsafePointer(to: ¢ralDirectorySignature, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &versionMadeBy, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &versionNeededToExtract, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &generalPurposeBitFlag, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &compressionMethod, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &lastModFileTime, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &lastModFileDate, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &crc32, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &compressedSize, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &uncompressedSize, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &fileNameLength, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &extraFieldLength, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &fileCommentLength, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &diskNumberStart, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &internalFileAttributes, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &externalFileAttributes, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &relativeOffsetOfLocalHeader, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
data.append(self.fileNameData)
|
||||
data.append(self.extraFieldData)
|
||||
data.append(self.fileCommentData)
|
||||
return data
|
||||
}
|
||||
|
||||
init?(data: Data, additionalDataProvider provider: (Int) throws -> Data) {
|
||||
guard data.count == Entry.CentralDirectoryStructure.size else { return nil }
|
||||
guard data.scanValue(start: 0) == centralDirectorySignature else { return nil }
|
||||
self.versionMadeBy = data.scanValue(start: 4)
|
||||
self.versionNeededToExtract = data.scanValue(start: 6)
|
||||
self.generalPurposeBitFlag = data.scanValue(start: 8)
|
||||
self.compressionMethod = data.scanValue(start: 10)
|
||||
self.lastModFileTime = data.scanValue(start: 12)
|
||||
self.lastModFileDate = data.scanValue(start: 14)
|
||||
self.crc32 = data.scanValue(start: 16)
|
||||
self.compressedSize = data.scanValue(start: 20)
|
||||
self.uncompressedSize = data.scanValue(start: 24)
|
||||
self.fileNameLength = data.scanValue(start: 28)
|
||||
self.extraFieldLength = data.scanValue(start: 30)
|
||||
self.fileCommentLength = data.scanValue(start: 32)
|
||||
self.diskNumberStart = data.scanValue(start: 34)
|
||||
self.internalFileAttributes = data.scanValue(start: 36)
|
||||
self.externalFileAttributes = data.scanValue(start: 38)
|
||||
self.relativeOffsetOfLocalHeader = data.scanValue(start: 42)
|
||||
let additionalDataLength = Int(self.fileNameLength) + Int(self.extraFieldLength) + Int(self.fileCommentLength)
|
||||
guard let additionalData = try? provider(additionalDataLength) else { return nil }
|
||||
guard additionalData.count == additionalDataLength else { return nil }
|
||||
var subRangeStart = 0
|
||||
var subRangeEnd = Int(self.fileNameLength)
|
||||
self.fileNameData = additionalData.subdata(in: subRangeStart..<subRangeEnd)
|
||||
subRangeStart += Int(self.fileNameLength)
|
||||
subRangeEnd = subRangeStart + Int(self.extraFieldLength)
|
||||
self.extraFieldData = additionalData.subdata(in: subRangeStart..<subRangeEnd)
|
||||
subRangeStart += Int(self.extraFieldLength)
|
||||
subRangeEnd = subRangeStart + Int(self.fileCommentLength)
|
||||
self.fileCommentData = additionalData.subdata(in: subRangeStart..<subRangeEnd)
|
||||
}
|
||||
|
||||
init(localFileHeader: Entry.LocalFileHeader, fileAttributes: UInt32, relativeOffset: UInt32) {
|
||||
versionMadeBy = UInt16(789)
|
||||
versionNeededToExtract = localFileHeader.versionNeededToExtract
|
||||
generalPurposeBitFlag = localFileHeader.generalPurposeBitFlag
|
||||
compressionMethod = localFileHeader.compressionMethod
|
||||
lastModFileTime = localFileHeader.lastModFileTime
|
||||
lastModFileDate = localFileHeader.lastModFileDate
|
||||
crc32 = localFileHeader.crc32
|
||||
compressedSize = localFileHeader.compressedSize
|
||||
uncompressedSize = localFileHeader.uncompressedSize
|
||||
fileNameLength = localFileHeader.fileNameLength
|
||||
extraFieldLength = UInt16(0)
|
||||
fileCommentLength = UInt16(0)
|
||||
diskNumberStart = UInt16(0)
|
||||
internalFileAttributes = UInt16(0)
|
||||
externalFileAttributes = fileAttributes
|
||||
relativeOffsetOfLocalHeader = relativeOffset
|
||||
fileNameData = localFileHeader.fileNameData
|
||||
extraFieldData = Data()
|
||||
fileCommentData = Data()
|
||||
}
|
||||
|
||||
init(centralDirectoryStructure: Entry.CentralDirectoryStructure, offset: UInt32) {
|
||||
let relativeOffset = centralDirectoryStructure.relativeOffsetOfLocalHeader - offset
|
||||
relativeOffsetOfLocalHeader = relativeOffset
|
||||
versionMadeBy = centralDirectoryStructure.versionMadeBy
|
||||
versionNeededToExtract = centralDirectoryStructure.versionNeededToExtract
|
||||
generalPurposeBitFlag = centralDirectoryStructure.generalPurposeBitFlag
|
||||
compressionMethod = centralDirectoryStructure.compressionMethod
|
||||
lastModFileTime = centralDirectoryStructure.lastModFileTime
|
||||
lastModFileDate = centralDirectoryStructure.lastModFileDate
|
||||
crc32 = centralDirectoryStructure.crc32
|
||||
compressedSize = centralDirectoryStructure.compressedSize
|
||||
uncompressedSize = centralDirectoryStructure.uncompressedSize
|
||||
fileNameLength = centralDirectoryStructure.fileNameLength
|
||||
extraFieldLength = centralDirectoryStructure.extraFieldLength
|
||||
fileCommentLength = centralDirectoryStructure.fileCommentLength
|
||||
diskNumberStart = centralDirectoryStructure.diskNumberStart
|
||||
internalFileAttributes = centralDirectoryStructure.internalFileAttributes
|
||||
externalFileAttributes = centralDirectoryStructure.externalFileAttributes
|
||||
fileNameData = centralDirectoryStructure.fileNameData
|
||||
extraFieldData = centralDirectoryStructure.extraFieldData
|
||||
fileCommentData = centralDirectoryStructure.fileCommentData
|
||||
}
|
||||
}
|
||||
|
||||
extension Entry.DataDescriptor {
|
||||
init?(data: Data, additionalDataProvider provider: (Int) throws -> Data) {
|
||||
guard data.count == Entry.DataDescriptor.size else { return nil }
|
||||
let signature: UInt32 = data.scanValue(start: 0)
|
||||
// The DataDescriptor signature is not mandatory so we have to re-arrange the input data if it is missing.
|
||||
var readOffset = 0
|
||||
if signature == self.dataDescriptorSignature { readOffset = 4 }
|
||||
self.crc32 = data.scanValue(start: readOffset + 0)
|
||||
self.compressedSize = data.scanValue(start: readOffset + 4)
|
||||
self.uncompressedSize = data.scanValue(start: readOffset + 8)
|
||||
// Our add(_ entry:) methods always maintain compressed & uncompressed
|
||||
// sizes and so we don't need a data descriptor for newly added entries.
|
||||
// Data descriptors of already existing entries are manually preserved
|
||||
// when copying those entries to the tempArchive during remove(_ entry:).
|
||||
self.data = Data()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
//
|
||||
// FileManager+ZIP.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2020 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors.
|
||||
// Released under the MIT License.
|
||||
//
|
||||
// See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension FileManager {
|
||||
typealias CentralDirectoryStructure = Entry.CentralDirectoryStructure
|
||||
|
||||
/// Zips the file or direcory contents at the specified source URL to the destination URL.
|
||||
///
|
||||
/// If the item at the source URL is a directory, the directory itself will be
|
||||
/// represented within the ZIP `Archive`. Calling this method with a directory URL
|
||||
/// `file:///path/directory/` will create an archive with a `directory/` entry at the root level.
|
||||
/// You can override this behavior by passing `false` for `shouldKeepParent`. In that case, the contents
|
||||
/// of the source directory will be placed at the root of the archive.
|
||||
/// - Parameters:
|
||||
/// - sourceURL: The file URL pointing to an existing file or directory.
|
||||
/// - destinationURL: The file URL that identifies the destination of the zip operation.
|
||||
/// - shouldKeepParent: Indicates that the directory name of a source item should be used as root element
|
||||
/// within the archive. Default is `true`.
|
||||
/// - compressionMethod: Indicates the `CompressionMethod` that should be applied.
|
||||
/// By default, `zipItem` will create uncompressed archives.
|
||||
/// - progress: A progress object that can be used to track or cancel the zip operation.
|
||||
/// - Throws: Throws an error if the source item does not exist or the destination URL is not writable.
|
||||
public func zipItem(at sourceURL: URL, to destinationURL: URL,
|
||||
shouldKeepParent: Bool = true, compressionMethod: CompressionMethod = .none,
|
||||
progress: Progress? = nil) throws {
|
||||
let fileManager = FileManager()
|
||||
guard fileManager.itemExists(at: sourceURL) else {
|
||||
throw CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: sourceURL.path])
|
||||
}
|
||||
guard !fileManager.itemExists(at: destinationURL) else {
|
||||
throw CocoaError(.fileWriteFileExists, userInfo: [NSFilePathErrorKey: destinationURL.path])
|
||||
}
|
||||
guard let archive = Archive(url: destinationURL, accessMode: .create) else {
|
||||
throw Archive.ArchiveError.unwritableArchive
|
||||
}
|
||||
let isDirectory = try FileManager.typeForItem(at: sourceURL) == .directory
|
||||
if isDirectory {
|
||||
let subPaths = try self.subpathsOfDirectory(atPath: sourceURL.path)
|
||||
var totalUnitCount = Int64(0)
|
||||
if let progress = progress {
|
||||
totalUnitCount = subPaths.reduce(Int64(0), {
|
||||
let itemURL = sourceURL.appendingPathComponent($1)
|
||||
let itemSize = archive.totalUnitCountForAddingItem(at: itemURL)
|
||||
return $0 + itemSize
|
||||
})
|
||||
progress.totalUnitCount = totalUnitCount
|
||||
}
|
||||
|
||||
// If the caller wants to keep the parent directory, we use the lastPathComponent of the source URL
|
||||
// as common base for all entries (similar to macOS' Archive Utility.app)
|
||||
let directoryPrefix = sourceURL.lastPathComponent
|
||||
for entryPath in subPaths {
|
||||
let finalEntryPath = shouldKeepParent ? directoryPrefix + "/" + entryPath : entryPath
|
||||
let finalBaseURL = shouldKeepParent ? sourceURL.deletingLastPathComponent() : sourceURL
|
||||
if let progress = progress {
|
||||
let itemURL = sourceURL.appendingPathComponent(entryPath)
|
||||
let entryProgress = archive.makeProgressForAddingItem(at: itemURL)
|
||||
progress.addChild(entryProgress, withPendingUnitCount: entryProgress.totalUnitCount)
|
||||
try archive.addEntry(with: finalEntryPath, relativeTo: finalBaseURL,
|
||||
compressionMethod: compressionMethod, progress: entryProgress)
|
||||
} else {
|
||||
try archive.addEntry(with: finalEntryPath, relativeTo: finalBaseURL,
|
||||
compressionMethod: compressionMethod)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
progress?.totalUnitCount = archive.totalUnitCountForAddingItem(at: sourceURL)
|
||||
let baseURL = sourceURL.deletingLastPathComponent()
|
||||
try archive.addEntry(with: sourceURL.lastPathComponent, relativeTo: baseURL,
|
||||
compressionMethod: compressionMethod, progress: progress)
|
||||
}
|
||||
}
|
||||
|
||||
/// Unzips the contents at the specified source URL to the destination URL.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - sourceURL: The file URL pointing to an existing ZIP file.
|
||||
/// - destinationURL: The file URL that identifies the destination directory of the unzip operation.
|
||||
/// - skipCRC32: Optional flag to skip calculation of the CRC32 checksum to improve performance.
|
||||
/// - progress: A progress object that can be used to track or cancel the unzip operation.
|
||||
/// - preferredEncoding: Encoding for entry paths. Overrides the encoding specified in the archive.
|
||||
/// - Throws: Throws an error if the source item does not exist or the destination URL is not writable.
|
||||
public func unzipItem(at sourceURL: URL, to destinationURL: URL, skipCRC32: Bool = false,
|
||||
progress: Progress? = nil, preferredEncoding: String.Encoding? = nil) throws {
|
||||
let fileManager = FileManager()
|
||||
guard fileManager.itemExists(at: sourceURL) else {
|
||||
throw CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: sourceURL.path])
|
||||
}
|
||||
guard let archive = Archive(url: sourceURL, accessMode: .read, preferredEncoding: preferredEncoding) else {
|
||||
throw Archive.ArchiveError.unreadableArchive
|
||||
}
|
||||
// Defer extraction of symlinks until all files & directories have been created.
|
||||
// This is necessary because we can't create links to files that haven't been created yet.
|
||||
let sortedEntries = archive.sorted { (left, right) -> Bool in
|
||||
switch (left.type, right.type) {
|
||||
case (.directory, .file): return true
|
||||
case (.directory, .symlink): return true
|
||||
case (.file, .symlink): return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
var totalUnitCount = Int64(0)
|
||||
if let progress = progress {
|
||||
totalUnitCount = sortedEntries.reduce(0, { $0 + archive.totalUnitCountForReading($1) })
|
||||
progress.totalUnitCount = totalUnitCount
|
||||
}
|
||||
|
||||
for entry in sortedEntries {
|
||||
let path = preferredEncoding == nil ? entry.path : entry.path(using: preferredEncoding!)
|
||||
let destinationEntryURL = destinationURL.appendingPathComponent(path)
|
||||
guard destinationEntryURL.isContained(in: destinationURL) else {
|
||||
throw CocoaError(.fileReadInvalidFileName,
|
||||
userInfo: [NSFilePathErrorKey: destinationEntryURL.path])
|
||||
}
|
||||
if let progress = progress {
|
||||
let entryProgress = archive.makeProgressForReading(entry)
|
||||
progress.addChild(entryProgress, withPendingUnitCount: entryProgress.totalUnitCount)
|
||||
_ = try archive.extract(entry, to: destinationEntryURL, skipCRC32: skipCRC32, progress: entryProgress)
|
||||
} else {
|
||||
_ = try archive.extract(entry, to: destinationEntryURL, skipCRC32: skipCRC32)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
func itemExists(at url: URL) -> Bool {
|
||||
// Use `URL.checkResourceIsReachable()` instead of `FileManager.fileExists()` here
|
||||
// because we don't want implicit symlink resolution.
|
||||
// As per documentation, `FileManager.fileExists()` traverses symlinks and therefore a broken symlink
|
||||
// would throw a `.fileReadNoSuchFile` false positive error.
|
||||
// For ZIP files it may be intended to archive "broken" symlinks because they might be
|
||||
// resolvable again when extracting the archive to a different destination.
|
||||
return (try? url.checkResourceIsReachable()) == true
|
||||
}
|
||||
|
||||
func createParentDirectoryStructure(for url: URL) throws {
|
||||
let parentDirectoryURL = url.deletingLastPathComponent()
|
||||
try self.createDirectory(at: parentDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
|
||||
class func attributes(from entry: Entry) -> [FileAttributeKey: Any] {
|
||||
let centralDirectoryStructure = entry.centralDirectoryStructure
|
||||
let entryType = entry.type
|
||||
let fileTime = centralDirectoryStructure.lastModFileTime
|
||||
let fileDate = centralDirectoryStructure.lastModFileDate
|
||||
let defaultPermissions = entryType == .directory ? defaultDirectoryPermissions : defaultFilePermissions
|
||||
var attributes = [.posixPermissions: defaultPermissions] as [FileAttributeKey: Any]
|
||||
// Certain keys are not yet supported in swift-corelibs
|
||||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
|
||||
attributes[.modificationDate] = Date(dateTime: (fileDate, fileTime))
|
||||
#endif
|
||||
let versionMadeBy = centralDirectoryStructure.versionMadeBy
|
||||
guard let osType = Entry.OSType(rawValue: UInt(versionMadeBy >> 8)) else { return attributes }
|
||||
|
||||
let externalFileAttributes = centralDirectoryStructure.externalFileAttributes
|
||||
let permissions = self.permissions(for: externalFileAttributes, osType: osType, entryType: entryType)
|
||||
attributes[.posixPermissions] = NSNumber(value: permissions)
|
||||
return attributes
|
||||
}
|
||||
|
||||
class func permissions(for externalFileAttributes: UInt32, osType: Entry.OSType,
|
||||
entryType: Entry.EntryType) -> UInt16 {
|
||||
switch osType {
|
||||
case .unix, .osx:
|
||||
let permissions = mode_t(externalFileAttributes >> 16) & (~S_IFMT)
|
||||
let defaultPermissions = entryType == .directory ? defaultDirectoryPermissions : defaultFilePermissions
|
||||
return permissions == 0 ? defaultPermissions : UInt16(permissions)
|
||||
default:
|
||||
return entryType == .directory ? defaultDirectoryPermissions : defaultFilePermissions
|
||||
}
|
||||
}
|
||||
|
||||
class func externalFileAttributesForEntry(of type: Entry.EntryType, permissions: UInt16) -> UInt32 {
|
||||
var typeInt: UInt16
|
||||
switch type {
|
||||
case .file:
|
||||
typeInt = UInt16(S_IFREG)
|
||||
case .directory:
|
||||
typeInt = UInt16(S_IFDIR)
|
||||
case .symlink:
|
||||
typeInt = UInt16(S_IFLNK)
|
||||
}
|
||||
var externalFileAttributes = UInt32(typeInt|UInt16(permissions))
|
||||
externalFileAttributes = (externalFileAttributes << 16)
|
||||
return externalFileAttributes
|
||||
}
|
||||
|
||||
class func permissionsForItem(at URL: URL) throws -> UInt16 {
|
||||
let fileManager = FileManager()
|
||||
let entryFileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: URL.path)
|
||||
var fileStat = stat()
|
||||
lstat(entryFileSystemRepresentation, &fileStat)
|
||||
let permissions = fileStat.st_mode
|
||||
return UInt16(permissions)
|
||||
}
|
||||
|
||||
class func fileModificationDateTimeForItem(at url: URL) throws -> Date {
|
||||
let fileManager = FileManager()
|
||||
guard fileManager.itemExists(at: url) else {
|
||||
throw CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
let entryFileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: url.path)
|
||||
var fileStat = stat()
|
||||
lstat(entryFileSystemRepresentation, &fileStat)
|
||||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
|
||||
let modTimeSpec = fileStat.st_mtimespec
|
||||
#else
|
||||
let modTimeSpec = fileStat.st_mtim
|
||||
#endif
|
||||
|
||||
let timeStamp = TimeInterval(modTimeSpec.tv_sec) + TimeInterval(modTimeSpec.tv_nsec)/1000000000.0
|
||||
let modDate = Date(timeIntervalSince1970: timeStamp)
|
||||
return modDate
|
||||
}
|
||||
|
||||
class func fileSizeForItem(at url: URL) throws -> UInt32 {
|
||||
let fileManager = FileManager()
|
||||
guard fileManager.itemExists(at: url) else {
|
||||
throw CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
let entryFileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: url.path)
|
||||
var fileStat = stat()
|
||||
lstat(entryFileSystemRepresentation, &fileStat)
|
||||
return UInt32(fileStat.st_size)
|
||||
}
|
||||
|
||||
class func typeForItem(at url: URL) throws -> Entry.EntryType {
|
||||
let fileManager = FileManager()
|
||||
guard url.isFileURL, fileManager.itemExists(at: url) else {
|
||||
throw CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
let entryFileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: url.path)
|
||||
var fileStat = stat()
|
||||
lstat(entryFileSystemRepresentation, &fileStat)
|
||||
return Entry.EntryType(mode: fileStat.st_mode)
|
||||
}
|
||||
}
|
||||
|
||||
extension Date {
|
||||
init(dateTime: (UInt16, UInt16)) {
|
||||
var msdosDateTime = Int(dateTime.0)
|
||||
msdosDateTime <<= 16
|
||||
msdosDateTime |= Int(dateTime.1)
|
||||
var unixTime = tm()
|
||||
unixTime.tm_sec = Int32((msdosDateTime&31)*2)
|
||||
unixTime.tm_min = Int32((msdosDateTime>>5)&63)
|
||||
unixTime.tm_hour = Int32((Int(dateTime.1)>>11)&31)
|
||||
unixTime.tm_mday = Int32((msdosDateTime>>16)&31)
|
||||
unixTime.tm_mon = Int32((msdosDateTime>>21)&15)
|
||||
unixTime.tm_mon -= 1 // UNIX time struct month entries are zero based.
|
||||
unixTime.tm_year = Int32(1980+(msdosDateTime>>25))
|
||||
unixTime.tm_year -= 1900 // UNIX time structs count in "years since 1900".
|
||||
let time = timegm(&unixTime)
|
||||
self = Date(timeIntervalSince1970: TimeInterval(time))
|
||||
}
|
||||
|
||||
var fileModificationDateTime: (UInt16, UInt16) {
|
||||
return (self.fileModificationDate, self.fileModificationTime)
|
||||
}
|
||||
|
||||
var fileModificationDate: UInt16 {
|
||||
var time = time_t(self.timeIntervalSince1970)
|
||||
guard let unixTime = gmtime(&time) else {
|
||||
return 0
|
||||
}
|
||||
var year = unixTime.pointee.tm_year + 1900 // UNIX time structs count in "years since 1900".
|
||||
// ZIP uses the MSDOS date format which has a valid range of 1980 - 2099.
|
||||
year = year >= 1980 ? year : 1980
|
||||
year = year <= 2099 ? year : 2099
|
||||
let month = unixTime.pointee.tm_mon + 1 // UNIX time struct month entries are zero based.
|
||||
let day = unixTime.pointee.tm_mday
|
||||
return (UInt16)(day + ((month) * 32) + ((year - 1980) * 512))
|
||||
}
|
||||
|
||||
var fileModificationTime: UInt16 {
|
||||
var time = time_t(self.timeIntervalSince1970)
|
||||
guard let unixTime = gmtime(&time) else {
|
||||
return 0
|
||||
}
|
||||
let hour = unixTime.pointee.tm_hour
|
||||
let minute = unixTime.pointee.tm_min
|
||||
let second = unixTime.pointee.tm_sec
|
||||
return (UInt16)((second/2) + (minute * 32) + (hour * 2048))
|
||||
}
|
||||
}
|
||||
|
||||
#if swift(>=4.2)
|
||||
#else
|
||||
|
||||
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
|
||||
#else
|
||||
|
||||
// The swift-corelibs-foundation version of NSError.swift was missing a convenience method to create
|
||||
// error objects from error codes. (https://github.com/apple/swift-corelibs-foundation/pull/1420)
|
||||
// We have to provide an implementation for non-Darwin platforms using Swift versions < 4.2.
|
||||
|
||||
public extension CocoaError {
|
||||
public static func error(_ code: CocoaError.Code, userInfo: [AnyHashable: Any]? = nil, url: URL? = nil) -> Error {
|
||||
var info: [String: Any] = userInfo as? [String: Any] ?? [:]
|
||||
if let url = url {
|
||||
info[NSURLErrorKey] = url
|
||||
}
|
||||
return NSError(domain: NSCocoaErrorDomain, code: code.rawValue, userInfo: info)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
public extension URL {
|
||||
func isContained(in parentDirectoryURL: URL) -> Bool {
|
||||
// Ensure this URL is contained in the passed in URL
|
||||
let parentDirectoryURL = URL(fileURLWithPath: parentDirectoryURL.path, isDirectory: true).standardized
|
||||
return self.standardized.absoluteString.hasPrefix(parentDirectoryURL.absoluteString)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user