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

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

View File

@ -8484,6 +8484,9 @@ Sorry for the inconvenience.";
"Conversation.SuggestedVideoTextYou" = "You suggested %@ to use this video for their Telegram account."; "Conversation.SuggestedVideoTextYou" = "You suggested %@ to use this video for their Telegram account.";
"Conversation.SuggestedVideoView" = "View"; "Conversation.SuggestedVideoView" = "View";
"CacheEvictionMenu.CategoryExceptions_1" = "%@ Exception";
"CacheEvictionMenu.CategoryExceptions_any" = "%@ Exceptions";
"Conversation.Messages_1" = "%@ message"; "Conversation.Messages_1" = "%@ message";
"Conversation.Messages_any" = "%@ messages"; "Conversation.Messages_any" = "%@ messages";

View File

@ -52,8 +52,9 @@ public final class PeerSelectionControllerParams {
public let multipleSelection: Bool public let multipleSelection: Bool
public let forwardedMessageIds: [EngineMessage.Id] public let forwardedMessageIds: [EngineMessage.Id]
public let hasTypeHeaders: Bool public let hasTypeHeaders: Bool
public let selectForumThreads: Bool
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filter: ChatListNodePeersFilter = [.onlyWriteable], forumPeerId: EnginePeer.Id? = nil, hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer, Int64?) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false, multipleSelection: Bool = false, forwardedMessageIds: [EngineMessage.Id] = [], hasTypeHeaders: Bool = false) { public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filter: ChatListNodePeersFilter = [.onlyWriteable], forumPeerId: EnginePeer.Id? = nil, hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer, Int64?) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false, multipleSelection: Bool = false, forwardedMessageIds: [EngineMessage.Id] = [], hasTypeHeaders: Bool = false, selectForumThreads: Bool = false) {
self.context = context self.context = context
self.updatedPresentationData = updatedPresentationData self.updatedPresentationData = updatedPresentationData
self.filter = filter self.filter = filter
@ -68,6 +69,7 @@ public final class PeerSelectionControllerParams {
self.multipleSelection = multipleSelection self.multipleSelection = multipleSelection
self.forwardedMessageIds = forwardedMessageIds self.forwardedMessageIds = forwardedMessageIds
self.hasTypeHeaders = hasTypeHeaders self.hasTypeHeaders = hasTypeHeaders
self.selectForumThreads = selectForumThreads
} }
} }

View File

@ -1213,7 +1213,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
} }
} }
let _ = (strongSelf.context.account.postbox.mediaBox.removeCachedResources(resourceIds, force: true, notify: true) let _ = (strongSelf.context.account.postbox.mediaBox.removeCachedResources(Array(resourceIds), force: true, notify: true)
|> deliverOnMainQueue).start(completed: { |> deliverOnMainQueue).start(completed: {
guard let strongSelf = self else { guard let strongSelf = self else {
return return
@ -1298,7 +1298,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
self.context.engine.messages.ensureMessagesAreLocallyAvailable(messages: messages.values.filter { messageIds.contains($0.id) }) 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 peerSelectionController.multiplePeersSelected = { [weak self, weak peerSelectionController] peers, peerMap, messageText, mode, forwardOptions in
guard let strongSelf = self, let strongController = peerSelectionController else { guard let strongSelf = self, let strongController = peerSelectionController else {
return return

View File

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

View File

@ -5,12 +5,22 @@ import AsyncDisplayKit
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
import ShimmerEffect import ShimmerEffect
import AvatarNode
import TelegramCore
import AccountContext
private let avatarFont = avatarPlaceholderFont(size: 16.0)
public enum ItemListDisclosureItemTitleColor { public enum ItemListDisclosureItemTitleColor {
case primary case primary
case accent case accent
} }
public enum ItemListDisclosureItemTitleFont {
case regular
case bold
}
public enum ItemListDisclosureStyle { public enum ItemListDisclosureStyle {
case arrow case arrow
case optionArrows case optionArrows
@ -31,11 +41,15 @@ public enum ItemListDisclosureLabelStyle {
public class ItemListDisclosureItem: ListViewItem, ItemListItem { public class ItemListDisclosureItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData let presentationData: ItemListPresentationData
let icon: UIImage? let icon: UIImage?
let context: AccountContext?
let iconPeer: EnginePeer?
let title: String let title: String
let titleColor: ItemListDisclosureItemTitleColor let titleColor: ItemListDisclosureItemTitleColor
let titleFont: ItemListDisclosureItemTitleFont
let enabled: Bool let enabled: Bool
let label: String let label: String
let labelStyle: ItemListDisclosureLabelStyle let labelStyle: ItemListDisclosureLabelStyle
let additionalDetailLabel: String?
public let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
let style: ItemListStyle let style: ItemListStyle
let disclosureStyle: ItemListDisclosureStyle let disclosureStyle: ItemListDisclosureStyle
@ -44,14 +58,18 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem {
public let tag: ItemListItemTag? public let tag: ItemListItemTag?
public let shimmeringIndex: Int? 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.presentationData = presentationData
self.icon = icon self.icon = icon
self.context = context
self.iconPeer = iconPeer
self.title = title self.title = title
self.titleColor = titleColor self.titleColor = titleColor
self.titleFont = titleFont
self.enabled = enabled self.enabled = enabled
self.labelStyle = labelStyle self.labelStyle = labelStyle
self.label = label self.label = label
self.additionalDetailLabel = additionalDetailLabel
self.sectionId = sectionId self.sectionId = sectionId
self.style = style self.style = style
self.disclosureStyle = disclosureStyle self.disclosureStyle = disclosureStyle
@ -115,9 +133,11 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
private let highlightedBackgroundNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode private let maskNode: ASImageNode
var avatarNode: AvatarNode?
let iconNode: ASImageNode let iconNode: ASImageNode
let titleNode: TextNode let titleNode: TextNode
public let labelNode: TextNode public let labelNode: TextNode
var additionalDetailLabelNode: TextNode?
let arrowNode: ASImageNode let arrowNode: ASImageNode
let labelBadgeNode: ASImageNode let labelBadgeNode: ASImageNode
let labelImageNode: ASImageNode let labelImageNode: ASImageNode
@ -213,6 +233,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
public func asyncLayout() -> (_ item: ItemListDisclosureItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { public func asyncLayout() -> (_ item: ItemListDisclosureItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makeAdditionalDetailLabelLayout = TextNode.asyncLayout(self.additionalDetailLabelNode)
let currentItem = self.item let currentItem = self.item
@ -284,8 +305,10 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
let itemSeparatorColor: UIColor let itemSeparatorColor: UIColor
var leftInset = 16.0 + params.leftInset var leftInset = 16.0 + params.leftInset
if let _ = item.icon { if item.icon != nil {
leftInset += 43.0 leftInset += 43.0
} else if item.iconPeer != nil {
leftInset += 46.0
} }
var additionalTextRightInset: CGFloat = 0.0 var additionalTextRightInset: CGFloat = 0.0
@ -303,15 +326,31 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
titleColor = item.presentationData.theme.list.itemDisabledTextColor 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 detailFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
let labelFont: UIFont let labelFont: UIFont
let labelBadgeColor: UIColor let labelBadgeColor: UIColor
var labelConstrain: CGFloat = params.width - params.rightInset - leftInset - 40.0 - titleLayout.size.width - 10.0 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 { switch item.labelStyle {
case .badge: case .badge:
labelBadgeColor = item.presentationData.theme.list.itemCheckColors.foregroundColor labelBadgeColor = item.presentationData.theme.list.itemCheckColors.foregroundColor
@ -322,22 +361,33 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
labelConstrain = params.width - params.rightInset - 40.0 - leftInset labelConstrain = params.width - params.rightInset - 40.0 - leftInset
case let .coloredText(color): case let .coloredText(color):
labelBadgeColor = color labelBadgeColor = color
labelFont = titleFont labelFont = defaultLabelFont
default: default:
labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor
labelFont = titleFont labelFont = defaultLabelFont
} }
var multilineLabel = false var multilineLabel = false
if case .multilineDetailText = item.labelStyle { if case .multilineDetailText = item.labelStyle {
multilineLabel = true 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 titleSpacing: CGFloat = 1.0
let height: CGFloat var height: CGFloat
switch item.labelStyle { switch item.labelStyle {
case .detailText: case .detailText:
height = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height height = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height
@ -346,6 +396,12 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
default: default:
height = verticalInset * 2.0 + titleLayout.size.height 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 { switch item.style {
case .plain: case .plain:
@ -394,6 +450,27 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.iconNode.removeFromSupernode() 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 { if let updateArrowImage = updateArrowImage {
strongSelf.arrowNode.image = 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)) 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 strongSelf.titleNode.frame = titleFrame
if let updateBadgeImage = updatedLabelBadgeImage { if let updateBadgeImage = updatedLabelBadgeImage {
@ -491,10 +581,25 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
case .detailText, .multilineDetailText: case .detailText, .multilineDetailText:
labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size) labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size)
default: default:
labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: 11.0), size: labelLayout.size) labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: floor((height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
} }
strongSelf.labelNode.frame = labelFrame 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 case .textWithIcon = item.labelStyle {
if let updatedLabelImage = updatedLabelImage { if let updatedLabelImage = updatedLabelImage {
strongSelf.labelImageNode.image = updatedLabelImage strongSelf.labelImageNode.image = updatedLabelImage

View File

@ -194,7 +194,7 @@ public final class MediaBox {
}), basePath: basePath + "/storage") }), basePath: basePath + "/storage")
self.timeBasedCleanup = TimeBasedCleanup(generalPaths: [ self.timeBasedCleanup = TimeBasedCleanup(generalPaths: [
self.basePath, //self.basePath,
self.basePath + "/cache", self.basePath + "/cache",
self.basePath + "/animation-cache" self.basePath + "/animation-cache"
], shortLivedPaths: [ ], shortLivedPaths: [
@ -595,7 +595,7 @@ public final class MediaBox {
} }
if let location = parameters?.location { 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 { guard let (fileContext, releaseContext) = self.fileContext(for: resource.id) else {
@ -761,7 +761,7 @@ public final class MediaBox {
let paths = self.storePathsForId(resource.id) let paths = self.storePathsForId(resource.id)
if let location = parameters?.location { 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) { 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> { public func collectResourceCacheUsage(_ ids: [MediaResourceId]) -> Signal<[MediaResourceId: Int64], NoError> {
return Signal { subscriber in return Signal { subscriber in
self.dataQueue.async { self.dataQueue.async {
@ -1472,7 +1490,7 @@ public final class MediaBox {
} }
} }
public func removeCachedResources(_ ids: Set<MediaResourceId>, force: Bool = false, notify: Bool = false) -> Signal<Float, NoError> { public func removeCachedResources(_ ids: [MediaResourceId], force: Bool = false, notify: Bool = false) -> Signal<Float, NoError> {
return Signal { subscriber in return Signal { subscriber in
self.dataQueue.async { self.dataQueue.async {
let uniqueIds = Set(ids.map { $0.stringRepresentation }) let uniqueIds = Set(ids.map { $0.stringRepresentation })

View File

@ -8,6 +8,7 @@ private struct SqliteValueBoxTable {
} }
let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
let SQLITE_PREPARE_PERSISTENT: UInt32 = 1
private func checkTableKey(_ table: ValueBoxTable, _ key: ValueBoxKey) { private func checkTableKey(_ table: ValueBoxTable, _ key: ValueBoxKey) {
switch table.keyType { switch table.keyType {
@ -731,7 +732,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.getStatements[table.id] = preparedStatement self.getStatements[table.id] = preparedStatement
@ -760,7 +761,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.getRowIdStatements[table.id] = preparedStatement self.getRowIdStatements[table.id] = preparedStatement
@ -790,7 +791,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.rangeKeyAscStatementsLimit[table.id] = preparedStatement self.rangeKeyAscStatementsLimit[table.id] = preparedStatement
@ -823,7 +824,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.rangeKeyAscStatementsNoLimit[table.id] = preparedStatement self.rangeKeyAscStatementsNoLimit[table.id] = preparedStatement
@ -854,7 +855,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.rangeKeyDescStatementsLimit[table.id] = preparedStatement self.rangeKeyDescStatementsLimit[table.id] = preparedStatement
@ -886,7 +887,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.rangeKeyDescStatementsNoLimit[table.id] = preparedStatement self.rangeKeyDescStatementsNoLimit[table.id] = preparedStatement
@ -918,7 +919,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.deleteRangeStatements[table.id] = preparedStatement self.deleteRangeStatements[table.id] = preparedStatement
@ -950,7 +951,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.rangeValueAscStatementsLimit[table.id] = preparedStatement self.rangeValueAscStatementsLimit[table.id] = preparedStatement
@ -982,7 +983,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.rangeValueAscStatementsNoLimit[table.id] = preparedStatement self.rangeValueAscStatementsNoLimit[table.id] = preparedStatement
@ -1014,7 +1015,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.rangeValueDescStatementsLimit[table.id] = preparedStatement self.rangeValueDescStatementsLimit[table.id] = preparedStatement
@ -1047,7 +1048,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.rangeValueDescStatementsNoLimit[table.id] = preparedStatement self.rangeValueDescStatementsNoLimit[table.id] = preparedStatement
@ -1077,7 +1078,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.scanStatements[table.id] = preparedStatement self.scanStatements[table.id] = preparedStatement
@ -1098,7 +1099,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.scanKeysStatements[table.id] = preparedStatement self.scanKeysStatements[table.id] = preparedStatement
@ -1120,7 +1121,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.existsStatements[table.id] = preparedStatement self.existsStatements[table.id] = preparedStatement
@ -1149,7 +1150,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.updateStatements[table.id] = preparedStatement self.updateStatements[table.id] = preparedStatement
@ -1180,7 +1181,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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 { if status != SQLITE_OK {
let errorText = self.database.currentError() ?? "Unknown error" let errorText = self.database.currentError() ?? "Unknown error"
preconditionFailure(errorText) preconditionFailure(errorText)
@ -1194,7 +1195,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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 { if status != SQLITE_OK {
let errorText = self.database.currentError() ?? "Unknown error" let errorText = self.database.currentError() ?? "Unknown error"
preconditionFailure(errorText) preconditionFailure(errorText)
@ -1233,7 +1234,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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 { if status != SQLITE_OK {
let errorText = self.database.currentError() ?? "Unknown error" let errorText = self.database.currentError() ?? "Unknown error"
preconditionFailure(errorText) preconditionFailure(errorText)
@ -1247,7 +1248,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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 { if status != SQLITE_OK {
let errorText = self.database.currentError() ?? "Unknown error" let errorText = self.database.currentError() ?? "Unknown error"
preconditionFailure(errorText) preconditionFailure(errorText)
@ -1285,7 +1286,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.deleteStatements[table.id] = preparedStatement self.deleteStatements[table.id] = preparedStatement
@ -1315,7 +1316,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.moveStatements[table.id] = preparedStatement self.moveStatements[table.id] = preparedStatement
@ -1349,7 +1350,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.copyStatements[TablePairKey(table1: fromTable.id, table2: toTable.id)] = preparedStatement self.copyStatements[TablePairKey(table1: fromTable.id, table2: toTable.id)] = preparedStatement
@ -1384,7 +1385,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.fullTextInsertStatements[table.id] = preparedStatement self.fullTextInsertStatements[table.id] = preparedStatement
@ -1423,7 +1424,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.fullTextDeleteStatements[table.id] = preparedStatement self.fullTextDeleteStatements[table.id] = preparedStatement
@ -1447,7 +1448,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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 { if status != SQLITE_OK {
self.printError() self.printError()
assertionFailure() assertionFailure()
@ -1474,7 +1475,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.fullTextMatchCollectionStatements[table.id] = preparedStatement self.fullTextMatchCollectionStatements[table.id] = preparedStatement
@ -1503,7 +1504,7 @@ public final class SqliteValueBox: ValueBox {
resultStatement = statement resultStatement = statement
} else { } else {
var statement: OpaquePointer? = nil 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) precondition(status == SQLITE_OK)
let preparedStatement = SqlitePreparedStatement(statement: statement) let preparedStatement = SqlitePreparedStatement(statement: statement)
self.fullTextMatchCollectionTagsStatements[table.id] = preparedStatement self.fullTextMatchCollectionTagsStatements[table.id] = preparedStatement

View File

@ -20,11 +20,11 @@ private func md5Hash(_ data: Data) -> HashId {
public final class StorageBox { public final class StorageBox {
public struct Reference { public struct Reference {
public var peerId: PeerId public var peerId: Int64
public var messageNamespace: UInt8 public var messageNamespace: UInt8
public var messageId: Int32 public var messageId: Int32
public init(peerId: PeerId, messageNamespace: UInt8, messageId: Int32) { public init(peerId: Int64, messageNamespace: UInt8, messageId: Int32) {
self.peerId = peerId self.peerId = peerId
self.messageNamespace = messageNamespace self.messageNamespace = messageNamespace
self.messageId = messageId self.messageId = messageId
@ -60,6 +60,8 @@ public final class StorageBox {
let valueBox: SqliteValueBox let valueBox: SqliteValueBox
let hashIdToIdTable: ValueBoxTable let hashIdToIdTable: ValueBoxTable
let idToReferenceTable: ValueBoxTable let idToReferenceTable: ValueBoxTable
let peerIdToIdTable: ValueBoxTable
let peerIdTable: ValueBoxTable
init(queue: Queue, logger: StorageBox.Logger, basePath: String) { init(queue: Queue, logger: StorageBox.Logger, basePath: String) {
self.queue = queue self.queue = queue
@ -80,6 +82,8 @@ public final class StorageBox {
self.hashIdToIdTable = ValueBoxTable(id: 5, keyType: .binary, compactValuesOnCreation: true) self.hashIdToIdTable = ValueBoxTable(id: 5, keyType: .binary, compactValuesOnCreation: true)
self.idToReferenceTable = ValueBoxTable(id: 6, 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) { func add(reference: Reference, to id: Data) {
@ -87,19 +91,154 @@ public final class StorageBox {
let hashId = md5Hash(id) 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)) self.valueBox.setOrIgnore(self.hashIdToIdTable, key: mainKey, value: MemoryBuffer(data: id))
let idKey = ValueBoxKey(length: hashId.data.count + 8 + 1 + 4) let idKey = ValueBoxKey(length: hashId.data.count + 8 + 1 + 4)
idKey.setData(0, value: hashId.data) 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.setUInt8(hashId.data.count + 8, value: reference.messageNamespace)
idKey.setInt32(hashId.data.count + 8 + 1, value: reference.messageId) idKey.setInt32(hashId.data.count + 8 + 1, value: reference.messageId)
var alreadyStored = false
if !self.valueBox.exists(self.idToReferenceTable, key: idKey) {
self.valueBox.setOrIgnore(self.idToReferenceTable, key: idKey, value: MemoryBuffer()) 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() self.valueBox.commit()
} }
func remove(ids: [Data]) {
self.valueBox.begin()
let mainKey = ValueBoxKey(length: 16)
let peerIdIdKey = ValueBoxKey(length: 8 + 16)
let peerIdKey = ValueBoxKey(length: 8)
for id in ids {
let hashId = md5Hash(id)
mainKey.setData(0, value: hashId.data)
self.valueBox.remove(self.hashIdToIdTable, key: mainKey, secure: false)
var referenceKeys: [ValueBoxKey] = []
self.valueBox.range(self.idToReferenceTable, start: mainKey, end: mainKey.successor, keys: { key in
referenceKeys.append(key)
return true
}, limit: 0)
var peerIds = Set<Int64>()
for key in referenceKeys {
peerIds.insert(key.getInt64(16))
self.valueBox.remove(self.idToReferenceTable, key: key, secure: false)
}
for peerId in peerIds {
peerIdIdKey.setInt64(0, value: peerId)
peerIdIdKey.setData(8, value: hashId.data)
if self.valueBox.exists(self.peerIdToIdTable, key: peerIdIdKey) {
self.valueBox.remove(self.peerIdToIdTable, key: peerIdIdKey, secure: false)
peerIdKey.setInt64(0, value: peerId)
if let value = self.valueBox.get(self.peerIdTable, key: peerIdKey) {
var peerIdCount: Int32 = 0
if value.length == 4 {
memcpy(&peerIdCount, value.memory, 4)
} else {
assertionFailure()
}
peerIdCount -= 1
if peerIdCount > 0 {
self.valueBox.set(self.peerIdTable, key: peerIdKey, value: MemoryBuffer(memory: &peerIdCount, capacity: 4, length: 4, freeWhenDone: false))
} else {
self.valueBox.remove(self.peerIdTable, key: peerIdKey, secure: false)
}
}
}
}
}
self.valueBox.commit()
}
func allPeerIds() -> [PeerId] {
var result: [PeerId] = []
self.valueBox.begin()
self.valueBox.scan(self.peerIdTable, keys: { key in
result.append(PeerId(key.getInt64(0)))
return true
})
self.valueBox.commit()
return result
}
func all(peerId: PeerId) -> [Data] {
self.valueBox.begin()
var hashIds: [Data] = []
let peerIdIdKey = ValueBoxKey(length: 8)
peerIdIdKey.setInt64(0, value: peerId.toInt64())
self.valueBox.range(self.peerIdToIdTable, start: peerIdIdKey, end: peerIdIdKey.successor, keys: { key in
hashIds.append(key.getData(8, length: 16))
return true
}, limit: 0)
var result: [Data] = []
let mainKey = ValueBoxKey(length: 16)
for hashId in hashIds {
mainKey.setData(0, value: hashId)
if let value = self.valueBox.get(self.hashIdToIdTable, key: mainKey) {
result.append(value.makeData())
}
}
self.valueBox.commit()
return result
}
func all() -> [Entry] { func all() -> [Entry] {
var result: [Entry] = [] var result: [Entry] = []
@ -111,7 +250,7 @@ public final class StorageBox {
self.valueBox.scan(self.idToReferenceTable, keys: { key in self.valueBox.scan(self.idToReferenceTable, keys: { key in
let id = key.getData(0, length: 16) 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 messageNamespace: UInt8 = key.getUInt8(16 + 8)
let messageId = key.getInt32(16 + 8 + 1) let messageId = key.getInt32(16 + 8 + 1)
@ -148,7 +287,7 @@ public final class StorageBox {
idKey.setData(0, value: hashId.data) idKey.setData(0, value: hashId.data)
var currentReferences: [Reference] = [] var currentReferences: [Reference] = []
self.valueBox.range(self.idToReferenceTable, start: idKey, end: idKey.successor, keys: { key in 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 messageNamespace: UInt8 = key.getUInt8(16 + 8)
let messageId = key.getInt32(16 + 8 + 1) 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> { public func all() -> Signal<[Entry], NoError> {
return self.impl.signalWith { impl, subscriber in return self.impl.signalWith { impl, subscriber in
subscriber.putNext(impl.all()) 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> { public func get(ids: [Data]) -> Signal<[Entry], NoError> {
return self.impl.signalWith { impl, subscriber in return self.impl.signalWith { impl, subscriber in
subscriber.putNext(impl.get(ids: ids)) subscriber.putNext(impl.get(ids: ids))

View File

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

View File

@ -17,6 +17,8 @@ import DeleteChatPeerActionSheetItem
import UndoUI import UndoUI
import AnimatedStickerNode import AnimatedStickerNode
import TelegramAnimatedStickerNode import TelegramAnimatedStickerNode
import ContextUI
import AnimatedAvatarSetNode
private func totalDiskSpace() -> Int64 { private func totalDiskSpace() -> Int64 {
do { do {
@ -44,8 +46,9 @@ private final class StorageUsageControllerArguments {
let openPeerMedia: (PeerId) -> Void let openPeerMedia: (PeerId) -> Void
let clearPeerMedia: (PeerId) -> Void let clearPeerMedia: (PeerId) -> Void
let setPeerIdWithRevealedOptions: (PeerId?, 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.context = context
self.updateKeepMediaTimeout = updateKeepMediaTimeout self.updateKeepMediaTimeout = updateKeepMediaTimeout
self.updateMaximumCacheSize = updateMaximumCacheSize self.updateMaximumCacheSize = updateMaximumCacheSize
@ -53,6 +56,7 @@ private final class StorageUsageControllerArguments {
self.openPeerMedia = openPeerMedia self.openPeerMedia = openPeerMedia
self.clearPeerMedia = clearPeerMedia self.clearPeerMedia = clearPeerMedia
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
self.openCategoryMenu = openCategoryMenu
} }
} }
@ -63,8 +67,27 @@ private enum StorageUsageSection: Int32 {
case peers 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 { private enum StorageUsageEntry: ItemListNodeEntry {
case keepMediaHeader(PresentationTheme, String) 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 keepMedia(PresentationTheme, PresentationStrings, Int32)
case keepMediaInfo(PresentationTheme, String) case keepMediaInfo(PresentationTheme, String)
@ -82,7 +105,7 @@ private enum StorageUsageEntry: ItemListNodeEntry {
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
case .keepMediaHeader, .keepMedia, .keepMediaInfo: case .keepMediaHeader, .keepMedia, .keepMediaInfo, .keepMediaPrivateChats, .keepMediaGroups, .keepMediaChannels:
return StorageUsageSection.keepMedia.rawValue return StorageUsageSection.keepMedia.rawValue
case .maximumSizeHeader, .maximumSize, .maximumSizeInfo: case .maximumSizeHeader, .maximumSize, .maximumSizeInfo:
return StorageUsageSection.maximumSize.rawValue return StorageUsageSection.maximumSize.rawValue
@ -99,26 +122,32 @@ private enum StorageUsageEntry: ItemListNodeEntry {
return 0 return 0
case .keepMedia: case .keepMedia:
return 1 return 1
case .keepMediaInfo: case .keepMediaPrivateChats:
return 2 return 2
case .maximumSizeHeader: case .keepMediaGroups:
return 3 return 3
case .maximumSize: case .keepMediaChannels:
return 4 return 4
case .maximumSizeInfo: case .keepMediaInfo:
return 5 return 5
case .storageHeader: case .maximumSizeHeader:
return 6 return 6
case .storageUsage: case .maximumSize:
return 7 return 7
case .collecting: case .maximumSizeInfo:
return 8 return 8
case .clearAll: case .storageHeader:
return 9 return 9
case .peersHeader: case .storageUsage:
return 10 return 10
case .collecting:
return 11
case .clearAll:
return 12
case .peersHeader:
return 13
case let .peer(index, _, _, _, _, _, _, _, _): case let .peer(index, _, _, _, _, _, _, _, _):
return 11 + index return 14 + index
} }
} }
@ -142,6 +171,24 @@ private enum StorageUsageEntry: ItemListNodeEntry {
} else { } else {
return false 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): case let .maximumSizeHeader(lhsTheme, lhsText):
if case let .maximumSizeHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { if case let .maximumSizeHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true return true
@ -235,6 +282,18 @@ private enum StorageUsageEntry: ItemListNodeEntry {
switch self { switch self {
case let .keepMediaHeader(_, text): case let .keepMediaHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) 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): case let .keepMedia(theme, strings, value):
return KeepMediaDurationPickerItem(theme: theme, strings: strings, value: value, sectionId: self.section, updated: { updatedValue in return KeepMediaDurationPickerItem(theme: theme, strings: strings, value: value, sectionId: self.section, updated: { updatedValue in
arguments.updateKeepMediaTimeout(updatedValue) arguments.updateKeepMediaTimeout(updatedValue)
@ -279,18 +338,46 @@ private enum StorageUsageEntry: ItemListNodeEntry {
} }
private struct StorageUsageState: Equatable { private struct StorageUsageState: Equatable {
let peerIdWithRevealedOptions: PeerId? var peerIdWithRevealedOptions: PeerId?
func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> StorageUsageState {
return StorageUsageState(peerIdWithRevealedOptions: peerIdWithRevealedOptions)
}
} }
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] = [] var entries: [StorageUsageEntry] = []
entries.append(.keepMediaHeader(presentationData.theme, presentationData.strings.Cache_KeepMedia.uppercased())) 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(.keepMediaInfo(presentationData.theme, presentationData.strings.Cache_KeepMediaHelp))
entries.append(.maximumSizeHeader(presentationData.theme, presentationData.strings.Cache_MaximumCacheSize.uppercased())) entries.append(.maximumSizeHeader(presentationData.theme, presentationData.strings.Cache_MaximumCacheSize.uppercased()))
@ -420,7 +507,24 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P
return cacheSettings return cacheSettings
}) })
let accountSpecificCacheSettingsPromise = Promise<AccountSpecificCacheStorageSettings>()
let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings]))
accountSpecificCacheSettingsPromise.set(context.account.postbox.combinedView(keys: [viewKey])
|> map { views -> AccountSpecificCacheStorageSettings in
let cacheSettings: AccountSpecificCacheStorageSettings
if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) {
cacheSettings = value
} else {
cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings
}
return cacheSettings
})
var presentControllerImpl: ((ViewController, PresentationContextType, Any?) -> Void)? var presentControllerImpl: ((ViewController, PresentationContextType, Any?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var findAutoremoveReferenceNode: ((StorageUsageEntryTag) -> ItemListDisclosureItemNode?)?
var presentInGlobalOverlay: ((ViewController) -> Void)?
var statsPromise: Promise<CacheUsageStatsResult?> var statsPromise: Promise<CacheUsageStatsResult?>
if let cacheUsagePromise = cacheUsagePromise { if let cacheUsagePromise = cacheUsagePromise {
@ -441,11 +545,15 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P
let arguments = StorageUsageControllerArguments(context: context, updateKeepMediaTimeout: { value in let arguments = StorageUsageControllerArguments(context: context, updateKeepMediaTimeout: { value in
let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
return current.withUpdatedDefaultCacheStorageTimeout(value) var current = current
current.defaultCacheStorageTimeout = value
return current
}).start() }).start()
}, updateMaximumCacheSize: { value in }, updateMaximumCacheSize: { value in
let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
return current.withUpdatedDefaultCacheStorageLimitGigabytes(value) var current = current
current.defaultCacheStorageLimitGigabytes = value
return current
}).start() }).start()
}, openClearAll: { }, openClearAll: {
let _ = (statsPromise.get() let _ = (statsPromise.get()
@ -957,28 +1065,197 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P
}) })
updateState { state in updateState { state in
return state.withUpdatedPeerIdWithRevealedOptions(nil) var state = state
state.peerIdWithRevealedOptions = nil
return state
} }
}, setPeerIdWithRevealedOptions: { peerId, fromPeerId in }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
updateState { state in updateState { state in
if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) { if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) {
return state.withUpdatedPeerIdWithRevealedOptions(peerId) var state = state
state.peerIdWithRevealedOptions = peerId
return state
} else { } else {
return state return state
} }
} }
}, openCategoryMenu: { category in
let mappedCategory: CacheStorageSettings.PeerStorageCategory
switch category {
case .privateChats:
mappedCategory = .privateChats
case .groups:
mappedCategory = .groups
case .channels:
mappedCategory = .channels
}
let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings]))
let accountSpecificSettings: Signal<AccountSpecificCacheStorageSettings, NoError> = context.account.postbox.combinedView(keys: [viewKey])
|> map { views -> AccountSpecificCacheStorageSettings in
let cacheSettings: AccountSpecificCacheStorageSettings
if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) {
cacheSettings = value
} else {
cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings
}
return cacheSettings
}
|> distinctUntilChanged
let peerExceptions: Signal<[(peer: FoundPeer, value: Int32)], NoError> = accountSpecificSettings
|> mapToSignal { accountSpecificSettings -> Signal<[(peer: FoundPeer, value: Int32)], NoError> in
return context.account.postbox.transaction { transaction -> [(peer: FoundPeer, value: Int32)] in
var result: [(peer: FoundPeer, value: Int32)] = []
for (peerId, value) in accountSpecificSettings.peerStorageTimeoutExceptions {
guard let peer = transaction.getPeer(peerId) else {
continue
}
let peerCategory: CacheStorageSettings.PeerStorageCategory
var subscriberCount: Int32?
if peer is TelegramUser {
peerCategory = .privateChats
} else if peer is TelegramGroup {
peerCategory = .groups
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData {
subscriberCount = (cachedData.participants?.participants.count).flatMap(Int32.init)
}
} else if let channel = peer as? TelegramChannel {
if case .group = channel.info {
peerCategory = .groups
} else {
peerCategory = .channels
}
if peerCategory == mappedCategory {
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
subscriberCount = cachedData.participantsSummary.memberCount
}
}
} else {
continue
}
if peerCategory != mappedCategory {
continue
}
result.append((peer: FoundPeer(peer: peer, subscribers: subscriberCount), value: value))
}
return result.sorted(by: { lhs, rhs in
if lhs.value != rhs.value {
return lhs.value < rhs.value
}
return lhs.peer.peer.debugDisplayTitle < rhs.peer.peer.debugDisplayTitle
})
}
}
let _ = (combineLatest(
cacheSettingsPromise.get() |> take(1),
peerExceptions |> take(1)
)
|> deliverOnMainQueue).start(next: { cacheSettings, peerExceptions in
let currentValue: Int32 = cacheSettings.categoryStorageTimeout[mappedCategory] ?? Int32.max
let applyValue: (Int32) -> Void = { value in
let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { cacheSettings in
var cacheSettings = cacheSettings
cacheSettings.categoryStorageTimeout[mappedCategory] = value
return cacheSettings
}).start()
}
var subItems: [ContextMenuItem] = []
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var presetValues: [Int32] = [
Int32.max,
31 * 24 * 60 * 60,
7 * 24 * 60 * 60,
1 * 24 * 60 * 60
]
if currentValue != 0 && !presetValues.contains(currentValue) {
presetValues.append(currentValue)
presetValues.sort(by: >)
}
for value in presetValues {
let optionText: String
if value == Int32.max {
optionText = presentationData.strings.ClearCache_Forever
} else {
optionText = timeIntervalString(strings: presentationData.strings, value: value)
}
subItems.append(.action(ContextMenuActionItem(text: optionText, icon: { theme in
if currentValue == value {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
} else {
return nil
}
}, action: { _, f in
applyValue(value)
f(.default)
})))
}
subItems.append(.separator)
if peerExceptions.isEmpty {
let exceptionsText = presentationData.strings.GroupInfo_Permissions_AddException
subItems.append(.action(ContextMenuActionItem(text: exceptionsText, icon: { theme in
if case .privateChats = category {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor)
} else {
return generateTintedImage(image: UIImage(bundleImageName: "Location/CreateGroupIcon"), color: theme.contextMenu.primaryColor)
}
}, action: { _, f in
f(.default)
pushControllerImpl?(storageUsageExceptionsScreen(context: context, category: mappedCategory))
})))
} else {
subItems.append(.custom(MultiplePeerAvatarsContextItem(context: context, peers: peerExceptions.prefix(3).map { EnginePeer($0.peer.peer) }, action: { c, _ in
c.dismiss(completion: {
})
pushControllerImpl?(storageUsageExceptionsScreen(context: context, category: mappedCategory))
}), false))
}
if let sourceNode = findAutoremoveReferenceNode?(category) {
let items: Signal<ContextController.Items, NoError> = .single(ContextController.Items(content: .list(subItems)))
let source: ContextContentSource = .reference(StorageUsageContextReferenceContentSource(sourceView: sourceNode.labelNode.view))
let contextController = ContextController(
account: context.account,
presentationData: presentationData,
source: source,
items: items,
gesture: nil
)
sourceNode.updateHasContextMenu(hasContextMenu: true)
contextController.dismissed = { [weak sourceNode] in
sourceNode?.updateHasContextMenu(hasContextMenu: false)
}
presentInGlobalOverlay?(contextController)
}
})
}) })
var dismissImpl: (() -> Void)? var dismissImpl: (() -> Void)?
let signal = combineLatest(context.sharedContext.presentationData, cacheSettingsPromise.get(), statsPromise.get(), statePromise.get()) |> deliverOnMainQueue let signal = combineLatest(context.sharedContext.presentationData, cacheSettingsPromise.get(), accountSpecificCacheSettingsPromise.get(), statsPromise.get(), statePromise.get()) |> deliverOnMainQueue
|> map { presentationData, cacheSettings, cacheStats, state -> (ItemListControllerState, (ItemListNodeState, Any)) in |> 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: { let leftNavigationButton = isModal ? ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?() dismissImpl?()
}) : nil }) : 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 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)) return (controllerState, (listState, arguments))
} |> afterDisposed { } |> afterDisposed {
@ -993,6 +1270,34 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P
presentControllerImpl = { [weak controller] c, contextType, a in presentControllerImpl = { [weak controller] c, contextType, a in
controller?.present(c, in: contextType, with: a) 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 dismissImpl = { [weak controller] in
controller?.dismiss() controller?.dismiss()
} }
@ -1110,3 +1415,215 @@ private class StorageUsageClearProgressOverlayNode: ASDisplayNode, ActionSheetGr
self.animationNode.updateLayout(size: imageSize) self.animationNode.updateLayout(size: imageSize)
} }
} }
private final class StorageUsageContextReferenceContentSource: ContextReferenceContentSource {
private let sourceView: UIView
init(sourceView: UIView) {
self.sourceView = sourceView
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, insets: UIEdgeInsets(top: -4.0, left: 0.0, bottom: -4.0, right: 0.0))
}
}
final class MultiplePeerAvatarsContextItem: ContextMenuCustomItem {
fileprivate let context: AccountContext
fileprivate let peers: [EnginePeer]
fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void
init(context: AccountContext, peers: [EnginePeer], action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void) {
self.context = context
self.peers = peers
self.action = action
}
func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
return MultiplePeerAvatarsContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected)
}
}
private final class MultiplePeerAvatarsContextItemNode: ASDisplayNode, ContextMenuCustomNode, ContextActionNodeProtocol {
private let item: MultiplePeerAvatarsContextItem
private var presentationData: PresentationData
private let getController: () -> ContextControllerProtocol?
private let actionSelected: (ContextMenuActionResult) -> Void
private let backgroundNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let textNode: ImmediateTextNode
private let avatarsNode: AnimatedAvatarSetNode
private let avatarsContext: AnimatedAvatarSetContext
private let buttonNode: HighlightTrackingButtonNode
private var pointerInteraction: PointerInteraction?
init(presentationData: PresentationData, item: MultiplePeerAvatarsContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
self.item = item
self.presentationData = presentationData
self.getController = getController
self.actionSelected = actionSelected
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isAccessibilityElement = false
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isAccessibilityElement = false
self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
self.highlightedBackgroundNode.alpha = 0.0
self.textNode = ImmediateTextNode()
self.textNode.isAccessibilityElement = false
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.textNode.attributedText = NSAttributedString(string: " ", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor)
self.textNode.maximumNumberOfLines = 1
self.buttonNode = HighlightTrackingButtonNode()
self.buttonNode.isAccessibilityElement = true
self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording
self.avatarsNode = AnimatedAvatarSetNode()
self.avatarsContext = AnimatedAvatarSetContext()
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.highlightedBackgroundNode)
self.addSubnode(self.textNode)
self.addSubnode(self.avatarsNode)
self.addSubnode(self.buttonNode)
self.buttonNode.highligthedChanged = { [weak self] highligted in
guard let strongSelf = self else {
return
}
if highligted {
strongSelf.highlightedBackgroundNode.alpha = 1.0
} else {
strongSelf.highlightedBackgroundNode.alpha = 0.0
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
}
}
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.buttonNode.isUserInteractionEnabled = true
}
deinit {
}
override func didLoad() {
super.didLoad()
self.pointerInteraction = PointerInteraction(node: self.buttonNode, style: .hover, willEnter: { [weak self] in
if let strongSelf = self {
strongSelf.highlightedBackgroundNode.alpha = 0.75
}
}, willExit: { [weak self] in
if let strongSelf = self {
strongSelf.highlightedBackgroundNode.alpha = 0.0
}
})
}
private var validLayout: (calculatedWidth: CGFloat, size: CGSize)?
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
let sideInset: CGFloat = 14.0
let verticalInset: CGFloat = 12.0
let rightTextInset: CGFloat = sideInset + 36.0
let calculatedWidth = min(constrainedWidth, 250.0)
let textFont = Font.regular(self.presentationData.listsFontSize.baseDisplaySize)
let text: String = self.presentationData.strings.CacheEvictionMenu_CategoryExceptions(Int32(self.item.peers.count))
self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor)
let textSize = self.textNode.updateLayout(CGSize(width: calculatedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude))
let combinedTextHeight = textSize.height
return (CGSize(width: calculatedWidth, height: verticalInset * 2.0 + combinedTextHeight), { size, transition in
self.validLayout = (calculatedWidth: calculatedWidth, size: size)
let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0)
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize)
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
let avatarsContent: AnimatedAvatarSetContext.Content
let avatarsPeers: [EnginePeer] = self.item.peers
avatarsContent = self.avatarsContext.update(peers: avatarsPeers, animated: false)
let avatarsSize = self.avatarsNode.update(context: self.item.context, content: avatarsContent, itemSize: CGSize(width: 24.0, height: 24.0), customSpacing: 10.0, animated: false, synchronousLoad: true)
self.avatarsNode.frame = CGRect(origin: CGPoint(x: size.width - sideInset - 12.0 - avatarsSize.width, y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize)
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
})
}
func updateTheme(presentationData: PresentationData) {
self.presentationData = presentationData
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor)
}
@objc private func buttonPressed() {
self.performAction()
}
private var actionTemporarilyDisabled: Bool = false
func canBeHighlighted() -> Bool {
return self.isActionEnabled
}
func updateIsHighlighted(isHighlighted: Bool) {
self.setIsHighlighted(isHighlighted)
}
func performAction() {
if self.actionTemporarilyDisabled {
return
}
self.actionTemporarilyDisabled = true
Queue.mainQueue().async { [weak self] in
self?.actionTemporarilyDisabled = false
}
guard let controller = self.getController() else {
return
}
self.item.action(controller, { [weak self] result in
self?.actionSelected(result)
})
}
var isActionEnabled: Bool {
return true
}
func setIsHighlighted(_ value: Bool) {
if value {
self.highlightedBackgroundNode.alpha = 1.0
} else {
self.highlightedBackgroundNode.alpha = 0.0
}
}
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol {
return self
}
}

View File

@ -0,0 +1,486 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import TelegramStringFormatting
import ItemListUI
import PresentationDataUtils
import OverlayStatusController
import AccountContext
import ItemListPeerItem
import UndoUI
import ContextUI
import ItemListPeerActionItem
private enum StorageUsageExceptionsEntryTag: Hashable, ItemListItemTag {
case peer(EnginePeer.Id)
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? StorageUsageExceptionsEntryTag, self == other {
return true
} else {
return false
}
}
}
private final class StorageUsageExceptionsScreenArguments {
let context: AccountContext
let openAddException: () -> Void
let openPeerMenu: (EnginePeer.Id, Int32) -> Void
init(
context: AccountContext,
openAddException: @escaping () -> Void,
openPeerMenu: @escaping (EnginePeer.Id, Int32) -> Void
) {
self.context = context
self.openAddException = openAddException
self.openPeerMenu = openPeerMenu
}
}
private enum StorageUsageExceptionsSection: Int32 {
case add
case items
}
private enum StorageUsageExceptionsEntry: ItemListNodeEntry {
enum SortIndex: Equatable, Comparable {
case index(Int)
case peer(index: Int, peerId: EnginePeer.Id)
static func <(lhs: SortIndex, rhs: SortIndex) -> Bool {
switch lhs {
case let .index(index):
if case let .index(rhsIndex) = rhs {
return index < rhsIndex
} else {
return true
}
case let .peer(index, peerId):
if case let .peer(rhsIndex, rhsPeerId) = rhs {
if index != rhsIndex {
return index < rhsIndex
} else {
return peerId < rhsPeerId
}
} else {
return false
}
}
}
}
enum StableId: Hashable {
case index(Int)
case peer(EnginePeer.Id)
}
case addException(String)
case exceptionsHeader(String)
case peer(index: Int, peer: FoundPeer, value: Int32)
var section: ItemListSectionId {
switch self {
case .addException:
return StorageUsageExceptionsSection.add.rawValue
case .exceptionsHeader, .peer:
return StorageUsageExceptionsSection.items.rawValue
}
}
var stableId: StableId {
switch self {
case .addException:
return .index(0)
case .exceptionsHeader:
return .index(1)
case let .peer(_, peer, _):
return .peer(peer.peer.id)
}
}
var sortIndex: SortIndex {
switch self {
case .addException:
return .index(0)
case .exceptionsHeader:
return .index(1)
case let .peer(index, peer, _):
return .peer(index: index, peerId: peer.peer.id)
}
}
static func ==(lhs: StorageUsageExceptionsEntry, rhs: StorageUsageExceptionsEntry) -> Bool {
switch lhs {
case let .addException(text):
if case .addException(text) = rhs {
return true
} else {
return false
}
case let .exceptionsHeader(text):
if case .exceptionsHeader(text) = rhs {
return true
} else {
return false
}
case let .peer(index, peer, value):
if case .peer(index, peer, value) = rhs {
return true
} else {
return false
}
}
}
static func <(lhs: StorageUsageExceptionsEntry, rhs: StorageUsageExceptionsEntry) -> Bool {
return lhs.sortIndex < rhs.sortIndex
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! StorageUsageExceptionsScreenArguments
switch self {
case let .addException(text):
let icon: UIImage? = PresentationResourcesItemList.createGroupIcon(presentationData.theme)
return ItemListPeerActionItem(presentationData: presentationData, icon: icon, title: text, alwaysPlain: false, sectionId: self.section, editing: false, action: {
arguments.openAddException()
})
case let .exceptionsHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .peer(_, peer, value):
var additionalDetailLabel: String?
if let subscribers = peer.subscribers {
additionalDetailLabel = presentationData.strings.VoiceChat_Panel_Members(subscribers)
}
let optionText: String
if value == Int32.max {
optionText = presentationData.strings.ClearCache_Forever
} else {
optionText = timeIntervalString(strings: presentationData.strings, value: value)
}
return ItemListDisclosureItem(presentationData: presentationData, icon: nil, context: arguments.context, iconPeer: EnginePeer(peer.peer), title: EnginePeer(peer.peer).displayTitle(strings: presentationData.strings, displayOrder: .firstLast), enabled: true, titleFont: .bold, label: optionText, labelStyle: .text, additionalDetailLabel: additionalDetailLabel, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: {
arguments.openPeerMenu(peer.peer.id, value)
}, tag: StorageUsageExceptionsEntryTag.peer(peer.peer.id))
}
}
}
private struct StorageUsageExceptionsState: Equatable {
}
private func storageUsageExceptionsScreenEntries(
presentationData: PresentationData,
peerExceptions: [(peer: FoundPeer, value: Int32)],
state: StorageUsageExceptionsState
) -> [StorageUsageExceptionsEntry] {
var entries: [StorageUsageExceptionsEntry] = []
entries.append(.addException(presentationData.strings.Notification_Exceptions_AddException))
if !peerExceptions.isEmpty {
entries.append(.exceptionsHeader(presentationData.strings.Notifications_CategoryExceptions(Int32(peerExceptions.count)).uppercased()))
var index = 100
for item in peerExceptions {
entries.append(.peer(index: index, peer: item.peer, value: item.value))
index += 1
}
}
return entries
}
public func storageUsageExceptionsScreen(
context: AccountContext,
category: CacheStorageSettings.PeerStorageCategory,
isModal: Bool = false
) -> ViewController {
let statePromise = ValuePromise(StorageUsageExceptionsState())
let stateValue = Atomic(value: StorageUsageExceptionsState())
let updateState: ((StorageUsageExceptionsState) -> StorageUsageExceptionsState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
let _ = updateState
let cacheSettingsPromise = Promise<CacheStorageSettings>()
cacheSettingsPromise.set(context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings])
|> map { sharedData -> CacheStorageSettings in
let cacheSettings: CacheStorageSettings
if let value = sharedData.entries[SharedDataKeys.cacheStorageSettings]?.get(CacheStorageSettings.self) {
cacheSettings = value
} else {
cacheSettings = CacheStorageSettings.defaultSettings
}
return cacheSettings
})
let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings]))
let accountSpecificSettings: Signal<AccountSpecificCacheStorageSettings, NoError> = context.account.postbox.combinedView(keys: [viewKey])
|> map { views -> AccountSpecificCacheStorageSettings in
let cacheSettings: AccountSpecificCacheStorageSettings
if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) {
cacheSettings = value
} else {
cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings
}
return cacheSettings
}
|> distinctUntilChanged
let peerExceptions: Signal<[(peer: FoundPeer, value: Int32)], NoError> = accountSpecificSettings
|> mapToSignal { accountSpecificSettings -> Signal<[(peer: FoundPeer, value: Int32)], NoError> in
return context.account.postbox.transaction { transaction -> [(peer: FoundPeer, value: Int32)] in
var result: [(peer: FoundPeer, value: Int32)] = []
for (peerId, value) in accountSpecificSettings.peerStorageTimeoutExceptions {
guard let peer = transaction.getPeer(peerId) else {
continue
}
let peerCategory: CacheStorageSettings.PeerStorageCategory
var subscriberCount: Int32?
if peer is TelegramUser {
peerCategory = .privateChats
} else if peer is TelegramGroup {
peerCategory = .groups
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData {
subscriberCount = (cachedData.participants?.participants.count).flatMap(Int32.init)
}
} else if let channel = peer as? TelegramChannel {
if case .group = channel.info {
peerCategory = .groups
} else {
peerCategory = .channels
}
if peerCategory == category {
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
subscriberCount = cachedData.participantsSummary.memberCount
}
}
} else {
continue
}
if peerCategory != category {
continue
}
result.append((peer: FoundPeer(peer: peer, subscribers: subscriberCount), value: value))
}
return result.sorted(by: { lhs, rhs in
if lhs.value != rhs.value {
return lhs.value < rhs.value
}
return lhs.peer.peer.debugDisplayTitle < rhs.peer.peer.debugDisplayTitle
})
}
}
var presentControllerImpl: ((ViewController, PresentationContextType, Any?) -> Void)?
let _ = presentControllerImpl
var pushControllerImpl: ((ViewController) -> Void)?
var findPeerReferenceNode: ((EnginePeer.Id) -> ItemListDisclosureItemNode?)?
let _ = findPeerReferenceNode
var presentInGlobalOverlay: ((ViewController) -> Void)?
let _ = presentInGlobalOverlay
let actionDisposables = DisposableSet()
let clearDisposable = MetaDisposable()
actionDisposables.add(clearDisposable)
let arguments = StorageUsageExceptionsScreenArguments(
context: context,
openAddException: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var filter: ChatListNodePeersFilter = [.excludeRecent, .doNotSearchMessages, .removeSearchHeader]
switch category {
case .groups:
filter.insert(.onlyGroups)
case .privateChats:
filter.insert(.onlyPrivateChats)
filter.insert(.excludeSavedMessages)
filter.insert(.excludeSecretChats)
case .channels:
filter.insert(.onlyChannels)
}
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: filter, hasContactSelector: false, title: presentationData.strings.Notifications_AddExceptionTitle))
controller.peerSelected = { [weak controller] peer, _ in
let peerId = peer.id
let _ = updateAccountSpecificCacheStorageSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
settings.peerStorageTimeoutExceptions[peerId] = Int32.max
return settings
}).start()
controller?.dismiss()
}
pushControllerImpl?(controller)
},
openPeerMenu: { peerId, currentValue in
let applyValue: (Int32?) -> Void = { value in
let _ = updateAccountSpecificCacheStorageSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
if let value = value {
settings.peerStorageTimeoutExceptions[peerId] = value
} else {
settings.peerStorageTimeoutExceptions.removeValue(forKey: peerId)
}
return settings
}).start()
}
var subItems: [ContextMenuItem] = []
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let presetValues: [Int32] = [
Int32.max,
31 * 24 * 60 * 60,
7 * 24 * 60 * 60,
1 * 24 * 60 * 60
]
for value in presetValues {
let optionText: String
if value == Int32.max {
optionText = presentationData.strings.ClearCache_Forever
} else {
optionText = timeIntervalString(strings: presentationData.strings, value: value)
}
subItems.append(.action(ContextMenuActionItem(text: optionText, icon: { theme in
if currentValue == value {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
} else {
return nil
}
}, action: { _, f in
applyValue(value)
f(.default)
})))
}
subItems.append(.separator)
//TODO:localize
subItems.append(.action(ContextMenuActionItem(text: presentationData.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { _, f in
f(.default)
applyValue(nil)
})))
if let sourceNode = findPeerReferenceNode?(peerId) {
let items: Signal<ContextController.Items, NoError> = .single(ContextController.Items(content: .list(subItems)))
let source: ContextContentSource = .reference(StorageUsageExceptionsContextReferenceContentSource(sourceView: sourceNode.labelNode.view))
let contextController = ContextController(
account: context.account,
presentationData: presentationData,
source: source,
items: items,
gesture: nil
)
sourceNode.updateHasContextMenu(hasContextMenu: true)
contextController.dismissed = { [weak sourceNode] in
sourceNode?.updateHasContextMenu(hasContextMenu: false)
}
presentInGlobalOverlay?(contextController)
}
}
)
let _ = cacheSettingsPromise
var dismissImpl: (() -> Void)?
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
peerExceptions,
statePromise.get()
)
|> deliverOnMainQueue
|> map { presentationData, peerExceptions, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = isModal ? ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
}) : nil
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Notifications_ExceptionsTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: storageUsageExceptionsScreenEntries(presentationData: presentationData, peerExceptions: peerExceptions, state: state), style: .blocks, emptyStateItem: nil, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionDisposables.dispose()
}
let controller = ItemListController(context: context, state: signal)
if isModal {
controller.navigationPresentation = .modal
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
}
presentControllerImpl = { [weak controller] c, contextType, a in
controller?.present(c, in: contextType, with: a)
}
pushControllerImpl = { [weak controller] c in
controller?.push(c)
}
presentInGlobalOverlay = { [weak controller] c in
controller?.presentInGlobalOverlay(c, with: nil)
}
findPeerReferenceNode = { [weak controller] peerId in
guard let controller else {
return nil
}
let targetTag: StorageUsageExceptionsEntryTag = .peer(peerId)
var resultItemNode: ItemListItemNode?
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode {
if let tag = itemNode.tag, tag.isEqual(to: targetTag) {
resultItemNode = itemNode
return
}
}
}
if let resultItemNode = resultItemNode as? ItemListDisclosureItemNode {
return resultItemNode
} else {
return nil
}
}
dismissImpl = { [weak controller] in
controller?.dismiss()
}
return controller
}
private final class StorageUsageExceptionsContextReferenceContentSource: ContextReferenceContentSource {
private let sourceView: UIView
init(sourceView: UIView) {
self.sourceView = sourceView
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, insets: UIEdgeInsets(top: -4.0, left: 0.0, bottom: -4.0, right: 0.0))
}
}

View File

@ -896,6 +896,7 @@ public class Account {
private let managedOperationsDisposable = DisposableSet() private let managedOperationsDisposable = DisposableSet()
private let managedTopReactionsDisposable = MetaDisposable() private let managedTopReactionsDisposable = MetaDisposable()
private var storageSettingsDisposable: Disposable? private var storageSettingsDisposable: Disposable?
private var automaticCacheEvictionContext: AutomaticCacheEvictionContext?
public let importableContacts = Promise<[DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData]>() public let importableContacts = Promise<[DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData]>()
@ -1190,13 +1191,15 @@ public class Account {
if !supplementary { if !supplementary {
let mediaBox = postbox.mediaBox 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 { guard let mediaBox = mediaBox else {
return return
} }
let settings: CacheStorageSettings = sharedData.entries[SharedDataKeys.cacheStorageSettings]?.get(CacheStorageSettings.self) ?? CacheStorageSettings.defaultSettings 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: 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 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()) strongSelf.managedTopReactionsDisposable.set(managedTopReactions(postbox: strongSelf.postbox, network: strongSelf.network).start())
} }
self.automaticCacheEvictionContext = AutomaticCacheEvictionContext(postbox: postbox, accountManager: accountManager)
/*#if DEBUG /*#if DEBUG
self.managedOperationsDisposable.add(debugFetchAllStickers(account: self).start(completed: { self.managedOperationsDisposable.add(debugFetchAllStickers(account: self).start(completed: {
print("debugFetchAllStickers done") print("debugFetchAllStickers done")

View File

@ -2,7 +2,6 @@ import Foundation
import Postbox import Postbox
import SwiftSignalKit import SwiftSignalKit
public func updateCacheStorageSettingsInteractively(accountManager: AccountManager<TelegramAccountManagerTypes>, _ f: @escaping (CacheStorageSettings) -> CacheStorageSettings) -> Signal<Void, NoError> { public func updateCacheStorageSettingsInteractively(accountManager: AccountManager<TelegramAccountManagerTypes>, _ f: @escaping (CacheStorageSettings) -> CacheStorageSettings) -> Signal<Void, NoError> {
return accountManager.transaction { transaction -> Void in return accountManager.transaction { transaction -> Void in
transaction.updateSharedData(SharedDataKeys.cacheStorageSettings, { entry in transaction.updateSharedData(SharedDataKeys.cacheStorageSettings, { entry in
@ -16,3 +15,17 @@ public func updateCacheStorageSettingsInteractively(accountManager: AccountManag
}) })
} }
} }
public func updateAccountSpecificCacheStorageSettingsInteractively(postbox: Postbox, _ f: @escaping (AccountSpecificCacheStorageSettings) -> AccountSpecificCacheStorageSettings) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
transaction.updatePreferencesEntry(key: PreferencesKeys.accountSpecificCacheStorageSettings, { entry in
let currentSettings: AccountSpecificCacheStorageSettings
if let entry = entry?.get(AccountSpecificCacheStorageSettings.self) {
currentSettings = entry
} else {
currentSettings = AccountSpecificCacheStorageSettings.defaultSettings
}
return PreferencesEntry(f(currentSettings))
})
}
}

View File

@ -3461,7 +3461,7 @@ func replayFinalState(
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
}) })
if !resourceIds.isEmpty { 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) }) deletedMessageIds.append(contentsOf: ids.map { .global($0) })
case let .DeleteMessages(ids): case let .DeleteMessages(ids):
@ -3478,7 +3478,7 @@ func replayFinalState(
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
}) })
if !resourceIds.isEmpty { 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): case let .UpdatePeerChatInclusion(peerId, groupId, changedGroup):
let currentInclusion = transaction.getPeerChatListInclusion(peerId) let currentInclusion = transaction.getPeerChatListInclusion(peerId)

View File

@ -1,16 +1,43 @@
import Foundation
import Postbox import Postbox
public struct CacheStorageSettings: Codable, Equatable { public struct CacheStorageSettings: Codable, Equatable {
public let defaultCacheStorageTimeout: Int32 public enum PeerStorageCategory: String, Codable, Hashable {
public let defaultCacheStorageLimitGigabytes: Int32 case privateChats = "privateChats"
case groups = "groups"
public static var defaultSettings: CacheStorageSettings { case channels = "channels"
return CacheStorageSettings(defaultCacheStorageTimeout: Int32.max, defaultCacheStorageLimitGigabytes: 8 * 1024 * 1024)
} }
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.defaultCacheStorageTimeout = defaultCacheStorageTimeout
self.defaultCacheStorageLimitGigabytes = defaultCacheStorageLimitGigabytes self.defaultCacheStorageLimitGigabytes = defaultCacheStorageLimitGigabytes
self.categoryStorageTimeout = categoryStorageTimeout
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
@ -25,6 +52,20 @@ public struct CacheStorageSettings: Codable, Equatable {
} else { } else {
self.defaultCacheStorageLimitGigabytes = 8 * 1024 * 1024 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 { 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.defaultCacheStorageTimeout, forKey: "dt")
try container.encode(self.defaultCacheStorageLimitGigabytes, forKey: "dl") try container.encode(self.defaultCacheStorageLimitGigabytes, forKey: "dl")
}
public func withUpdatedDefaultCacheStorageTimeout(_ defaultCacheStorageTimeout: Int32) -> CacheStorageSettings { var categoryStorageTimeoutValues: [CategoryStorageTimeoutRepresentation] = []
return CacheStorageSettings(defaultCacheStorageTimeout: defaultCacheStorageTimeout, defaultCacheStorageLimitGigabytes: self.defaultCacheStorageLimitGigabytes) 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")
} }
public func withUpdatedDefaultCacheStorageLimitGigabytes(_ defaultCacheStorageLimitGigabytes: Int32) -> CacheStorageSettings {
return CacheStorageSettings(defaultCacheStorageTimeout: self.defaultCacheStorageTimeout, defaultCacheStorageLimitGigabytes: defaultCacheStorageLimitGigabytes)
} }
} }

View File

@ -247,6 +247,7 @@ private enum PreferencesKeyValues: Int32 {
case reactionSettings = 24 case reactionSettings = 24
case premiumPromo = 26 case premiumPromo = 26
case globalMessageAutoremoveTimeoutSettings = 27 case globalMessageAutoremoveTimeoutSettings = 27
case accountSpecificCacheStorageSettings = 28
} }
public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey { public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey {
@ -381,6 +382,12 @@ public struct PreferencesKeys {
key.setInt32(0, value: PreferencesKeyValues.globalMessageAutoremoveTimeoutSettings.rawValue) key.setInt32(0, value: PreferencesKeyValues.globalMessageAutoremoveTimeoutSettings.rawValue)
return key 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 { private enum SharedDataKeyValues: Int32 {

View File

@ -34,7 +34,7 @@ public func _internal_deleteMessages(transaction: Transaction, mediaBox: MediaBo
} }
} }
if !resourceIds.isEmpty { if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start() let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
} }
for id in ids { for id in ids {
if id.peerId.namespace == Namespaces.Peer.CloudChannel && id.namespace == Namespaces.Message.Cloud { 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) addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
}) })
if !resourceIds.isEmpty { 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) addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
}) })
if !resourceIds.isEmpty { 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 return true
}) })
if !resourceIds.isEmpty { 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 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 return true
}) })
if !resourceIds.isEmpty { 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 transaction.clearHistory(peerId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: namespaces, forEachMedia: { _ in

View File

@ -108,7 +108,7 @@ func managedApplyPendingScheduledMessagesActions(postbox: Postbox, network: Netw
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
}) })
if !resourceIds.isEmpty { if !resourceIds.isEmpty {
let _ = postbox.mediaBox.removeCachedResources(Set(resourceIds)).start() let _ = postbox.mediaBox.removeCachedResources(Array(Set(resourceIds))).start()
} }
} }
|> ignoreValues |> ignoreValues

View File

@ -665,7 +665,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
}) })
if !resourceIds.isEmpty { if !resourceIds.isEmpty {
let _ = postbox.mediaBox.removeCachedResources(Set(resourceIds)).start() let _ = postbox.mediaBox.removeCachedResources(Array(Set(resourceIds))).start()
} }
} }
case .chatFull: case .chatFull:

View File

@ -81,7 +81,7 @@ func _internal_collectCacheUsageStats(account: Account, peerId: PeerId? = nil, a
totalSize += resourceSize totalSize += resourceSize
for reference in entry.references { 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 { for mediaItem in message.media {
guard let mediaId = mediaItem.id else { guard let mediaId = mediaItem.id else {
continue continue
@ -102,7 +102,7 @@ func _internal_collectCacheUsageStats(account: Account, peerId: PeerId? = nil, a
mediaSize += resourceSize mediaSize += resourceSize
processedResourceIds.insert(resourceId.stringRepresentation) 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 let index = mediaResourceIds.index(forKey: mediaId) {
if !mediaResourceIds[index].value.contains(resourceId) { if !mediaResourceIds[index].value.contains(resourceId) {
mediaResourceIds[mediaId]?.append(resourceId) mediaResourceIds[mediaId]?.append(resourceId)
@ -489,5 +489,5 @@ func _internal_collectCacheUsageStats(account: Account, peerId: PeerId? = nil, a
} }
func _internal_clearCachedMediaResources(account: Account, mediaResourceIds: Set<MediaResourceId>) -> Signal<Float, NoError> { func _internal_clearCachedMediaResources(account: Account, mediaResourceIds: Set<MediaResourceId>) -> Signal<Float, NoError> {
return account.postbox.mediaBox.removeCachedResources(mediaResourceIds) return account.postbox.mediaBox.removeCachedResources(Array(mediaResourceIds))
} }

View File

@ -0,0 +1,202 @@
import Foundation
import SwiftSignalKit
import Postbox
final class AutomaticCacheEvictionContext {
private final class Impl {
private struct CombinedSettings: Equatable {
var categoryStorageTimeout: [CacheStorageSettings.PeerStorageCategory: Int32]
var exceptions: [PeerId: Int32]
}
let queue: Queue
let processingQueue: Queue
let accountManager: AccountManager<TelegramAccountManagerTypes>
let postbox: Postbox
var settingsDisposable: Disposable?
var processDisposable: Disposable?
init(queue: Queue, accountManager: AccountManager<TelegramAccountManagerTypes>, postbox: Postbox) {
self.queue = queue
self.processingQueue = Queue(name: "AutomaticCacheEviction-Processing", qos: .background)
self.accountManager = accountManager
self.postbox = postbox
self.start()
}
deinit {
self.settingsDisposable?.dispose()
self.processDisposable?.dispose()
}
func start() {
self.settingsDisposable?.dispose()
self.processDisposable?.dispose()
let cacheSettings = self.accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings])
|> map { sharedData -> CacheStorageSettings in
let cacheSettings: CacheStorageSettings
if let value = sharedData.entries[SharedDataKeys.cacheStorageSettings]?.get(CacheStorageSettings.self) {
cacheSettings = value
} else {
cacheSettings = CacheStorageSettings.defaultSettings
}
return cacheSettings
}
let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings]))
let accountSpecificSettings = self.postbox.combinedView(keys: [viewKey])
|> map { views -> AccountSpecificCacheStorageSettings in
let cacheSettings: AccountSpecificCacheStorageSettings
if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) {
cacheSettings = value
} else {
cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings
}
return cacheSettings
}
self.settingsDisposable = (combineLatest(queue: self.queue,
cacheSettings,
accountSpecificSettings
)
|> map { cacheSettings, accountSpecificSettings -> CombinedSettings in
return CombinedSettings(
categoryStorageTimeout: cacheSettings.categoryStorageTimeout,
exceptions: accountSpecificSettings.peerStorageTimeoutExceptions
)
}
|> distinctUntilChanged
|> deliverOn(self.queue)).start(next: { [weak self] combinedSettings in
self?.restart(settings: combinedSettings)
})
}
private func restart(settings: CombinedSettings) {
self.processDisposable?.dispose()
let processingQueue = self.processingQueue
let postbox = self.postbox
let mediaBox = self.postbox.mediaBox
let _ = processingQueue
let _ = mediaBox
self.processDisposable = (self.postbox.mediaBox.storageBox.allPeerIds()
|> mapToSignal { peerIds -> Signal<Never, NoError> in
return postbox.transaction { transaction -> [PeerId: CacheStorageSettings.PeerStorageCategory] in
var channelCategoryMapping: [PeerId: CacheStorageSettings.PeerStorageCategory] = [:]
for peerId in peerIds {
if peerId.namespace == Namespaces.Peer.CloudChannel {
var category: CacheStorageSettings.PeerStorageCategory = .channels
if let peer = transaction.getPeer(peerId) as? TelegramChannel, case .group = peer.info {
category = .groups
}
channelCategoryMapping[peerId] = category
}
}
return channelCategoryMapping
}
|> mapToSignal { channelCategoryMapping -> Signal<Never, NoError> in
var signals: Signal<Never, NoError> = .complete()
var matchingPeers = 0
for peerId in peerIds {
let timeout: Int32
if let value = settings.exceptions[peerId] {
timeout = value
} else {
switch peerId.namespace {
case Namespaces.Peer.CloudUser, Namespaces.Peer.SecretChat:
timeout = settings.categoryStorageTimeout[.privateChats] ?? Int32.max
case Namespaces.Peer.CloudGroup:
timeout = settings.categoryStorageTimeout[.groups] ?? Int32.max
default:
if let category = channelCategoryMapping[peerId], case .groups = category {
timeout = settings.categoryStorageTimeout[.groups] ?? Int32.max
} else {
timeout = settings.categoryStorageTimeout[.channels] ?? Int32.max
}
}
}
if timeout == Int32.max {
continue
}
matchingPeers += 1
let minPeerTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - timeout
//let minPeerTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
signals = signals |> then(mediaBox.storageBox.all(peerId: peerId)
|> mapToSignal { peerResourceIds -> Signal<Never, NoError> in
return Signal { subscriber in
var isCancelled = false
processingQueue.justDispatch {
var removeIds: [MediaResourceId] = []
var removeRawIds: [Data] = []
var localCounter = 0
for resourceId in peerResourceIds {
localCounter += 1
if localCounter % 100 == 0 {
if isCancelled {
subscriber.putCompletion()
return
}
}
removeRawIds.append(resourceId)
let id = MediaResourceId(String(data: resourceId, encoding: .utf8)!)
let resourceTimestamp = mediaBox.resourceUsageWithInfo(id: id)
if resourceTimestamp != 0 && resourceTimestamp < minPeerTimestamp {
removeIds.append(id)
}
}
if !removeIds.isEmpty {
Logger.shared.log("AutomaticCacheEviction", "peer \(peerId): cleaning \(removeIds.count) resources")
let _ = mediaBox.removeCachedResources(removeIds).start(completed: {
mediaBox.storageBox.remove(ids: removeRawIds)
subscriber.putCompletion()
})
} else {
subscriber.putCompletion()
}
}
return ActionDisposable {
isCancelled = true
}
}
})
}
Logger.shared.log("AutomaticCacheEviction", "have \(matchingPeers) peers with data")
return signals
}
}).start()
}
}
private let queue: Queue
private let impl: QueueLocalObject<Impl>
init(postbox: Postbox, accountManager: AccountManager<TelegramAccountManagerTypes>) {
let queue = Queue(name: "AutomaticCacheEviction")
self.queue = queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, accountManager: accountManager, postbox: postbox)
})
}
}

View File

@ -15343,7 +15343,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var attemptSelectionImpl: ((Peer) -> Void)? var attemptSelectionImpl: ((Peer) -> Void)?
let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, updatedPresentationData: self.updatedPresentationData, filter: filter, attemptSelection: { peer, _ in let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, updatedPresentationData: self.updatedPresentationData, filter: filter, attemptSelection: { peer, _ in
attemptSelectionImpl?(peer) attemptSelectionImpl?(peer)
}, multipleSelection: true, forwardedMessageIds: messages.map { $0.id })) }, multipleSelection: true, forwardedMessageIds: messages.map { $0.id }, selectForumThreads: true))
let context = self.context let context = self.context
attemptSelectionImpl = { [weak self, weak controller] peer in attemptSelectionImpl = { [weak self, weak controller] peer in
guard let strongSelf = self, let controller = controller else { guard let strongSelf = self, let controller = controller else {
@ -15700,7 +15700,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
break break
case let .chat(textInputState, _, _): case let .chat(textInputState, _, _):
if let textInputState = 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 controller.peerSelected = { [weak self, weak controller] peer, threadId in
let peerId = peer.id let peerId = peer.id

View File

@ -713,7 +713,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
}, action: { _, f in }, action: { _, f in
f(.dismissWithoutContent) 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 controller.peerSelected = { [weak controller] peer, _ in
let peerId = peer.id let peerId = peer.id

View File

@ -19,11 +19,16 @@ private final class MultiScaleTextStateNode: ASDisplayNode {
} }
final class MultiScaleTextState { final class MultiScaleTextState {
let attributedText: NSAttributedString struct Attributes {
var font: UIFont
var color: UIColor
}
let attributes: Attributes
let constrainedSize: CGSize let constrainedSize: CGSize
init(attributedText: NSAttributedString, constrainedSize: CGSize) { init(attributes: Attributes, constrainedSize: CGSize) {
self.attributedText = attributedText self.attributes = attributes
self.constrainedSize = constrainedSize self.constrainedSize = constrainedSize
} }
} }
@ -49,7 +54,7 @@ final class MultiScaleTextNode: ASDisplayNode {
return self.stateNodes[key]?.textNode 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(Set(states.keys) == Set(self.stateNodes.keys))
assert(states[mainState] != nil) assert(states[mainState] != nil)
@ -57,7 +62,7 @@ final class MultiScaleTextNode: ASDisplayNode {
var mainLayout: MultiScaleTextLayout? var mainLayout: MultiScaleTextLayout?
for (key, state) in states { for (key, state) in states {
if let node = self.stateNodes[key] { 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 nodeSize = node.textNode.updateLayout(state.constrainedSize)
let nodeLayout = MultiScaleTextLayout(size: nodeSize) let nodeLayout = MultiScaleTextLayout(size: nodeSize)
if key == mainState { if key == mainState {

View File

@ -72,7 +72,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
case let .botStart(peer, payload): case let .botStart(peer, payload):
openPeer(EnginePeer(peer), .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive))) openPeer(EnginePeer(peer), .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive)))
case let .groupBotStart(botPeerId, payload, adminRights): 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 controller.peerSelected = { [weak controller] peer, _ in
let peerId = peer.id let peerId = peer.id
@ -322,7 +322,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
present(shareController, nil) present(shareController, nil)
context.sharedContext.applicationBindings.dismissNativeController() context.sharedContext.applicationBindings.dismissNativeController()
} else { } 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 controller.peerSelected = { [weak controller] peer, _ in
let peerId = peer.id let peerId = peer.id
@ -604,7 +604,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
} }
if let navigationController = navigationController { 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 controller.peerSelected = { [weak navigationController] peer, _ in
guard let navigationController else { guard let navigationController else {
return return
@ -656,7 +656,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
} }
if let navigationController = navigationController { 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 controller.peerSelected = { [weak navigationController] peer, _ in
guard let navigationController else { guard let navigationController else {
return return

View File

@ -2645,14 +2645,16 @@ final class PeerInfoHeaderNode: ASDisplayNode {
var isPremium = false var isPremium = false
var isVerified = false var isVerified = false
var isFake = false var isFake = false
let smallTitleString: NSAttributedString let titleStringText: String
let titleString: NSAttributedString let smallTitleAttributes: MultiScaleTextState.Attributes
let smallSubtitleString: NSAttributedString let titleAttributes: MultiScaleTextState.Attributes
let subtitleString: NSAttributedString let subtitleStringText: String
let smallSubtitleAttributes: MultiScaleTextState.Attributes
let subtitleAttributes: MultiScaleTextState.Attributes
var subtitleIsButton: Bool = false var subtitleIsButton: Bool = false
var panelSubtitleString: NSAttributedString? var panelSubtitleString: (text: String, attributes: MultiScaleTextState.Attributes)?
var nextPanelSubtitleString: NSAttributedString? var nextPanelSubtitleString: (text: String, attributes: MultiScaleTextState.Attributes)?
let usernameString: NSAttributedString let usernameString: (text: String, attributes: MultiScaleTextState.Attributes)
if let peer = peer { if let peer = peer {
isPremium = peer.isPremium isPremium = peer.isPremium
isVerified = peer.isVerified 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) titleStringText = title
smallTitleString = NSAttributedString(string: title, font: Font.regular(30.0), textColor: .white) 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 { if self.isSettings, let user = peer as? TelegramUser {
var subtitle = formatPhoneNumber(context: self.context, number: user.phone ?? "") var subtitle = formatPhoneNumber(context: self.context, number: user.phone ?? "")
if let mainUsername = user.addressName, !mainUsername.isEmpty { if let mainUsername = user.addressName, !mainUsername.isEmpty {
subtitle = "\(subtitle) • @\(mainUsername)" subtitle = "\(subtitle) • @\(mainUsername)"
} }
smallSubtitleString = NSAttributedString(string: subtitle, font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.7)) subtitleStringText = subtitle
subtitleString = NSAttributedString(string: subtitle, font: Font.regular(17.0), textColor: presentationData.theme.list.itemSecondaryTextColor) subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(17.0), color: presentationData.theme.list.itemSecondaryTextColor)
usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: 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 { } else if let _ = threadData {
let subtitleColor: UIColor let subtitleColor: UIColor
subtitleColor = presentationData.theme.list.itemAccentColor subtitleColor = presentationData.theme.list.itemAccentColor
@ -2699,9 +2705,11 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let statusText: String let statusText: String
statusText = peer.debugDisplayTitle statusText = peer.debugDisplayTitle
smallSubtitleString = NSAttributedString(string: statusText, font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.7)) subtitleStringText = statusText
subtitleString = NSAttributedString(string: statusText, font: Font.semibold(15.0), textColor: subtitleColor) subtitleAttributes = MultiScaleTextState.Attributes(font: Font.semibold(15.0), color: subtitleColor)
usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) 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 subtitleIsButton = true
@ -2713,10 +2721,10 @@ final class PeerInfoHeaderNode: ASDisplayNode {
} else { } else {
subtitleColor = presentationData.theme.list.itemSecondaryTextColor 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 { 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 { } else if let statusData = statusData {
let subtitleColor: UIColor let subtitleColor: UIColor
@ -2725,9 +2733,12 @@ final class PeerInfoHeaderNode: ASDisplayNode {
} else { } else {
subtitleColor = presentationData.theme.list.itemSecondaryTextColor 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) subtitleStringText = statusData.text
usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) 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 let (maybePanelStatusData, maybeNextPanelStatusData, _) = panelStatusData
if let panelStatusData = maybePanelStatusData { if let panelStatusData = maybePanelStatusData {
@ -2737,22 +2748,28 @@ final class PeerInfoHeaderNode: ASDisplayNode {
} else { } else {
subtitleColor = presentationData.theme.list.itemSecondaryTextColor 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 { 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 { } else {
subtitleString = NSAttributedString(string: " ", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) subtitleStringText = " "
smallSubtitleString = subtitleString subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)
usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: 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 { } else {
titleString = NSAttributedString(string: " ", font: Font.semibold(24.0), textColor: presentationData.theme.list.itemPrimaryTextColor) titleStringText = " "
smallTitleString = titleString titleAttributes = MultiScaleTextState.Attributes(font: Font.regular(24.0), color: presentationData.theme.list.itemPrimaryTextColor)
subtitleString = NSAttributedString(string: " ", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) smallTitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(24.0), color: .white)
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))
} }
let textSideInset: CGFloat = 36.0 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 titleConstrainedSize = CGSize(width: width - textSideInset * 2.0 - (isPremium || isVerified || isFake ? 20.0 : 0.0), height: .greatestFiniteMagnitude)
let titleNodeLayout = self.titleNode.updateLayout(states: [ let titleNodeLayout = self.titleNode.updateLayout(text: titleStringText, states: [
TitleNodeStateRegular: MultiScaleTextState(attributedText: titleString, constrainedSize: titleConstrainedSize), TitleNodeStateRegular: MultiScaleTextState(attributes: titleAttributes, constrainedSize: titleConstrainedSize),
TitleNodeStateExpanded: MultiScaleTextState(attributedText: smallTitleString, constrainedSize: titleConstrainedSize) TitleNodeStateExpanded: MultiScaleTextState(attributes: smallTitleAttributes, constrainedSize: titleConstrainedSize)
], mainState: TitleNodeStateRegular) ], mainState: TitleNodeStateRegular)
self.titleNode.accessibilityLabel = titleString.string self.titleNode.accessibilityLabel = titleStringText
let subtitleNodeLayout = self.subtitleNode.updateLayout(states: [ let subtitleNodeLayout = self.subtitleNode.updateLayout(text: subtitleStringText, states: [
TitleNodeStateRegular: MultiScaleTextState(attributedText: subtitleString, constrainedSize: titleConstrainedSize), TitleNodeStateRegular: MultiScaleTextState(attributes: subtitleAttributes, constrainedSize: titleConstrainedSize),
TitleNodeStateExpanded: MultiScaleTextState(attributedText: smallSubtitleString, constrainedSize: titleConstrainedSize) TitleNodeStateExpanded: MultiScaleTextState(attributes: smallSubtitleAttributes, constrainedSize: titleConstrainedSize)
], mainState: TitleNodeStateRegular) ], mainState: TitleNodeStateRegular)
self.subtitleNode.accessibilityLabel = subtitleString.string self.subtitleNode.accessibilityLabel = subtitleStringText
if subtitleIsButton { if subtitleIsButton {
let subtitleBackgroundNode: ASDisplayNode let subtitleBackgroundNode: ASDisplayNode
@ -2863,25 +2880,25 @@ final class PeerInfoHeaderNode: ASDisplayNode {
} }
} }
let panelSubtitleNodeLayout = self.panelSubtitleNode.updateLayout(states: [ let panelSubtitleNodeLayout = self.panelSubtitleNode.updateLayout(text: panelSubtitleString?.text ?? subtitleStringText, states: [
TitleNodeStateRegular: MultiScaleTextState(attributedText: panelSubtitleString ?? subtitleString, constrainedSize: titleConstrainedSize), TitleNodeStateRegular: MultiScaleTextState(attributes: panelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize),
TitleNodeStateExpanded: MultiScaleTextState(attributedText: panelSubtitleString ?? subtitleString, constrainedSize: titleConstrainedSize) TitleNodeStateExpanded: MultiScaleTextState(attributes: panelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize)
], mainState: TitleNodeStateRegular) ], mainState: TitleNodeStateRegular)
self.panelSubtitleNode.accessibilityLabel = (panelSubtitleString ?? subtitleString).string self.panelSubtitleNode.accessibilityLabel = panelSubtitleString?.text ?? subtitleStringText
let nextPanelSubtitleNodeLayout = self.nextPanelSubtitleNode.updateLayout(states: [ let nextPanelSubtitleNodeLayout = self.nextPanelSubtitleNode.updateLayout(text: nextPanelSubtitleString?.text ?? subtitleStringText, states: [
TitleNodeStateRegular: MultiScaleTextState(attributedText: nextPanelSubtitleString ?? subtitleString, constrainedSize: titleConstrainedSize), TitleNodeStateRegular: MultiScaleTextState(attributes: nextPanelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize),
TitleNodeStateExpanded: MultiScaleTextState(attributedText: nextPanelSubtitleString ?? subtitleString, constrainedSize: titleConstrainedSize) TitleNodeStateExpanded: MultiScaleTextState(attributes: nextPanelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize)
], mainState: TitleNodeStateRegular) ], mainState: TitleNodeStateRegular)
if let _ = nextPanelSubtitleString { if let _ = nextPanelSubtitleString {
self.nextPanelSubtitleNode.isHidden = false self.nextPanelSubtitleNode.isHidden = false
} }
let usernameNodeLayout = self.usernameNode.updateLayout(states: [ let usernameNodeLayout = self.usernameNode.updateLayout(text: usernameString.text, states: [
TitleNodeStateRegular: MultiScaleTextState(attributedText: usernameString, constrainedSize: CGSize(width: titleConstrainedSize.width, height: titleConstrainedSize.height)), TitleNodeStateRegular: MultiScaleTextState(attributes: usernameString.attributes, 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)) TitleNodeStateExpanded: MultiScaleTextState(attributes: usernameString.attributes, constrainedSize: CGSize(width: width - titleNodeLayout[TitleNodeStateExpanded]!.size.width - 8.0, height: titleConstrainedSize.height))
], mainState: TitleNodeStateRegular) ], mainState: TitleNodeStateRegular)
self.usernameNode.accessibilityLabel = usernameString.string self.usernameNode.accessibilityLabel = usernameString.text
let avatarCenter: CGPoint let avatarCenter: CGPoint
if let transitionSourceAvatarFrame = transitionSourceAvatarFrame { if let transitionSourceAvatarFrame = transitionSourceAvatarFrame {
@ -2987,7 +3004,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
subtitleAlpha = 1.0 - titleCollapseFraction subtitleAlpha = 1.0 - titleCollapseFraction
panelSubtitleAlpha = 0.0 panelSubtitleAlpha = 0.0
} else { } else {
if (panelSubtitleString ?? subtitleString).string != subtitleString.string { if (panelSubtitleString?.text ?? subtitleStringText) != subtitleStringText {
subtitleAlpha = 1.0 - effectiveAreaExpansionFraction subtitleAlpha = 1.0 - effectiveAreaExpansionFraction
panelSubtitleAlpha = effectiveAreaExpansionFraction panelSubtitleAlpha = effectiveAreaExpansionFraction
subtitleOffset = -effectiveAreaExpansionFraction * 5.0 subtitleOffset = -effectiveAreaExpansionFraction * 5.0

View File

@ -7659,7 +7659,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
func forwardMessages(messageIds: Set<MessageId>?) { func forwardMessages(messageIds: Set<MessageId>?) {
if let messageIds = messageIds ?? self.state.selectedMessageIds, !messageIds.isEmpty { 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 peerSelectionController.multiplePeersSelected = { [weak self, weak peerSelectionController] peers, peerMap, messageText, mode, forwardOptions in
guard let strongSelf = self, let strongController = peerSelectionController else { guard let strongSelf = self, let strongController = peerSelectionController else {
return return

View File

@ -22,6 +22,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
public var multiplePeersSelected: (([Peer], [PeerId: Peer], NSAttributedString, AttachmentTextInputPanelSendMode, ChatInterfaceForwardOptionsState?) -> Void)? public var multiplePeersSelected: (([Peer], [PeerId: Peer], NSAttributedString, AttachmentTextInputPanelSendMode, ChatInterfaceForwardOptionsState?) -> Void)?
private let filter: ChatListNodePeersFilter private let filter: ChatListNodePeersFilter
private let forumPeerId: EnginePeer.Id? private let forumPeerId: EnginePeer.Id?
private let selectForumThreads: Bool
private let attemptSelection: ((Peer, Int64?) -> Void)? private let attemptSelection: ((Peer, Int64?) -> Void)?
private let createNewGroup: (() -> Void)? private let createNewGroup: (() -> Void)?
@ -91,6 +92,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
self.pretendPresentedInModal = params.pretendPresentedInModal self.pretendPresentedInModal = params.pretendPresentedInModal
self.forwardedMessageIds = params.forwardedMessageIds self.forwardedMessageIds = params.forwardedMessageIds
self.hasTypeHeaders = params.hasTypeHeaders self.hasTypeHeaders = params.hasTypeHeaders
self.selectForumThreads = params.selectForumThreads
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) 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 self.peerSelectionNode.requestOpenPeer = { [weak self] peer, threadId in
if let strongSelf = self, let peerSelected = strongSelf.peerSelected { 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( let controller = PeerSelectionControllerImpl(
PeerSelectionControllerParams( PeerSelectionControllerParams(
context: strongSelf.context, context: strongSelf.context,
@ -197,7 +199,9 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
pretendPresentedInModal: false, pretendPresentedInModal: false,
multipleSelection: false, multipleSelection: false,
forwardedMessageIds: [], forwardedMessageIds: [],
hasTypeHeaders: false) hasTypeHeaders: false,
selectForumThreads: false
)
) )
controller.peerSelected = strongSelf.peerSelected controller.peerSelected = strongSelf.peerSelected
strongSelf.push(controller) strongSelf.push(controller)

View File

@ -663,7 +663,7 @@ public class ShareRootControllerImpl {
attemptSelectionImpl?(peer) attemptSelectionImpl?(peer)
}, createNewGroup: { }, createNewGroup: {
createNewGroupImpl?() createNewGroupImpl?()
}, pretendPresentedInModal: true)) }, pretendPresentedInModal: true, selectForumThreads: true))
controller.customDismiss = { controller.customDismiss = {
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
@ -837,7 +837,7 @@ public class ShareRootControllerImpl {
var attemptSelectionImpl: ((Peer) -> Void)? 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 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) attemptSelectionImpl?(peer)
}, pretendPresentedInModal: true)) }, pretendPresentedInModal: true, selectForumThreads: true))
controller.customDismiss = { controller.customDismiss = {
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
@ -912,7 +912,7 @@ public class ShareRootControllerImpl {
attemptSelectionImpl?(peer) attemptSelectionImpl?(peer)
}, createNewGroup: { }, createNewGroup: {
createNewGroupImpl?() createNewGroupImpl?()
}, pretendPresentedInModal: true)) }, pretendPresentedInModal: true, selectForumThreads: true))
controller.customDismiss = { controller.customDismiss = {
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)