Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin
2021-01-21 16:38:07 +03:00
91 changed files with 8568 additions and 4808 deletions
@@ -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";
-8
View File
@@ -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 }
}
+32 -6
View File
@@ -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))
+27
View File
@@ -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
}
}
+10 -1
View File
@@ -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
}
}
+20 -3
View File
@@ -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?
+1 -1
View File
@@ -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()
@@ -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
}
@@ -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")
}
}
}
+6 -3
View File
@@ -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:
+138 -100
View File
@@ -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()
+85 -37
View File
@@ -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
+2
View File
@@ -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
}
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_phoneavatar.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -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 {
+12
View File
@@ -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
+133
View File
@@ -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)
})
}
}
+354
View File
@@ -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
View File
@@ -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
}
}
+349
View File
@@ -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
View File
@@ -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: &centralDirectorySignature, { 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()
}
}
+326
View File
@@ -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)
}
}