This commit is contained in:
Isaac 2025-07-17 10:34:19 +02:00
parent 7a1b0d3364
commit 5ad4331eeb
25 changed files with 1776 additions and 285 deletions

3
.gitmodules vendored
View file

@ -32,3 +32,6 @@ url=../tgcalls.git
[submodule "third-party/td/td"]
path = third-party/td/td
url = https://github.com/tdlib/td
[submodule "third-party/XcodeGen"]
path = third-party/XcodeGen
url = https://github.com/yonaskolb/XcodeGen.git

View file

@ -1137,6 +1137,7 @@ public protocol SharedAccountContext: AnyObject {
func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, stories: Bool, forceDark: Bool) -> ViewController
func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController
func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController
func makeStorySelectionController(context: AccountContext, peerId: EnginePeer.Id, completion: @escaping ([EngineStoryItem]) -> Void) -> ViewController
func makeArchiveSettingsController(context: AccountContext) -> ViewController
func makeFilterSettingsController(context: AccountContext, modal: Bool, scrollToTags: Bool, dismissed: (() -> Void)?) -> ViewController
func makeBusinessSetupScreen(context: AccountContext) -> ViewController

View file

@ -165,8 +165,10 @@ private final class PromptAlertContentNode: AlertContentNode {
private let strings: PresentationStrings
private let text: String
private let titleFont: PromptControllerTitleFont
private let subtitle: String?
private let textNode: ASTextNode
private let subtitleNode: ASTextNode?
let inputFieldNode: PromptInputFieldNode
private let actionNodesSeparator: ASDisplayNode
@ -189,14 +191,23 @@ private final class PromptAlertContentNode: AlertContentNode {
return self.isUserInteractionEnabled
}
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, titleFont: PromptControllerTitleFont, value: String?, placeholder: String?, characterLimit: Int) {
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, titleFont: PromptControllerTitleFont, subtitle: String?, value: String?, placeholder: String?, characterLimit: Int) {
self.strings = strings
self.text = text
self.titleFont = titleFont
self.subtitle = subtitle
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 2
if subtitle != nil {
let subtitleNode = ASTextNode()
subtitleNode.maximumNumberOfLines = 0
self.subtitleNode = subtitleNode
} else {
self.subtitleNode = nil
}
self.inputFieldNode = PromptInputFieldNode(theme: ptheme, placeholder: placeholder ?? "", characterLimit: characterLimit)
self.inputFieldNode.text = value ?? ""
@ -220,6 +231,9 @@ private final class PromptAlertContentNode: AlertContentNode {
super.init()
self.addSubnode(self.textNode)
if let subtitleNode = self.subtitleNode {
self.addSubnode(subtitleNode)
}
self.addSubnode(self.inputFieldNode)
@ -268,6 +282,10 @@ private final class PromptAlertContentNode: AlertContentNode {
titleFontValue = Font.semibold(17.0)
}
self.textNode.attributedText = NSAttributedString(string: self.text, font: titleFontValue, textColor: theme.primaryColor, paragraphAlignment: .center)
if let subtitle = self.subtitle, let subtitleNode = self.subtitleNode {
subtitleNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
}
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
@ -302,6 +320,14 @@ private final class PromptAlertContentNode: AlertContentNode {
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 6.0 + spacing
var subtitleSize: CGSize?
if let subtitleNode {
let subtitleSizeValue = subtitleNode.measure(measureSize)
subtitleSize = subtitleSizeValue
transition.updateFrame(node: subtitleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - subtitleSizeValue.width) / 2.0), y: origin.y), size: subtitleSizeValue))
origin.y += subtitleSizeValue.height + 6.0 + spacing
}
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
@ -324,6 +350,9 @@ private final class PromptAlertContentNode: AlertContentNode {
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
var contentWidth = max(titleSize.width, minActionsWidth)
if let subtitleSize {
contentWidth = max(contentWidth, subtitleSize.width)
}
contentWidth = max(contentWidth, 234.0)
var actionsHeight: CGFloat = 0.0
@ -342,7 +371,10 @@ private final class PromptAlertContentNode: AlertContentNode {
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight))
transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0)
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom)
var resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom)
if let subtitleSize {
resultSize.height += subtitleSize.height + spacing
}
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
@ -407,7 +439,7 @@ public enum PromptControllerTitleFont {
case bold
}
public func promptController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, text: String, titleFont: PromptControllerTitleFont = .regular, value: String?, placeholder: String? = nil, characterLimit: Int = 1000, apply: @escaping (String?) -> Void) -> AlertController {
public func promptController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, text: String, titleFont: PromptControllerTitleFont = .regular, subtitle: String? = nil, value: String?, placeholder: String? = nil, characterLimit: Int = 1000, apply: @escaping (String?) -> Void) -> AlertController {
let presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 }
var dismissImpl: ((Bool) -> Void)?
@ -421,7 +453,7 @@ public func promptController(sharedContext: SharedAccountContext, updatedPresent
applyImpl?()
})]
let contentNode = PromptAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, titleFont: titleFont, value: value, placeholder: placeholder, characterLimit: characterLimit)
let contentNode = PromptAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, titleFont: titleFont, subtitle: subtitle, value: value, placeholder: placeholder, characterLimit: characterLimit)
contentNode.complete = {
dismissImpl?(true)
applyImpl?()

View file

@ -2127,7 +2127,7 @@ public func channelStatsController(
}
messagesPromise.set(.single(nil) |> then(messageView))
let storyList = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false)
let storyList = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false, folderId: nil)
storyList.loadMore()
storiesPromise.set(
.single(nil)

View file

@ -943,7 +943,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[2109703795] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) }
dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) }
dict[-963180333] = { return Api.SponsoredPeer.parse_sponsoredPeer($0) }
dict[2139438098] = { return Api.StarGift.parse_starGift($0) }
dict[12386139] = { return Api.StarGift.parse_starGift($0) }
dict[-164136786] = { return Api.StarGift.parse_starGiftUnique($0) }
dict[-650279524] = { return Api.StarGiftAttribute.parse_starGiftAttributeBackdrop($0) }
dict[970559507] = { return Api.StarGiftAttribute.parse_starGiftAttributeModel($0) }

View file

@ -636,14 +636,14 @@ public extension Api {
}
public extension Api {
enum StarGift: TypeConstructorDescription {
case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, availabilityResale: Int64?, convertStars: Int64, firstSaleDate: Int32?, lastSaleDate: Int32?, upgradeStars: Int64?, resellMinStars: Int64?, title: String?, releasedBy: Api.Peer?)
case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, availabilityResale: Int64?, convertStars: Int64, firstSaleDate: Int32?, lastSaleDate: Int32?, upgradeStars: Int64?, resellMinStars: Int64?, title: String?, releasedBy: Api.Peer?, perUserTotal: Int32?, perUserRemains: Int32?)
case starGiftUnique(flags: Int32, id: Int64, title: String, slug: String, num: Int32, ownerId: Api.Peer?, ownerName: String?, ownerAddress: String?, attributes: [Api.StarGiftAttribute], availabilityIssued: Int32, availabilityTotal: Int32, giftAddress: String?, resellStars: Int64?, releasedBy: Api.Peer?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy):
case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy, let perUserTotal, let perUserRemains):
if boxed {
buffer.appendInt32(2139438098)
buffer.appendInt32(12386139)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(id, buffer: buffer, boxed: false)
@ -659,6 +659,8 @@ public extension Api {
if Int(flags) & Int(1 << 4) != 0 {serializeInt64(resellMinStars!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 5) != 0 {serializeString(title!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 6) != 0 {releasedBy!.serialize(buffer, true)}
if Int(flags) & Int(1 << 8) != 0 {serializeInt32(perUserTotal!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 8) != 0 {serializeInt32(perUserRemains!, buffer: buffer, boxed: false)}
break
case .starGiftUnique(let flags, let id, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellStars, let releasedBy):
if boxed {
@ -688,8 +690,8 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy):
return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("availabilityResale", availabilityResale as Any), ("convertStars", convertStars as Any), ("firstSaleDate", firstSaleDate as Any), ("lastSaleDate", lastSaleDate as Any), ("upgradeStars", upgradeStars as Any), ("resellMinStars", resellMinStars as Any), ("title", title as Any), ("releasedBy", releasedBy as Any)])
case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy, let perUserTotal, let perUserRemains):
return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("availabilityResale", availabilityResale as Any), ("convertStars", convertStars as Any), ("firstSaleDate", firstSaleDate as Any), ("lastSaleDate", lastSaleDate as Any), ("upgradeStars", upgradeStars as Any), ("resellMinStars", resellMinStars as Any), ("title", title as Any), ("releasedBy", releasedBy as Any), ("perUserTotal", perUserTotal as Any), ("perUserRemains", perUserRemains as Any)])
case .starGiftUnique(let flags, let id, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellStars, let releasedBy):
return ("starGiftUnique", [("flags", flags as Any), ("id", id as Any), ("title", title as Any), ("slug", slug as Any), ("num", num as Any), ("ownerId", ownerId as Any), ("ownerName", ownerName as Any), ("ownerAddress", ownerAddress as Any), ("attributes", attributes as Any), ("availabilityIssued", availabilityIssued as Any), ("availabilityTotal", availabilityTotal as Any), ("giftAddress", giftAddress as Any), ("resellStars", resellStars as Any), ("releasedBy", releasedBy as Any)])
}
@ -728,6 +730,10 @@ public extension Api {
if Int(_1!) & Int(1 << 6) != 0 {if let signature = reader.readInt32() {
_14 = Api.parse(reader, signature: signature) as? Api.Peer
} }
var _15: Int32?
if Int(_1!) & Int(1 << 8) != 0 {_15 = reader.readInt32() }
var _16: Int32?
if Int(_1!) & Int(1 << 8) != 0 {_16 = reader.readInt32() }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
@ -742,8 +748,10 @@ public extension Api {
let _c12 = (Int(_1!) & Int(1 << 4) == 0) || _12 != nil
let _c13 = (Int(_1!) & Int(1 << 5) == 0) || _13 != nil
let _c14 = (Int(_1!) & Int(1 << 6) == 0) || _14 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 {
return Api.StarGift.starGift(flags: _1!, id: _2!, sticker: _3!, stars: _4!, availabilityRemains: _5, availabilityTotal: _6, availabilityResale: _7, convertStars: _8!, firstSaleDate: _9, lastSaleDate: _10, upgradeStars: _11, resellMinStars: _12, title: _13, releasedBy: _14)
let _c15 = (Int(_1!) & Int(1 << 8) == 0) || _15 != nil
let _c16 = (Int(_1!) & Int(1 << 8) == 0) || _16 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 {
return Api.StarGift.starGift(flags: _1!, id: _2!, sticker: _3!, stars: _4!, availabilityRemains: _5, availabilityTotal: _6, availabilityResale: _7, convertStars: _8!, firstSaleDate: _9, lastSaleDate: _10, upgradeStars: _11, resellMinStars: _12, title: _13, releasedBy: _14, perUserTotal: _15, perUserRemains: _16)
}
else {
return nil

View file

@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt {
return 207
return 210
}
public func parseMessage(_ data: Data!) -> Any! {

View file

@ -531,11 +531,13 @@ private final class CachedPeerStoryListHead: Codable {
let items: [Stories.StoredItem]
let pinnedIds: [Int32]
let totalCount: Int32
let folders: [StoryListContext.State.Folder]
init(items: [Stories.StoredItem], pinnedIds: [Int32], totalCount: Int32) {
init(items: [Stories.StoredItem], pinnedIds: [Int32], totalCount: Int32, folders: [StoryListContext.State.Folder]) {
self.items = items
self.pinnedIds = pinnedIds
self.totalCount = totalCount
self.folders = folders
}
}
@ -578,9 +580,20 @@ public struct StoryListContextState: Equatable {
}
}
public struct Folder: Equatable, Codable {
public let id: Int64
public let title: String
public init(id: Int64, title: String) {
self.id = id
self.title = title
}
}
public var peerReference: PeerReference?
public var items: [Item]
public var availableLanguages: [Language]
public var availableFolders: [Folder]
public var pinnedIds: [Int32]
public var totalCount: Int
public var loadMoreToken: AnyHashable?
@ -592,6 +605,7 @@ public struct StoryListContextState: Equatable {
peerReference: PeerReference?,
items: [Item],
availableLanguages: [Language],
availableFolders: [Folder],
pinnedIds: [Int32],
totalCount: Int,
loadMoreToken: AnyHashable?,
@ -603,6 +617,7 @@ public struct StoryListContextState: Equatable {
self.peerReference = peerReference
self.items = items
self.availableLanguages = availableLanguages
self.availableFolders = availableFolders
self.pinnedIds = pinnedIds
self.totalCount = totalCount
self.loadMoreToken = loadMoreToken
@ -621,12 +636,13 @@ public protocol StoryListContext: AnyObject {
func loadMore(completion: (() -> Void)?)
}
public final class PeerStoryListContext: StoryListContext {
public final class PeerStoryListContext: StoryListContext {
private final class Impl {
private let queue: Queue
private let account: Account
private let peerId: EnginePeer.Id
private let isArchived: Bool
private let folderId: Int64?
private let statePromise = Promise<State>()
private var stateValue: State {
@ -645,22 +661,40 @@ public final class PeerStoryListContext: StoryListContext {
private var completionCallbacksByToken: [AnyHashable: [() -> Void]] = [:]
init(queue: Queue, account: Account, peerId: EnginePeer.Id, isArchived: Bool) {
init(queue: Queue, account: Account, peerId: EnginePeer.Id, isArchived: Bool, folderId: Int64?) {
self.queue = queue
self.account = account
self.peerId = peerId
self.isArchived = isArchived
self.folderId = folderId
self.stateValue = State(peerReference: nil, items: [], availableLanguages: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false)
self.stateValue = State(peerReference: nil, items: [], availableLanguages: [], availableFolders: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false)
let _ = (account.postbox.transaction { transaction -> (PeerReference?, [State.Item], [Int32], Int, [MediaId: TelegramMediaFile], Bool) in
let _ = (account.postbox.transaction { transaction -> (PeerReference?, [State.Item], [Int32], Int, [MediaId: TelegramMediaFile], [StoryListContext.State.Folder], Bool) in
let key = ValueBoxKey(length: 8 + 1)
key.setInt64(0, value: peerId.toInt64())
key.setInt8(8, value: isArchived ? 1 : 0)
let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self)
guard let cached = cached else {
return (nil, [], [], 0, [:], false)
let cachedMain = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self)
guard let cachedMain else {
return (nil, [], [], 0, [:], [], false)
}
let cached: CachedPeerStoryListHead
if let folderId {
let key = ValueBoxKey(length: 8 + 1 + 8)
key.setInt64(0, value: peerId.toInt64())
key.setInt8(8, value: isArchived ? 1 : 0)
key.setInt64(8 + 1, value: folderId)
let cachedFolder = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self)
if let cachedFolder {
cached = cachedFolder
} else {
return (nil, [], [], 0, [:], cachedMain.folders, false)
}
} else {
cached = cachedMain
}
var items: [State.Item] = []
var allEntityFiles: [MediaId: TelegramMediaFile] = [:]
for storedItem in cached.items {
@ -734,28 +768,30 @@ public final class PeerStoryListContext: StoryListContext {
let peerReference = transaction.getPeer(peerId).flatMap(PeerReference.init)
return (peerReference, items, cached.pinnedIds, Int(cached.totalCount), allEntityFiles, true)
return (peerReference, items, cached.pinnedIds, Int(cached.totalCount), allEntityFiles, cached.folders, true)
}
|> deliverOn(self.queue)).start(next: { [weak self] peerReference, items, pinnedIds, totalCount, allEntityFiles, hasCache in
guard let `self` = self else {
|> deliverOn(self.queue)).start(next: { [weak self] peerReference, items, pinnedIds, totalCount, allEntityFiles, folders, hasCache in
guard let self else {
return
}
var updatedState = State(peerReference: peerReference, items: items, availableLanguages: [], pinnedIds: pinnedIds, totalCount: totalCount, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: hasCache, allEntityFiles: allEntityFiles, isLoading: false)
updatedState.items.sort(by: { lhs, rhs in
let lhsPinned = updatedState.pinnedIds.firstIndex(of: lhs.storyItem.id)
let rhsPinned = updatedState.pinnedIds.firstIndex(of: rhs.storyItem.id)
if let lhsPinned, let rhsPinned {
if lhsPinned != rhsPinned {
return lhsPinned < rhsPinned
var updatedState = State(peerReference: peerReference, items: items, availableLanguages: [], availableFolders: folders, pinnedIds: pinnedIds, totalCount: totalCount, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: hasCache, allEntityFiles: allEntityFiles, isLoading: false)
if self.folderId == nil {
updatedState.items.sort(by: { lhs, rhs in
let lhsPinned = updatedState.pinnedIds.firstIndex(of: lhs.storyItem.id)
let rhsPinned = updatedState.pinnedIds.firstIndex(of: rhs.storyItem.id)
if let lhsPinned, let rhsPinned {
if lhsPinned != rhsPinned {
return lhsPinned < rhsPinned
}
} else if (lhsPinned == nil) != (rhsPinned == nil) {
return lhsPinned != nil
}
} else if (lhsPinned == nil) != (rhsPinned == nil) {
return lhsPinned != nil
}
return lhs.storyItem.timestamp > rhs.storyItem.timestamp
})
return lhs.storyItem.timestamp > rhs.storyItem.timestamp
})
}
self.stateValue = updatedState
self.loadMore(completion: nil)
@ -791,6 +827,8 @@ public final class PeerStoryListContext: StoryListContext {
let account = self.account
let accountPeerId = account.peerId
let isArchived = self.isArchived
let folderId = self.folderId
let folders = self.stateValue.availableFolders
self.requestDisposable = (self.account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
@ -822,6 +860,73 @@ public final class PeerStoryListContext: StoryListContext {
var totalCount: Int = 0
var hasMore: Bool = false
if let folderId {
let key = ValueBoxKey(length: 8 + 1 + 8)
key.setInt64(0, value: peerId.toInt64())
key.setInt8(8, value: isArchived ? 1 : 0)
key.setInt64(8 + 1, value: folderId)
var updatedItems: [Stories.StoredItem] = []
if let currentEntry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self) {
updatedItems = currentEntry.items
}
if let entry = CodableEntry(CachedPeerStoryListHead(items: updatedItems, pinnedIds: [], totalCount: Int32(updatedItems.count), folders: [])) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry)
}
for item in updatedItems {
if case let .item(item) = item, let media = item.media {
let mappedItem = EngineStoryItem(
id: item.id,
timestamp: item.timestamp,
expirationTimestamp: item.expirationTimestamp,
media: EngineMedia(media),
alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init),
mediaAreas: item.mediaAreas,
text: item.text,
entities: item.entities,
views: item.views.flatMap { views in
return EngineStoryItem.Views(
seenCount: views.seenCount,
reactedCount: views.reactedCount,
forwardCount: views.forwardCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return transaction.getPeer(id).flatMap(EnginePeer.init)
},
reactions: views.reactions,
hasList: views.hasList
)
},
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
isPinned: item.isPinned,
isExpired: item.isExpired,
isPublic: item.isPublic,
isPending: false,
isCloseFriends: item.isCloseFriends,
isContacts: item.isContacts,
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction,
forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) },
author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }
)
storyItems.append(State.Item(
id: StoryId(peerId: peerId, id: mappedItem.id),
storyItem: mappedItem,
peer: nil
))
}
}
totalCount = storyItems.count
hasMore = false
return (storyItems, totalCount, transaction.getPeer(peerId).flatMap(PeerReference.init), hasMore)
}
switch result {
case let .stories(_, count, stories, pinnedStories, chats, users):
totalCount = Int(count)
@ -883,7 +988,7 @@ public final class PeerStoryListContext: StoryListContext {
let key = ValueBoxKey(length: 8 + 1)
key.setInt64(0, value: peerId.toInt64())
key.setInt8(8, value: isArchived ? 1 : 0)
if let entry = CodableEntry(CachedPeerStoryListHead(items: storyItems.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: pinnedIds, totalCount: count)) {
if let entry = CodableEntry(CachedPeerStoryListHead(items: storyItems.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: pinnedIds, totalCount: count, folders: folders)) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry)
}
}
@ -939,7 +1044,7 @@ public final class PeerStoryListContext: StoryListContext {
}
}
if self.updatesDisposable == nil {
if self.updatesDisposable == nil && self.folderId == nil {
self.updatesDisposable = (self.account.stateManager.storyUpdates
|> deliverOn(self.queue)).start(next: { [weak self] updates in
guard let `self` = self else {
@ -1255,11 +1360,12 @@ public final class PeerStoryListContext: StoryListContext {
let items = finalUpdatedState.items
let pinnedIds = finalUpdatedState.pinnedIds
let totalCount = finalUpdatedState.totalCount
let folders = finalUpdatedState.availableFolders
let _ = (self.account.postbox.transaction { transaction -> Void in
let key = ValueBoxKey(length: 8 + 1)
key.setInt64(0, value: peerId.toInt64())
key.setInt8(8, value: isArchived ? 1 : 0)
if let entry = CodableEntry(CachedPeerStoryListHead(items: items.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: pinnedIds, totalCount: Int32(totalCount))) {
if let entry = CodableEntry(CachedPeerStoryListHead(items: items.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: pinnedIds, totalCount: Int32(totalCount), folders: folders)) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry)
}
}).start()
@ -1269,6 +1375,242 @@ public final class PeerStoryListContext: StoryListContext {
}
})
}
func addFolder(title: String) -> Int64 {
let id = Int64.random(in: Int64.min ... Int64.max)
var state = self.stateValue
state.availableFolders.append(StoryListContextState.Folder(id: id, title: title))
self.stateValue = state
let peerId = self.peerId
let isArchived = self.isArchived
let items = state.items
let pinnedIds = state.pinnedIds
let totalCount = state.totalCount
let folders = state.availableFolders
let _ = (self.account.postbox.transaction { transaction -> Void in
let key = ValueBoxKey(length: 8 + 1)
key.setInt64(0, value: peerId.toInt64())
key.setInt8(8, value: isArchived ? 1 : 0)
if let entry = CodableEntry(CachedPeerStoryListHead(items: items.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: pinnedIds, totalCount: Int32(totalCount), folders: folders)) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry)
}
}).start()
return id
}
func removeFolder(id: Int64) {
var state = self.stateValue
if let index = state.availableFolders.firstIndex(where: { $0.id == id }) {
state.availableFolders.remove(at: index)
}
self.stateValue = state
let peerId = self.peerId
let isArchived = self.isArchived
let items = state.items
let pinnedIds = state.pinnedIds
let totalCount = state.totalCount
let folders = state.availableFolders
let _ = (self.account.postbox.transaction { transaction -> Void in
let key = ValueBoxKey(length: 8 + 1)
key.setInt64(0, value: peerId.toInt64())
key.setInt8(8, value: isArchived ? 1 : 0)
if let entry = CodableEntry(CachedPeerStoryListHead(items: items.prefix(100).map { .item($0.storyItem.asStoryItem()) }, pinnedIds: pinnedIds, totalCount: Int32(totalCount), folders: folders)) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry)
}
}).start()
}
func addToFolder(id: Int64, items: [EngineStoryItem]) {
let peerId = self.peerId
let _ = (self.account.postbox.transaction { transaction -> Void in
let key = ValueBoxKey(length: 8 + 1 + 8)
key.setInt64(0, value: peerId.toInt64())
key.setInt8(8, value: 0)
key.setInt64(8 + 1, value: id)
var updatedItems: [Stories.Item] = []
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self) {
updatedItems = cached.items.compactMap { item -> Stories.Item? in
switch item {
case let .item(item):
return item
case .placeholder:
return nil
}
}
}
for item in items {
let mappedItem = Stories.Item(
id: item.id,
timestamp: item.timestamp,
expirationTimestamp: item.expirationTimestamp,
media: item.media._asMedia(),
alternativeMediaList: item.alternativeMediaList.map({ $0._asMedia() }),
mediaAreas: item.mediaAreas,
text: item.text,
entities: item.entities,
views: item.views.flatMap {
return Stories.Item.Views(
seenCount: $0.seenCount,
reactedCount: $0.reactedCount,
forwardCount: $0.forwardCount,
seenPeerIds: $0.seenPeers.map { $0.id },
reactions: $0.reactions,
hasList: $0.hasList
)
},
privacy: item.privacy.flatMap {
return Stories.Item.Privacy(
base: $0.base,
additionallyIncludePeers: $0.additionallyIncludePeers
)
},
isPinned: item.isPinned,
isExpired: item.isExpired,
isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends,
isContacts: item.isContacts,
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction,
forwardInfo: item.forwardInfo.flatMap {
switch $0 {
case let .known(peer, storyId, isModified):
return .known(peerId: peer.id, storyId: storyId, isModified: isModified)
case let .unknown(name, isModified):
return .unknown(name: name, isModified: isModified)
}
},
authorId: item.author?.id
)
if !updatedItems.contains(where: { $0.id == mappedItem.id }) {
updatedItems.insert(mappedItem, at: 0)
}
}
if let entry = CodableEntry(CachedPeerStoryListHead(items: updatedItems.map { .item($0) }, pinnedIds: [], totalCount: Int32(updatedItems.count), folders: [])) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry)
}
}).start()
if self.folderId == id {
var state = self.stateValue
for item in items {
state.items.removeAll(where: { $0.id.id == item.id })
state.items.insert(StoryListContextState.Item(
id: StoryId(peerId: self.peerId, id: item.id),
storyItem: item,
peer: nil
), at: 0)
}
self.stateValue = state
}
}
func removeFromFolder(id: Int64, itemIds: [Int32]) {
let peerId = self.peerId
let _ = (self.account.postbox.transaction { transaction -> Void in
let key = ValueBoxKey(length: 8 + 1 + 8)
key.setInt64(0, value: peerId.toInt64())
key.setInt8(8, value: 0)
key.setInt64(8 + 1, value: id)
var updatedItems: [Stories.Item] = []
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self) {
updatedItems = cached.items.compactMap { item -> Stories.Item? in
switch item {
case let .item(item):
return item
case .placeholder:
return nil
}
}
}
for itemId in itemIds {
if let index = updatedItems.firstIndex(where: { $0.id == itemId }) {
updatedItems.remove(at: index)
}
}
if let entry = CodableEntry(CachedPeerStoryListHead(items: updatedItems.map { .item($0) }, pinnedIds: [], totalCount: Int32(updatedItems.count), folders: [])) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry)
}
}).start()
if self.folderId == id {
var state = self.stateValue
state.items.removeAll(where: { itemIds.contains($0.id.id) })
self.stateValue = state
}
}
func reorderItemsInFolder(itemIds: [Int32]) {
guard let id = self.folderId else {
return
}
let peerId = self.peerId
let _ = (self.account.postbox.transaction { transaction -> Void in
let key = ValueBoxKey(length: 8 + 1 + 8)
key.setInt64(0, value: peerId.toInt64())
key.setInt8(8, value: 0)
key.setInt64(8 + 1, value: id)
var previousItems: [Stories.Item] = []
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self) {
previousItems = cached.items.compactMap { item -> Stories.Item? in
switch item {
case let .item(item):
return item
case .placeholder:
return nil
}
}
}
var updatedItems: [Stories.Item] = []
for itemId in itemIds {
if let index = previousItems.firstIndex(where: { $0.id == itemId }) {
updatedItems.append(previousItems[index])
}
}
for item in previousItems {
if !updatedItems.contains(where: { $0.id == item.id }) {
updatedItems.append(item)
}
}
if let entry = CodableEntry(CachedPeerStoryListHead(items: updatedItems.map { .item($0) }, pinnedIds: [], totalCount: Int32(updatedItems.count), folders: [])) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry)
}
}).start()
if self.folderId == id {
var state = self.stateValue
let previousItems = state.items
var updatedItems: [State.Item] = []
for itemId in itemIds {
if let index = previousItems.firstIndex(where: { $0.id.id == itemId }) {
updatedItems.append(previousItems[index])
}
}
for item in previousItems {
if !updatedItems.contains(where: { $0.id == item.id }) {
updatedItems.append(item)
}
}
state.items = updatedItems
self.stateValue = state
}
}
}
public var state: Signal<State, NoError> {
@ -1280,11 +1622,18 @@ public final class PeerStoryListContext: StoryListContext {
private let queue: Queue
private let impl: QueueLocalObject<Impl>
public init(account: Account, peerId: EnginePeer.Id, isArchived: Bool) {
private let account: Account
public let peerId: EnginePeer.Id
public let folderId: Int64?
public init(account: Account, peerId: EnginePeer.Id, isArchived: Bool, folderId: Int64?) {
let queue = Queue.mainQueue()
self.queue = queue
self.account = account
self.peerId = peerId
self.folderId = folderId
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, account: account, peerId: peerId, isArchived: isArchived)
return Impl(queue: queue, account: account, peerId: peerId, isArchived: isArchived, folderId: folderId)
})
}
@ -1293,6 +1642,111 @@ public final class PeerStoryListContext: StoryListContext {
impl.loadMore(completion : completion)
}
}
public func addFolder(title: String, completion: @escaping (Int64) -> Void) {
self.impl.with { impl in
completion(impl.addFolder(title: title))
}
}
public func removeFolder(id: Int64) {
self.impl.with { impl in
impl.removeFolder(id: id)
}
}
public func addToFolder(id: Int64, items: [EngineStoryItem]) {
self.impl.with { impl in
impl.addToFolder(id: id, items: items)
}
}
public func removeFromFolder(id: Int64, itemIds: [Int32]) {
self.impl.with { impl in
impl.removeFromFolder(id: id, itemIds: itemIds)
}
}
public func reorderItemsInFolder(itemIds: [Int32]) {
self.impl.with { impl in
impl.reorderItemsInFolder(itemIds: itemIds)
}
}
public static func folderPreviews(peerId: EnginePeer.Id, account: Account) -> Signal<(peer: PeerReference, [(folder: StoryListContext.State.Folder, item: EngineStoryItem?)]), NoError> {
return account.postbox.transaction { transaction -> (peer: PeerReference, [(folder: StoryListContext.State.Folder, item: EngineStoryItem?)]) in
let key = ValueBoxKey(length: 8 + 1)
key.setInt64(0, value: peerId.toInt64())
key.setInt8(8, value: 0)
var folders: [StoryListContext.State.Folder] = []
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self) {
folders = cached.folders
}
var result: [(folder: StoryListContext.State.Folder, item: EngineStoryItem?)] = []
for folder in folders {
let key = ValueBoxKey(length: 8 + 1 + 8)
key.setInt64(0, value: peerId.toInt64())
key.setInt8(8, value: 0)
key.setInt64(8 + 1, value: folder.id)
var mappedItem: EngineStoryItem?
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self) {
if let firstItem = cached.items.first, case let .item(item) = firstItem, let media = item.media {
mappedItem = EngineStoryItem(
id: item.id,
timestamp: item.timestamp,
expirationTimestamp: item.expirationTimestamp,
media: EngineMedia(media),
alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init),
mediaAreas: item.mediaAreas,
text: item.text,
entities: item.entities,
views: item.views.flatMap { views in
return EngineStoryItem.Views(
seenCount: views.seenCount,
reactedCount: views.reactedCount,
forwardCount: views.forwardCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return transaction.getPeer(id).flatMap(EnginePeer.init)
},
reactions: views.reactions,
hasList: views.hasList
)
},
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
isPinned: item.isPinned,
isExpired: item.isExpired,
isPublic: item.isPublic,
isPending: false,
isCloseFriends: item.isCloseFriends,
isContacts: item.isContacts,
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction,
forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) },
author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }
)
}
}
result.append((folder, mappedItem))
}
let peerReference: PeerReference
if let peer = transaction.getPeer(peerId) {
peerReference = PeerReference(peer) ?? .user(id: peerId.id._internalGetInt64Value(), accessHash: 0)
} else {
peerReference = .user(id: peerId.id._internalGetInt64Value(), accessHash: 0)
}
return (peerReference, result)
}
}
}
public final class SearchStoryListContext: StoryListContext {
@ -1332,7 +1786,7 @@ public final class SearchStoryListContext: StoryListContext {
self.account = account
self.source = source
self.stateValue = State(peerReference: nil, items: [], availableLanguages: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(""), isCached: false, hasCache: false, allEntityFiles: [:], isLoading: false)
self.stateValue = State(peerReference: nil, items: [], availableLanguages: [], availableFolders: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(""), isCached: false, hasCache: false, allEntityFiles: [:], isLoading: false)
self.statePromise.set(.single(self.stateValue))
self.loadMore(completion: nil)
@ -2149,7 +2603,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
self.isArchived = isArchived
self.stateValue = State(peerReference: nil, items: [], availableLanguages: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false)
self.stateValue = State(peerReference: nil, items: [], availableLanguages: [], availableFolders: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false)
let localStateKey: PostboxViewKey = .storiesState(key: .local)
@ -2166,6 +2620,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
peerReference: peer.flatMap(PeerReference.init),
items: [],
availableLanguages: [],
availableFolders: [],
pinnedIds: [],
totalCount: 0,
loadMoreToken: AnyHashable(0),
@ -2313,6 +2768,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
peerReference: (peer?._asPeer()).flatMap(PeerReference.init),
items: items,
availableLanguages: availableLanguages,
availableFolders: [],
pinnedIds: [],
totalCount: items.count,
loadMoreToken: nil,
@ -2413,6 +2869,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
peerReference: PeerReference(peer),
items: items,
availableLanguages: [],
availableFolders: [],
pinnedIds: [],
totalCount: items.count,
loadMoreToken: nil,
@ -2592,6 +3049,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
peerReference: self.stateValue.peerReference,
items: items,
availableLanguages: [],
availableFolders: [],
pinnedIds: [],
totalCount: items.count,
loadMoreToken: nil,

View file

@ -720,7 +720,7 @@ public extension StarGift {
extension StarGift {
init?(apiStarGift: Api.StarGift) {
switch apiStarGift {
case let .starGift(apiFlags, id, sticker, stars, availabilityRemains, availabilityTotal, availabilityResale, convertStars, firstSale, lastSale, upgradeStars, minResaleStars, title, releasedBy):
case let .starGift(apiFlags, id, sticker, stars, availabilityRemains, availabilityTotal, availabilityResale, convertStars, firstSale, lastSale, upgradeStars, minResaleStars, title, releasedBy, _, _):
var flags = StarGift.Gift.Flags()
if (apiFlags & (1 << 2)) != 0 {
flags.insert(.isBirthdayGift)

View file

@ -15,6 +15,9 @@ swift_library(
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/TextLoadingEffect",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TooltipUI",
"//submodules/AccountContext",
"//submodules/UIKitRuntimeUtils",
],
visibility = [
"//visibility:public",

View file

@ -5,44 +5,62 @@ import ComponentFlow
import MultilineTextComponent
import TextLoadingEffect
import ComponentDisplayAdapters
import TooltipUI
import AccountContext
import UIKitRuntimeUtils
public final class PeerInfoRatingComponent: Component {
let context: AccountContext
let backgroundColor: UIColor
let foregroundColor: UIColor
let tooltipBackgroundColor: UIColor
let isExpanded: Bool
let compactLabel: String
let fraction: CGFloat
let label: String
let nextLabel: String
let tooltipLabel: String
let action: () -> Void
public init(
context: AccountContext,
backgroundColor: UIColor,
foregroundColor: UIColor,
tooltipBackgroundColor: UIColor,
isExpanded: Bool,
compactLabel: String,
fraction: CGFloat,
label: String,
nextLabel: String,
tooltipLabel: String,
action: @escaping () -> Void
) {
self.context = context
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
self.tooltipBackgroundColor = tooltipBackgroundColor
self.isExpanded = isExpanded
self.compactLabel = compactLabel
self.fraction = fraction
self.label = label
self.nextLabel = nextLabel
self.tooltipLabel = tooltipLabel
self.action = action
}
public static func ==(lhs: PeerInfoRatingComponent, rhs: PeerInfoRatingComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.foregroundColor != rhs.foregroundColor {
return false
}
if lhs.tooltipBackgroundColor != rhs.tooltipBackgroundColor {
return false
}
if lhs.isExpanded != rhs.isExpanded {
return false
}
@ -58,6 +76,9 @@ public final class PeerInfoRatingComponent: Component {
if lhs.nextLabel != rhs.nextLabel {
return false
}
if lhs.tooltipLabel != rhs.tooltipLabel {
return false
}
return true
}
@ -77,6 +98,8 @@ public final class PeerInfoRatingComponent: Component {
private var component: PeerInfoRatingComponent?
private var tooltipController: TooltipScreen?
override public init(frame: CGRect) {
self.backgroundView = UIImageView()
@ -261,6 +284,50 @@ public final class PeerInfoRatingComponent: Component {
})
}
let tooltipController: TooltipScreen
if let current = self.tooltipController {
tooltipController = current
} else {
tooltipController = TooltipScreen(
context: component.context,
account: component.context.account,
sharedContext: component.context.sharedContext,
text: .attributedString(text: NSAttributedString(string: component.tooltipLabel, font: Font.semibold(11.0), textColor: .white)),
style: .customBlur(component.tooltipBackgroundColor, -4.0),
arrowStyle: .small,
location: .point(CGRect(origin: CGPoint(x: 100.0, y: 100.0), size: CGSize()), .bottom),
displayDuration: .infinite,
isShimmering: true,
cornerRadius: 10.0,
shouldDismissOnTouch: { _, _ in
return .ignore
}
)
self.tooltipController = tooltipController
tooltipController.containerLayoutUpdated(ContainerViewLayout(
size: CGSize(width: 200.0, height: 200.0),
metrics: LayoutMetrics(),
deviceMetrics: DeviceMetrics.iPhoneXSMax,
intrinsicInsets: UIEdgeInsets(),
safeInsets: UIEdgeInsets(),
additionalInsets: UIEdgeInsets(),
statusBarHeight: nil,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
), transition: .immediate)
self.layer.addSublayer(tooltipController.view.layer)
tooltipController.viewWillAppear(false)
tooltipController.viewDidAppear(false)
tooltipController.setIgnoreAppearanceMethodInvocations(true)
tooltipController.view.isUserInteractionEnabled = false
}
transition.setFrame(view: tooltipController.view, frame: CGRect(origin: CGPoint(), size: CGSize(width: 200.0, height: 200.0)).offsetBy(dx: -200.0 * 0.5 + foregroundFrame.width - 7.0, dy: -200.0 * 0.5))
alphaTransition.setAlpha(view: tooltipController.view, alpha: component.isExpanded ? 1.0 : 0.0)
return size
}
}

View file

@ -677,7 +677,7 @@ public func keepPeerInfoScreenDataHot(context: AccountContext, peerId: PeerId, c
if case .user = inputData {
signals.append(Signal { _ in
let listContext = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false)
let listContext = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false, folderId: nil)
let expiringListContext = PeerExpiringStoryListContext(account: context.account, peerId: peerId)
return ActionDisposable {
@ -829,7 +829,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
)
|> distinctUntilChanged
let storyListContext = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false)
let storyListContext = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false, folderId: nil)
let hasStories: Signal<Bool?, NoError> = storyListContext.state
|> map { state -> Bool? in
if !state.hasCache {
@ -1188,7 +1188,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
secretChatKeyFingerprint = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.SecretChatKeyFingerprint(id: secretChatId))
}
let storyListContext = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false)
let storyListContext = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false, folderId: nil)
let hasStories: Signal<Bool?, NoError> = storyListContext.state
|> map { state -> Bool? in
if !state.hasCache {
@ -1201,7 +1201,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
let hasStoryArchive: Signal<Bool?, NoError>
var storyArchiveListContext: StoryListContext?
if isMyProfile {
let storyArchiveListContextValue = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: true)
let storyArchiveListContextValue = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: true, folderId: nil)
storyArchiveListContext = storyArchiveListContextValue
hasStoryArchive = storyArchiveListContextValue.state
|> map { state -> Bool? in
@ -1535,7 +1535,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
let requestsContextPromise = Promise<PeerInvitationImportersContext?>(nil)
let requestsStatePromise = Promise<PeerInvitationImportersState?>(nil)
let storyListContext = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false)
let storyListContext = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false, folderId: nil)
let hasStories: Signal<Bool?, NoError> = storyListContext.state
|> map { state -> Bool? in
if !state.hasCache {
@ -1857,7 +1857,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
let storyListContext: StoryListContext?
let hasStories: Signal<Bool?, NoError>
if peerId.namespace == Namespaces.Peer.CloudChannel {
storyListContext = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false)
storyListContext = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false, folderId: nil)
hasStories = storyListContext!.state
|> map { state -> Bool? in
if !state.hasCache {

View file

@ -1926,22 +1926,47 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.subtitleRating = subtitleRating
}
let fraction: CGFloat
let tooltipLabel: String
if let nextLevelStars = starRating.nextLevelStars {
fraction = CGFloat(starRating.currentLevelStars) / CGFloat(nextLevelStars)
tooltipLabel = "\(starRating.currentLevelStars) / \(nextLevelStars)"
} else {
fraction = 1.0
tooltipLabel = ""
}
let tooltipBackgroundColor: UIColor
let ratingBackgroundColor: UIColor
let ratingForegroundColor: UIColor
if peer?.profileColor != nil {
ratingBackgroundColor = UIColor(white: 1.0, alpha: 0.1)
ratingForegroundColor = UIColor(white: 1.0, alpha: 1.0)
if !self.isAvatarExpanded {
tooltipBackgroundColor = contentButtonBackgroundColor
} else {
tooltipBackgroundColor = UIColor(rgb: 0x000000, alpha: 0.65)
}
} else {
ratingBackgroundColor = presentationData.theme.list.freeTextColor.withMultipliedAlpha(0.1)
ratingForegroundColor = presentationData.theme.list.freeTextColor.withMultipliedAlpha(1.0)
tooltipBackgroundColor = UIColor(rgb: 0x000000, alpha: 0.65)
}
//TODO:localize
subtitleRatingSize = subtitleRating.update(
transition: subtitleRatingTransition,
component: AnyComponent(PeerInfoRatingComponent(
backgroundColor: UIColor(white: 1.0, alpha: 0.1),
foregroundColor: UIColor(white: 1.0, alpha: 1.0),
context: self.context,
backgroundColor: ratingBackgroundColor,
foregroundColor: ratingForegroundColor,
tooltipBackgroundColor: tooltipBackgroundColor,
isExpanded: self.subtitleRatingIsExpanded,
compactLabel: "\(starRating.level)",
fraction: fraction,
label: "Level \(starRating.level)",
nextLabel: starRating.nextLevelStars != nil ? "\(starRating.level + 1)" : "",
tooltipLabel: tooltipLabel,
action: { [weak self] in
guard let self else {
return
@ -1994,7 +2019,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let rawSubtitleFrame = CGRect(origin: CGPoint(x: subtitleCenter.x - subtitleFrame.size.width / 2.0, y: subtitleCenter.y - subtitleFrame.size.height / 2.0), size: subtitleFrame.size)
self.subtitleNodeRawContainer.frame = rawSubtitleFrame
transition.updateFrameAdditiveToCenter(node: self.subtitleNodeContainer, frame: CGRect(origin: rawSubtitleFrame.center, size: CGSize()))
transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: subtitleOffset), size: CGSize()))
transition.updatePosition(node: self.subtitleNode, position: CGPoint(x: 0.0, y: subtitleOffset))
transition.updateFrame(node: self.panelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset - 1.0), size: CGSize()))
transition.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize()))
transition.updateSublayerTransformScale(node: self.titleNodeContainer, scale: titleScale)
@ -2006,6 +2031,17 @@ final class PeerInfoHeaderNode: ASDisplayNode {
transition.updateFrameAdditive(view: subtitleBadgeView, frame: subtitleBadgeFrame)
transition.updateAlpha(layer: subtitleBadgeView.layer, alpha: (1.0 - transitionFraction))
}
if let subtitleRatingView = self.subtitleRating?.view, let subtitleRatingSize {
let subtitleBadgeFrame: CGRect
if self.subtitleRatingIsExpanded {
subtitleBadgeFrame = CGRect(origin: CGPoint(x: -subtitleRatingSize.width * 0.5, y: floor((-subtitleRatingSize.height) * 0.5)), size: subtitleRatingSize)
} else {
subtitleBadgeFrame = CGRect(origin: CGPoint(x: (-subtitleSize.width) * 0.5 - 4.0 - subtitleRatingSize.width, y: floor((-subtitleRatingSize.height) * 0.5)), size: subtitleRatingSize)
}
transition.updateFrameAdditive(view: subtitleRatingView, frame: subtitleBadgeFrame)
transition.updateAlpha(layer: subtitleRatingView.layer, alpha: (1.0 - transitionFraction))
}
} else {
let titleScale: CGFloat
let subtitleScale: CGFloat
@ -2048,7 +2084,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
usernameCenter.x = rawTitleFrame.center.x + (usernameCenter.x - rawTitleFrame.center.x) * subtitleScale
transition.updateFrameAdditiveToCenter(node: self.usernameNodeContainer, frame: CGRect(origin: usernameCenter, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset))
}
transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: subtitleOffset), size: CGSize()))
transition.updatePosition(node: self.subtitleNode, position: CGPoint(x: 0.0, y: subtitleOffset))
transition.updateFrame(node: self.panelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset - 1.0), size: CGSize()))
transition.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize()))
transition.updateSublayerTransformScaleAdditive(node: self.titleNodeContainer, scale: titleScale)
@ -2072,7 +2108,10 @@ final class PeerInfoHeaderNode: ASDisplayNode {
transition.updateAlpha(layer: subtitleRatingView.layer, alpha: (1.0 - transitionFraction) * subtitleRatingFraction)
}
transition.updateAlpha(node: self.subtitleNode, alpha: self.subtitleRatingIsExpanded ? 0.0 : 1.0)
let subtitleAlpha: CGFloat = subtitleRatingFraction * (self.subtitleRatingIsExpanded ? 0.0 : 1.0) + (1.0 - subtitleRatingFraction) * 1.0
let subtitleInnerScale: CGFloat = subtitleRatingFraction * (self.subtitleRatingIsExpanded ? 0.001 : 1.0) + (1.0 - subtitleRatingFraction) * 1.0
transition.updateAlpha(node: self.subtitleNode, alpha: subtitleAlpha)
transition.updateTransformScale(node: self.subtitleNode, scale: subtitleInnerScale)
}
}
@ -2600,6 +2639,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.isAvatarExpanded = isAvatarExpanded
if isAvatarExpanded {
self.avatarListNode.listContainerNode.selectFirstItem()
self.subtitleRatingIsExpanded = false
}
if case .animated = transition, !isAvatarExpanded {
self.avatarListNode.animateAvatarCollapse(transition: transition)

View file

@ -11496,26 +11496,150 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
private func displayMediaGalleryContextMenu(source: ContextReferenceContentNode, gesture: ContextGesture?) {
let peerId = self.peerId
if let currentPaneKey = self.paneContainerNode.currentPaneKey, case .botPreview = currentPaneKey {
var isBotPreviewOrStories = false
if let currentPaneKey = self.paneContainerNode.currentPaneKey {
if case .botPreview = currentPaneKey {
isBotPreviewOrStories = true
} else if case .stories = currentPaneKey {
isBotPreviewOrStories = true
}
}
if isBotPreviewOrStories {
guard let controller = self.controller else {
return
}
guard let pane = self.paneContainerNode.currentPane?.node as? PeerInfoStoryPaneNode else {
return
}
guard let data = self.data, let user = data.peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) else {
return
}
var items: [ContextMenuItem] = []
let strings = self.presentationData.strings
var ignoreNextActions = false
if pane.canAddMoreBotPreviews() {
items.append(.action(ContextMenuActionItem(text: strings.BotPreviews_MenuAddPreview, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
if case .botPreview = pane.scope {
guard let data = self.data, let user = data.peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) else {
return
}
var items: [ContextMenuItem] = []
let strings = self.presentationData.strings
var ignoreNextActions = false
if pane.canAddMoreBotPreviews() {
items.append(.action(ContextMenuActionItem(text: strings.BotPreviews_MenuAddPreview, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
if ignoreNextActions {
return
}
ignoreNextActions = true
a(.default)
if let self {
self.headerNode.navigationButtonContainer.performAction?(.postStory, nil, nil)
}
})))
}
if pane.canReorder() {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuReorder, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor)
}, action: { [weak pane] _, a in
if ignoreNextActions {
return
}
ignoreNextActions = true
a(.default)
if let pane {
pane.beginReordering()
}
})))
}
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuSelect, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
if ignoreNextActions {
return
}
ignoreNextActions = true
a(.default)
if let self {
self.toggleStorySelection(ids: [], isSelected: true)
}
})))
if let language = pane.currentBotPreviewLanguage {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuDeleteLanguage(language.name).string, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { [weak pane] _, a in
if ignoreNextActions {
return
}
ignoreNextActions = true
a(.default)
if let pane {
pane.presentDeleteBotPreviewLanguage()
}
})))
}
let contextController = ContextController(presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
contextController.passthroughTouchEvent = { [weak self] sourceView, point in
guard let strongSelf = self else {
return .ignore
}
let localPoint = strongSelf.view.convert(sourceView.convert(point, to: nil), from: nil)
guard let localResult = strongSelf.hitTest(localPoint, with: nil) else {
return .dismiss(consume: true, result: nil)
}
var testView: UIView? = localResult
while true {
if let testViewValue = testView {
if let node = testViewValue.asyncdisplaykit_node as? PeerInfoHeaderNavigationButton {
node.isUserInteractionEnabled = false
DispatchQueue.main.async {
node.isUserInteractionEnabled = true
}
return .dismiss(consume: false, result: nil)
} else if let node = testViewValue.asyncdisplaykit_node as? PeerInfoVisualMediaPaneNode {
node.brieflyDisableTouchActions()
return .dismiss(consume: false, result: nil)
} else if let node = testViewValue.asyncdisplaykit_node as? PeerInfoStoryPaneNode {
node.brieflyDisableTouchActions()
return .dismiss(consume: false, result: nil)
} else {
testView = testViewValue.superview
}
} else {
break
}
}
return .dismiss(consume: true, result: nil)
}
self.mediaGalleryContextMenu = contextController
controller.presentInGlobalOverlay(contextController)
} else if case .peer = pane.scope {
guard let data = self.data, let user = data.peer as? TelegramUser else {
return
}
let _ = user
var items: [ContextMenuItem] = []
let strings = self.presentationData.strings
let _ = strings
var ignoreNextActions = false
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Add Stories", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/AddStoryIcon"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
if ignoreNextActions {
return
@ -11527,92 +11651,96 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
self.headerNode.navigationButtonContainer.performAction?(.postStory, nil, nil)
}
})))
}
if pane.canReorder() {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuReorder, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor)
}, action: { [weak pane] _, a in
if ignoreNextActions {
return
}
ignoreNextActions = true
a(.default)
if let pane {
pane.beginReordering()
}
})))
}
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuSelect, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
if ignoreNextActions {
return
}
ignoreNextActions = true
a(.default)
if let self {
self.toggleStorySelection(ids: [], isSelected: true)
}
})))
if let language = pane.currentBotPreviewLanguage {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuDeleteLanguage(language.name).string, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { [weak pane] _, a in
if ignoreNextActions {
return
}
ignoreNextActions = true
a(.default)
if let pane {
pane.presentDeleteBotPreviewLanguage()
}
})))
}
let contextController = ContextController(presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
contextController.passthroughTouchEvent = { [weak self] sourceView, point in
guard let strongSelf = self else {
return .ignore
if pane.canReorder() {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuReorder, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor)
}, action: { [weak pane] _, a in
if ignoreNextActions {
return
}
ignoreNextActions = true
a(.default)
if let pane {
pane.beginReordering()
}
})))
}
let localPoint = strongSelf.view.convert(sourceView.convert(point, to: nil), from: nil)
guard let localResult = strongSelf.hitTest(localPoint, with: nil) else {
if let folder = pane.currentStoryFolder {
let _ = folder
items.append(.action(ContextMenuActionItem(text: "Delete Album", textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { [weak pane] _, a in
if ignoreNextActions {
return
}
ignoreNextActions = true
a(.default)
if let pane {
pane.presentDeleteCurrentStoryFolder()
}
})))
}
if let language = pane.currentBotPreviewLanguage {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuDeleteLanguage(language.name).string, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { [weak pane] _, a in
if ignoreNextActions {
return
}
ignoreNextActions = true
a(.default)
if let pane {
pane.presentDeleteBotPreviewLanguage()
}
})))
}
let contextController = ContextController(presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
contextController.passthroughTouchEvent = { [weak self] sourceView, point in
guard let strongSelf = self else {
return .ignore
}
let localPoint = strongSelf.view.convert(sourceView.convert(point, to: nil), from: nil)
guard let localResult = strongSelf.hitTest(localPoint, with: nil) else {
return .dismiss(consume: true, result: nil)
}
var testView: UIView? = localResult
while true {
if let testViewValue = testView {
if let node = testViewValue.asyncdisplaykit_node as? PeerInfoHeaderNavigationButton {
node.isUserInteractionEnabled = false
DispatchQueue.main.async {
node.isUserInteractionEnabled = true
}
return .dismiss(consume: false, result: nil)
} else if let node = testViewValue.asyncdisplaykit_node as? PeerInfoVisualMediaPaneNode {
node.brieflyDisableTouchActions()
return .dismiss(consume: false, result: nil)
} else if let node = testViewValue.asyncdisplaykit_node as? PeerInfoStoryPaneNode {
node.brieflyDisableTouchActions()
return .dismiss(consume: false, result: nil)
} else {
testView = testViewValue.superview
}
} else {
break
}
}
return .dismiss(consume: true, result: nil)
}
var testView: UIView? = localResult
while true {
if let testViewValue = testView {
if let node = testViewValue.asyncdisplaykit_node as? PeerInfoHeaderNavigationButton {
node.isUserInteractionEnabled = false
DispatchQueue.main.async {
node.isUserInteractionEnabled = true
}
return .dismiss(consume: false, result: nil)
} else if let node = testViewValue.asyncdisplaykit_node as? PeerInfoVisualMediaPaneNode {
node.brieflyDisableTouchActions()
return .dismiss(consume: false, result: nil)
} else if let node = testViewValue.asyncdisplaykit_node as? PeerInfoStoryPaneNode {
node.brieflyDisableTouchActions()
return .dismiss(consume: false, result: nil)
} else {
testView = testViewValue.superview
}
} else {
break
}
}
return .dismiss(consume: true, result: nil)
self.mediaGalleryContextMenu = contextController
controller.presentInGlobalOverlay(contextController)
}
self.mediaGalleryContextMenu = contextController
controller.presentInGlobalOverlay(contextController)
} else {
let _ = (self.context.engine.data.get(EngineDataMap([
TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: peerId, threadId: self.chatLocation.threadId, tag: .photo),
@ -12482,6 +12610,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
if let data = self.data, data.hasBotPreviewItems, let user = data.peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) {
rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .more, isForExpandedView: true))
}
case .stories:
if let data = self.data, data.peer?.id == self.context.account.peerId {
rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .more, isForExpandedView: true))
}
case .gifts:
//if let data = self.data, let channel = data.peer as? TelegramChannel, case .broadcast = channel.info {
rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .sort, isForExpandedView: true))

View file

@ -24,15 +24,18 @@ final class PeerInfoStoryGridScreenComponent: Component {
let context: AccountContext
let peerId: EnginePeer.Id
let scope: PeerInfoStoryGridScreen.Scope
let selectionModeCompletion: (([EngineStoryItem]) -> Void)?
init(
context: AccountContext,
peerId: EnginePeer.Id,
scope: PeerInfoStoryGridScreen.Scope
scope: PeerInfoStoryGridScreen.Scope,
selectionModeCompletion: (([EngineStoryItem]) -> Void)?
) {
self.context = context
self.peerId = peerId
self.scope = scope
self.selectionModeCompletion = selectionModeCompletion
}
static func ==(lhs: PeerInfoStoryGridScreenComponent, rhs: PeerInfoStoryGridScreenComponent) -> Bool {
@ -342,11 +345,24 @@ final class PeerInfoStoryGridScreenComponent: Component {
}
let buttonText: String
switch component.scope {
case .saved:
buttonText = self.selectedCount > 0 ? environment.strings.ChatList_Context_Archive : environment.strings.StoryList_SavedAddAction
case .archive:
buttonText = environment.strings.StoryList_SaveToProfile
var buttonIsEnabled = true
if component.selectionModeCompletion != nil {
//TODO:localize
if self.selectedCount == 0 {
buttonText = "Add Stories"
buttonIsEnabled = false
} else if self.selectedCount == 1 {
buttonText = "Add 1 Story"
} else {
buttonText = "Add \(self.selectedCount) Stories"
}
} else {
switch component.scope {
case .saved:
buttonText = self.selectedCount > 0 ? environment.strings.ChatList_Context_Archive : environment.strings.StoryList_SavedAddAction
case .archive:
buttonText = environment.strings.StoryList_SaveToProfile
}
}
let selectionPanelSize = selectionPanel.update(
@ -355,7 +371,7 @@ final class PeerInfoStoryGridScreenComponent: Component {
theme: environment.theme,
title: buttonText,
label: nil,
isEnabled: true,
isEnabled: buttonIsEnabled,
insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: environment.safeInsets.bottom, right: sideInset),
action: { [weak self] in
guard let self, let component = self.component, let environment = self.environment else {
@ -365,6 +381,12 @@ final class PeerInfoStoryGridScreenComponent: Component {
return
}
if let selectionModeCompletion = component.selectionModeCompletion {
selectionModeCompletion(Array(paneNode.selectedItems.values))
environment.controller()?.dismiss()
return
}
let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
@ -522,6 +544,12 @@ final class PeerInfoStoryGridScreenComponent: Component {
}
(self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle()
})
if component.selectionModeCompletion != nil {
paneNode.shouldOpenItemsWhileInSelectionMode = false
paneNode.setIsSelectionModeActive(true)
}
applyState = true
}
@ -562,6 +590,7 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
private let context: AccountContext
private let scope: Scope
private let selectionModeCompletion: (([EngineStoryItem]) -> Void)?
private var isDismissed: Bool = false
private var titleView: ChatTitleView?
@ -573,15 +602,18 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
public init(
context: AccountContext,
peerId: EnginePeer.Id,
scope: Scope
scope: Scope,
selectionModeCompletion: (([EngineStoryItem]) -> Void)? = nil
) {
self.context = context
self.scope = scope
self.selectionModeCompletion = selectionModeCompletion
super.init(context: context, component: PeerInfoStoryGridScreenComponent(
context: context,
peerId: peerId,
scope: scope
scope: scope,
selectionModeCompletion: selectionModeCompletion
), navigationBarAppearance: .default, theme: .default)
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
@ -636,49 +668,54 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
func updateTitle() {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
switch self.scope {
case .saved:
guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View, let paneNode = componentView.paneNode else {
return
}
let title: String?
if componentView.selectedCount != 0 {
title = presentationData.strings.StoryList_SubtitleSelected(Int32(componentView.selectedCount))
} else if let paneStatusText = componentView.paneStatusText, !paneStatusText.isEmpty {
title = paneStatusText
} else {
title = nil
}
self.titleView?.titleContent = .custom(presentationData.strings.StoryList_TitleSaved, title, false)
if paneNode.isSelectionModeActive {
self.navigationItem.setRightBarButton(self.doneBarButtonItem, animated: false)
} else {
self.navigationItem.setRightBarButton(self.moreBarButtonItem, animated: false)
}
case .archive:
guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else {
return
}
let title: String
if componentView.selectedCount != 0 {
title = presentationData.strings.StoryList_SubtitleSelected(Int32(componentView.selectedCount))
} else {
title = presentationData.strings.StoryList_TitleArchive
}
self.titleView?.titleContent = .custom(title, nil, false)
var hasMenu = false
if componentView.selectedCount != 0 {
hasMenu = true
} else if let paneNode = componentView.paneNode, !paneNode.isEmpty {
hasMenu = true
}
if hasMenu {
self.navigationItem.setRightBarButton(self.moreBarButtonItem, animated: false)
} else {
self.navigationItem.setRightBarButton(nil, animated: false)
if self.selectionModeCompletion != nil {
//TODO:localize
self.titleView?.titleContent = .custom("Add Stories", nil, false)
} else {
switch self.scope {
case .saved:
guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View, let paneNode = componentView.paneNode else {
return
}
let title: String?
if componentView.selectedCount != 0 {
title = presentationData.strings.StoryList_SubtitleSelected(Int32(componentView.selectedCount))
} else if let paneStatusText = componentView.paneStatusText, !paneStatusText.isEmpty {
title = paneStatusText
} else {
title = nil
}
self.titleView?.titleContent = .custom(presentationData.strings.StoryList_TitleSaved, title, false)
if paneNode.isSelectionModeActive {
self.navigationItem.setRightBarButton(self.doneBarButtonItem, animated: false)
} else {
self.navigationItem.setRightBarButton(self.moreBarButtonItem, animated: false)
}
case .archive:
guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else {
return
}
let title: String
if componentView.selectedCount != 0 {
title = presentationData.strings.StoryList_SubtitleSelected(Int32(componentView.selectedCount))
} else {
title = presentationData.strings.StoryList_TitleArchive
}
self.titleView?.titleContent = .custom(title, nil, false)
var hasMenu = false
if componentView.selectedCount != 0 {
hasMenu = true
} else if let paneNode = componentView.paneNode, !paneNode.isEmpty {
hasMenu = true
}
if hasMenu {
self.navigationItem.setRightBarButton(self.moreBarButtonItem, animated: false)
} else {
self.navigationItem.setRightBarButton(nil, animated: false)
}
}
}
}

View file

@ -54,6 +54,8 @@ swift_library(
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramUI/Components/CheckComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/BottomButtonPanelComponent",
"//submodules/PromptUI",
],
visibility = [
"//visibility:public",

View file

@ -44,6 +44,8 @@ import MultilineTextComponent
import LocationUI
import TabSelectorComponent
import LanguageSelectionScreen
import PromptUI
import BottomButtonPanelComponent
private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6)
private let mediaBadgeTextColor = UIColor.white
@ -1543,7 +1545,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
}
private let context: AccountContext
private let scope: Scope
public let scope: Scope
private let isProfileEmbedded: Bool
private let canManageStories: Bool
@ -1568,7 +1570,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
private var mapInfoNode: LocationInfoListItemNode?
private var searchHeader: ComponentView<Empty>?
private var botPreviewLanguageTab: ComponentView<Empty>?
private var folderTab: ComponentView<Empty>?
private var botPreviewFooter: ComponentView<Empty>?
private var barBackgroundLayer: SimpleLayer?
@ -1583,6 +1585,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
private var didUpdateItemsOnce: Bool = false
private var selectionPanel: ComponentView<Empty>?
private var actionPanel: ComponentView<Empty>?
private var isDeceleratingAfterTracking = false
@ -1627,6 +1630,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
public var isEmptyUpdated: (Bool) -> Void = { _ in }
public var shouldOpenItemsWhileInSelectionMode: Bool = true
public private(set) var isSelectionModeActive: Bool
private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData)?
@ -1647,6 +1651,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
public var tabBarOffset: CGFloat {
if case .botPreview = self.scope {
return 0.0
} else if case let .peer(peerId, _, isArchived) = self.scope, peerId == self.context.account.peerId, !isArchived, self.isProfileEmbedded {
return 0.0
} else {
return self.itemGrid.coveringInsetOffset
}
@ -1660,6 +1666,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
private var currentBotPreviewLanguages: [StoryListContext.State.Language] = []
private var removedBotPreviewLanguages = Set<String>()
private var currentStoryFolders: [StoryListContext.State.Folder] = []
private var removedStoryFolders = Set<Int64>()
private var numberOfItemsToRequest: Int = 50
private var isRequestingView: Bool = false
private var isFirstHistoryView: Bool = true
@ -1674,7 +1683,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
private let maxBotPreviewCount: Int
private let defaultListSource: StoryListContext
private var cachedListSources: [String: StoryListContext] = [:]
private var cachedListSources: [AnyHashable: StoryListContext] = [:]
public var currentBotPreviewLanguage: (id: String, name: String)? {
guard let listSource = self.listSource as? BotPreviewStoryListContext else {
@ -1688,6 +1697,19 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
}
return (language.id, language.name)
}
public var currentStoryFolder: (id: Int64, title: String)? {
guard let listSource = self.listSource as? PeerStoryListContext else {
return nil
}
guard let id = listSource.folderId else {
return nil
}
guard let folder = self.currentStoryFolders.first(where: { $0.id == id }) else {
return nil
}
return (folder.id, folder.title)
}
public var openCurrentDate: (() -> Void)?
public var paneDidScroll: (() -> Void)?
@ -1749,7 +1771,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
} else {
switch self.scope {
case let .peer(id, _, isArchived):
self.listSource = PeerStoryListContext(account: context.account, peerId: id, isArchived: isArchived)
self.listSource = PeerStoryListContext(account: context.account, peerId: id, isArchived: isArchived, folderId: nil)
case let .search(peerId, query):
self.listSource = SearchStoryListContext(account: context.account, source: .hashtag(peerId, query))
case let .location(coordinates, venue):
@ -1798,7 +1820,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
return
}
if self.isProfileEmbedded {
if self.isProfileEmbedded || !self.shouldOpenItemsWhileInSelectionMode {
if let selectedIds = self.itemInteraction.selectedIds {
self.itemInteraction.toggleSelection(item.story.id, !selectedIds.contains(item.story.id))
return
@ -2275,7 +2297,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: false)
if case let .peer(id, _, isArchived) = self.scope, id == context.account.peerId, !isArchived {
self.preloadArchiveListContext = PeerStoryListContext(account: context.account, peerId: context.account.peerId, isArchived: true)
self.preloadArchiveListContext = PeerStoryListContext(account: context.account, peerId: context.account.peerId, isArchived: true, folderId: nil)
}
if case let .location(_, venue) = scope, let mapNode = self.mapNode {
@ -2447,6 +2469,122 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
var items: [ContextMenuItem] = []
if canManage, case let .peer(peerId, _, isArchived) = self.scope {
if peerId == self.context.account.peerId && self.isProfileEmbedded {
if let folder = self.currentStoryFolder {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Remove from Album", textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, f in
guard let self else {
f(.default)
return
}
if let listSource = self.listSource as? PeerStoryListContext {
listSource.removeFromFolder(id: folder.id, itemIds: [item.id])
}
f(.dismissWithoutContent)
})))
} else {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Add to Album", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
guard let self, let c else {
f(.default)
return
}
Task { @MainActor [weak self, weak c] in
guard let self, let c else {
return
}
let (peerReference, folderPreviews) = await PeerStoryListContext.folderPreviews(peerId: peerId, account: self.context.account).get()
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
}, iconPosition: .left, action: { c ,f in
c?.popItems()
})))
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: "New Album", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { [weak self] c, f in
guard let self else {
f(.default)
return
}
c?.dismiss(completion: { [weak self] in
guard let self else {
return
}
self.presentAddStoryFolder(addItems: [item])
})
})))
for folderPreview in folderPreviews {
var iconSource: ContextMenuActionItemIconSource?
if let story = folderPreview.item {
var imageSignal: Signal<UIImage?, NoError>?
var selectedMedia: Media?
if let image = story.media._asMedia() as? TelegramMediaImage {
selectedMedia = image
} else if let file = story.media._asMedia() as? TelegramMediaFile {
selectedMedia = file
}
if let selectedMedia {
if let result = self.directMediaImageCache.getImage(peer: peerReference, story: story, media: selectedMedia, width: 24, aspectRatio: 1.0, possibleWidths: [24], includeBlurred: false, synchronous: true) {
if let loadSignal = result.loadSignal {
imageSignal = .single(result.image) |> then(loadSignal)
} else {
imageSignal = .single(result.image)
}
}
}
if let imageSignal {
iconSource = ContextMenuActionItemIconSource(
size: CGSize(width: 24.0, height: 24.0),
cornerRadius: 5.0,
signal: imageSignal
)
}
}
var icon: (PresentationTheme) -> UIImage? = { _ in nil }
if iconSource == nil {
icon = { theme in
return generateImage(CGSize(width: 24.0, height: 24.0), opaque: false, scale: nil, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(theme.contextMenu.primaryColor.withMultipliedAlpha(0.1).cgColor)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 5.0).cgPath)
context.fillPath()
})
}
}
items.append(.action(ContextMenuActionItem(text: folderPreview.folder.title, icon: icon, iconSource: iconSource, iconPosition: .left, action: { [weak self] c, f in
guard let self else {
f(.default)
return
}
c?.dismiss(completion: {})
if let listSource = self.listSource as? PeerStoryListContext {
listSource.addToFolder(id: folderPreview.folder.id, items: [item])
}
})))
}
c.pushItems(items: .single(ContextController.Items(content: .list(items))))
}
})))
}
items.append(.separator)
}
items.append(.action(ContextMenuActionItem(text: !isArchived ? self.presentationData.strings.StoryList_ItemAction_Archive : self.presentationData.strings.StoryList_ItemAction_Unarchive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isArchived ? "Chat/Context Menu/Archive" : "Chat/Context Menu/Unarchive"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
guard let self else {
f(.default)
@ -2784,6 +2922,13 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
}
botPreviewLanguages.sort(by: { $0.name < $1.name })
var storyFolders = self.currentStoryFolders
for folder in state.availableFolders {
if !storyFolders.contains(where: { $0.id == folder.id }) && !self.removedStoryFolders.contains(folder.id) {
storyFolders.append(folder)
}
}
var hadLocalItems = false
if let currentListState = self.currentListState {
for item in currentListState.items {
@ -2811,8 +2956,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
self.updateItemsFromState(state: state, firstTime: firstTime, reloadAtTop: reloadAtTop, synchronous: synchronous, animated: false)
if self.currentBotPreviewLanguages != botPreviewLanguages || reloadAtTop {
if self.currentBotPreviewLanguages != botPreviewLanguages || self.currentStoryFolders != storyFolders || reloadAtTop {
self.currentBotPreviewLanguages = botPreviewLanguages
self.currentStoryFolders = storyFolders
if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams {
self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: synchronous, transition: .immediate)
}
@ -2870,7 +3016,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
isReorderable = !item.storyItem.isPending
case let .peer(id, _, _):
if id == self.context.account.peerId {
isReorderable = state.pinnedIds.contains(item.storyItem.id)
if self.currentStoryFolder != nil {
isReorderable = true
} else {
isReorderable = state.pinnedIds.contains(item.storyItem.id)
}
}
case let .search(peerId, _):
if peerId != nil {
@ -2963,11 +3113,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
if case .botPreview = self.scope {
} else if case let .peer(id, _, _) = self.scope {
if id == self.context.account.peerId {
let maxPinnedIndex = items.items.lastIndex(where: { ($0 as? VisualMediaItem)?.isPinned == true })
if let maxPinnedIndex {
toIndex = min(toIndex, maxPinnedIndex)
if self.currentStoryFolder != nil {
} else {
return
let maxPinnedIndex = items.items.lastIndex(where: { ($0 as? VisualMediaItem)?.isPinned == true })
if let maxPinnedIndex {
toIndex = min(toIndex, maxPinnedIndex)
} else {
return
}
}
}
} else {
@ -3379,8 +3532,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
if let _ = self.mapNode {
self.updateMapLayout(size: currentParams.size, topInset: currentParams.topInset, bottomInset: currentParams.bottomInset, deviceMetrics: currentParams.deviceMetrics, transition: transition)
}
if case .botPreview = self.scope, self.canManageStories {
self.updateBotPreviewLanguageTab(size: currentParams.size, topInset: currentParams.topInset, transition: transition)
if case let .peer(peerId, _, isArchived) = self.scope, peerId == self.context.account.peerId, !isArchived, self.isProfileEmbedded {
self.updateFolderTab(size: currentParams.size, topInset: currentParams.topInset, transition: transition)
} else if case .botPreview = self.scope, self.canManageStories {
self.updateFolderTab(size: currentParams.size, topInset: currentParams.topInset, transition: transition)
self.updateBotPreviewFooter(size: currentParams.size, bottomInset: 0.0, transition: transition)
}
}
@ -3536,40 +3691,69 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
}
}
private func updateBotPreviewLanguageTab(size: CGSize, topInset: CGFloat, transition: ContainedViewLayoutTransition) {
guard case .botPreview = self.scope, self.canManageStories else {
private func updateFolderTab(size: CGSize, topInset: CGFloat, transition: ContainedViewLayoutTransition) {
var displayFolderTab = false
if case .botPreview = self.scope, self.canManageStories {
displayFolderTab = true
} else if case let .peer(peerId, _, isArchived) = self.scope, peerId == self.context.account.peerId, !isArchived, self.isProfileEmbedded {
displayFolderTab = true
}
if !displayFolderTab {
return
}
let botPreviewLanguageTab: ComponentView<Empty>
if let current = self.botPreviewLanguageTab {
botPreviewLanguageTab = current
let folderTab: ComponentView<Empty>
if let current = self.folderTab {
folderTab = current
} else {
botPreviewLanguageTab = ComponentView()
self.botPreviewLanguageTab = botPreviewLanguageTab
folderTab = ComponentView()
self.folderTab = folderTab
}
var languageItems: [TabSelectorComponent.Item] = []
languageItems.append(TabSelectorComponent.Item(
var folderItems: [TabSelectorComponent.Item] = []
let mainTitle: String
let addTitle: String
if case .botPreview = self.scope {
mainTitle = self.presentationData.strings.BotPreviews_LanguageTab_Main
addTitle = self.presentationData.strings.BotPreviews_LanguageTab_Add
} else {
//TODO:localize
mainTitle = "All Stories"
addTitle = "+ Add Album"
}
folderItems.append(TabSelectorComponent.Item(
id: AnyHashable("_main"),
title: self.presentationData.strings.BotPreviews_LanguageTab_Main
title: mainTitle
))
for language in self.currentBotPreviewLanguages {
languageItems.append(TabSelectorComponent.Item(
id: AnyHashable(language.id),
title: language.name
))
if case .botPreview = self.scope {
for language in self.currentBotPreviewLanguages {
folderItems.append(TabSelectorComponent.Item(
id: AnyHashable(language.id),
title: language.name
))
}
} else {
for folder in self.currentStoryFolders {
folderItems.append(TabSelectorComponent.Item(
id: AnyHashable(folder.id),
title: folder.title
))
}
}
languageItems.append(TabSelectorComponent.Item(
folderItems.append(TabSelectorComponent.Item(
id: AnyHashable("_add"),
title: self.presentationData.strings.BotPreviews_LanguageTab_Add
title: addTitle
))
var selectedLanguageId = "_main"
var selectedId = AnyHashable("_main")
if let listSource = self.listSource as? BotPreviewStoryListContext, let language = listSource.language {
selectedLanguageId = language
selectedId = AnyHashable(language)
} else if let listSource = self.listSource as? PeerStoryListContext, let folderId = listSource.folderId {
selectedId = AnyHashable(folderId)
}
let botPreviewLanguageTabSize = botPreviewLanguageTab.update(
let folderTabSize = folderTab.update(
transition: ComponentTransition(transition),
component: AnyComponent(TabSelectorComponent(
colors: TabSelectorComponent.Colors(
@ -3581,39 +3765,53 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
spacing: 9.0,
verticalInset: 11.0
),
items: languageItems,
selectedId: AnyHashable(selectedLanguageId),
items: folderItems,
selectedId: selectedId,
setSelectedId: { [weak self] id in
guard let self, let id = id.base as? String else {
guard let self else {
return
}
if id == "_add" {
self.presentAddBotPreviewLanguage()
} else if id == "_main" {
self.setBotPreviewLanguage(id: nil, assumeEmpty: false)
} else if let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) {
self.setBotPreviewLanguage(id: language.id, assumeEmpty: false)
if let id = id.base as? String {
if id == "_add" {
if case .botPreview = self.scope {
self.presentAddBotPreviewLanguage()
} else {
self.presentAddStoryFolder()
}
} else if id == "_main" {
if case .botPreview = self.scope {
self.setBotPreviewLanguage(id: nil, assumeEmpty: false)
} else {
self.setStoryFolder(id: nil, assumeEmpty: false)
}
} else if let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) {
self.setBotPreviewLanguage(id: language.id, assumeEmpty: false)
}
} else if let id = id.base as? Int64 {
if let folder = self.currentStoryFolders.first(where: { $0.id == id }) {
self.setStoryFolder(id: folder.id, assumeEmpty: false)
}
}
}
)),
environment: {},
containerSize: CGSize(width: size.width, height: 44.0)
)
var botPreviewLanguageTabFrame = CGRect(origin: CGPoint(x: floor((size.width - botPreviewLanguageTabSize.width) * 0.5), y: topInset - 11.0), size: botPreviewLanguageTabSize)
var folderTabFrame = CGRect(origin: CGPoint(x: floor((size.width - folderTabSize.width) * 0.5), y: topInset - 11.0), size: folderTabSize)
let effectiveScrollingOffset: CGFloat
effectiveScrollingOffset = self.itemGrid.scrollingOffset
botPreviewLanguageTabFrame.origin.y -= effectiveScrollingOffset
folderTabFrame.origin.y -= effectiveScrollingOffset
let isSelectingOrReordering = self.isReordering || self.itemInteraction.selectedIds != nil
if let botPreviewLanguageTabView = botPreviewLanguageTab.view {
if botPreviewLanguageTabView.superview == nil {
self.view.addSubview(botPreviewLanguageTabView)
if let folderTabView = folderTab.view {
if folderTabView.superview == nil {
self.view.addSubview(folderTabView)
}
transition.updateFrame(view: botPreviewLanguageTabView, frame: botPreviewLanguageTabFrame)
transition.updateAlpha(layer: botPreviewLanguageTabView.layer, alpha: isSelectingOrReordering ? 0.5 : 1.0)
botPreviewLanguageTabView.isUserInteractionEnabled = !isSelectingOrReordering
transition.updateFrame(view: folderTabView, frame: folderTabFrame)
transition.updateAlpha(layer: folderTabView.layer, alpha: isSelectingOrReordering ? 0.5 : 1.0)
folderTabView.isUserInteractionEnabled = !isSelectingOrReordering
}
}
@ -3742,15 +3940,24 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
var listBottomInset = bottomInset
var bottomInset = bottomInset
var displayFolderTab = false
if case .botPreview = self.scope, self.canManageStories {
updateBotPreviewLanguageTab(size: size, topInset: topInset, transition: transition)
displayFolderTab = true
} else if case let .peer(peerId, _, isArchived) = self.scope, peerId == self.context.account.peerId, !isArchived, self.isProfileEmbedded {
displayFolderTab = true
}
if displayFolderTab {
updateFolderTab(size: size, topInset: topInset, transition: transition)
gridTopInset += 50.0
updateBotPreviewFooter(size: size, bottomInset: 0.0, transition: transition)
if let botPreviewFooterView = self.botPreviewFooter?.view {
if case .botPreview = self.scope {
updateBotPreviewFooter(size: size, bottomInset: 0.0, transition: transition)
if let botPreviewFooterView = self.botPreviewFooter?.view {
listBottomInset += 18.0 + botPreviewFooterView.bounds.height
}
}
}
if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories, case let .peer(peerId, _, isArchived) = self.scope {
let selectionPanel: ComponentView<Empty>
@ -3930,13 +4137,200 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
} else if let selectionPanel = self.selectionPanel {
self.selectionPanel = nil
if let selectionPanelView = selectionPanel.view {
transition.updateFrame(view: selectionPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height), size: selectionPanelView.bounds.size))
transition.updateFrame(view: selectionPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height), size: selectionPanelView.bounds.size), completion: { [weak selectionPanelView] _ in
selectionPanelView?.removeFromSuperview()
})
}
}
if self.selectionPanel == nil, self.isProfileEmbedded, self.canManageStories, case let .peer(peerId, _, isArchived) = self.scope, peerId == self.context.account.peerId, !isArchived, self.isProfileEmbedded, self.currentStoryFolder != nil, let items = self.items, !items.items.isEmpty {
let actionPanel: ComponentView<Empty>
var actionPanelTransition = ComponentTransition(transition)
if let current = self.actionPanel {
actionPanel = current
} else {
actionPanelTransition = actionPanelTransition.withAnimation(.none)
actionPanel = ComponentView()
self.actionPanel = actionPanel
}
//TODO:localize
let actionPanelSize = actionPanel.update(
transition: actionPanelTransition,
component: AnyComponent(BottomButtonPanelComponent(
theme: presentationData.theme,
title: "Add Stories",
label: nil,
isEnabled: true,
insets: UIEdgeInsets(top: 0.0, left: sideInset + 12.0, bottom: bottomInset, right: sideInset + 12.0),
action: { [weak self] in
guard let self else {
return
}
self.presentAddStoriesToFolder()
}
)),
environment: {},
containerSize: size
)
let actionPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - actionPanelSize.height), size: actionPanelSize)
if let actionPanelView = actionPanel.view {
if actionPanelView.superview == nil {
self.view.addSubview(actionPanelView)
transition.animatePositionAdditive(layer: actionPanelView.layer, offset: CGPoint(x: 0.0, y: actionPanelFrame.height))
}
actionPanelTransition.setFrame(view: actionPanelView, frame: actionPanelFrame)
}
bottomInset = actionPanelSize.height
listBottomInset += actionPanelSize.height
} else if let actionPanel = self.actionPanel {
self.actionPanel = nil
if let actionPanelView = actionPanel.view {
transition.updateFrame(view: actionPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height), size: actionPanelView.bounds.size), completion: { [weak actionPanelView] _ in
actionPanelView?.removeFromSuperview()
})
}
}
transition.updateFrame(node: self.contextGestureContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
if case let .peer(_, _, isArchived) = self.scope, let items = self.items, items.items.isEmpty, items.count == 0 {
if case let .peer(peerId, _, isArchived) = self.scope, let items = self.items, items.items.isEmpty, items.count == 0 {
if peerId == self.context.account.peerId, self.isProfileEmbedded, self.currentStoryFolder != nil {
let emptyStateView: ComponentView<Empty>
var emptyStateTransition = ComponentTransition(transition)
if let current = self.emptyStateView {
emptyStateView = current
} else {
emptyStateTransition = .immediate
emptyStateView = ComponentView()
self.emptyStateView = emptyStateView
}
//TODO:localize
let emptyStateSize = emptyStateView.update(
transition: emptyStateTransition,
component: AnyComponent(EmptyStateIndicatorComponent(
context: self.context,
theme: presentationData.theme,
fitToHeight: self.isProfileEmbedded,
animationName: nil,
title: "Organize Your Stories",
text: "Add some stories to this album.",
actionTitle: "Add to Album",
action: { [weak self] in
guard let self else {
return
}
self.presentAddStoriesToFolder()
},
additionalActionTitle: nil,
additionalAction: {},
additionalActionSeparator: nil
)),
environment: {},
containerSize: CGSize(width: size.width, height: size.height - gridTopInset - bottomInset)
)
let emptyStateFrame: CGRect
if self.isProfileEmbedded {
emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: max(gridTopInset + 22.0, floor((visibleHeight - gridTopInset - bottomInset - emptyStateSize.height) * 0.5))), size: emptyStateSize)
} else {
emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: gridTopInset), size: emptyStateSize)
}
if let emptyStateComponentView = emptyStateView.view {
if emptyStateComponentView.superview == nil {
self.view.addSubview(emptyStateComponentView)
if self.didUpdateItemsOnce {
emptyStateComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
emptyStateTransition.setFrame(view: emptyStateComponentView, frame: emptyStateFrame)
}
let backgroundColor: UIColor
if self.isProfileEmbedded, case .botPreview = self.scope {
backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else if self.isProfileEmbedded {
backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else {
backgroundColor = presentationData.theme.list.blocksBackgroundColor
}
if self.didUpdateItemsOnce {
ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)).setBackgroundColor(view: self.view, color: backgroundColor)
} else {
self.view.backgroundColor = backgroundColor
}
} else {
let emptyStateView: ComponentView<Empty>
var emptyStateTransition = ComponentTransition(transition)
if let current = self.emptyStateView {
emptyStateView = current
} else {
emptyStateTransition = .immediate
emptyStateView = ComponentView()
self.emptyStateView = emptyStateView
}
let emptyStateSize = emptyStateView.update(
transition: emptyStateTransition,
component: AnyComponent(EmptyStateIndicatorComponent(
context: self.context,
theme: presentationData.theme,
fitToHeight: self.isProfileEmbedded,
animationName: "StoryListEmpty",
title: isArchived ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyPosts_Title,
text: isArchived ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyPosts_Text,
actionTitle: isArchived ? nil : presentationData.strings.StoryList_SavedAddAction,
action: { [weak self] in
guard let self else {
return
}
self.emptyAction?()
},
additionalActionTitle: (isArchived || self.isProfileEmbedded) ? nil : presentationData.strings.StoryList_SavedEmptyAction,
additionalAction: { [weak self] in
guard let self else {
return
}
self.additionalEmptyAction?()
}
)),
environment: {},
containerSize: CGSize(width: size.width, height: size.height - gridTopInset - bottomInset)
)
let emptyStateFrame: CGRect
if self.isProfileEmbedded {
emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: max(gridTopInset, floor((visibleHeight - gridTopInset - bottomInset - emptyStateSize.height) * 0.5))), size: emptyStateSize)
} else {
emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: gridTopInset), size: emptyStateSize)
}
if let emptyStateComponentView = emptyStateView.view {
if emptyStateComponentView.superview == nil {
self.view.addSubview(emptyStateComponentView)
if self.didUpdateItemsOnce {
emptyStateComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
emptyStateTransition.setFrame(view: emptyStateComponentView, frame: emptyStateFrame)
}
let backgroundColor: UIColor
if self.isProfileEmbedded {
backgroundColor = presentationData.theme.list.plainBackgroundColor
} else {
backgroundColor = presentationData.theme.list.blocksBackgroundColor
}
if self.didUpdateItemsOnce {
ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)).setBackgroundColor(view: self.view, color: backgroundColor)
} else {
self.view.backgroundColor = backgroundColor
}
}
} else if case .botPreview = self.scope, let items = self.items, items.items.isEmpty, items.count == 0 {
let emptyStateView: ComponentView<Empty>
var emptyStateTransition = ComponentTransition(transition)
if let current = self.emptyStateView {
@ -3946,29 +4340,44 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
emptyStateView = ComponentView()
self.emptyStateView = emptyStateView
}
var isMainLanguage = true
if let listSource = self.listSource as? BotPreviewStoryListContext, let _ = listSource.language {
isMainLanguage = false
}
let emptyStateSize = emptyStateView.update(
transition: emptyStateTransition,
component: AnyComponent(EmptyStateIndicatorComponent(
context: self.context,
theme: presentationData.theme,
fitToHeight: self.isProfileEmbedded,
animationName: "StoryListEmpty",
title: isArchived ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyPosts_Title,
text: isArchived ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyPosts_Text,
actionTitle: isArchived ? nil : presentationData.strings.StoryList_SavedAddAction,
animationName: nil,
title: presentationData.strings.BotPreviews_Empty_Title,
text: presentationData.strings.BotPreviews_Empty_Text(Int32(self.maxBotPreviewCount)),
actionTitle: self.canManageStories ? presentationData.strings.BotPreviews_Empty_Add : nil,
action: { [weak self] in
guard let self else {
return
}
self.emptyAction?()
if self.canAddMoreBotPreviews() {
self.emptyAction?()
} else {
self.presentUnableToAddMorePreviewsAlert()
}
},
additionalActionTitle: (isArchived || self.isProfileEmbedded) ? nil : presentationData.strings.StoryList_SavedEmptyAction,
additionalActionTitle: self.canManageStories ? (isMainLanguage ? presentationData.strings.BotPreviews_Empty_AddTranslation : presentationData.strings.BotPreviews_Empty_DeleteTranslation) : nil,
additionalAction: { [weak self] in
guard let self else {
return
}
self.additionalEmptyAction?()
}
if isMainLanguage {
self.presentAddBotPreviewLanguage()
} else {
self.presentDeleteBotPreviewLanguage()
}
},
additionalActionSeparator: self.canManageStories ? presentationData.strings.BotPreviews_Empty_Separator : nil
)),
environment: {},
containerSize: CGSize(width: size.width, height: size.height - gridTopInset - bottomInset)
@ -3976,7 +4385,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
let emptyStateFrame: CGRect
if self.isProfileEmbedded {
emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: max(gridTopInset, floor((visibleHeight - gridTopInset - bottomInset - emptyStateSize.height) * 0.5))), size: emptyStateSize)
emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: max(gridTopInset + 22.0, floor((visibleHeight - gridTopInset - bottomInset - emptyStateSize.height) * 0.5))), size: emptyStateSize)
} else {
emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: gridTopInset), size: emptyStateSize)
}
@ -3992,8 +4401,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
}
let backgroundColor: UIColor
if self.isProfileEmbedded {
backgroundColor = presentationData.theme.list.plainBackgroundColor
if self.isProfileEmbedded, case .botPreview = self.scope {
backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else if self.isProfileEmbedded {
backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else {
backgroundColor = presentationData.theme.list.blocksBackgroundColor
}
@ -4003,7 +4414,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
} else {
self.view.backgroundColor = backgroundColor
}
} else if case .botPreview = self.scope, let items = self.items, items.items.isEmpty, items.count == 0 {
} else if case let .peer(peerId, _, isArchived) = self.scope, peerId == self.context.account.peerId, !isArchived, self.isProfileEmbedded, let items = self.items, items.items.isEmpty, items.count == 0 {
let emptyStateView: ComponentView<Empty>
var emptyStateTransition = ComponentTransition(transition)
if let current = self.emptyStateView {
@ -4073,6 +4484,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
let backgroundColor: UIColor
if self.isProfileEmbedded, case .botPreview = self.scope {
backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else if self.isProfileEmbedded, case let .peer(peerId, _, isArchived) = self.scope, peerId == self.context.account.peerId, self.isProfileEmbedded, !isArchived {
backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else if self.isProfileEmbedded {
backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else {
@ -4101,6 +4514,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
if self.isProfileEmbedded, case .botPreview = self.scope {
subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor)
} else if self.isProfileEmbedded, case let .peer(peerId, _, isArchived) = self.scope, peerId == self.context.account.peerId, !isArchived, self.isProfileEmbedded {
subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor)
} else if self.isProfileEmbedded {
subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.plainBackgroundColor)
} else {
@ -4109,6 +4524,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
} else {
if self.isProfileEmbedded, case .botPreview = self.scope {
self.view.backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else if self.isProfileEmbedded, case let .peer(peerId, _, isArchived) = self.scope, peerId == self.context.account.peerId, self.isProfileEmbedded, !isArchived {
self.view.backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else {
if case let .search(peerId, _) = self.scope, peerId != nil {
@ -4132,6 +4549,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
var adjustForSmallCount = true
if case .botPreview = self.scope {
adjustForSmallCount = false
} else if self.currentStoryFolder != nil {
adjustForSmallCount = false
}
self.itemGrid.pinchEnabled = items.count > 2 && !self.isReordering
@ -4234,10 +4653,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
}
public func canReorder() -> Bool {
guard let items = self.items else {
return false
if case .botPreview = self.scope {
guard let items = self.items else {
return false
}
return items.count > 1
} else {
if self.currentStoryFolder == nil {
return false
}
guard let items = self.items else {
return false
}
return items.count > 1
}
return items.count > 1
}
private func presentAddBotPreviewLanguage() {
@ -4250,6 +4680,78 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
}))
}
private func presentAddStoryFolder(addItems: [EngineStoryItem] = []) {
//TODO:localize
let promptController = promptController(
sharedContext: self.context.sharedContext,
updatedPresentationData: nil,
text: "Create a New Album",
titleFont: .bold,
subtitle: "Choose a name for your album and start adding your stories there.",
value: "",
placeholder: "Title",
characterLimit: 20,
apply: { [weak self] value in
guard let self else {
return
}
if let value {
if let listSource = self.listSource as? PeerStoryListContext {
listSource.addFolder(title: value, completion: { [weak self] id in
Queue.mainQueue().async {
guard let self, let listSource = self.listSource as? PeerStoryListContext else {
return
}
if !addItems.isEmpty {
listSource.addToFolder(id: id, items: addItems)
}
self.setStoryFolder(id: id, assumeEmpty: addItems.isEmpty)
}
})
}
}
}
)
self.parentController?.present(promptController, in: .window(.root))
}
private func presentAddStoriesToFolder() {
guard case let .peer(peerId, _, _) = self.scope else {
return
}
guard let folder = self.currentStoryFolder else {
return
}
let controller = self.context.sharedContext.makeStorySelectionController(context: self.context, peerId: peerId, completion: { [weak self] items in
guard let self else {
return
}
if let listSource = self.listSource as? PeerStoryListContext {
listSource.addToFolder(id: folder.id, items: items)
}
})
controller.navigationPresentation = .modal
self.parentController?.push(controller)
}
public func presentDeleteCurrentStoryFolder() {
if let folder = self.currentStoryFolder {
self.setStoryFolder(id: nil, assumeEmpty: false)
self.currentStoryFolders.removeAll(where: { $0.id == folder.id })
self.removedStoryFolders.insert(folder.id)
if let listContext = self.listSource as? PeerStoryListContext {
listContext.removeFolder(id: folder.id)
}
if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams {
self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: .immediate)
}
}
}
public func presentUnableToAddMorePreviewsAlert() {
self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.BotPreviews_AlertTooManyPreviews(Int32(self.maxBotPreviewCount)), actions: [
TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {
@ -4335,12 +4837,32 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
}
if let id {
if let cachedListSource = self.cachedListSources[id] {
if let cachedListSource = self.cachedListSources[AnyHashable(id)] {
self.listSource = cachedListSource
} else {
let listSource = BotPreviewStoryListContext(account: self.context.account, engine: self.context.engine, peerId: peerId, language: id, assumeEmpty: assumeEmpty)
self.listSource = listSource
self.cachedListSources[id] = listSource
self.cachedListSources[AnyHashable(id)] = listSource
}
} else {
self.listSource = self.defaultListSource
}
self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: true)
}
private func setStoryFolder(id: Int64?, assumeEmpty: Bool) {
if let listSource = self.listSource as? PeerStoryListContext, listSource.folderId == id {
return
}
if let id {
if let cachedListSource = self.cachedListSources[AnyHashable(id)] {
self.listSource = cachedListSource
} else {
let listSource = PeerStoryListContext(account: self.context.account, peerId: self.context.account.peerId, isArchived: false, folderId: id)
self.listSource = listSource
//self.cachedListSources[AnyHashable(id)] = listSource
}
} else {
self.listSource = self.defaultListSource
@ -4388,20 +4910,26 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
listSource.reorderItems(media: reorderedMedia)
}
} else if case let .peer(id, _, _) = self.scope, id == self.context.account.peerId, let items = self.items {
var updatedPinnedIds: [Int32] = []
for id in reorderedIds {
inner: for item in items.items {
if let item = item as? VisualMediaItem {
if item.storyId == id {
if item.isPinned {
updatedPinnedIds.append(id.id)
break inner
if let _ = self.currentStoryFolder {
if let listSource = self.listSource as? PeerStoryListContext {
listSource.reorderItemsInFolder(itemIds: reorderedIds.map { $0.id })
}
} else {
var updatedPinnedIds: [Int32] = []
for id in reorderedIds {
inner: for item in items.items {
if let item = item as? VisualMediaItem {
if item.storyId == id {
if item.isPinned {
updatedPinnedIds.append(id.id)
break inner
}
}
}
}
}
let _ = self.context.engine.messages.updatePinnedToTopStories(peerId: id, ids: updatedPinnedIds).startStandalone()
}
let _ = self.context.engine.messages.updatePinnedToTopStories(peerId: id, ids: updatedPinnedIds).startStandalone()
}
}

View file

@ -99,6 +99,8 @@ swift_library(
"//submodules/TelegramUI/Components/InteractiveTextComponent",
"//submodules/TelegramUI/Components/SaveProgressScreen",
"//submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController",
"//submodules/DirectMediaImageCache",
"//submodules/PromptUI",
],
visibility = [
"//visibility:public",

View file

@ -1456,6 +1456,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
}
private let context: AccountContext
let listContext: StoryListContext
public private(set) var stateValue: StoryContentContextState?
public var state: Signal<StoryContentContextState, NoError> {
@ -1486,6 +1487,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
public init(context: AccountContext, listContext: StoryListContext, initialId: StoryId?, splitIndexIntoDays: Bool) {
self.context = context
self.listContext = listContext
let preferHighQualityStories: Signal<Bool, NoError> = combineLatest(
context.sharedContext.automaticMediaDownloadSettings

View file

@ -1688,6 +1688,39 @@ private final class StoryContainerScreenComponent: Component {
performReorderAction?()
})
},
createToFolder: { [weak self] title, items in
guard let self, let component = self.component else {
return
}
if let content = component.content as? PeerStoryListContentContextImpl {
if let listSource = content.listContext as? PeerStoryListContext {
listSource.addFolder(title: title, completion: { [weak listSource] id in
Queue.mainQueue().async {
guard let listSource else {
return
}
if !items.isEmpty {
listSource.addToFolder(id: id, items: items)
}
}
})
}
}
},
addToFolder: { [weak self] folderId in
guard let self, let component = self.component else {
return
}
guard let stateValue = self.stateValue, let slice = stateValue.slice else {
return
}
if let content = component.content as? PeerStoryListContentContextImpl {
if let listSource = content.listContext as? PeerStoryListContext {
listSource.addToFolder(id: folderId, items: [slice.item.storyItem])
}
}
},
controller: { [weak self] in
return self?.environment?.controller()
},

View file

@ -44,6 +44,8 @@ import StoryFooterPanelComponent
import TelegramNotices
import SliderContextItem
import SaveProgressScreen
import DirectMediaImageCache
import PromptUI
public final class StoryAvailableReactions: Equatable {
let reactionItems: [ReactionItem]
@ -120,6 +122,8 @@ public final class StoryItemSetContainerComponent: Component {
public let delete: () -> Void
public let markAsSeen: (StoryId) -> Void
public let reorder: () -> Void
public let createToFolder: (String, [EngineStoryItem]) -> Void
public let addToFolder: (Int64) -> Void
public let controller: () -> ViewController?
public let toggleAmbientMode: () -> Void
public let keyboardInputData: Signal<ChatEntityKeyboardInputNode.InputData, NoError>
@ -157,6 +161,8 @@ public final class StoryItemSetContainerComponent: Component {
delete: @escaping () -> Void,
markAsSeen: @escaping (StoryId) -> Void,
reorder: @escaping () -> Void,
createToFolder: @escaping (String, [EngineStoryItem]) -> Void,
addToFolder: @escaping (Int64) -> Void,
controller: @escaping () -> ViewController?,
toggleAmbientMode: @escaping () -> Void,
keyboardInputData: Signal<ChatEntityKeyboardInputNode.InputData, NoError>,
@ -193,6 +199,8 @@ public final class StoryItemSetContainerComponent: Component {
self.delete = delete
self.markAsSeen = markAsSeen
self.reorder = reorder
self.createToFolder = createToFolder
self.addToFolder = addToFolder
self.controller = controller
self.toggleAmbientMode = toggleAmbientMode
self.keyboardInputData = keyboardInputData
@ -6101,6 +6109,102 @@ public final class StoryItemSetContainerComponent: Component {
var items: [ContextMenuItem] = []
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Add to Album", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
guard let self, let c else {
f(.default)
return
}
Task { @MainActor [weak self, weak c] in
guard let self, let component = self.component, let peerId = component.slice.item.peerId, let c else {
return
}
let (peerReference, folderPreviews) = await PeerStoryListContext.folderPreviews(peerId: peerId, account: component.context.account).get()
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
}, iconPosition: .left, action: { c ,f in
c?.popItems()
})))
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: "New Album", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { [weak self] c, f in
guard let self else {
f(.default)
return
}
c?.dismiss(completion: { [weak self] in
guard let self, let component = self.component else {
return
}
self.presentAddStoryFolder(addItems: [component.slice.item.storyItem])
})
})))
for folderPreview in folderPreviews {
var iconSource: ContextMenuActionItemIconSource?
if let story = folderPreview.item {
var imageSignal: Signal<UIImage?, NoError>?
var selectedMedia: Media?
if let image = story.media._asMedia() as? TelegramMediaImage {
selectedMedia = image
} else if let file = story.media._asMedia() as? TelegramMediaFile {
selectedMedia = file
}
if let selectedMedia {
let directMediaImageCache = DirectMediaImageCache(account: component.context.account)
if let result = directMediaImageCache.getImage(peer: peerReference, story: story, media: selectedMedia, width: 24, aspectRatio: 1.0, possibleWidths: [24], includeBlurred: false, synchronous: true) {
if let loadSignal = result.loadSignal {
imageSignal = .single(result.image) |> then(loadSignal)
} else {
imageSignal = .single(result.image)
}
}
}
if let imageSignal {
iconSource = ContextMenuActionItemIconSource(
size: CGSize(width: 24.0, height: 24.0),
cornerRadius: 5.0,
signal: imageSignal
)
}
}
var icon: (PresentationTheme) -> UIImage? = { _ in nil }
if iconSource == nil {
icon = { theme in
return generateImage(CGSize(width: 24.0, height: 24.0), opaque: false, scale: nil, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(theme.contextMenu.primaryColor.withMultipliedAlpha(0.1).cgColor)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 5.0).cgPath)
context.fillPath()
})
}
}
items.append(.action(ContextMenuActionItem(text: folderPreview.folder.title, icon: icon, iconSource: iconSource, iconPosition: .left, action: { [weak self] c, f in
guard let self, let component = self.component else {
f(.default)
return
}
c?.dismiss(completion: {})
component.addToFolder(folderPreview.folder.id)
})))
}
c.pushItems(items: .single(ContextController.Items(content: .list(items))))
}
})))
if case .file = component.slice.item.storyItem.media {
var speedValue: String = presentationData.strings.PlaybackSpeed_Normal
var speedIconText: String = "1x"
@ -7028,6 +7132,34 @@ public final class StoryItemSetContainerComponent: Component {
})
}
private func presentAddStoryFolder(addItems: [EngineStoryItem] = []) {
guard let component = self.component else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
let promptController = promptController(
sharedContext: component.context.sharedContext,
updatedPresentationData: (initial: presentationData, signal: .single(presentationData)),
text: "Create a New Album",
titleFont: .bold,
subtitle: "Choose a name for your album and start adding your stories there.",
value: "",
placeholder: "Title",
characterLimit: 20,
apply: { [weak self] value in
guard let self, let component = self.component else {
return
}
if let value {
component.createToFolder(value, addItems)
}
}
)
component.presentController(promptController, nil)
}
func displayMutedVideoTooltip() {
guard let component = self.component else {
return

View file

@ -2546,6 +2546,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return PeerInfoStoryGridScreen(context: context, peerId: context.account.peerId, scope: isArchive ? .archive : .saved)
}
public func makeStorySelectionController(context: AccountContext, peerId: EnginePeer.Id, completion: @escaping ([EngineStoryItem]) -> Void) -> ViewController {
return PeerInfoStoryGridScreen(context: context, peerId: peerId, scope: .saved, selectionModeCompletion: completion)
}
public func makeArchiveSettingsController(context: AccountContext) -> ViewController {
return archiveSettingsController(context: context)
}

View file

@ -28,6 +28,7 @@ swift_library(
"//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/ShimmerEffect",
"//submodules/UIKitRuntimeUtils",
],
visibility = [
"//visibility:public",

View file

@ -20,6 +20,7 @@ import BalancedTextComponent
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import ShimmerEffect
import UIKitRuntimeUtils
public enum TooltipActiveTextItem {
case url(String, Bool)
@ -1328,6 +1329,10 @@ public final class TooltipScreen: ViewController {
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if self.ignoreAppearanceMethodInvocations() {
return
}
self.controllerNode.animateIn()
self.resetDismissTimeout(duration: self.displayDuration)
}

1
third-party/XcodeGen vendored Submodule

@ -0,0 +1 @@
Subproject commit 53cb43cb66908a28812d7629d03fed94c9827a24