mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-28 02:45:58 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
aeafae62df
@ -8484,6 +8484,9 @@ Sorry for the inconvenience.";
|
||||
"Conversation.SuggestedVideoTextYou" = "You suggested %@ to use this video for their Telegram account.";
|
||||
"Conversation.SuggestedVideoView" = "View";
|
||||
|
||||
"CacheEvictionMenu.CategoryExceptions_1" = "%@ Exception";
|
||||
"CacheEvictionMenu.CategoryExceptions_any" = "%@ Exceptions";
|
||||
|
||||
"Conversation.Messages_1" = "%@ message";
|
||||
"Conversation.Messages_any" = "%@ messages";
|
||||
|
||||
|
||||
@ -52,8 +52,9 @@ public final class PeerSelectionControllerParams {
|
||||
public let multipleSelection: Bool
|
||||
public let forwardedMessageIds: [EngineMessage.Id]
|
||||
public let hasTypeHeaders: Bool
|
||||
public let selectForumThreads: Bool
|
||||
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filter: ChatListNodePeersFilter = [.onlyWriteable], forumPeerId: EnginePeer.Id? = nil, hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer, Int64?) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false, multipleSelection: Bool = false, forwardedMessageIds: [EngineMessage.Id] = [], hasTypeHeaders: Bool = false) {
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filter: ChatListNodePeersFilter = [.onlyWriteable], forumPeerId: EnginePeer.Id? = nil, hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer, Int64?) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false, multipleSelection: Bool = false, forwardedMessageIds: [EngineMessage.Id] = [], hasTypeHeaders: Bool = false, selectForumThreads: Bool = false) {
|
||||
self.context = context
|
||||
self.updatedPresentationData = updatedPresentationData
|
||||
self.filter = filter
|
||||
@ -68,6 +69,7 @@ public final class PeerSelectionControllerParams {
|
||||
self.multipleSelection = multipleSelection
|
||||
self.forwardedMessageIds = forwardedMessageIds
|
||||
self.hasTypeHeaders = hasTypeHeaders
|
||||
self.selectForumThreads = selectForumThreads
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1213,7 +1213,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
}
|
||||
}
|
||||
|
||||
let _ = (strongSelf.context.account.postbox.mediaBox.removeCachedResources(resourceIds, force: true, notify: true)
|
||||
let _ = (strongSelf.context.account.postbox.mediaBox.removeCachedResources(Array(resourceIds), force: true, notify: true)
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -1298,7 +1298,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
|
||||
self.context.engine.messages.ensureMessagesAreLocallyAvailable(messages: messages.values.filter { messageIds.contains($0.id) })
|
||||
|
||||
let peerSelectionController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.onlyWriteable, .excludeDisabled], multipleSelection: true))
|
||||
let peerSelectionController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.onlyWriteable, .excludeDisabled], multipleSelection: true, selectForumThreads: true))
|
||||
peerSelectionController.multiplePeersSelected = { [weak self, weak peerSelectionController] peers, peerMap, messageText, mode, forwardOptions in
|
||||
guard let strongSelf = self, let strongController = peerSelectionController else {
|
||||
return
|
||||
|
||||
@ -27,6 +27,8 @@ swift_library(
|
||||
"//submodules/AnimationUI:AnimationUI",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/TelegramCore",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -5,12 +5,22 @@ import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import ShimmerEffect
|
||||
import AvatarNode
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
|
||||
private let avatarFont = avatarPlaceholderFont(size: 16.0)
|
||||
|
||||
public enum ItemListDisclosureItemTitleColor {
|
||||
case primary
|
||||
case accent
|
||||
}
|
||||
|
||||
public enum ItemListDisclosureItemTitleFont {
|
||||
case regular
|
||||
case bold
|
||||
}
|
||||
|
||||
public enum ItemListDisclosureStyle {
|
||||
case arrow
|
||||
case optionArrows
|
||||
@ -31,11 +41,15 @@ public enum ItemListDisclosureLabelStyle {
|
||||
public class ItemListDisclosureItem: ListViewItem, ItemListItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let icon: UIImage?
|
||||
let context: AccountContext?
|
||||
let iconPeer: EnginePeer?
|
||||
let title: String
|
||||
let titleColor: ItemListDisclosureItemTitleColor
|
||||
let titleFont: ItemListDisclosureItemTitleFont
|
||||
let enabled: Bool
|
||||
let label: String
|
||||
let labelStyle: ItemListDisclosureLabelStyle
|
||||
let additionalDetailLabel: String?
|
||||
public let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
let disclosureStyle: ItemListDisclosureStyle
|
||||
@ -44,14 +58,18 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem {
|
||||
public let tag: ItemListItemTag?
|
||||
public let shimmeringIndex: Int?
|
||||
|
||||
public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, title: String, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, label: String, labelStyle: ItemListDisclosureLabelStyle = .text, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) {
|
||||
public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, label: String, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) {
|
||||
self.presentationData = presentationData
|
||||
self.icon = icon
|
||||
self.context = context
|
||||
self.iconPeer = iconPeer
|
||||
self.title = title
|
||||
self.titleColor = titleColor
|
||||
self.titleFont = titleFont
|
||||
self.enabled = enabled
|
||||
self.labelStyle = labelStyle
|
||||
self.label = label
|
||||
self.additionalDetailLabel = additionalDetailLabel
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
self.disclosureStyle = disclosureStyle
|
||||
@ -115,9 +133,11 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
var avatarNode: AvatarNode?
|
||||
let iconNode: ASImageNode
|
||||
let titleNode: TextNode
|
||||
public let labelNode: TextNode
|
||||
var additionalDetailLabelNode: TextNode?
|
||||
let arrowNode: ASImageNode
|
||||
let labelBadgeNode: ASImageNode
|
||||
let labelImageNode: ASImageNode
|
||||
@ -213,6 +233,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
|
||||
public func asyncLayout() -> (_ item: ItemListDisclosureItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
||||
let makeAdditionalDetailLabelLayout = TextNode.asyncLayout(self.additionalDetailLabelNode)
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
@ -284,8 +305,10 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
|
||||
let itemSeparatorColor: UIColor
|
||||
|
||||
var leftInset = 16.0 + params.leftInset
|
||||
if let _ = item.icon {
|
||||
if item.icon != nil {
|
||||
leftInset += 43.0
|
||||
} else if item.iconPeer != nil {
|
||||
leftInset += 46.0
|
||||
}
|
||||
|
||||
var additionalTextRightInset: CGFloat = 0.0
|
||||
@ -303,15 +326,31 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
|
||||
titleColor = item.presentationData.theme.list.itemDisabledTextColor
|
||||
}
|
||||
|
||||
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
|
||||
let titleFont: UIFont
|
||||
let defaultLabelFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
|
||||
switch item.titleFont {
|
||||
case .regular:
|
||||
titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
|
||||
case .bold:
|
||||
titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize)
|
||||
}
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset - additionalTextRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
var maxTitleWidth: CGFloat = params.width - params.rightInset - 20.0 - leftInset - additionalTextRightInset
|
||||
if item.iconPeer != nil {
|
||||
maxTitleWidth -= 12.0
|
||||
}
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let detailFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
|
||||
|
||||
let labelFont: UIFont
|
||||
let labelBadgeColor: UIColor
|
||||
var labelConstrain: CGFloat = params.width - params.rightInset - leftInset - 40.0 - titleLayout.size.width - 10.0
|
||||
if item.iconPeer != nil {
|
||||
labelConstrain -= 6.0
|
||||
}
|
||||
|
||||
switch item.labelStyle {
|
||||
case .badge:
|
||||
labelBadgeColor = item.presentationData.theme.list.itemCheckColors.foregroundColor
|
||||
@ -322,10 +361,10 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
|
||||
labelConstrain = params.width - params.rightInset - 40.0 - leftInset
|
||||
case let .coloredText(color):
|
||||
labelBadgeColor = color
|
||||
labelFont = titleFont
|
||||
labelFont = defaultLabelFont
|
||||
default:
|
||||
labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor
|
||||
labelFont = titleFont
|
||||
labelFont = defaultLabelFont
|
||||
}
|
||||
var multilineLabel = false
|
||||
if case .multilineDetailText = item.labelStyle {
|
||||
@ -334,10 +373,21 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
|
||||
|
||||
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: labelBadgeColor), backgroundColor: nil, maximumNumberOfLines: multilineLabel ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: labelConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let verticalInset: CGFloat = 11.0
|
||||
var additionalDetailLabelInfo: (TextNodeLayout, () -> TextNode)?
|
||||
if let additionalDetailLabel = item.additionalDetailLabel {
|
||||
additionalDetailLabelInfo = makeAdditionalDetailLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: additionalDetailLabel, font: detailFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset - additionalTextRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
}
|
||||
|
||||
let verticalInset: CGFloat
|
||||
if item.iconPeer != nil {
|
||||
verticalInset = 6.0
|
||||
} else {
|
||||
verticalInset = 11.0
|
||||
}
|
||||
|
||||
let titleSpacing: CGFloat = 1.0
|
||||
|
||||
let height: CGFloat
|
||||
var height: CGFloat
|
||||
switch item.labelStyle {
|
||||
case .detailText:
|
||||
height = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height
|
||||
@ -346,6 +396,12 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
|
||||
default:
|
||||
height = verticalInset * 2.0 + titleLayout.size.height
|
||||
}
|
||||
if let additionalDetailLabelInfo = additionalDetailLabelInfo {
|
||||
height += titleSpacing + additionalDetailLabelInfo.0.size.height
|
||||
}
|
||||
if item.iconPeer != nil {
|
||||
height = max(height, 40.0 + verticalInset * 2.0)
|
||||
}
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
@ -394,6 +450,27 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
|
||||
strongSelf.iconNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if let context = item.context, let iconPeer = item.iconPeer {
|
||||
let avatarNode: AvatarNode
|
||||
if let current = strongSelf.avatarNode {
|
||||
avatarNode = current
|
||||
} else {
|
||||
avatarNode = AvatarNode(font: avatarFont)
|
||||
strongSelf.avatarNode = avatarNode
|
||||
strongSelf.addSubnode(avatarNode)
|
||||
}
|
||||
let avatarSize: CGFloat = 40.0
|
||||
avatarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - avatarSize) / 2.0), y: floor((height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
|
||||
var clipStyle: AvatarNodeClipStyle = .round
|
||||
if case let .channel(channel) = iconPeer, channel.flags.contains(.isForum) {
|
||||
clipStyle = .roundedRect
|
||||
}
|
||||
avatarNode.setPeer(context: context, theme: item.presentationData.theme, peer: iconPeer, clipStyle: clipStyle)
|
||||
} else if let avatarNode = strongSelf.avatarNode {
|
||||
strongSelf.avatarNode = nil
|
||||
avatarNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if let updateArrowImage = updateArrowImage {
|
||||
strongSelf.arrowNode.image = updateArrowImage
|
||||
}
|
||||
@ -466,7 +543,20 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
|
||||
}
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size)
|
||||
var centralContentHeight: CGFloat = titleLayout.size.height
|
||||
switch item.labelStyle {
|
||||
case .detailText, .multilineDetailText:
|
||||
centralContentHeight += titleSpacing
|
||||
centralContentHeight += labelLayout.size.height
|
||||
default:
|
||||
break
|
||||
}
|
||||
if let additionalDetailLabelInfo {
|
||||
centralContentHeight += titleSpacing
|
||||
centralContentHeight += additionalDetailLabelInfo.0.size.height
|
||||
}
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - centralContentHeight) / 2.0)), size: titleLayout.size)
|
||||
strongSelf.titleNode.frame = titleFrame
|
||||
|
||||
if let updateBadgeImage = updatedLabelBadgeImage {
|
||||
@ -491,10 +581,25 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
|
||||
case .detailText, .multilineDetailText:
|
||||
labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size)
|
||||
default:
|
||||
labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: 11.0), size: labelLayout.size)
|
||||
labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: floor((height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
|
||||
}
|
||||
strongSelf.labelNode.frame = labelFrame
|
||||
|
||||
if let additionalDetailLabelInfo = additionalDetailLabelInfo {
|
||||
let additionalDetailLabelNode = additionalDetailLabelInfo.1()
|
||||
|
||||
if strongSelf.additionalDetailLabelNode !== additionalDetailLabelNode {
|
||||
strongSelf.additionalDetailLabelNode?.removeFromSupernode()
|
||||
strongSelf.additionalDetailLabelNode = additionalDetailLabelNode
|
||||
strongSelf.addSubnode(additionalDetailLabelNode)
|
||||
}
|
||||
|
||||
additionalDetailLabelNode.frame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: additionalDetailLabelInfo.0.size)
|
||||
} else if let additionalDetailLabelNode = strongSelf.additionalDetailLabelNode {
|
||||
strongSelf.additionalDetailLabelNode = nil
|
||||
additionalDetailLabelNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if case .textWithIcon = item.labelStyle {
|
||||
if let updatedLabelImage = updatedLabelImage {
|
||||
strongSelf.labelImageNode.image = updatedLabelImage
|
||||
|
||||
@ -194,7 +194,7 @@ public final class MediaBox {
|
||||
}), basePath: basePath + "/storage")
|
||||
|
||||
self.timeBasedCleanup = TimeBasedCleanup(generalPaths: [
|
||||
self.basePath,
|
||||
//self.basePath,
|
||||
self.basePath + "/cache",
|
||||
self.basePath + "/animation-cache"
|
||||
], shortLivedPaths: [
|
||||
@ -595,7 +595,7 @@ public final class MediaBox {
|
||||
}
|
||||
|
||||
if let location = parameters?.location {
|
||||
self.storageBox.add(reference: StorageBox.Reference(peerId: location.peerId, messageNamespace: UInt8(clamping: location.messageId.namespace), messageId: location.messageId.id), to: resource.id.stringRepresentation.data(using: .utf8)!)
|
||||
self.storageBox.add(reference: StorageBox.Reference(peerId: location.peerId.toInt64(), messageNamespace: UInt8(clamping: location.messageId.namespace), messageId: location.messageId.id), to: resource.id.stringRepresentation.data(using: .utf8)!)
|
||||
}
|
||||
|
||||
guard let (fileContext, releaseContext) = self.fileContext(for: resource.id) else {
|
||||
@ -761,7 +761,7 @@ public final class MediaBox {
|
||||
let paths = self.storePathsForId(resource.id)
|
||||
|
||||
if let location = parameters?.location {
|
||||
self.storageBox.add(reference: StorageBox.Reference(peerId: location.peerId, messageNamespace: UInt8(clamping: location.messageId.namespace), messageId: location.messageId.id), to: resource.id.stringRepresentation.data(using: .utf8)!)
|
||||
self.storageBox.add(reference: StorageBox.Reference(peerId: location.peerId.toInt64(), messageNamespace: UInt8(clamping: location.messageId.namespace), messageId: location.messageId.id), to: resource.id.stringRepresentation.data(using: .utf8)!)
|
||||
}
|
||||
|
||||
if let _ = fileSize(paths.complete) {
|
||||
@ -1207,6 +1207,24 @@ public final class MediaBox {
|
||||
}
|
||||
}
|
||||
|
||||
public func resourceUsageWithInfo(id: MediaResourceId) -> Int32 {
|
||||
let paths = self.storePathsForId(id)
|
||||
|
||||
var value = stat()
|
||||
|
||||
if stat(paths.complete, &value) == 0 {
|
||||
return Int32(value.st_mtimespec.tv_sec)
|
||||
}
|
||||
|
||||
value = stat()
|
||||
|
||||
if stat(paths.partial, &value) == 0 {
|
||||
return Int32(value.st_mtimespec.tv_sec)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
public func collectResourceCacheUsage(_ ids: [MediaResourceId]) -> Signal<[MediaResourceId: Int64], NoError> {
|
||||
return Signal { subscriber in
|
||||
self.dataQueue.async {
|
||||
@ -1472,7 +1490,7 @@ public final class MediaBox {
|
||||
}
|
||||
}
|
||||
|
||||
public func removeCachedResources(_ ids: Set<MediaResourceId>, force: Bool = false, notify: Bool = false) -> Signal<Float, NoError> {
|
||||
public func removeCachedResources(_ ids: [MediaResourceId], force: Bool = false, notify: Bool = false) -> Signal<Float, NoError> {
|
||||
return Signal { subscriber in
|
||||
self.dataQueue.async {
|
||||
let uniqueIds = Set(ids.map { $0.stringRepresentation })
|
||||
|
||||
@ -8,6 +8,7 @@ private struct SqliteValueBoxTable {
|
||||
}
|
||||
|
||||
let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
|
||||
let SQLITE_PREPARE_PERSISTENT: UInt32 = 1
|
||||
|
||||
private func checkTableKey(_ table: ValueBoxTable, _ key: ValueBoxKey) {
|
||||
switch table.keyType {
|
||||
@ -731,7 +732,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "SELECT value FROM t\(table.id) WHERE key=?", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "SELECT value FROM t\(table.id) WHERE key=?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.getStatements[table.id] = preparedStatement
|
||||
@ -760,7 +761,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "SELECT rowid FROM t\(table.id) WHERE key=?", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "SELECT rowid FROM t\(table.id) WHERE key=?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.getRowIdStatements[table.id] = preparedStatement
|
||||
@ -790,7 +791,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "SELECT key FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key ASC LIMIT ?", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "SELECT key FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key ASC LIMIT ?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.rangeKeyAscStatementsLimit[table.id] = preparedStatement
|
||||
@ -823,7 +824,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "SELECT key FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key ASC", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "SELECT key FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key ASC", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.rangeKeyAscStatementsNoLimit[table.id] = preparedStatement
|
||||
@ -854,7 +855,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "SELECT key FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key DESC LIMIT ?", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "SELECT key FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key DESC LIMIT ?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.rangeKeyDescStatementsLimit[table.id] = preparedStatement
|
||||
@ -886,7 +887,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "SELECT key FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key DESC", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "SELECT key FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key DESC", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.rangeKeyDescStatementsNoLimit[table.id] = preparedStatement
|
||||
@ -918,7 +919,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "DELETE FROM t\(table.id) WHERE key >= ? AND key <= ?", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "DELETE FROM t\(table.id) WHERE key >= ? AND key <= ?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.deleteRangeStatements[table.id] = preparedStatement
|
||||
@ -950,7 +951,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "SELECT key, value FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key ASC LIMIT ?", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "SELECT key, value FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key ASC LIMIT ?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.rangeValueAscStatementsLimit[table.id] = preparedStatement
|
||||
@ -982,7 +983,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "SELECT key, value FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key ASC", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "SELECT key, value FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key ASC", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.rangeValueAscStatementsNoLimit[table.id] = preparedStatement
|
||||
@ -1014,7 +1015,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "SELECT key, value FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key DESC LIMIT ?", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "SELECT key, value FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key DESC LIMIT ?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.rangeValueDescStatementsLimit[table.id] = preparedStatement
|
||||
@ -1047,7 +1048,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "SELECT key, value FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key DESC", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "SELECT key, value FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key DESC", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.rangeValueDescStatementsNoLimit[table.id] = preparedStatement
|
||||
@ -1077,7 +1078,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "SELECT key, value FROM t\(table.id) ORDER BY key ASC", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "SELECT key, value FROM t\(table.id) ORDER BY key ASC", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.scanStatements[table.id] = preparedStatement
|
||||
@ -1098,7 +1099,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "SELECT key FROM t\(table.id) ORDER BY key ASC", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "SELECT key FROM t\(table.id) ORDER BY key ASC", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.scanKeysStatements[table.id] = preparedStatement
|
||||
@ -1120,7 +1121,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "SELECT rowid FROM t\(table.id) WHERE key=?", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "SELECT rowid FROM t\(table.id) WHERE key=?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.existsStatements[table.id] = preparedStatement
|
||||
@ -1149,7 +1150,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "UPDATE t\(table.id) SET value=? WHERE key=?", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "UPDATE t\(table.id) SET value=? WHERE key=?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.updateStatements[table.id] = preparedStatement
|
||||
@ -1180,7 +1181,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
if status != SQLITE_OK {
|
||||
let errorText = self.database.currentError() ?? "Unknown error"
|
||||
preconditionFailure(errorText)
|
||||
@ -1194,7 +1195,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?)", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?)", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
if status != SQLITE_OK {
|
||||
let errorText = self.database.currentError() ?? "Unknown error"
|
||||
preconditionFailure(errorText)
|
||||
@ -1233,7 +1234,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?) ON CONFLICT(key) DO NOTHING", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?) ON CONFLICT(key) DO NOTHING", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
if status != SQLITE_OK {
|
||||
let errorText = self.database.currentError() ?? "Unknown error"
|
||||
preconditionFailure(errorText)
|
||||
@ -1247,7 +1248,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?)", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?)", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
if status != SQLITE_OK {
|
||||
let errorText = self.database.currentError() ?? "Unknown error"
|
||||
preconditionFailure(errorText)
|
||||
@ -1285,7 +1286,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "DELETE FROM t\(table.id) WHERE key=?", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "DELETE FROM t\(table.id) WHERE key=?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.deleteStatements[table.id] = preparedStatement
|
||||
@ -1315,7 +1316,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "UPDATE t\(table.id) SET key=? WHERE key=?", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "UPDATE t\(table.id) SET key=? WHERE key=?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.moveStatements[table.id] = preparedStatement
|
||||
@ -1349,7 +1350,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "INSERT INTO t\(toTable.id) (key, value) SELECT ?, t\(fromTable.id).value FROM t\(fromTable.id) WHERE t\(fromTable.id).key=?", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO t\(toTable.id) (key, value) SELECT ?, t\(fromTable.id).value FROM t\(fromTable.id) WHERE t\(fromTable.id).key=?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.copyStatements[TablePairKey(table1: fromTable.id, table2: toTable.id)] = preparedStatement
|
||||
@ -1384,7 +1385,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "INSERT INTO ft\(table.id) (collectionId, itemId, contents, tags) VALUES(?, ?, ?, ?)", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO ft\(table.id) (collectionId, itemId, contents, tags) VALUES(?, ?, ?, ?)", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.fullTextInsertStatements[table.id] = preparedStatement
|
||||
@ -1423,7 +1424,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "DELETE FROM ft\(table.id) WHERE itemId=?", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "DELETE FROM ft\(table.id) WHERE itemId=?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.fullTextDeleteStatements[table.id] = preparedStatement
|
||||
@ -1447,7 +1448,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "SELECT collectionId, itemId FROM ft\(table.id) WHERE ft\(table.id) MATCH 'contents:\"' || ? || '\"'", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "SELECT collectionId, itemId FROM ft\(table.id) WHERE ft\(table.id) MATCH 'contents:\"' || ? || '\"'", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
if status != SQLITE_OK {
|
||||
self.printError()
|
||||
assertionFailure()
|
||||
@ -1474,7 +1475,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "SELECT collectionId, itemId FROM ft\(table.id) WHERE ft\(table.id) MATCH 'contents:\"' || ? || '\" AND collectionId:\"' || ? || '\"'", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "SELECT collectionId, itemId FROM ft\(table.id) WHERE ft\(table.id) MATCH 'contents:\"' || ? || '\" AND collectionId:\"' || ? || '\"'", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.fullTextMatchCollectionStatements[table.id] = preparedStatement
|
||||
@ -1503,7 +1504,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
resultStatement = statement
|
||||
} else {
|
||||
var statement: OpaquePointer? = nil
|
||||
let status = sqlite3_prepare_v2(self.database.handle, "SELECT collectionId, itemId FROM ft\(table.id) WHERE ft\(table.id) MATCH 'contents:\"' || ? || '\" AND collectionId:\"' || ? || '\" AND tags:\"' || ? || '\"'", -1, &statement, nil)
|
||||
let status = sqlite3_prepare_v3(self.database.handle, "SELECT collectionId, itemId FROM ft\(table.id) WHERE ft\(table.id) MATCH 'contents:\"' || ? || '\" AND collectionId:\"' || ? || '\" AND tags:\"' || ? || '\"'", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
|
||||
precondition(status == SQLITE_OK)
|
||||
let preparedStatement = SqlitePreparedStatement(statement: statement)
|
||||
self.fullTextMatchCollectionTagsStatements[table.id] = preparedStatement
|
||||
|
||||
@ -20,11 +20,11 @@ private func md5Hash(_ data: Data) -> HashId {
|
||||
|
||||
public final class StorageBox {
|
||||
public struct Reference {
|
||||
public var peerId: PeerId
|
||||
public var peerId: Int64
|
||||
public var messageNamespace: UInt8
|
||||
public var messageId: Int32
|
||||
|
||||
public init(peerId: PeerId, messageNamespace: UInt8, messageId: Int32) {
|
||||
public init(peerId: Int64, messageNamespace: UInt8, messageId: Int32) {
|
||||
self.peerId = peerId
|
||||
self.messageNamespace = messageNamespace
|
||||
self.messageId = messageId
|
||||
@ -60,6 +60,8 @@ public final class StorageBox {
|
||||
let valueBox: SqliteValueBox
|
||||
let hashIdToIdTable: ValueBoxTable
|
||||
let idToReferenceTable: ValueBoxTable
|
||||
let peerIdToIdTable: ValueBoxTable
|
||||
let peerIdTable: ValueBoxTable
|
||||
|
||||
init(queue: Queue, logger: StorageBox.Logger, basePath: String) {
|
||||
self.queue = queue
|
||||
@ -80,6 +82,8 @@ public final class StorageBox {
|
||||
|
||||
self.hashIdToIdTable = ValueBoxTable(id: 5, keyType: .binary, compactValuesOnCreation: true)
|
||||
self.idToReferenceTable = ValueBoxTable(id: 6, keyType: .binary, compactValuesOnCreation: true)
|
||||
self.peerIdToIdTable = ValueBoxTable(id: 7, keyType: .binary, compactValuesOnCreation: true)
|
||||
self.peerIdTable = ValueBoxTable(id: 8, keyType: .binary, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
func add(reference: Reference, to id: Data) {
|
||||
@ -87,19 +91,154 @@ public final class StorageBox {
|
||||
|
||||
let hashId = md5Hash(id)
|
||||
|
||||
let mainKey = ValueBoxKey(length: hashId.data.count)
|
||||
let mainKey = ValueBoxKey(length: 16)
|
||||
mainKey.setData(0, value: hashId.data)
|
||||
self.valueBox.setOrIgnore(self.hashIdToIdTable, key: mainKey, value: MemoryBuffer(data: id))
|
||||
|
||||
let idKey = ValueBoxKey(length: hashId.data.count + 8 + 1 + 4)
|
||||
idKey.setData(0, value: hashId.data)
|
||||
idKey.setInt64(hashId.data.count, value: reference.peerId.toInt64())
|
||||
idKey.setInt64(hashId.data.count, value: reference.peerId)
|
||||
idKey.setUInt8(hashId.data.count + 8, value: reference.messageNamespace)
|
||||
idKey.setInt32(hashId.data.count + 8 + 1, value: reference.messageId)
|
||||
|
||||
var alreadyStored = false
|
||||
if !self.valueBox.exists(self.idToReferenceTable, key: idKey) {
|
||||
self.valueBox.setOrIgnore(self.idToReferenceTable, key: idKey, value: MemoryBuffer())
|
||||
} else {
|
||||
alreadyStored = true
|
||||
}
|
||||
|
||||
if !alreadyStored {
|
||||
var idInPeerIdStored = false
|
||||
|
||||
let peerIdIdKey = ValueBoxKey(length: 8 + 16)
|
||||
peerIdIdKey.setInt64(0, value: reference.peerId)
|
||||
peerIdIdKey.setData(8, value: hashId.data)
|
||||
var peerIdIdCount: Int32 = 0
|
||||
if let value = self.valueBox.get(self.peerIdToIdTable, key: peerIdIdKey) {
|
||||
idInPeerIdStored = true
|
||||
if value.length == 4 {
|
||||
memcpy(&peerIdIdCount, value.memory, 4)
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
peerIdIdCount += 1
|
||||
self.valueBox.set(self.peerIdToIdTable, key: peerIdIdKey, value: MemoryBuffer(memory: &peerIdIdCount, capacity: 4, length: 4, freeWhenDone: false))
|
||||
|
||||
if !idInPeerIdStored {
|
||||
let peerIdKey = ValueBoxKey(length: 8)
|
||||
peerIdKey.setInt64(0, value: reference.peerId)
|
||||
var peerIdCount: Int32 = 0
|
||||
if let value = self.valueBox.get(self.peerIdTable, key: peerIdKey) {
|
||||
if value.length == 4 {
|
||||
memcpy(&peerIdCount, value.memory, 4)
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
peerIdCount += 1
|
||||
self.valueBox.set(self.peerIdTable, key: peerIdKey, value: MemoryBuffer(memory: &peerIdCount, capacity: 4, length: 4, freeWhenDone: false))
|
||||
}
|
||||
}
|
||||
|
||||
self.valueBox.commit()
|
||||
}
|
||||
|
||||
func remove(ids: [Data]) {
|
||||
self.valueBox.begin()
|
||||
|
||||
let mainKey = ValueBoxKey(length: 16)
|
||||
let peerIdIdKey = ValueBoxKey(length: 8 + 16)
|
||||
let peerIdKey = ValueBoxKey(length: 8)
|
||||
|
||||
for id in ids {
|
||||
let hashId = md5Hash(id)
|
||||
mainKey.setData(0, value: hashId.data)
|
||||
|
||||
self.valueBox.remove(self.hashIdToIdTable, key: mainKey, secure: false)
|
||||
|
||||
var referenceKeys: [ValueBoxKey] = []
|
||||
self.valueBox.range(self.idToReferenceTable, start: mainKey, end: mainKey.successor, keys: { key in
|
||||
referenceKeys.append(key)
|
||||
return true
|
||||
}, limit: 0)
|
||||
var peerIds = Set<Int64>()
|
||||
for key in referenceKeys {
|
||||
peerIds.insert(key.getInt64(16))
|
||||
self.valueBox.remove(self.idToReferenceTable, key: key, secure: false)
|
||||
}
|
||||
|
||||
for peerId in peerIds {
|
||||
peerIdIdKey.setInt64(0, value: peerId)
|
||||
peerIdIdKey.setData(8, value: hashId.data)
|
||||
|
||||
if self.valueBox.exists(self.peerIdToIdTable, key: peerIdIdKey) {
|
||||
self.valueBox.remove(self.peerIdToIdTable, key: peerIdIdKey, secure: false)
|
||||
|
||||
peerIdKey.setInt64(0, value: peerId)
|
||||
if let value = self.valueBox.get(self.peerIdTable, key: peerIdKey) {
|
||||
var peerIdCount: Int32 = 0
|
||||
if value.length == 4 {
|
||||
memcpy(&peerIdCount, value.memory, 4)
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
peerIdCount -= 1
|
||||
if peerIdCount > 0 {
|
||||
self.valueBox.set(self.peerIdTable, key: peerIdKey, value: MemoryBuffer(memory: &peerIdCount, capacity: 4, length: 4, freeWhenDone: false))
|
||||
} else {
|
||||
self.valueBox.remove(self.peerIdTable, key: peerIdKey, secure: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.valueBox.commit()
|
||||
}
|
||||
|
||||
func allPeerIds() -> [PeerId] {
|
||||
var result: [PeerId] = []
|
||||
|
||||
self.valueBox.begin()
|
||||
|
||||
self.valueBox.scan(self.peerIdTable, keys: { key in
|
||||
result.append(PeerId(key.getInt64(0)))
|
||||
return true
|
||||
})
|
||||
|
||||
self.valueBox.commit()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func all(peerId: PeerId) -> [Data] {
|
||||
self.valueBox.begin()
|
||||
|
||||
var hashIds: [Data] = []
|
||||
let peerIdIdKey = ValueBoxKey(length: 8)
|
||||
peerIdIdKey.setInt64(0, value: peerId.toInt64())
|
||||
self.valueBox.range(self.peerIdToIdTable, start: peerIdIdKey, end: peerIdIdKey.successor, keys: { key in
|
||||
hashIds.append(key.getData(8, length: 16))
|
||||
return true
|
||||
}, limit: 0)
|
||||
|
||||
var result: [Data] = []
|
||||
let mainKey = ValueBoxKey(length: 16)
|
||||
for hashId in hashIds {
|
||||
mainKey.setData(0, value: hashId)
|
||||
if let value = self.valueBox.get(self.hashIdToIdTable, key: mainKey) {
|
||||
result.append(value.makeData())
|
||||
}
|
||||
}
|
||||
|
||||
self.valueBox.commit()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func all() -> [Entry] {
|
||||
var result: [Entry] = []
|
||||
|
||||
@ -111,7 +250,7 @@ public final class StorageBox {
|
||||
self.valueBox.scan(self.idToReferenceTable, keys: { key in
|
||||
let id = key.getData(0, length: 16)
|
||||
|
||||
let peerId = PeerId(key.getInt64(16))
|
||||
let peerId = key.getInt64(16)
|
||||
let messageNamespace: UInt8 = key.getUInt8(16 + 8)
|
||||
let messageId = key.getInt32(16 + 8 + 1)
|
||||
|
||||
@ -148,7 +287,7 @@ public final class StorageBox {
|
||||
idKey.setData(0, value: hashId.data)
|
||||
var currentReferences: [Reference] = []
|
||||
self.valueBox.range(self.idToReferenceTable, start: idKey, end: idKey.successor, keys: { key in
|
||||
let peerId = PeerId(key.getInt64(16))
|
||||
let peerId = key.getInt64(16)
|
||||
let messageNamespace: UInt8 = key.getUInt8(16 + 8)
|
||||
let messageId = key.getInt32(16 + 8 + 1)
|
||||
|
||||
@ -185,6 +324,12 @@ public final class StorageBox {
|
||||
}
|
||||
}
|
||||
|
||||
public func remove(ids: [Data]) {
|
||||
self.impl.with { impl in
|
||||
impl.remove(ids: ids)
|
||||
}
|
||||
}
|
||||
|
||||
public func all() -> Signal<[Entry], NoError> {
|
||||
return self.impl.signalWith { impl, subscriber in
|
||||
subscriber.putNext(impl.all())
|
||||
@ -194,6 +339,24 @@ public final class StorageBox {
|
||||
}
|
||||
}
|
||||
|
||||
public func allPeerIds() -> Signal<[PeerId], NoError> {
|
||||
return self.impl.signalWith { impl, subscriber in
|
||||
subscriber.putNext(impl.allPeerIds())
|
||||
subscriber.putCompletion()
|
||||
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public func all(peerId: PeerId) -> Signal<[Data], NoError> {
|
||||
return self.impl.signalWith { impl, subscriber in
|
||||
subscriber.putNext(impl.all(peerId: peerId))
|
||||
subscriber.putCompletion()
|
||||
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public func get(ids: [Data]) -> Signal<[Entry], NoError> {
|
||||
return self.impl.signalWith { impl, subscriber in
|
||||
subscriber.putNext(impl.get(ids: ids))
|
||||
|
||||
@ -108,6 +108,7 @@ swift_library(
|
||||
"//submodules/PersistentStringHash:PersistentStringHash",
|
||||
"//submodules/TelegramUI/Components/NotificationPeerExceptionController",
|
||||
"//submodules/TelegramUI/Components/ChatTimerScreen",
|
||||
"//submodules/AnimatedAvatarSetNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -17,6 +17,8 @@ import DeleteChatPeerActionSheetItem
|
||||
import UndoUI
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import ContextUI
|
||||
import AnimatedAvatarSetNode
|
||||
|
||||
private func totalDiskSpace() -> Int64 {
|
||||
do {
|
||||
@ -44,8 +46,9 @@ private final class StorageUsageControllerArguments {
|
||||
let openPeerMedia: (PeerId) -> Void
|
||||
let clearPeerMedia: (PeerId) -> Void
|
||||
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
|
||||
let openCategoryMenu: (StorageUsageEntryTag) -> Void
|
||||
|
||||
init(context: AccountContext, updateKeepMediaTimeout: @escaping (Int32) -> Void, updateMaximumCacheSize: @escaping (Int32) -> Void, openClearAll: @escaping () -> Void, openPeerMedia: @escaping (PeerId) -> Void, clearPeerMedia: @escaping (PeerId) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void) {
|
||||
init(context: AccountContext, updateKeepMediaTimeout: @escaping (Int32) -> Void, updateMaximumCacheSize: @escaping (Int32) -> Void, openClearAll: @escaping () -> Void, openPeerMedia: @escaping (PeerId) -> Void, clearPeerMedia: @escaping (PeerId) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, openCategoryMenu: @escaping (StorageUsageEntryTag) -> Void) {
|
||||
self.context = context
|
||||
self.updateKeepMediaTimeout = updateKeepMediaTimeout
|
||||
self.updateMaximumCacheSize = updateMaximumCacheSize
|
||||
@ -53,6 +56,7 @@ private final class StorageUsageControllerArguments {
|
||||
self.openPeerMedia = openPeerMedia
|
||||
self.clearPeerMedia = clearPeerMedia
|
||||
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
|
||||
self.openCategoryMenu = openCategoryMenu
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,8 +67,27 @@ private enum StorageUsageSection: Int32 {
|
||||
case peers
|
||||
}
|
||||
|
||||
private enum StorageUsageEntryTag: Hashable, ItemListItemTag {
|
||||
case privateChats
|
||||
case groups
|
||||
case channels
|
||||
|
||||
public func isEqual(to other: ItemListItemTag) -> Bool {
|
||||
if let other = other as? StorageUsageEntryTag, self == other {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum StorageUsageEntry: ItemListNodeEntry {
|
||||
case keepMediaHeader(PresentationTheme, String)
|
||||
|
||||
case keepMediaPrivateChats(title: String, text: String?, value: String)
|
||||
case keepMediaGroups(title: String, text: String?, value: String)
|
||||
case keepMediaChannels(title: String, text: String?, value: String)
|
||||
|
||||
case keepMedia(PresentationTheme, PresentationStrings, Int32)
|
||||
case keepMediaInfo(PresentationTheme, String)
|
||||
|
||||
@ -82,7 +105,7 @@ private enum StorageUsageEntry: ItemListNodeEntry {
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .keepMediaHeader, .keepMedia, .keepMediaInfo:
|
||||
case .keepMediaHeader, .keepMedia, .keepMediaInfo, .keepMediaPrivateChats, .keepMediaGroups, .keepMediaChannels:
|
||||
return StorageUsageSection.keepMedia.rawValue
|
||||
case .maximumSizeHeader, .maximumSize, .maximumSizeInfo:
|
||||
return StorageUsageSection.maximumSize.rawValue
|
||||
@ -99,26 +122,32 @@ private enum StorageUsageEntry: ItemListNodeEntry {
|
||||
return 0
|
||||
case .keepMedia:
|
||||
return 1
|
||||
case .keepMediaInfo:
|
||||
case .keepMediaPrivateChats:
|
||||
return 2
|
||||
case .maximumSizeHeader:
|
||||
case .keepMediaGroups:
|
||||
return 3
|
||||
case .maximumSize:
|
||||
case .keepMediaChannels:
|
||||
return 4
|
||||
case .maximumSizeInfo:
|
||||
case .keepMediaInfo:
|
||||
return 5
|
||||
case .storageHeader:
|
||||
case .maximumSizeHeader:
|
||||
return 6
|
||||
case .storageUsage:
|
||||
case .maximumSize:
|
||||
return 7
|
||||
case .collecting:
|
||||
case .maximumSizeInfo:
|
||||
return 8
|
||||
case .clearAll:
|
||||
case .storageHeader:
|
||||
return 9
|
||||
case .peersHeader:
|
||||
case .storageUsage:
|
||||
return 10
|
||||
case .collecting:
|
||||
return 11
|
||||
case .clearAll:
|
||||
return 12
|
||||
case .peersHeader:
|
||||
return 13
|
||||
case let .peer(index, _, _, _, _, _, _, _, _):
|
||||
return 11 + index
|
||||
return 14 + index
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,6 +171,24 @@ private enum StorageUsageEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .keepMediaPrivateChats(title, text, value):
|
||||
if case .keepMediaPrivateChats(title, text, value) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .keepMediaGroups(title, text, value):
|
||||
if case .keepMediaGroups(title, text, value) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .keepMediaChannels(title, text, value):
|
||||
if case .keepMediaChannels(title, text, value) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .maximumSizeHeader(lhsTheme, lhsText):
|
||||
if case let .maximumSizeHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
@ -235,6 +282,18 @@ private enum StorageUsageEntry: ItemListNodeEntry {
|
||||
switch self {
|
||||
case let .keepMediaHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .keepMediaPrivateChats(title, text, value):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/EditProfile")?.precomposed(), title: title, enabled: true, label: value, labelStyle: .text, additionalDetailLabel: text, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: {
|
||||
arguments.openCategoryMenu(.privateChats)
|
||||
}, tag: StorageUsageEntryTag.privateChats)
|
||||
case let .keepMediaGroups(title, text, value):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/GroupChats")?.precomposed(), title: title, enabled: true, label: value, labelStyle: .text, additionalDetailLabel: text, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: {
|
||||
arguments.openCategoryMenu(.groups)
|
||||
}, tag: StorageUsageEntryTag.groups)
|
||||
case let .keepMediaChannels(title, text, value):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Channels")?.precomposed(), title: title, enabled: true, label: value, labelStyle: .text, additionalDetailLabel: text, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: {
|
||||
arguments.openCategoryMenu(.channels)
|
||||
}, tag: StorageUsageEntryTag.channels)
|
||||
case let .keepMedia(theme, strings, value):
|
||||
return KeepMediaDurationPickerItem(theme: theme, strings: strings, value: value, sectionId: self.section, updated: { updatedValue in
|
||||
arguments.updateKeepMediaTimeout(updatedValue)
|
||||
@ -279,18 +338,46 @@ private enum StorageUsageEntry: ItemListNodeEntry {
|
||||
}
|
||||
|
||||
private struct StorageUsageState: Equatable {
|
||||
let peerIdWithRevealedOptions: PeerId?
|
||||
|
||||
func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> StorageUsageState {
|
||||
return StorageUsageState(peerIdWithRevealedOptions: peerIdWithRevealedOptions)
|
||||
}
|
||||
var peerIdWithRevealedOptions: PeerId?
|
||||
}
|
||||
|
||||
private func storageUsageControllerEntries(presentationData: PresentationData, cacheSettings: CacheStorageSettings, cacheStats: CacheUsageStatsResult?, state: StorageUsageState) -> [StorageUsageEntry] {
|
||||
private func storageUsageControllerEntries(presentationData: PresentationData, cacheSettings: CacheStorageSettings, accountSpecificCacheSettings: AccountSpecificCacheStorageSettings, cacheStats: CacheUsageStatsResult?, state: StorageUsageState) -> [StorageUsageEntry] {
|
||||
var entries: [StorageUsageEntry] = []
|
||||
|
||||
entries.append(.keepMediaHeader(presentationData.theme, presentationData.strings.Cache_KeepMedia.uppercased()))
|
||||
entries.append(.keepMedia(presentationData.theme, presentationData.strings, cacheSettings.defaultCacheStorageTimeout))
|
||||
|
||||
let sections: [StorageUsageEntryTag] = [.privateChats, .groups, .channels]
|
||||
for section in sections {
|
||||
let mappedCategory: CacheStorageSettings.PeerStorageCategory
|
||||
switch section {
|
||||
case .privateChats:
|
||||
mappedCategory = .privateChats
|
||||
case .groups:
|
||||
mappedCategory = .groups
|
||||
case .channels:
|
||||
mappedCategory = .channels
|
||||
}
|
||||
let value = cacheSettings.categoryStorageTimeout[mappedCategory] ?? Int32.max
|
||||
|
||||
let optionText: String
|
||||
if value == Int32.max {
|
||||
optionText = presentationData.strings.ClearCache_Forever
|
||||
} else {
|
||||
optionText = timeIntervalString(strings: presentationData.strings, value: value)
|
||||
}
|
||||
|
||||
switch section {
|
||||
case .privateChats:
|
||||
entries.append(.keepMediaPrivateChats(title: presentationData.strings.Notifications_PrivateChats, text: nil, value: optionText))
|
||||
case .groups:
|
||||
entries.append(.keepMediaGroups(title: presentationData.strings.Notifications_GroupChats, text: nil, value: optionText))
|
||||
case .channels:
|
||||
entries.append(.keepMediaChannels(title: presentationData.strings.Notifications_Channels, text: nil, value: optionText))
|
||||
}
|
||||
}
|
||||
|
||||
//entries.append(.keepMedia(presentationData.theme, presentationData.strings, cacheSettings.defaultCacheStorageTimeout))
|
||||
|
||||
entries.append(.keepMediaInfo(presentationData.theme, presentationData.strings.Cache_KeepMediaHelp))
|
||||
|
||||
entries.append(.maximumSizeHeader(presentationData.theme, presentationData.strings.Cache_MaximumCacheSize.uppercased()))
|
||||
@ -420,7 +507,24 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P
|
||||
return cacheSettings
|
||||
})
|
||||
|
||||
let accountSpecificCacheSettingsPromise = Promise<AccountSpecificCacheStorageSettings>()
|
||||
let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings]))
|
||||
accountSpecificCacheSettingsPromise.set(context.account.postbox.combinedView(keys: [viewKey])
|
||||
|> map { views -> AccountSpecificCacheStorageSettings in
|
||||
let cacheSettings: AccountSpecificCacheStorageSettings
|
||||
if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) {
|
||||
cacheSettings = value
|
||||
} else {
|
||||
cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings
|
||||
}
|
||||
|
||||
return cacheSettings
|
||||
})
|
||||
|
||||
var presentControllerImpl: ((ViewController, PresentationContextType, Any?) -> Void)?
|
||||
var pushControllerImpl: ((ViewController) -> Void)?
|
||||
var findAutoremoveReferenceNode: ((StorageUsageEntryTag) -> ItemListDisclosureItemNode?)?
|
||||
var presentInGlobalOverlay: ((ViewController) -> Void)?
|
||||
|
||||
var statsPromise: Promise<CacheUsageStatsResult?>
|
||||
if let cacheUsagePromise = cacheUsagePromise {
|
||||
@ -441,11 +545,15 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P
|
||||
|
||||
let arguments = StorageUsageControllerArguments(context: context, updateKeepMediaTimeout: { value in
|
||||
let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
|
||||
return current.withUpdatedDefaultCacheStorageTimeout(value)
|
||||
var current = current
|
||||
current.defaultCacheStorageTimeout = value
|
||||
return current
|
||||
}).start()
|
||||
}, updateMaximumCacheSize: { value in
|
||||
let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
|
||||
return current.withUpdatedDefaultCacheStorageLimitGigabytes(value)
|
||||
var current = current
|
||||
current.defaultCacheStorageLimitGigabytes = value
|
||||
return current
|
||||
}).start()
|
||||
}, openClearAll: {
|
||||
let _ = (statsPromise.get()
|
||||
@ -957,28 +1065,197 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P
|
||||
})
|
||||
|
||||
updateState { state in
|
||||
return state.withUpdatedPeerIdWithRevealedOptions(nil)
|
||||
var state = state
|
||||
state.peerIdWithRevealedOptions = nil
|
||||
return state
|
||||
}
|
||||
}, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
|
||||
updateState { state in
|
||||
if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) {
|
||||
return state.withUpdatedPeerIdWithRevealedOptions(peerId)
|
||||
var state = state
|
||||
state.peerIdWithRevealedOptions = peerId
|
||||
return state
|
||||
} else {
|
||||
return state
|
||||
}
|
||||
}
|
||||
}, openCategoryMenu: { category in
|
||||
let mappedCategory: CacheStorageSettings.PeerStorageCategory
|
||||
switch category {
|
||||
case .privateChats:
|
||||
mappedCategory = .privateChats
|
||||
case .groups:
|
||||
mappedCategory = .groups
|
||||
case .channels:
|
||||
mappedCategory = .channels
|
||||
}
|
||||
|
||||
let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings]))
|
||||
let accountSpecificSettings: Signal<AccountSpecificCacheStorageSettings, NoError> = context.account.postbox.combinedView(keys: [viewKey])
|
||||
|> map { views -> AccountSpecificCacheStorageSettings in
|
||||
let cacheSettings: AccountSpecificCacheStorageSettings
|
||||
if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) {
|
||||
cacheSettings = value
|
||||
} else {
|
||||
cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings
|
||||
}
|
||||
|
||||
return cacheSettings
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let peerExceptions: Signal<[(peer: FoundPeer, value: Int32)], NoError> = accountSpecificSettings
|
||||
|> mapToSignal { accountSpecificSettings -> Signal<[(peer: FoundPeer, value: Int32)], NoError> in
|
||||
return context.account.postbox.transaction { transaction -> [(peer: FoundPeer, value: Int32)] in
|
||||
var result: [(peer: FoundPeer, value: Int32)] = []
|
||||
|
||||
for (peerId, value) in accountSpecificSettings.peerStorageTimeoutExceptions {
|
||||
guard let peer = transaction.getPeer(peerId) else {
|
||||
continue
|
||||
}
|
||||
let peerCategory: CacheStorageSettings.PeerStorageCategory
|
||||
var subscriberCount: Int32?
|
||||
if peer is TelegramUser {
|
||||
peerCategory = .privateChats
|
||||
} else if peer is TelegramGroup {
|
||||
peerCategory = .groups
|
||||
|
||||
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData {
|
||||
subscriberCount = (cachedData.participants?.participants.count).flatMap(Int32.init)
|
||||
}
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
if case .group = channel.info {
|
||||
peerCategory = .groups
|
||||
} else {
|
||||
peerCategory = .channels
|
||||
}
|
||||
if peerCategory == mappedCategory {
|
||||
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
|
||||
subscriberCount = cachedData.participantsSummary.memberCount
|
||||
}
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
if peerCategory != mappedCategory {
|
||||
continue
|
||||
}
|
||||
|
||||
result.append((peer: FoundPeer(peer: peer, subscribers: subscriberCount), value: value))
|
||||
}
|
||||
|
||||
return result.sorted(by: { lhs, rhs in
|
||||
if lhs.value != rhs.value {
|
||||
return lhs.value < rhs.value
|
||||
}
|
||||
return lhs.peer.peer.debugDisplayTitle < rhs.peer.peer.debugDisplayTitle
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let _ = (combineLatest(
|
||||
cacheSettingsPromise.get() |> take(1),
|
||||
peerExceptions |> take(1)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { cacheSettings, peerExceptions in
|
||||
let currentValue: Int32 = cacheSettings.categoryStorageTimeout[mappedCategory] ?? Int32.max
|
||||
|
||||
let applyValue: (Int32) -> Void = { value in
|
||||
let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { cacheSettings in
|
||||
var cacheSettings = cacheSettings
|
||||
cacheSettings.categoryStorageTimeout[mappedCategory] = value
|
||||
return cacheSettings
|
||||
}).start()
|
||||
}
|
||||
|
||||
var subItems: [ContextMenuItem] = []
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
var presetValues: [Int32] = [
|
||||
Int32.max,
|
||||
31 * 24 * 60 * 60,
|
||||
7 * 24 * 60 * 60,
|
||||
1 * 24 * 60 * 60
|
||||
]
|
||||
if currentValue != 0 && !presetValues.contains(currentValue) {
|
||||
presetValues.append(currentValue)
|
||||
presetValues.sort(by: >)
|
||||
}
|
||||
|
||||
for value in presetValues {
|
||||
let optionText: String
|
||||
if value == Int32.max {
|
||||
optionText = presentationData.strings.ClearCache_Forever
|
||||
} else {
|
||||
optionText = timeIntervalString(strings: presentationData.strings, value: value)
|
||||
}
|
||||
subItems.append(.action(ContextMenuActionItem(text: optionText, icon: { theme in
|
||||
if currentValue == value {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}, action: { _, f in
|
||||
applyValue(value)
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
|
||||
subItems.append(.separator)
|
||||
|
||||
if peerExceptions.isEmpty {
|
||||
let exceptionsText = presentationData.strings.GroupInfo_Permissions_AddException
|
||||
subItems.append(.action(ContextMenuActionItem(text: exceptionsText, icon: { theme in
|
||||
if case .privateChats = category {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor)
|
||||
} else {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Location/CreateGroupIcon"), color: theme.contextMenu.primaryColor)
|
||||
}
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
|
||||
pushControllerImpl?(storageUsageExceptionsScreen(context: context, category: mappedCategory))
|
||||
})))
|
||||
} else {
|
||||
subItems.append(.custom(MultiplePeerAvatarsContextItem(context: context, peers: peerExceptions.prefix(3).map { EnginePeer($0.peer.peer) }, action: { c, _ in
|
||||
c.dismiss(completion: {
|
||||
|
||||
})
|
||||
pushControllerImpl?(storageUsageExceptionsScreen(context: context, category: mappedCategory))
|
||||
}), false))
|
||||
}
|
||||
|
||||
if let sourceNode = findAutoremoveReferenceNode?(category) {
|
||||
let items: Signal<ContextController.Items, NoError> = .single(ContextController.Items(content: .list(subItems)))
|
||||
let source: ContextContentSource = .reference(StorageUsageContextReferenceContentSource(sourceView: sourceNode.labelNode.view))
|
||||
|
||||
let contextController = ContextController(
|
||||
account: context.account,
|
||||
presentationData: presentationData,
|
||||
source: source,
|
||||
items: items,
|
||||
gesture: nil
|
||||
)
|
||||
sourceNode.updateHasContextMenu(hasContextMenu: true)
|
||||
contextController.dismissed = { [weak sourceNode] in
|
||||
sourceNode?.updateHasContextMenu(hasContextMenu: false)
|
||||
}
|
||||
presentInGlobalOverlay?(contextController)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
|
||||
let signal = combineLatest(context.sharedContext.presentationData, cacheSettingsPromise.get(), statsPromise.get(), statePromise.get()) |> deliverOnMainQueue
|
||||
|> map { presentationData, cacheSettings, cacheStats, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let signal = combineLatest(context.sharedContext.presentationData, cacheSettingsPromise.get(), accountSpecificCacheSettingsPromise.get(), statsPromise.get(), statePromise.get()) |> deliverOnMainQueue
|
||||
|> map { presentationData, cacheSettings, accountSpecificCacheSettings, cacheStats, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let leftNavigationButton = isModal ? ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
|
||||
dismissImpl?()
|
||||
}) : nil
|
||||
|
||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Cache_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: storageUsageControllerEntries(presentationData: presentationData, cacheSettings: cacheSettings, cacheStats: cacheStats, state: state), style: .blocks, emptyStateItem: nil, animateChanges: false)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: storageUsageControllerEntries(presentationData: presentationData, cacheSettings: cacheSettings, accountSpecificCacheSettings: accountSpecificCacheSettings, cacheStats: cacheStats, state: state), style: .blocks, emptyStateItem: nil, animateChanges: false)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
} |> afterDisposed {
|
||||
@ -993,6 +1270,34 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P
|
||||
presentControllerImpl = { [weak controller] c, contextType, a in
|
||||
controller?.present(c, in: contextType, with: a)
|
||||
}
|
||||
pushControllerImpl = { [weak controller] c in
|
||||
controller?.push(c)
|
||||
}
|
||||
presentInGlobalOverlay = { [weak controller] c in
|
||||
controller?.presentInGlobalOverlay(c, with: nil)
|
||||
}
|
||||
findAutoremoveReferenceNode = { [weak controller] category in
|
||||
guard let controller else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let targetTag: StorageUsageEntryTag = category
|
||||
var resultItemNode: ItemListItemNode?
|
||||
controller.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ItemListItemNode {
|
||||
if let tag = itemNode.tag, tag.isEqual(to: targetTag) {
|
||||
resultItemNode = itemNode
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let resultItemNode = resultItemNode as? ItemListDisclosureItemNode {
|
||||
return resultItemNode
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
dismissImpl = { [weak controller] in
|
||||
controller?.dismiss()
|
||||
}
|
||||
@ -1110,3 +1415,215 @@ private class StorageUsageClearProgressOverlayNode: ASDisplayNode, ActionSheetGr
|
||||
self.animationNode.updateLayout(size: imageSize)
|
||||
}
|
||||
}
|
||||
|
||||
private final class StorageUsageContextReferenceContentSource: ContextReferenceContentSource {
|
||||
private let sourceView: UIView
|
||||
|
||||
init(sourceView: UIView) {
|
||||
self.sourceView = sourceView
|
||||
}
|
||||
|
||||
func transitionInfo() -> ContextControllerReferenceViewInfo? {
|
||||
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, insets: UIEdgeInsets(top: -4.0, left: 0.0, bottom: -4.0, right: 0.0))
|
||||
}
|
||||
}
|
||||
|
||||
final class MultiplePeerAvatarsContextItem: ContextMenuCustomItem {
|
||||
fileprivate let context: AccountContext
|
||||
fileprivate let peers: [EnginePeer]
|
||||
fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void
|
||||
|
||||
init(context: AccountContext, peers: [EnginePeer], action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void) {
|
||||
self.context = context
|
||||
self.peers = peers
|
||||
self.action = action
|
||||
}
|
||||
|
||||
func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
|
||||
return MultiplePeerAvatarsContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected)
|
||||
}
|
||||
}
|
||||
|
||||
private final class MultiplePeerAvatarsContextItemNode: ASDisplayNode, ContextMenuCustomNode, ContextActionNodeProtocol {
|
||||
private let item: MultiplePeerAvatarsContextItem
|
||||
private var presentationData: PresentationData
|
||||
private let getController: () -> ContextControllerProtocol?
|
||||
private let actionSelected: (ContextMenuActionResult) -> Void
|
||||
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
private let textNode: ImmediateTextNode
|
||||
|
||||
private let avatarsNode: AnimatedAvatarSetNode
|
||||
private let avatarsContext: AnimatedAvatarSetContext
|
||||
|
||||
private let buttonNode: HighlightTrackingButtonNode
|
||||
|
||||
private var pointerInteraction: PointerInteraction?
|
||||
|
||||
init(presentationData: PresentationData, item: MultiplePeerAvatarsContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
|
||||
self.item = item
|
||||
self.presentationData = presentationData
|
||||
self.getController = getController
|
||||
self.actionSelected = actionSelected
|
||||
|
||||
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isAccessibilityElement = false
|
||||
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isAccessibilityElement = false
|
||||
self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.isAccessibilityElement = false
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode.attributedText = NSAttributedString(string: " ", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor)
|
||||
self.textNode.maximumNumberOfLines = 1
|
||||
|
||||
self.buttonNode = HighlightTrackingButtonNode()
|
||||
self.buttonNode.isAccessibilityElement = true
|
||||
self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording
|
||||
|
||||
self.avatarsNode = AnimatedAvatarSetNode()
|
||||
self.avatarsContext = AnimatedAvatarSetContext()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.avatarsNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
|
||||
self.buttonNode.highligthedChanged = { [weak self] highligted in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if highligted {
|
||||
strongSelf.highlightedBackgroundNode.alpha = 1.0
|
||||
} else {
|
||||
strongSelf.highlightedBackgroundNode.alpha = 0.0
|
||||
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
}
|
||||
}
|
||||
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
self.buttonNode.isUserInteractionEnabled = true
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.pointerInteraction = PointerInteraction(node: self.buttonNode, style: .hover, willEnter: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.highlightedBackgroundNode.alpha = 0.75
|
||||
}
|
||||
}, willExit: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.highlightedBackgroundNode.alpha = 0.0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private var validLayout: (calculatedWidth: CGFloat, size: CGSize)?
|
||||
|
||||
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
|
||||
let sideInset: CGFloat = 14.0
|
||||
let verticalInset: CGFloat = 12.0
|
||||
|
||||
let rightTextInset: CGFloat = sideInset + 36.0
|
||||
|
||||
let calculatedWidth = min(constrainedWidth, 250.0)
|
||||
|
||||
let textFont = Font.regular(self.presentationData.listsFontSize.baseDisplaySize)
|
||||
let text: String = self.presentationData.strings.CacheEvictionMenu_CategoryExceptions(Int32(self.item.peers.count))
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor)
|
||||
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: calculatedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude))
|
||||
|
||||
let combinedTextHeight = textSize.height
|
||||
return (CGSize(width: calculatedWidth, height: verticalInset * 2.0 + combinedTextHeight), { size, transition in
|
||||
self.validLayout = (calculatedWidth: calculatedWidth, size: size)
|
||||
let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0)
|
||||
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize)
|
||||
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
|
||||
|
||||
let avatarsContent: AnimatedAvatarSetContext.Content
|
||||
|
||||
let avatarsPeers: [EnginePeer] = self.item.peers
|
||||
|
||||
avatarsContent = self.avatarsContext.update(peers: avatarsPeers, animated: false)
|
||||
|
||||
let avatarsSize = self.avatarsNode.update(context: self.item.context, content: avatarsContent, itemSize: CGSize(width: 24.0, height: 24.0), customSpacing: 10.0, animated: false, synchronousLoad: true)
|
||||
self.avatarsNode.frame = CGRect(origin: CGPoint(x: size.width - sideInset - 12.0 - avatarsSize.width, y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize)
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||
})
|
||||
}
|
||||
|
||||
func updateTheme(presentationData: PresentationData) {
|
||||
self.presentationData = presentationData
|
||||
|
||||
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
|
||||
self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
||||
|
||||
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
|
||||
|
||||
self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor)
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
self.performAction()
|
||||
}
|
||||
|
||||
private var actionTemporarilyDisabled: Bool = false
|
||||
|
||||
func canBeHighlighted() -> Bool {
|
||||
return self.isActionEnabled
|
||||
}
|
||||
|
||||
func updateIsHighlighted(isHighlighted: Bool) {
|
||||
self.setIsHighlighted(isHighlighted)
|
||||
}
|
||||
|
||||
func performAction() {
|
||||
if self.actionTemporarilyDisabled {
|
||||
return
|
||||
}
|
||||
self.actionTemporarilyDisabled = true
|
||||
Queue.mainQueue().async { [weak self] in
|
||||
self?.actionTemporarilyDisabled = false
|
||||
}
|
||||
|
||||
guard let controller = self.getController() else {
|
||||
return
|
||||
}
|
||||
self.item.action(controller, { [weak self] result in
|
||||
self?.actionSelected(result)
|
||||
})
|
||||
}
|
||||
|
||||
var isActionEnabled: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func setIsHighlighted(_ value: Bool) {
|
||||
if value {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,486 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import TelegramStringFormatting
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import OverlayStatusController
|
||||
import AccountContext
|
||||
import ItemListPeerItem
|
||||
import UndoUI
|
||||
import ContextUI
|
||||
import ItemListPeerActionItem
|
||||
|
||||
private enum StorageUsageExceptionsEntryTag: Hashable, ItemListItemTag {
|
||||
case peer(EnginePeer.Id)
|
||||
|
||||
public func isEqual(to other: ItemListItemTag) -> Bool {
|
||||
if let other = other as? StorageUsageExceptionsEntryTag, self == other {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class StorageUsageExceptionsScreenArguments {
|
||||
let context: AccountContext
|
||||
let openAddException: () -> Void
|
||||
let openPeerMenu: (EnginePeer.Id, Int32) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
openAddException: @escaping () -> Void,
|
||||
openPeerMenu: @escaping (EnginePeer.Id, Int32) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.openAddException = openAddException
|
||||
self.openPeerMenu = openPeerMenu
|
||||
}
|
||||
}
|
||||
|
||||
private enum StorageUsageExceptionsSection: Int32 {
|
||||
case add
|
||||
case items
|
||||
}
|
||||
|
||||
private enum StorageUsageExceptionsEntry: ItemListNodeEntry {
|
||||
enum SortIndex: Equatable, Comparable {
|
||||
case index(Int)
|
||||
case peer(index: Int, peerId: EnginePeer.Id)
|
||||
|
||||
static func <(lhs: SortIndex, rhs: SortIndex) -> Bool {
|
||||
switch lhs {
|
||||
case let .index(index):
|
||||
if case let .index(rhsIndex) = rhs {
|
||||
return index < rhsIndex
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
case let .peer(index, peerId):
|
||||
if case let .peer(rhsIndex, rhsPeerId) = rhs {
|
||||
if index != rhsIndex {
|
||||
return index < rhsIndex
|
||||
} else {
|
||||
return peerId < rhsPeerId
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum StableId: Hashable {
|
||||
case index(Int)
|
||||
case peer(EnginePeer.Id)
|
||||
}
|
||||
|
||||
case addException(String)
|
||||
case exceptionsHeader(String)
|
||||
case peer(index: Int, peer: FoundPeer, value: Int32)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .addException:
|
||||
return StorageUsageExceptionsSection.add.rawValue
|
||||
case .exceptionsHeader, .peer:
|
||||
return StorageUsageExceptionsSection.items.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: StableId {
|
||||
switch self {
|
||||
case .addException:
|
||||
return .index(0)
|
||||
case .exceptionsHeader:
|
||||
return .index(1)
|
||||
case let .peer(_, peer, _):
|
||||
return .peer(peer.peer.id)
|
||||
}
|
||||
}
|
||||
|
||||
var sortIndex: SortIndex {
|
||||
switch self {
|
||||
case .addException:
|
||||
return .index(0)
|
||||
case .exceptionsHeader:
|
||||
return .index(1)
|
||||
case let .peer(index, peer, _):
|
||||
return .peer(index: index, peerId: peer.peer.id)
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: StorageUsageExceptionsEntry, rhs: StorageUsageExceptionsEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .addException(text):
|
||||
if case .addException(text) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .exceptionsHeader(text):
|
||||
if case .exceptionsHeader(text) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .peer(index, peer, value):
|
||||
if case .peer(index, peer, value) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: StorageUsageExceptionsEntry, rhs: StorageUsageExceptionsEntry) -> Bool {
|
||||
return lhs.sortIndex < rhs.sortIndex
|
||||
}
|
||||
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! StorageUsageExceptionsScreenArguments
|
||||
switch self {
|
||||
case let .addException(text):
|
||||
let icon: UIImage? = PresentationResourcesItemList.createGroupIcon(presentationData.theme)
|
||||
return ItemListPeerActionItem(presentationData: presentationData, icon: icon, title: text, alwaysPlain: false, sectionId: self.section, editing: false, action: {
|
||||
arguments.openAddException()
|
||||
})
|
||||
case let .exceptionsHeader(text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .peer(_, peer, value):
|
||||
var additionalDetailLabel: String?
|
||||
if let subscribers = peer.subscribers {
|
||||
additionalDetailLabel = presentationData.strings.VoiceChat_Panel_Members(subscribers)
|
||||
}
|
||||
let optionText: String
|
||||
if value == Int32.max {
|
||||
optionText = presentationData.strings.ClearCache_Forever
|
||||
} else {
|
||||
optionText = timeIntervalString(strings: presentationData.strings, value: value)
|
||||
}
|
||||
|
||||
return ItemListDisclosureItem(presentationData: presentationData, icon: nil, context: arguments.context, iconPeer: EnginePeer(peer.peer), title: EnginePeer(peer.peer).displayTitle(strings: presentationData.strings, displayOrder: .firstLast), enabled: true, titleFont: .bold, label: optionText, labelStyle: .text, additionalDetailLabel: additionalDetailLabel, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: {
|
||||
arguments.openPeerMenu(peer.peer.id, value)
|
||||
}, tag: StorageUsageExceptionsEntryTag.peer(peer.peer.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct StorageUsageExceptionsState: Equatable {
|
||||
}
|
||||
|
||||
private func storageUsageExceptionsScreenEntries(
|
||||
presentationData: PresentationData,
|
||||
peerExceptions: [(peer: FoundPeer, value: Int32)],
|
||||
state: StorageUsageExceptionsState
|
||||
) -> [StorageUsageExceptionsEntry] {
|
||||
var entries: [StorageUsageExceptionsEntry] = []
|
||||
|
||||
entries.append(.addException(presentationData.strings.Notification_Exceptions_AddException))
|
||||
|
||||
if !peerExceptions.isEmpty {
|
||||
entries.append(.exceptionsHeader(presentationData.strings.Notifications_CategoryExceptions(Int32(peerExceptions.count)).uppercased()))
|
||||
|
||||
var index = 100
|
||||
for item in peerExceptions {
|
||||
entries.append(.peer(index: index, peer: item.peer, value: item.value))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
public func storageUsageExceptionsScreen(
|
||||
context: AccountContext,
|
||||
category: CacheStorageSettings.PeerStorageCategory,
|
||||
isModal: Bool = false
|
||||
) -> ViewController {
|
||||
let statePromise = ValuePromise(StorageUsageExceptionsState())
|
||||
let stateValue = Atomic(value: StorageUsageExceptionsState())
|
||||
let updateState: ((StorageUsageExceptionsState) -> StorageUsageExceptionsState) -> Void = { f in
|
||||
statePromise.set(stateValue.modify { f($0) })
|
||||
}
|
||||
let _ = updateState
|
||||
|
||||
let cacheSettingsPromise = Promise<CacheStorageSettings>()
|
||||
cacheSettingsPromise.set(context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings])
|
||||
|> map { sharedData -> CacheStorageSettings in
|
||||
let cacheSettings: CacheStorageSettings
|
||||
if let value = sharedData.entries[SharedDataKeys.cacheStorageSettings]?.get(CacheStorageSettings.self) {
|
||||
cacheSettings = value
|
||||
} else {
|
||||
cacheSettings = CacheStorageSettings.defaultSettings
|
||||
}
|
||||
|
||||
return cacheSettings
|
||||
})
|
||||
|
||||
let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings]))
|
||||
let accountSpecificSettings: Signal<AccountSpecificCacheStorageSettings, NoError> = context.account.postbox.combinedView(keys: [viewKey])
|
||||
|> map { views -> AccountSpecificCacheStorageSettings in
|
||||
let cacheSettings: AccountSpecificCacheStorageSettings
|
||||
if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) {
|
||||
cacheSettings = value
|
||||
} else {
|
||||
cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings
|
||||
}
|
||||
|
||||
return cacheSettings
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let peerExceptions: Signal<[(peer: FoundPeer, value: Int32)], NoError> = accountSpecificSettings
|
||||
|> mapToSignal { accountSpecificSettings -> Signal<[(peer: FoundPeer, value: Int32)], NoError> in
|
||||
return context.account.postbox.transaction { transaction -> [(peer: FoundPeer, value: Int32)] in
|
||||
var result: [(peer: FoundPeer, value: Int32)] = []
|
||||
|
||||
for (peerId, value) in accountSpecificSettings.peerStorageTimeoutExceptions {
|
||||
guard let peer = transaction.getPeer(peerId) else {
|
||||
continue
|
||||
}
|
||||
let peerCategory: CacheStorageSettings.PeerStorageCategory
|
||||
var subscriberCount: Int32?
|
||||
if peer is TelegramUser {
|
||||
peerCategory = .privateChats
|
||||
} else if peer is TelegramGroup {
|
||||
peerCategory = .groups
|
||||
|
||||
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData {
|
||||
subscriberCount = (cachedData.participants?.participants.count).flatMap(Int32.init)
|
||||
}
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
if case .group = channel.info {
|
||||
peerCategory = .groups
|
||||
} else {
|
||||
peerCategory = .channels
|
||||
}
|
||||
if peerCategory == category {
|
||||
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
|
||||
subscriberCount = cachedData.participantsSummary.memberCount
|
||||
}
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
if peerCategory != category {
|
||||
continue
|
||||
}
|
||||
|
||||
result.append((peer: FoundPeer(peer: peer, subscribers: subscriberCount), value: value))
|
||||
}
|
||||
|
||||
return result.sorted(by: { lhs, rhs in
|
||||
if lhs.value != rhs.value {
|
||||
return lhs.value < rhs.value
|
||||
}
|
||||
return lhs.peer.peer.debugDisplayTitle < rhs.peer.peer.debugDisplayTitle
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var presentControllerImpl: ((ViewController, PresentationContextType, Any?) -> Void)?
|
||||
let _ = presentControllerImpl
|
||||
var pushControllerImpl: ((ViewController) -> Void)?
|
||||
|
||||
var findPeerReferenceNode: ((EnginePeer.Id) -> ItemListDisclosureItemNode?)?
|
||||
let _ = findPeerReferenceNode
|
||||
|
||||
var presentInGlobalOverlay: ((ViewController) -> Void)?
|
||||
let _ = presentInGlobalOverlay
|
||||
|
||||
let actionDisposables = DisposableSet()
|
||||
|
||||
let clearDisposable = MetaDisposable()
|
||||
actionDisposables.add(clearDisposable)
|
||||
|
||||
let arguments = StorageUsageExceptionsScreenArguments(
|
||||
context: context,
|
||||
openAddException: {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
var filter: ChatListNodePeersFilter = [.excludeRecent, .doNotSearchMessages, .removeSearchHeader]
|
||||
switch category {
|
||||
case .groups:
|
||||
filter.insert(.onlyGroups)
|
||||
case .privateChats:
|
||||
filter.insert(.onlyPrivateChats)
|
||||
filter.insert(.excludeSavedMessages)
|
||||
filter.insert(.excludeSecretChats)
|
||||
case .channels:
|
||||
filter.insert(.onlyChannels)
|
||||
}
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: filter, hasContactSelector: false, title: presentationData.strings.Notifications_AddExceptionTitle))
|
||||
controller.peerSelected = { [weak controller] peer, _ in
|
||||
let peerId = peer.id
|
||||
|
||||
let _ = updateAccountSpecificCacheStorageSettingsInteractively(postbox: context.account.postbox, { settings in
|
||||
var settings = settings
|
||||
|
||||
settings.peerStorageTimeoutExceptions[peerId] = Int32.max
|
||||
|
||||
return settings
|
||||
}).start()
|
||||
|
||||
controller?.dismiss()
|
||||
}
|
||||
pushControllerImpl?(controller)
|
||||
},
|
||||
openPeerMenu: { peerId, currentValue in
|
||||
let applyValue: (Int32?) -> Void = { value in
|
||||
let _ = updateAccountSpecificCacheStorageSettingsInteractively(postbox: context.account.postbox, { settings in
|
||||
var settings = settings
|
||||
|
||||
if let value = value {
|
||||
settings.peerStorageTimeoutExceptions[peerId] = value
|
||||
} else {
|
||||
settings.peerStorageTimeoutExceptions.removeValue(forKey: peerId)
|
||||
}
|
||||
|
||||
return settings
|
||||
}).start()
|
||||
}
|
||||
|
||||
var subItems: [ContextMenuItem] = []
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let presetValues: [Int32] = [
|
||||
Int32.max,
|
||||
31 * 24 * 60 * 60,
|
||||
7 * 24 * 60 * 60,
|
||||
1 * 24 * 60 * 60
|
||||
]
|
||||
|
||||
for value in presetValues {
|
||||
let optionText: String
|
||||
if value == Int32.max {
|
||||
optionText = presentationData.strings.ClearCache_Forever
|
||||
} else {
|
||||
optionText = timeIntervalString(strings: presentationData.strings, value: value)
|
||||
}
|
||||
subItems.append(.action(ContextMenuActionItem(text: optionText, icon: { theme in
|
||||
if currentValue == value {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}, action: { _, f in
|
||||
applyValue(value)
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
|
||||
subItems.append(.separator)
|
||||
//TODO:localize
|
||||
subItems.append(.action(ContextMenuActionItem(text: presentationData.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
|
||||
applyValue(nil)
|
||||
})))
|
||||
|
||||
if let sourceNode = findPeerReferenceNode?(peerId) {
|
||||
let items: Signal<ContextController.Items, NoError> = .single(ContextController.Items(content: .list(subItems)))
|
||||
let source: ContextContentSource = .reference(StorageUsageExceptionsContextReferenceContentSource(sourceView: sourceNode.labelNode.view))
|
||||
|
||||
let contextController = ContextController(
|
||||
account: context.account,
|
||||
presentationData: presentationData,
|
||||
source: source,
|
||||
items: items,
|
||||
gesture: nil
|
||||
)
|
||||
sourceNode.updateHasContextMenu(hasContextMenu: true)
|
||||
contextController.dismissed = { [weak sourceNode] in
|
||||
sourceNode?.updateHasContextMenu(hasContextMenu: false)
|
||||
}
|
||||
presentInGlobalOverlay?(contextController)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let _ = cacheSettingsPromise
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
|
||||
let signal = combineLatest(queue: .mainQueue(),
|
||||
context.sharedContext.presentationData,
|
||||
peerExceptions,
|
||||
statePromise.get()
|
||||
)
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, peerExceptions, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let leftNavigationButton = isModal ? ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
|
||||
dismissImpl?()
|
||||
}) : nil
|
||||
|
||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Notifications_ExceptionsTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: storageUsageExceptionsScreenEntries(presentationData: presentationData, peerExceptions: peerExceptions, state: state), style: .blocks, emptyStateItem: nil, animateChanges: false)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|> afterDisposed {
|
||||
actionDisposables.dispose()
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
if isModal {
|
||||
controller.navigationPresentation = .modal
|
||||
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
}
|
||||
presentControllerImpl = { [weak controller] c, contextType, a in
|
||||
controller?.present(c, in: contextType, with: a)
|
||||
}
|
||||
pushControllerImpl = { [weak controller] c in
|
||||
controller?.push(c)
|
||||
}
|
||||
presentInGlobalOverlay = { [weak controller] c in
|
||||
controller?.presentInGlobalOverlay(c, with: nil)
|
||||
}
|
||||
findPeerReferenceNode = { [weak controller] peerId in
|
||||
guard let controller else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let targetTag: StorageUsageExceptionsEntryTag = .peer(peerId)
|
||||
var resultItemNode: ItemListItemNode?
|
||||
controller.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ItemListItemNode {
|
||||
if let tag = itemNode.tag, tag.isEqual(to: targetTag) {
|
||||
resultItemNode = itemNode
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let resultItemNode = resultItemNode as? ItemListDisclosureItemNode {
|
||||
return resultItemNode
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
dismissImpl = { [weak controller] in
|
||||
controller?.dismiss()
|
||||
}
|
||||
return controller
|
||||
}
|
||||
|
||||
private final class StorageUsageExceptionsContextReferenceContentSource: ContextReferenceContentSource {
|
||||
private let sourceView: UIView
|
||||
|
||||
init(sourceView: UIView) {
|
||||
self.sourceView = sourceView
|
||||
}
|
||||
|
||||
func transitionInfo() -> ContextControllerReferenceViewInfo? {
|
||||
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, insets: UIEdgeInsets(top: -4.0, left: 0.0, bottom: -4.0, right: 0.0))
|
||||
}
|
||||
}
|
||||
@ -896,6 +896,7 @@ public class Account {
|
||||
private let managedOperationsDisposable = DisposableSet()
|
||||
private let managedTopReactionsDisposable = MetaDisposable()
|
||||
private var storageSettingsDisposable: Disposable?
|
||||
private var automaticCacheEvictionContext: AutomaticCacheEvictionContext?
|
||||
|
||||
public let importableContacts = Promise<[DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData]>()
|
||||
|
||||
@ -1190,13 +1191,15 @@ public class Account {
|
||||
|
||||
if !supplementary {
|
||||
let mediaBox = postbox.mediaBox
|
||||
self.storageSettingsDisposable = accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings]).start(next: { [weak mediaBox] sharedData in
|
||||
/*self.storageSettingsDisposable = accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings]).start(next: { [weak mediaBox] sharedData in
|
||||
guard let mediaBox = mediaBox else {
|
||||
return
|
||||
}
|
||||
let settings: CacheStorageSettings = sharedData.entries[SharedDataKeys.cacheStorageSettings]?.get(CacheStorageSettings.self) ?? CacheStorageSettings.defaultSettings
|
||||
mediaBox.setMaxStoreTimes(general: settings.defaultCacheStorageTimeout, shortLived: 60 * 60, gigabytesLimit: settings.defaultCacheStorageLimitGigabytes)
|
||||
})
|
||||
})*/
|
||||
|
||||
mediaBox.setMaxStoreTimes(general: 1 * 24 * 60 * 60, shortLived: 60 * 60, gigabytesLimit: 100 * 1024 * 1024)
|
||||
}
|
||||
|
||||
let _ = masterNotificationsKey(masterNotificationKeyValue: self.masterNotificationKey, postbox: self.postbox, ignoreDisabled: false, createIfNotExists: true).start(next: { key in
|
||||
@ -1218,6 +1221,8 @@ public class Account {
|
||||
strongSelf.managedTopReactionsDisposable.set(managedTopReactions(postbox: strongSelf.postbox, network: strongSelf.network).start())
|
||||
}
|
||||
|
||||
self.automaticCacheEvictionContext = AutomaticCacheEvictionContext(postbox: postbox, accountManager: accountManager)
|
||||
|
||||
/*#if DEBUG
|
||||
self.managedOperationsDisposable.add(debugFetchAllStickers(account: self).start(completed: {
|
||||
print("debugFetchAllStickers done")
|
||||
|
||||
@ -2,7 +2,6 @@ import Foundation
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
|
||||
|
||||
public func updateCacheStorageSettingsInteractively(accountManager: AccountManager<TelegramAccountManagerTypes>, _ f: @escaping (CacheStorageSettings) -> CacheStorageSettings) -> Signal<Void, NoError> {
|
||||
return accountManager.transaction { transaction -> Void in
|
||||
transaction.updateSharedData(SharedDataKeys.cacheStorageSettings, { entry in
|
||||
@ -16,3 +15,17 @@ public func updateCacheStorageSettingsInteractively(accountManager: AccountManag
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func updateAccountSpecificCacheStorageSettingsInteractively(postbox: Postbox, _ f: @escaping (AccountSpecificCacheStorageSettings) -> AccountSpecificCacheStorageSettings) -> Signal<Void, NoError> {
|
||||
return postbox.transaction { transaction -> Void in
|
||||
transaction.updatePreferencesEntry(key: PreferencesKeys.accountSpecificCacheStorageSettings, { entry in
|
||||
let currentSettings: AccountSpecificCacheStorageSettings
|
||||
if let entry = entry?.get(AccountSpecificCacheStorageSettings.self) {
|
||||
currentSettings = entry
|
||||
} else {
|
||||
currentSettings = AccountSpecificCacheStorageSettings.defaultSettings
|
||||
}
|
||||
return PreferencesEntry(f(currentSettings))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -3461,7 +3461,7 @@ func replayFinalState(
|
||||
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
|
||||
})
|
||||
if !resourceIds.isEmpty {
|
||||
let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start()
|
||||
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
|
||||
}
|
||||
deletedMessageIds.append(contentsOf: ids.map { .global($0) })
|
||||
case let .DeleteMessages(ids):
|
||||
@ -3478,7 +3478,7 @@ func replayFinalState(
|
||||
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
|
||||
})
|
||||
if !resourceIds.isEmpty {
|
||||
let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start()
|
||||
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
|
||||
}
|
||||
case let .UpdatePeerChatInclusion(peerId, groupId, changedGroup):
|
||||
let currentInclusion = transaction.getPeerChatListInclusion(peerId)
|
||||
|
||||
@ -1,16 +1,43 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
|
||||
public struct CacheStorageSettings: Codable, Equatable {
|
||||
public let defaultCacheStorageTimeout: Int32
|
||||
public let defaultCacheStorageLimitGigabytes: Int32
|
||||
|
||||
public static var defaultSettings: CacheStorageSettings {
|
||||
return CacheStorageSettings(defaultCacheStorageTimeout: Int32.max, defaultCacheStorageLimitGigabytes: 8 * 1024 * 1024)
|
||||
public enum PeerStorageCategory: String, Codable, Hashable {
|
||||
case privateChats = "privateChats"
|
||||
case groups = "groups"
|
||||
case channels = "channels"
|
||||
}
|
||||
|
||||
public init(defaultCacheStorageTimeout: Int32, defaultCacheStorageLimitGigabytes: Int32) {
|
||||
private struct CategoryStorageTimeoutRepresentation: Codable {
|
||||
var key: PeerStorageCategory
|
||||
var value: Int32
|
||||
}
|
||||
|
||||
public var defaultCacheStorageTimeout: Int32
|
||||
public var defaultCacheStorageLimitGigabytes: Int32
|
||||
|
||||
public var categoryStorageTimeout: [PeerStorageCategory: Int32]
|
||||
|
||||
public static var defaultSettings: CacheStorageSettings {
|
||||
return CacheStorageSettings(
|
||||
defaultCacheStorageTimeout: Int32.max,
|
||||
defaultCacheStorageLimitGigabytes: 8 * 1024 * 1024,
|
||||
categoryStorageTimeout: [
|
||||
.privateChats: Int32.max,
|
||||
.groups: Int32.max,
|
||||
.channels: Int32(1 * 24 * 60 * 60)
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
public init(
|
||||
defaultCacheStorageTimeout: Int32,
|
||||
defaultCacheStorageLimitGigabytes: Int32,
|
||||
categoryStorageTimeout: [PeerStorageCategory: Int32]
|
||||
) {
|
||||
self.defaultCacheStorageTimeout = defaultCacheStorageTimeout
|
||||
self.defaultCacheStorageLimitGigabytes = defaultCacheStorageLimitGigabytes
|
||||
self.categoryStorageTimeout = categoryStorageTimeout
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@ -25,6 +52,20 @@ public struct CacheStorageSettings: Codable, Equatable {
|
||||
} else {
|
||||
self.defaultCacheStorageLimitGigabytes = 8 * 1024 * 1024
|
||||
}
|
||||
|
||||
if let data = try container.decodeIfPresent(Data.self, forKey: "categoryStorageTimeoutJson") {
|
||||
if let items = try? JSONDecoder().decode([CategoryStorageTimeoutRepresentation].self, from: data) {
|
||||
var categoryStorageTimeout: [PeerStorageCategory: Int32] = [:]
|
||||
for item in items {
|
||||
categoryStorageTimeout[item.key] = item.value
|
||||
}
|
||||
self.categoryStorageTimeout = categoryStorageTimeout
|
||||
} else {
|
||||
self.categoryStorageTimeout = CacheStorageSettings.defaultSettings.categoryStorageTimeout
|
||||
}
|
||||
} else {
|
||||
self.categoryStorageTimeout = CacheStorageSettings.defaultSettings.categoryStorageTimeout
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@ -32,12 +73,64 @@ public struct CacheStorageSettings: Codable, Equatable {
|
||||
|
||||
try container.encode(self.defaultCacheStorageTimeout, forKey: "dt")
|
||||
try container.encode(self.defaultCacheStorageLimitGigabytes, forKey: "dl")
|
||||
|
||||
var categoryStorageTimeoutValues: [CategoryStorageTimeoutRepresentation] = []
|
||||
for (key, value) in self.categoryStorageTimeout {
|
||||
categoryStorageTimeoutValues.append(CategoryStorageTimeoutRepresentation(key: key, value: value))
|
||||
}
|
||||
if let data = try? JSONEncoder().encode(categoryStorageTimeoutValues) {
|
||||
try container.encode(data, forKey: "categoryStorageTimeoutJson")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func withUpdatedDefaultCacheStorageTimeout(_ defaultCacheStorageTimeout: Int32) -> CacheStorageSettings {
|
||||
return CacheStorageSettings(defaultCacheStorageTimeout: defaultCacheStorageTimeout, defaultCacheStorageLimitGigabytes: self.defaultCacheStorageLimitGigabytes)
|
||||
public struct AccountSpecificCacheStorageSettings: Codable, Equatable {
|
||||
private struct PeerStorageTimeoutExceptionRepresentation: Codable {
|
||||
var key: PeerId
|
||||
var value: Int32
|
||||
}
|
||||
|
||||
public var peerStorageTimeoutExceptions: [PeerId: Int32]
|
||||
|
||||
public static var defaultSettings: AccountSpecificCacheStorageSettings {
|
||||
return AccountSpecificCacheStorageSettings(
|
||||
peerStorageTimeoutExceptions: [:]
|
||||
)
|
||||
}
|
||||
|
||||
public init(
|
||||
peerStorageTimeoutExceptions: [PeerId: Int32]
|
||||
) {
|
||||
self.peerStorageTimeoutExceptions = peerStorageTimeoutExceptions
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
if let data = try container.decodeIfPresent(Data.self, forKey: "peerStorageTimeoutExceptionsJson") {
|
||||
if let items = try? JSONDecoder().decode([PeerStorageTimeoutExceptionRepresentation].self, from: data) {
|
||||
var peerStorageTimeoutExceptions: [PeerId: Int32] = [:]
|
||||
for item in items {
|
||||
peerStorageTimeoutExceptions[item.key] = item.value
|
||||
}
|
||||
self.peerStorageTimeoutExceptions = peerStorageTimeoutExceptions
|
||||
} else {
|
||||
self.peerStorageTimeoutExceptions = AccountSpecificCacheStorageSettings.defaultSettings.peerStorageTimeoutExceptions
|
||||
}
|
||||
} else {
|
||||
self.peerStorageTimeoutExceptions = AccountSpecificCacheStorageSettings.defaultSettings.peerStorageTimeoutExceptions
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
var peerStorageTimeoutExceptionsValues: [PeerStorageTimeoutExceptionRepresentation] = []
|
||||
for (key, value) in self.peerStorageTimeoutExceptions {
|
||||
peerStorageTimeoutExceptionsValues.append(PeerStorageTimeoutExceptionRepresentation(key: key, value: value))
|
||||
}
|
||||
if let data = try? JSONEncoder().encode(peerStorageTimeoutExceptionsValues) {
|
||||
try container.encode(data, forKey: "peerStorageTimeoutExceptionsJson")
|
||||
}
|
||||
public func withUpdatedDefaultCacheStorageLimitGigabytes(_ defaultCacheStorageLimitGigabytes: Int32) -> CacheStorageSettings {
|
||||
return CacheStorageSettings(defaultCacheStorageTimeout: self.defaultCacheStorageTimeout, defaultCacheStorageLimitGigabytes: defaultCacheStorageLimitGigabytes)
|
||||
}
|
||||
}
|
||||
|
||||
@ -247,6 +247,7 @@ private enum PreferencesKeyValues: Int32 {
|
||||
case reactionSettings = 24
|
||||
case premiumPromo = 26
|
||||
case globalMessageAutoremoveTimeoutSettings = 27
|
||||
case accountSpecificCacheStorageSettings = 28
|
||||
}
|
||||
|
||||
public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey {
|
||||
@ -381,6 +382,12 @@ public struct PreferencesKeys {
|
||||
key.setInt32(0, value: PreferencesKeyValues.globalMessageAutoremoveTimeoutSettings.rawValue)
|
||||
return key
|
||||
}()
|
||||
|
||||
public static let accountSpecificCacheStorageSettings: ValueBoxKey = {
|
||||
let key = ValueBoxKey(length: 4)
|
||||
key.setInt32(0, value: PreferencesKeyValues.accountSpecificCacheStorageSettings.rawValue)
|
||||
return key
|
||||
}()
|
||||
}
|
||||
|
||||
private enum SharedDataKeyValues: Int32 {
|
||||
|
||||
@ -34,7 +34,7 @@ public func _internal_deleteMessages(transaction: Transaction, mediaBox: MediaBo
|
||||
}
|
||||
}
|
||||
if !resourceIds.isEmpty {
|
||||
let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start()
|
||||
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
|
||||
}
|
||||
for id in ids {
|
||||
if id.peerId.namespace == Namespaces.Peer.CloudChannel && id.namespace == Namespaces.Message.Cloud {
|
||||
@ -62,7 +62,7 @@ func _internal_deleteAllMessagesWithAuthor(transaction: Transaction, mediaBox: M
|
||||
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
|
||||
})
|
||||
if !resourceIds.isEmpty {
|
||||
let _ = mediaBox.removeCachedResources(Set(resourceIds)).start()
|
||||
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds))).start()
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ func _internal_deleteAllMessagesWithForwardAuthor(transaction: Transaction, medi
|
||||
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
|
||||
})
|
||||
if !resourceIds.isEmpty {
|
||||
let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start()
|
||||
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,7 +84,7 @@ func _internal_clearHistory(transaction: Transaction, mediaBox: MediaBox, peerId
|
||||
return true
|
||||
})
|
||||
if !resourceIds.isEmpty {
|
||||
let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start()
|
||||
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
|
||||
}
|
||||
}
|
||||
transaction.clearHistory(peerId, threadId: threadId, minTimestamp: nil, maxTimestamp: nil, namespaces: namespaces, forEachMedia: { _ in
|
||||
@ -101,7 +101,7 @@ func _internal_clearHistoryInRange(transaction: Transaction, mediaBox: MediaBox,
|
||||
return true
|
||||
})
|
||||
if !resourceIds.isEmpty {
|
||||
let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start()
|
||||
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
|
||||
}
|
||||
}
|
||||
transaction.clearHistory(peerId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: namespaces, forEachMedia: { _ in
|
||||
|
||||
@ -108,7 +108,7 @@ func managedApplyPendingScheduledMessagesActions(postbox: Postbox, network: Netw
|
||||
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
|
||||
})
|
||||
if !resourceIds.isEmpty {
|
||||
let _ = postbox.mediaBox.removeCachedResources(Set(resourceIds)).start()
|
||||
let _ = postbox.mediaBox.removeCachedResources(Array(Set(resourceIds))).start()
|
||||
}
|
||||
}
|
||||
|> ignoreValues
|
||||
|
||||
@ -665,7 +665,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee
|
||||
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
|
||||
})
|
||||
if !resourceIds.isEmpty {
|
||||
let _ = postbox.mediaBox.removeCachedResources(Set(resourceIds)).start()
|
||||
let _ = postbox.mediaBox.removeCachedResources(Array(Set(resourceIds))).start()
|
||||
}
|
||||
}
|
||||
case .chatFull:
|
||||
|
||||
@ -81,7 +81,7 @@ func _internal_collectCacheUsageStats(account: Account, peerId: PeerId? = nil, a
|
||||
totalSize += resourceSize
|
||||
|
||||
for reference in entry.references {
|
||||
if let message = transaction.getMessage(MessageId(peerId: reference.peerId, namespace: MessageId.Namespace(reference.messageNamespace), id: reference.messageId)) {
|
||||
if let message = transaction.getMessage(MessageId(peerId: PeerId(reference.peerId), namespace: MessageId.Namespace(reference.messageNamespace), id: reference.messageId)) {
|
||||
for mediaItem in message.media {
|
||||
guard let mediaId = mediaItem.id else {
|
||||
continue
|
||||
@ -102,7 +102,7 @@ func _internal_collectCacheUsageStats(account: Account, peerId: PeerId? = nil, a
|
||||
mediaSize += resourceSize
|
||||
processedResourceIds.insert(resourceId.stringRepresentation)
|
||||
|
||||
media[reference.peerId, default: [:]][category, default: [:]][mediaId, default: 0] += resourceSize
|
||||
media[PeerId(reference.peerId), default: [:]][category, default: [:]][mediaId, default: 0] += resourceSize
|
||||
if let index = mediaResourceIds.index(forKey: mediaId) {
|
||||
if !mediaResourceIds[index].value.contains(resourceId) {
|
||||
mediaResourceIds[mediaId]?.append(resourceId)
|
||||
@ -489,5 +489,5 @@ func _internal_collectCacheUsageStats(account: Account, peerId: PeerId? = nil, a
|
||||
}
|
||||
|
||||
func _internal_clearCachedMediaResources(account: Account, mediaResourceIds: Set<MediaResourceId>) -> Signal<Float, NoError> {
|
||||
return account.postbox.mediaBox.removeCachedResources(mediaResourceIds)
|
||||
return account.postbox.mediaBox.removeCachedResources(Array(mediaResourceIds))
|
||||
}
|
||||
|
||||
@ -0,0 +1,202 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
|
||||
final class AutomaticCacheEvictionContext {
|
||||
private final class Impl {
|
||||
private struct CombinedSettings: Equatable {
|
||||
var categoryStorageTimeout: [CacheStorageSettings.PeerStorageCategory: Int32]
|
||||
var exceptions: [PeerId: Int32]
|
||||
}
|
||||
|
||||
let queue: Queue
|
||||
let processingQueue: Queue
|
||||
let accountManager: AccountManager<TelegramAccountManagerTypes>
|
||||
let postbox: Postbox
|
||||
|
||||
var settingsDisposable: Disposable?
|
||||
var processDisposable: Disposable?
|
||||
|
||||
init(queue: Queue, accountManager: AccountManager<TelegramAccountManagerTypes>, postbox: Postbox) {
|
||||
self.queue = queue
|
||||
self.processingQueue = Queue(name: "AutomaticCacheEviction-Processing", qos: .background)
|
||||
self.accountManager = accountManager
|
||||
self.postbox = postbox
|
||||
|
||||
self.start()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.settingsDisposable?.dispose()
|
||||
self.processDisposable?.dispose()
|
||||
}
|
||||
|
||||
func start() {
|
||||
self.settingsDisposable?.dispose()
|
||||
self.processDisposable?.dispose()
|
||||
|
||||
let cacheSettings = self.accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings])
|
||||
|> map { sharedData -> CacheStorageSettings in
|
||||
let cacheSettings: CacheStorageSettings
|
||||
if let value = sharedData.entries[SharedDataKeys.cacheStorageSettings]?.get(CacheStorageSettings.self) {
|
||||
cacheSettings = value
|
||||
} else {
|
||||
cacheSettings = CacheStorageSettings.defaultSettings
|
||||
}
|
||||
|
||||
return cacheSettings
|
||||
}
|
||||
|
||||
let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings]))
|
||||
let accountSpecificSettings = self.postbox.combinedView(keys: [viewKey])
|
||||
|> map { views -> AccountSpecificCacheStorageSettings in
|
||||
let cacheSettings: AccountSpecificCacheStorageSettings
|
||||
if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) {
|
||||
cacheSettings = value
|
||||
} else {
|
||||
cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings
|
||||
}
|
||||
|
||||
return cacheSettings
|
||||
}
|
||||
|
||||
self.settingsDisposable = (combineLatest(queue: self.queue,
|
||||
cacheSettings,
|
||||
accountSpecificSettings
|
||||
)
|
||||
|> map { cacheSettings, accountSpecificSettings -> CombinedSettings in
|
||||
return CombinedSettings(
|
||||
categoryStorageTimeout: cacheSettings.categoryStorageTimeout,
|
||||
exceptions: accountSpecificSettings.peerStorageTimeoutExceptions
|
||||
)
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] combinedSettings in
|
||||
self?.restart(settings: combinedSettings)
|
||||
})
|
||||
}
|
||||
|
||||
private func restart(settings: CombinedSettings) {
|
||||
self.processDisposable?.dispose()
|
||||
|
||||
let processingQueue = self.processingQueue
|
||||
let postbox = self.postbox
|
||||
let mediaBox = self.postbox.mediaBox
|
||||
|
||||
let _ = processingQueue
|
||||
let _ = mediaBox
|
||||
|
||||
self.processDisposable = (self.postbox.mediaBox.storageBox.allPeerIds()
|
||||
|> mapToSignal { peerIds -> Signal<Never, NoError> in
|
||||
return postbox.transaction { transaction -> [PeerId: CacheStorageSettings.PeerStorageCategory] in
|
||||
var channelCategoryMapping: [PeerId: CacheStorageSettings.PeerStorageCategory] = [:]
|
||||
for peerId in peerIds {
|
||||
if peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||
var category: CacheStorageSettings.PeerStorageCategory = .channels
|
||||
if let peer = transaction.getPeer(peerId) as? TelegramChannel, case .group = peer.info {
|
||||
category = .groups
|
||||
}
|
||||
channelCategoryMapping[peerId] = category
|
||||
}
|
||||
}
|
||||
|
||||
return channelCategoryMapping
|
||||
}
|
||||
|> mapToSignal { channelCategoryMapping -> Signal<Never, NoError> in
|
||||
var signals: Signal<Never, NoError> = .complete()
|
||||
|
||||
var matchingPeers = 0
|
||||
|
||||
for peerId in peerIds {
|
||||
let timeout: Int32
|
||||
if let value = settings.exceptions[peerId] {
|
||||
timeout = value
|
||||
} else {
|
||||
switch peerId.namespace {
|
||||
case Namespaces.Peer.CloudUser, Namespaces.Peer.SecretChat:
|
||||
timeout = settings.categoryStorageTimeout[.privateChats] ?? Int32.max
|
||||
case Namespaces.Peer.CloudGroup:
|
||||
timeout = settings.categoryStorageTimeout[.groups] ?? Int32.max
|
||||
default:
|
||||
if let category = channelCategoryMapping[peerId], case .groups = category {
|
||||
timeout = settings.categoryStorageTimeout[.groups] ?? Int32.max
|
||||
} else {
|
||||
timeout = settings.categoryStorageTimeout[.channels] ?? Int32.max
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if timeout == Int32.max {
|
||||
continue
|
||||
}
|
||||
|
||||
matchingPeers += 1
|
||||
|
||||
let minPeerTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - timeout
|
||||
//let minPeerTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||
|
||||
signals = signals |> then(mediaBox.storageBox.all(peerId: peerId)
|
||||
|> mapToSignal { peerResourceIds -> Signal<Never, NoError> in
|
||||
return Signal { subscriber in
|
||||
var isCancelled = false
|
||||
|
||||
processingQueue.justDispatch {
|
||||
var removeIds: [MediaResourceId] = []
|
||||
var removeRawIds: [Data] = []
|
||||
var localCounter = 0
|
||||
for resourceId in peerResourceIds {
|
||||
localCounter += 1
|
||||
if localCounter % 100 == 0 {
|
||||
if isCancelled {
|
||||
subscriber.putCompletion()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
removeRawIds.append(resourceId)
|
||||
let id = MediaResourceId(String(data: resourceId, encoding: .utf8)!)
|
||||
let resourceTimestamp = mediaBox.resourceUsageWithInfo(id: id)
|
||||
if resourceTimestamp != 0 && resourceTimestamp < minPeerTimestamp {
|
||||
removeIds.append(id)
|
||||
}
|
||||
}
|
||||
|
||||
if !removeIds.isEmpty {
|
||||
Logger.shared.log("AutomaticCacheEviction", "peer \(peerId): cleaning \(removeIds.count) resources")
|
||||
|
||||
let _ = mediaBox.removeCachedResources(removeIds).start(completed: {
|
||||
mediaBox.storageBox.remove(ids: removeRawIds)
|
||||
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
} else {
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
|
||||
return ActionDisposable {
|
||||
isCancelled = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Logger.shared.log("AutomaticCacheEviction", "have \(matchingPeers) peers with data")
|
||||
|
||||
return signals
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
}
|
||||
|
||||
private let queue: Queue
|
||||
private let impl: QueueLocalObject<Impl>
|
||||
|
||||
init(postbox: Postbox, accountManager: AccountManager<TelegramAccountManagerTypes>) {
|
||||
let queue = Queue(name: "AutomaticCacheEviction")
|
||||
self.queue = queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return Impl(queue: queue, accountManager: accountManager, postbox: postbox)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -15343,7 +15343,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
var attemptSelectionImpl: ((Peer) -> Void)?
|
||||
let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, updatedPresentationData: self.updatedPresentationData, filter: filter, attemptSelection: { peer, _ in
|
||||
attemptSelectionImpl?(peer)
|
||||
}, multipleSelection: true, forwardedMessageIds: messages.map { $0.id }))
|
||||
}, multipleSelection: true, forwardedMessageIds: messages.map { $0.id }, selectForumThreads: true))
|
||||
let context = self.context
|
||||
attemptSelectionImpl = { [weak self, weak controller] peer in
|
||||
guard let strongSelf = self, let controller = controller else {
|
||||
@ -15700,7 +15700,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
break
|
||||
case let .chat(textInputState, _, _):
|
||||
if let textInputState = textInputState {
|
||||
let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, updatedPresentationData: self.updatedPresentationData))
|
||||
let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, updatedPresentationData: self.updatedPresentationData, selectForumThreads: true))
|
||||
controller.peerSelected = { [weak self, weak controller] peer, threadId in
|
||||
let peerId = peer.id
|
||||
|
||||
|
||||
@ -713,7 +713,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
}, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled]))
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled], selectForumThreads: true))
|
||||
controller.peerSelected = { [weak controller] peer, _ in
|
||||
let peerId = peer.id
|
||||
|
||||
|
||||
@ -19,11 +19,16 @@ private final class MultiScaleTextStateNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
final class MultiScaleTextState {
|
||||
let attributedText: NSAttributedString
|
||||
struct Attributes {
|
||||
var font: UIFont
|
||||
var color: UIColor
|
||||
}
|
||||
|
||||
let attributes: Attributes
|
||||
let constrainedSize: CGSize
|
||||
|
||||
init(attributedText: NSAttributedString, constrainedSize: CGSize) {
|
||||
self.attributedText = attributedText
|
||||
init(attributes: Attributes, constrainedSize: CGSize) {
|
||||
self.attributes = attributes
|
||||
self.constrainedSize = constrainedSize
|
||||
}
|
||||
}
|
||||
@ -49,7 +54,7 @@ final class MultiScaleTextNode: ASDisplayNode {
|
||||
return self.stateNodes[key]?.textNode
|
||||
}
|
||||
|
||||
func updateLayout(states: [AnyHashable: MultiScaleTextState], mainState: AnyHashable) -> [AnyHashable: MultiScaleTextLayout] {
|
||||
func updateLayout(text: String, states: [AnyHashable: MultiScaleTextState], mainState: AnyHashable) -> [AnyHashable: MultiScaleTextLayout] {
|
||||
assert(Set(states.keys) == Set(self.stateNodes.keys))
|
||||
assert(states[mainState] != nil)
|
||||
|
||||
@ -57,7 +62,7 @@ final class MultiScaleTextNode: ASDisplayNode {
|
||||
var mainLayout: MultiScaleTextLayout?
|
||||
for (key, state) in states {
|
||||
if let node = self.stateNodes[key] {
|
||||
node.textNode.attributedText = state.attributedText
|
||||
node.textNode.attributedText = NSAttributedString(string: text, font: state.attributes.font, textColor: state.attributes.color)
|
||||
let nodeSize = node.textNode.updateLayout(state.constrainedSize)
|
||||
let nodeLayout = MultiScaleTextLayout(size: nodeSize)
|
||||
if key == mainState {
|
||||
|
||||
@ -72,7 +72,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
||||
case let .botStart(peer, payload):
|
||||
openPeer(EnginePeer(peer), .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive)))
|
||||
case let .groupBotStart(botPeerId, payload, adminRights):
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyGroupsAndChannels, .onlyManageable, .excludeDisabled, .excludeRecent, .doNotSearchMessages], hasContactSelector: false, title: presentationData.strings.Bot_AddToChat_Title))
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyGroupsAndChannels, .onlyManageable, .excludeDisabled, .excludeRecent, .doNotSearchMessages], hasContactSelector: false, title: presentationData.strings.Bot_AddToChat_Title, selectForumThreads: true))
|
||||
controller.peerSelected = { [weak controller] peer, _ in
|
||||
let peerId = peer.id
|
||||
|
||||
@ -322,7 +322,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
||||
present(shareController, nil)
|
||||
context.sharedContext.applicationBindings.dismissNativeController()
|
||||
} else {
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled]))
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled], selectForumThreads: true))
|
||||
controller.peerSelected = { [weak controller] peer, _ in
|
||||
let peerId = peer.id
|
||||
|
||||
@ -604,7 +604,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
||||
}
|
||||
|
||||
if let navigationController = navigationController {
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, updatedPresentationData: updatedPresentationData, filter: filters, hasChatListSelector: true, hasContactSelector: false, title: presentationData.strings.WebApp_SelectChat))
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, updatedPresentationData: updatedPresentationData, filter: filters, hasChatListSelector: true, hasContactSelector: false, title: presentationData.strings.WebApp_SelectChat, selectForumThreads: true))
|
||||
controller.peerSelected = { [weak navigationController] peer, _ in
|
||||
guard let navigationController else {
|
||||
return
|
||||
@ -656,7 +656,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
||||
}
|
||||
|
||||
if let navigationController = navigationController {
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, updatedPresentationData: updatedPresentationData, filter: filters, hasChatListSelector: true, hasContactSelector: false, title: presentationData.strings.WebApp_SelectChat))
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, updatedPresentationData: updatedPresentationData, filter: filters, hasChatListSelector: true, hasContactSelector: false, title: presentationData.strings.WebApp_SelectChat, selectForumThreads: true))
|
||||
controller.peerSelected = { [weak navigationController] peer, _ in
|
||||
guard let navigationController else {
|
||||
return
|
||||
|
||||
@ -2645,14 +2645,16 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
var isPremium = false
|
||||
var isVerified = false
|
||||
var isFake = false
|
||||
let smallTitleString: NSAttributedString
|
||||
let titleString: NSAttributedString
|
||||
let smallSubtitleString: NSAttributedString
|
||||
let subtitleString: NSAttributedString
|
||||
let titleStringText: String
|
||||
let smallTitleAttributes: MultiScaleTextState.Attributes
|
||||
let titleAttributes: MultiScaleTextState.Attributes
|
||||
let subtitleStringText: String
|
||||
let smallSubtitleAttributes: MultiScaleTextState.Attributes
|
||||
let subtitleAttributes: MultiScaleTextState.Attributes
|
||||
var subtitleIsButton: Bool = false
|
||||
var panelSubtitleString: NSAttributedString?
|
||||
var nextPanelSubtitleString: NSAttributedString?
|
||||
let usernameString: NSAttributedString
|
||||
var panelSubtitleString: (text: String, attributes: MultiScaleTextState.Attributes)?
|
||||
var nextPanelSubtitleString: (text: String, attributes: MultiScaleTextState.Attributes)?
|
||||
let usernameString: (text: String, attributes: MultiScaleTextState.Attributes)
|
||||
if let peer = peer {
|
||||
isPremium = peer.isPremium
|
||||
isVerified = peer.isVerified
|
||||
@ -2681,17 +2683,21 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
titleString = NSAttributedString(string: title, font: Font.regular(30.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
|
||||
smallTitleString = NSAttributedString(string: title, font: Font.regular(30.0), textColor: .white)
|
||||
titleStringText = title
|
||||
titleAttributes = MultiScaleTextState.Attributes(font: Font.regular(30.0), color: presentationData.theme.list.itemPrimaryTextColor)
|
||||
smallTitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(30.0), color: .white)
|
||||
|
||||
if self.isSettings, let user = peer as? TelegramUser {
|
||||
var subtitle = formatPhoneNumber(context: self.context, number: user.phone ?? "")
|
||||
|
||||
if let mainUsername = user.addressName, !mainUsername.isEmpty {
|
||||
subtitle = "\(subtitle) • @\(mainUsername)"
|
||||
}
|
||||
smallSubtitleString = NSAttributedString(string: subtitle, font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.7))
|
||||
subtitleString = NSAttributedString(string: subtitle, font: Font.regular(17.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
|
||||
usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
|
||||
subtitleStringText = subtitle
|
||||
subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(17.0), color: presentationData.theme.list.itemSecondaryTextColor)
|
||||
smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)
|
||||
|
||||
usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor))
|
||||
} else if let _ = threadData {
|
||||
let subtitleColor: UIColor
|
||||
subtitleColor = presentationData.theme.list.itemAccentColor
|
||||
@ -2699,9 +2705,11 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
let statusText: String
|
||||
statusText = peer.debugDisplayTitle
|
||||
|
||||
smallSubtitleString = NSAttributedString(string: statusText, font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.7))
|
||||
subtitleString = NSAttributedString(string: statusText, font: Font.semibold(15.0), textColor: subtitleColor)
|
||||
usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
|
||||
subtitleStringText = statusText
|
||||
subtitleAttributes = MultiScaleTextState.Attributes(font: Font.semibold(15.0), color: subtitleColor)
|
||||
smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: UIColor(white: 1.0, alpha: 0.7))
|
||||
|
||||
usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor))
|
||||
|
||||
subtitleIsButton = true
|
||||
|
||||
@ -2713,10 +2721,10 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
} else {
|
||||
subtitleColor = presentationData.theme.list.itemSecondaryTextColor
|
||||
}
|
||||
panelSubtitleString = NSAttributedString(string: panelStatusData.text, font: Font.regular(17.0), textColor: subtitleColor)
|
||||
panelSubtitleString = (panelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor))
|
||||
}
|
||||
if let nextPanelStatusData = maybeNextPanelStatusData {
|
||||
nextPanelSubtitleString = NSAttributedString(string: nextPanelStatusData.text, font: Font.regular(17.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
|
||||
nextPanelSubtitleString = (nextPanelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: presentationData.theme.list.itemSecondaryTextColor))
|
||||
}
|
||||
} else if let statusData = statusData {
|
||||
let subtitleColor: UIColor
|
||||
@ -2725,9 +2733,12 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
} else {
|
||||
subtitleColor = presentationData.theme.list.itemSecondaryTextColor
|
||||
}
|
||||
smallSubtitleString = NSAttributedString(string: statusData.text, font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.7))
|
||||
subtitleString = NSAttributedString(string: statusData.text, font: Font.regular(17.0), textColor: subtitleColor)
|
||||
usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
|
||||
|
||||
subtitleStringText = statusData.text
|
||||
subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor)
|
||||
smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: UIColor(white: 1.0, alpha: 0.7))
|
||||
|
||||
usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor))
|
||||
|
||||
let (maybePanelStatusData, maybeNextPanelStatusData, _) = panelStatusData
|
||||
if let panelStatusData = maybePanelStatusData {
|
||||
@ -2737,22 +2748,28 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
} else {
|
||||
subtitleColor = presentationData.theme.list.itemSecondaryTextColor
|
||||
}
|
||||
panelSubtitleString = NSAttributedString(string: panelStatusData.text, font: Font.regular(17.0), textColor: subtitleColor)
|
||||
panelSubtitleString = (panelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor))
|
||||
}
|
||||
if let nextPanelStatusData = maybeNextPanelStatusData {
|
||||
nextPanelSubtitleString = NSAttributedString(string: nextPanelStatusData.text, font: Font.regular(17.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
|
||||
nextPanelSubtitleString = (nextPanelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: presentationData.theme.list.itemSecondaryTextColor))
|
||||
}
|
||||
} else {
|
||||
subtitleString = NSAttributedString(string: " ", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
|
||||
smallSubtitleString = subtitleString
|
||||
usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
|
||||
subtitleStringText = " "
|
||||
subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)
|
||||
smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)
|
||||
|
||||
usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor))
|
||||
}
|
||||
} else {
|
||||
titleString = NSAttributedString(string: " ", font: Font.semibold(24.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
|
||||
smallTitleString = titleString
|
||||
subtitleString = NSAttributedString(string: " ", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
|
||||
smallSubtitleString = subtitleString
|
||||
usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
|
||||
titleStringText = " "
|
||||
titleAttributes = MultiScaleTextState.Attributes(font: Font.regular(24.0), color: presentationData.theme.list.itemPrimaryTextColor)
|
||||
smallTitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(24.0), color: .white)
|
||||
|
||||
subtitleStringText = " "
|
||||
subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)
|
||||
smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)
|
||||
|
||||
usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor))
|
||||
}
|
||||
|
||||
let textSideInset: CGFloat = 36.0
|
||||
@ -2760,17 +2777,17 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
|
||||
let titleConstrainedSize = CGSize(width: width - textSideInset * 2.0 - (isPremium || isVerified || isFake ? 20.0 : 0.0), height: .greatestFiniteMagnitude)
|
||||
|
||||
let titleNodeLayout = self.titleNode.updateLayout(states: [
|
||||
TitleNodeStateRegular: MultiScaleTextState(attributedText: titleString, constrainedSize: titleConstrainedSize),
|
||||
TitleNodeStateExpanded: MultiScaleTextState(attributedText: smallTitleString, constrainedSize: titleConstrainedSize)
|
||||
let titleNodeLayout = self.titleNode.updateLayout(text: titleStringText, states: [
|
||||
TitleNodeStateRegular: MultiScaleTextState(attributes: titleAttributes, constrainedSize: titleConstrainedSize),
|
||||
TitleNodeStateExpanded: MultiScaleTextState(attributes: smallTitleAttributes, constrainedSize: titleConstrainedSize)
|
||||
], mainState: TitleNodeStateRegular)
|
||||
self.titleNode.accessibilityLabel = titleString.string
|
||||
self.titleNode.accessibilityLabel = titleStringText
|
||||
|
||||
let subtitleNodeLayout = self.subtitleNode.updateLayout(states: [
|
||||
TitleNodeStateRegular: MultiScaleTextState(attributedText: subtitleString, constrainedSize: titleConstrainedSize),
|
||||
TitleNodeStateExpanded: MultiScaleTextState(attributedText: smallSubtitleString, constrainedSize: titleConstrainedSize)
|
||||
let subtitleNodeLayout = self.subtitleNode.updateLayout(text: subtitleStringText, states: [
|
||||
TitleNodeStateRegular: MultiScaleTextState(attributes: subtitleAttributes, constrainedSize: titleConstrainedSize),
|
||||
TitleNodeStateExpanded: MultiScaleTextState(attributes: smallSubtitleAttributes, constrainedSize: titleConstrainedSize)
|
||||
], mainState: TitleNodeStateRegular)
|
||||
self.subtitleNode.accessibilityLabel = subtitleString.string
|
||||
self.subtitleNode.accessibilityLabel = subtitleStringText
|
||||
|
||||
if subtitleIsButton {
|
||||
let subtitleBackgroundNode: ASDisplayNode
|
||||
@ -2863,25 +2880,25 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
let panelSubtitleNodeLayout = self.panelSubtitleNode.updateLayout(states: [
|
||||
TitleNodeStateRegular: MultiScaleTextState(attributedText: panelSubtitleString ?? subtitleString, constrainedSize: titleConstrainedSize),
|
||||
TitleNodeStateExpanded: MultiScaleTextState(attributedText: panelSubtitleString ?? subtitleString, constrainedSize: titleConstrainedSize)
|
||||
let panelSubtitleNodeLayout = self.panelSubtitleNode.updateLayout(text: panelSubtitleString?.text ?? subtitleStringText, states: [
|
||||
TitleNodeStateRegular: MultiScaleTextState(attributes: panelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize),
|
||||
TitleNodeStateExpanded: MultiScaleTextState(attributes: panelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize)
|
||||
], mainState: TitleNodeStateRegular)
|
||||
self.panelSubtitleNode.accessibilityLabel = (panelSubtitleString ?? subtitleString).string
|
||||
self.panelSubtitleNode.accessibilityLabel = panelSubtitleString?.text ?? subtitleStringText
|
||||
|
||||
let nextPanelSubtitleNodeLayout = self.nextPanelSubtitleNode.updateLayout(states: [
|
||||
TitleNodeStateRegular: MultiScaleTextState(attributedText: nextPanelSubtitleString ?? subtitleString, constrainedSize: titleConstrainedSize),
|
||||
TitleNodeStateExpanded: MultiScaleTextState(attributedText: nextPanelSubtitleString ?? subtitleString, constrainedSize: titleConstrainedSize)
|
||||
let nextPanelSubtitleNodeLayout = self.nextPanelSubtitleNode.updateLayout(text: nextPanelSubtitleString?.text ?? subtitleStringText, states: [
|
||||
TitleNodeStateRegular: MultiScaleTextState(attributes: nextPanelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize),
|
||||
TitleNodeStateExpanded: MultiScaleTextState(attributes: nextPanelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize)
|
||||
], mainState: TitleNodeStateRegular)
|
||||
if let _ = nextPanelSubtitleString {
|
||||
self.nextPanelSubtitleNode.isHidden = false
|
||||
}
|
||||
|
||||
let usernameNodeLayout = self.usernameNode.updateLayout(states: [
|
||||
TitleNodeStateRegular: MultiScaleTextState(attributedText: usernameString, constrainedSize: CGSize(width: titleConstrainedSize.width, height: titleConstrainedSize.height)),
|
||||
TitleNodeStateExpanded: MultiScaleTextState(attributedText: usernameString, constrainedSize: CGSize(width: width - titleNodeLayout[TitleNodeStateExpanded]!.size.width - 8.0, height: titleConstrainedSize.height))
|
||||
let usernameNodeLayout = self.usernameNode.updateLayout(text: usernameString.text, states: [
|
||||
TitleNodeStateRegular: MultiScaleTextState(attributes: usernameString.attributes, constrainedSize: CGSize(width: titleConstrainedSize.width, height: titleConstrainedSize.height)),
|
||||
TitleNodeStateExpanded: MultiScaleTextState(attributes: usernameString.attributes, constrainedSize: CGSize(width: width - titleNodeLayout[TitleNodeStateExpanded]!.size.width - 8.0, height: titleConstrainedSize.height))
|
||||
], mainState: TitleNodeStateRegular)
|
||||
self.usernameNode.accessibilityLabel = usernameString.string
|
||||
self.usernameNode.accessibilityLabel = usernameString.text
|
||||
|
||||
let avatarCenter: CGPoint
|
||||
if let transitionSourceAvatarFrame = transitionSourceAvatarFrame {
|
||||
@ -2987,7 +3004,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
subtitleAlpha = 1.0 - titleCollapseFraction
|
||||
panelSubtitleAlpha = 0.0
|
||||
} else {
|
||||
if (panelSubtitleString ?? subtitleString).string != subtitleString.string {
|
||||
if (panelSubtitleString?.text ?? subtitleStringText) != subtitleStringText {
|
||||
subtitleAlpha = 1.0 - effectiveAreaExpansionFraction
|
||||
panelSubtitleAlpha = effectiveAreaExpansionFraction
|
||||
subtitleOffset = -effectiveAreaExpansionFraction * 5.0
|
||||
|
||||
@ -7659,7 +7659,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
|
||||
func forwardMessages(messageIds: Set<MessageId>?) {
|
||||
if let messageIds = messageIds ?? self.state.selectedMessageIds, !messageIds.isEmpty {
|
||||
let peerSelectionController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, filter: [.onlyWriteable, .excludeDisabled], multipleSelection: true))
|
||||
let peerSelectionController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, filter: [.onlyWriteable, .excludeDisabled], multipleSelection: true, selectForumThreads: true))
|
||||
peerSelectionController.multiplePeersSelected = { [weak self, weak peerSelectionController] peers, peerMap, messageText, mode, forwardOptions in
|
||||
guard let strongSelf = self, let strongController = peerSelectionController else {
|
||||
return
|
||||
|
||||
@ -22,6 +22,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
|
||||
public var multiplePeersSelected: (([Peer], [PeerId: Peer], NSAttributedString, AttachmentTextInputPanelSendMode, ChatInterfaceForwardOptionsState?) -> Void)?
|
||||
private let filter: ChatListNodePeersFilter
|
||||
private let forumPeerId: EnginePeer.Id?
|
||||
private let selectForumThreads: Bool
|
||||
|
||||
private let attemptSelection: ((Peer, Int64?) -> Void)?
|
||||
private let createNewGroup: (() -> Void)?
|
||||
@ -91,6 +92,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
|
||||
self.pretendPresentedInModal = params.pretendPresentedInModal
|
||||
self.forwardedMessageIds = params.forwardedMessageIds
|
||||
self.hasTypeHeaders = params.hasTypeHeaders
|
||||
self.selectForumThreads = params.selectForumThreads
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
|
||||
|
||||
@ -181,7 +183,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
|
||||
|
||||
self.peerSelectionNode.requestOpenPeer = { [weak self] peer, threadId in
|
||||
if let strongSelf = self, let peerSelected = strongSelf.peerSelected {
|
||||
if let peer = peer as? TelegramChannel, peer.flags.contains(.isForum), threadId == nil {
|
||||
if let peer = peer as? TelegramChannel, peer.flags.contains(.isForum), threadId == nil, strongSelf.selectForumThreads {
|
||||
let controller = PeerSelectionControllerImpl(
|
||||
PeerSelectionControllerParams(
|
||||
context: strongSelf.context,
|
||||
@ -197,7 +199,9 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
|
||||
pretendPresentedInModal: false,
|
||||
multipleSelection: false,
|
||||
forwardedMessageIds: [],
|
||||
hasTypeHeaders: false)
|
||||
hasTypeHeaders: false,
|
||||
selectForumThreads: false
|
||||
)
|
||||
)
|
||||
controller.peerSelected = strongSelf.peerSelected
|
||||
strongSelf.push(controller)
|
||||
|
||||
@ -663,7 +663,7 @@ public class ShareRootControllerImpl {
|
||||
attemptSelectionImpl?(peer)
|
||||
}, createNewGroup: {
|
||||
createNewGroupImpl?()
|
||||
}, pretendPresentedInModal: true))
|
||||
}, pretendPresentedInModal: true, selectForumThreads: true))
|
||||
|
||||
controller.customDismiss = {
|
||||
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||
@ -837,7 +837,7 @@ public class ShareRootControllerImpl {
|
||||
var attemptSelectionImpl: ((Peer) -> Void)?
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyPrivateChats, .excludeDisabled, .doNotSearchMessages, .excludeSecretChats], hasChatListSelector: false, hasContactSelector: true, hasGlobalSearch: false, title: presentationData.strings.ChatImport_Title, attemptSelection: { peer, _ in
|
||||
attemptSelectionImpl?(peer)
|
||||
}, pretendPresentedInModal: true))
|
||||
}, pretendPresentedInModal: true, selectForumThreads: true))
|
||||
|
||||
controller.customDismiss = {
|
||||
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||
@ -912,7 +912,7 @@ public class ShareRootControllerImpl {
|
||||
attemptSelectionImpl?(peer)
|
||||
}, createNewGroup: {
|
||||
createNewGroupImpl?()
|
||||
}, pretendPresentedInModal: true))
|
||||
}, pretendPresentedInModal: true, selectForumThreads: true))
|
||||
|
||||
controller.customDismiss = {
|
||||
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user