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

This commit is contained in:
Ilya Laktyushin 2022-12-14 14:25:42 +04:00
commit aeafae62df
29 changed files with 1844 additions and 200 deletions

View File

@ -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";

View File

@ -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
}
}

View File

@ -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

View File

@ -27,6 +27,8 @@ swift_library(
"//submodules/AnimationUI:AnimationUI",
"//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
"//submodules/AvatarNode",
"//submodules/TelegramCore",
],
visibility = [
"//visibility:public",

View File

@ -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

View File

@ -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 })

View File

@ -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

View File

@ -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))

View File

@ -108,6 +108,7 @@ swift_library(
"//submodules/PersistentStringHash:PersistentStringHash",
"//submodules/TelegramUI/Components/NotificationPeerExceptionController",
"//submodules/TelegramUI/Components/ChatTimerScreen",
"//submodules/AnimatedAvatarSetNode",
],
visibility = [
"//visibility:public",

View File

@ -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
}
}

View File

@ -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))
}
}

View File

@ -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")

View File

@ -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))
})
}
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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))
}

View File

@ -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)
})
}
}

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)