diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 4ac130ef01..5226eaafc6 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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"; diff --git a/submodules/AccountContext/Sources/PeerSelectionController.swift b/submodules/AccountContext/Sources/PeerSelectionController.swift index 9a45af7efd..4619663321 100644 --- a/submodules/AccountContext/Sources/PeerSelectionController.swift +++ b/submodules/AccountContext/Sources/PeerSelectionController.swift @@ -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)? = 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)? = 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 } } diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index dc61b1acc4..8358be449d 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -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 diff --git a/submodules/ItemListUI/BUILD b/submodules/ItemListUI/BUILD index e2a17f9755..59b5c92032 100644 --- a/submodules/ItemListUI/BUILD +++ b/submodules/ItemListUI/BUILD @@ -27,6 +27,8 @@ swift_library( "//submodules/AnimationUI:AnimationUI", "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/ManagedAnimationNode:ManagedAnimationNode", + "//submodules/AvatarNode", + "//submodules/TelegramCore", ], visibility = [ "//visibility:public", diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index be05f8597b..4911acb673 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -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,22 +361,33 @@ 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 { multilineLabel = true } - 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 (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())) + + 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 verticalInset: CGFloat = 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 { @@ -486,14 +576,29 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { let labelFrame: CGRect switch item.labelStyle { - case .badge: - labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: badgeFrame.minY + 1), size: labelLayout.size) - 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) + case .badge: + labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: badgeFrame.minY + 1), size: labelLayout.size) + 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: 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 { diff --git a/submodules/Postbox/Sources/MediaBox.swift b/submodules/Postbox/Sources/MediaBox.swift index f06225fce9..e571086051 100644 --- a/submodules/Postbox/Sources/MediaBox.swift +++ b/submodules/Postbox/Sources/MediaBox.swift @@ -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, force: Bool = false, notify: Bool = false) -> Signal { + public func removeCachedResources(_ ids: [MediaResourceId], force: Bool = false, notify: Bool = false) -> Signal { return Signal { subscriber in self.dataQueue.async { let uniqueIds = Set(ids.map { $0.stringRepresentation }) diff --git a/submodules/Postbox/Sources/SqliteValueBox.swift b/submodules/Postbox/Sources/SqliteValueBox.swift index 2c8f462a53..188f963a75 100644 --- a/submodules/Postbox/Sources/SqliteValueBox.swift +++ b/submodules/Postbox/Sources/SqliteValueBox.swift @@ -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 diff --git a/submodules/Postbox/Sources/StorageBox/StorageBox.swift b/submodules/Postbox/Sources/StorageBox/StorageBox.swift index e3a2a51988..3fb83de136 100644 --- a/submodules/Postbox/Sources/StorageBox/StorageBox.swift +++ b/submodules/Postbox/Sources/StorageBox/StorageBox.swift @@ -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) - self.valueBox.setOrIgnore(self.idToReferenceTable, key: idKey, value: MemoryBuffer()) + + 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() + 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)) diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index df3e542697..f609a3f352 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -108,6 +108,7 @@ swift_library( "//submodules/PersistentStringHash:PersistentStringHash", "//submodules/TelegramUI/Components/NotificationPeerExceptionController", "//submodules/TelegramUI/Components/ChatTimerScreen", + "//submodules/AnimatedAvatarSetNode", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift index d06be8a8ce..acfbd9886a 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift @@ -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,43 +105,49 @@ private enum StorageUsageEntry: ItemListNodeEntry { var section: ItemListSectionId { switch self { - case .keepMediaHeader, .keepMedia, .keepMediaInfo: - return StorageUsageSection.keepMedia.rawValue - case .maximumSizeHeader, .maximumSize, .maximumSizeInfo: - return StorageUsageSection.maximumSize.rawValue - case .storageHeader, .storageUsage, .collecting, .clearAll: - return StorageUsageSection.storage.rawValue - case .peersHeader, .peer: - return StorageUsageSection.peers.rawValue + case .keepMediaHeader, .keepMedia, .keepMediaInfo, .keepMediaPrivateChats, .keepMediaGroups, .keepMediaChannels: + return StorageUsageSection.keepMedia.rawValue + case .maximumSizeHeader, .maximumSize, .maximumSizeInfo: + return StorageUsageSection.maximumSize.rawValue + case .storageHeader, .storageUsage, .collecting, .clearAll: + return StorageUsageSection.storage.rawValue + case .peersHeader, .peer: + return StorageUsageSection.peers.rawValue } } var stableId: Int32 { switch self { - case .keepMediaHeader: - return 0 - case .keepMedia: - return 1 - case .keepMediaInfo: - return 2 - case .maximumSizeHeader: - return 3 - case .maximumSize: - return 4 - case .maximumSizeInfo: - return 5 - case .storageHeader: - return 6 - case .storageUsage: - return 7 - case .collecting: - return 8 - case .clearAll: - return 9 - case .peersHeader: - return 10 - case let .peer(index, _, _, _, _, _, _, _, _): - return 11 + index + case .keepMediaHeader: + return 0 + case .keepMedia: + return 1 + case .keepMediaPrivateChats: + return 2 + case .keepMediaGroups: + return 3 + case .keepMediaChannels: + return 4 + case .keepMediaInfo: + return 5 + case .maximumSizeHeader: + return 6 + case .maximumSize: + return 7 + case .maximumSizeInfo: + return 8 + case .storageHeader: + return 9 + case .storageUsage: + return 10 + case .collecting: + return 11 + case .clearAll: + return 12 + case .peersHeader: + return 13 + case let .peer(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() + 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 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 = 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 = .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 + } +} diff --git a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift new file mode 100644 index 0000000000..63fdca9fd6 --- /dev/null +++ b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift @@ -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() + 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 = 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 = .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)) + } +} diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index 5e970f1973..ece6277ef4 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -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") diff --git a/submodules/TelegramCore/Sources/Settings/CacheStorageSettings.swift b/submodules/TelegramCore/Sources/Settings/CacheStorageSettings.swift index 0ecd9b9e9e..e8aa7e0f0f 100644 --- a/submodules/TelegramCore/Sources/Settings/CacheStorageSettings.swift +++ b/submodules/TelegramCore/Sources/Settings/CacheStorageSettings.swift @@ -2,7 +2,6 @@ import Foundation import Postbox import SwiftSignalKit - public func updateCacheStorageSettingsInteractively(accountManager: AccountManager, _ f: @escaping (CacheStorageSettings) -> CacheStorageSettings) -> Signal { 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 { + 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)) + }) + } +} diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index ba04e25306..7026d7ed9d 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -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) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CacheStorageSettings.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CacheStorageSettings.swift index 518d24441a..8741ceea7d 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CacheStorageSettings.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CacheStorageSettings.swift @@ -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") - } - - public func withUpdatedDefaultCacheStorageTimeout(_ defaultCacheStorageTimeout: Int32) -> CacheStorageSettings { - return CacheStorageSettings(defaultCacheStorageTimeout: defaultCacheStorageTimeout, defaultCacheStorageLimitGigabytes: self.defaultCacheStorageLimitGigabytes) - } - public func withUpdatedDefaultCacheStorageLimitGigabytes(_ defaultCacheStorageLimitGigabytes: Int32) -> CacheStorageSettings { - return CacheStorageSettings(defaultCacheStorageTimeout: self.defaultCacheStorageTimeout, defaultCacheStorageLimitGigabytes: defaultCacheStorageLimitGigabytes) + + 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 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") + } } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 9e2533826b..95d0072217 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift index 3a5ba96711..4c8c2638ee 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift @@ -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 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ScheduledMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ScheduledMessages.swift index ef1b48f563..329902ac3f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ScheduledMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ScheduledMessages.swift @@ -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 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 6856e9b53b..b430d70792 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -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: diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift index 8a48c5f4a7..1f9542ead2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift @@ -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) -> Signal { - return account.postbox.mediaBox.removeCachedResources(mediaResourceIds) + return account.postbox.mediaBox.removeCachedResources(Array(mediaResourceIds)) } diff --git a/submodules/TelegramCore/Sources/Utils/AutomaticCacheEviction.swift b/submodules/TelegramCore/Sources/Utils/AutomaticCacheEviction.swift new file mode 100644 index 0000000000..3a75cb3716 --- /dev/null +++ b/submodules/TelegramCore/Sources/Utils/AutomaticCacheEviction.swift @@ -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 + let postbox: Postbox + + var settingsDisposable: Disposable? + var processDisposable: Disposable? + + init(queue: Queue, accountManager: AccountManager, 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 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 in + var signals: Signal = .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 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 + + init(postbox: Postbox, accountManager: AccountManager) { + let queue = Queue(name: "AutomaticCacheEviction") + self.queue = queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, accountManager: accountManager, postbox: postbox) + }) + } +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index ce9b23f0d4..3f511f8d0d 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 7cf50fa436..e44425b188 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/MultiScaleTextNode.swift b/submodules/TelegramUI/Sources/MultiScaleTextNode.swift index 3c5a42fe2f..37548d5d82 100644 --- a/submodules/TelegramUI/Sources/MultiScaleTextNode.swift +++ b/submodules/TelegramUI/Sources/MultiScaleTextNode.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index f7e3cc0d7f..daf0fb0c56 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index b842fb8166..30d719f700 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index edb45767ab..13c4625a37 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -7659,7 +7659,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate func forwardMessages(messageIds: Set?) { 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 diff --git a/submodules/TelegramUI/Sources/PeerSelectionController.swift b/submodules/TelegramUI/Sources/PeerSelectionController.swift index 9a2f0c710e..e2bdace938 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionController.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionController.swift @@ -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) diff --git a/submodules/TelegramUI/Sources/ShareExtensionContext.swift b/submodules/TelegramUI/Sources/ShareExtensionContext.swift index d5bf5d2110..12fb7471d7 100644 --- a/submodules/TelegramUI/Sources/ShareExtensionContext.swift +++ b/submodules/TelegramUI/Sources/ShareExtensionContext.swift @@ -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)