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"] [submodule "third-party/td/td"]
path = third-party/td/td path = third-party/td/td
url = https://github.com/tdlib/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 makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, stories: Bool, forceDark: Bool) -> ViewController
func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController
func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> 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 makeArchiveSettingsController(context: AccountContext) -> ViewController
func makeFilterSettingsController(context: AccountContext, modal: Bool, scrollToTags: Bool, dismissed: (() -> Void)?) -> ViewController func makeFilterSettingsController(context: AccountContext, modal: Bool, scrollToTags: Bool, dismissed: (() -> Void)?) -> ViewController
func makeBusinessSetupScreen(context: AccountContext) -> ViewController func makeBusinessSetupScreen(context: AccountContext) -> ViewController

View file

@ -165,8 +165,10 @@ private final class PromptAlertContentNode: AlertContentNode {
private let strings: PresentationStrings private let strings: PresentationStrings
private let text: String private let text: String
private let titleFont: PromptControllerTitleFont private let titleFont: PromptControllerTitleFont
private let subtitle: String?
private let textNode: ASTextNode private let textNode: ASTextNode
private let subtitleNode: ASTextNode?
let inputFieldNode: PromptInputFieldNode let inputFieldNode: PromptInputFieldNode
private let actionNodesSeparator: ASDisplayNode private let actionNodesSeparator: ASDisplayNode
@ -189,14 +191,23 @@ private final class PromptAlertContentNode: AlertContentNode {
return self.isUserInteractionEnabled 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.strings = strings
self.text = text self.text = text
self.titleFont = titleFont self.titleFont = titleFont
self.subtitle = subtitle
self.textNode = ASTextNode() self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 2 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 = PromptInputFieldNode(theme: ptheme, placeholder: placeholder ?? "", characterLimit: characterLimit)
self.inputFieldNode.text = value ?? "" self.inputFieldNode.text = value ?? ""
@ -220,6 +231,9 @@ private final class PromptAlertContentNode: AlertContentNode {
super.init() super.init()
self.addSubnode(self.textNode) self.addSubnode(self.textNode)
if let subtitleNode = self.subtitleNode {
self.addSubnode(subtitleNode)
}
self.addSubnode(self.inputFieldNode) self.addSubnode(self.inputFieldNode)
@ -268,6 +282,10 @@ private final class PromptAlertContentNode: AlertContentNode {
titleFontValue = Font.semibold(17.0) titleFontValue = Font.semibold(17.0)
} }
self.textNode.attributedText = NSAttributedString(string: self.text, font: titleFontValue, textColor: theme.primaryColor, paragraphAlignment: .center) 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 self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes { 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)) 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 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 let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0 var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) 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) let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
var contentWidth = max(titleSize.width, minActionsWidth) var contentWidth = max(titleSize.width, minActionsWidth)
if let subtitleSize {
contentWidth = max(contentWidth, subtitleSize.width)
}
contentWidth = max(contentWidth, 234.0) contentWidth = max(contentWidth, 234.0)
var actionsHeight: CGFloat = 0.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.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) 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))) 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 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 } let presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 }
var dismissImpl: ((Bool) -> Void)? var dismissImpl: ((Bool) -> Void)?
@ -421,7 +453,7 @@ public func promptController(sharedContext: SharedAccountContext, updatedPresent
applyImpl?() 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 = { contentNode.complete = {
dismissImpl?(true) dismissImpl?(true)
applyImpl?() applyImpl?()

View file

@ -2127,7 +2127,7 @@ public func channelStatsController(
} }
messagesPromise.set(.single(nil) |> then(messageView)) 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() storyList.loadMore()
storiesPromise.set( storiesPromise.set(
.single(nil) .single(nil)

View file

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

View file

@ -636,14 +636,14 @@ public extension Api {
} }
public extension Api { public extension Api {
enum StarGift: TypeConstructorDescription { 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?) 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) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { 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 { if boxed {
buffer.appendInt32(2139438098) buffer.appendInt32(12386139)
} }
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(id, 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 << 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 << 5) != 0 {serializeString(title!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 6) != 0 {releasedBy!.serialize(buffer, true)} 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 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): 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 { if boxed {
@ -688,8 +690,8 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { 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):
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)]) 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): 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)]) 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() { if Int(_1!) & Int(1 << 6) != 0 {if let signature = reader.readInt32() {
_14 = Api.parse(reader, signature: signature) as? Api.Peer _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 _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = _3 != nil
@ -742,8 +748,10 @@ public extension Api {
let _c12 = (Int(_1!) & Int(1 << 4) == 0) || _12 != nil let _c12 = (Int(_1!) & Int(1 << 4) == 0) || _12 != nil
let _c13 = (Int(_1!) & Int(1 << 5) == 0) || _13 != nil let _c13 = (Int(_1!) & Int(1 << 5) == 0) || _13 != nil
let _c14 = (Int(_1!) & Int(1 << 6) == 0) || _14 != 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 { let _c15 = (Int(_1!) & Int(1 << 8) == 0) || _15 != nil
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 _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 { else {
return nil return nil

View file

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

View file

@ -531,11 +531,13 @@ private final class CachedPeerStoryListHead: Codable {
let items: [Stories.StoredItem] let items: [Stories.StoredItem]
let pinnedIds: [Int32] let pinnedIds: [Int32]
let totalCount: 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.items = items
self.pinnedIds = pinnedIds self.pinnedIds = pinnedIds
self.totalCount = totalCount 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 peerReference: PeerReference?
public var items: [Item] public var items: [Item]
public var availableLanguages: [Language] public var availableLanguages: [Language]
public var availableFolders: [Folder]
public var pinnedIds: [Int32] public var pinnedIds: [Int32]
public var totalCount: Int public var totalCount: Int
public var loadMoreToken: AnyHashable? public var loadMoreToken: AnyHashable?
@ -592,6 +605,7 @@ public struct StoryListContextState: Equatable {
peerReference: PeerReference?, peerReference: PeerReference?,
items: [Item], items: [Item],
availableLanguages: [Language], availableLanguages: [Language],
availableFolders: [Folder],
pinnedIds: [Int32], pinnedIds: [Int32],
totalCount: Int, totalCount: Int,
loadMoreToken: AnyHashable?, loadMoreToken: AnyHashable?,
@ -603,6 +617,7 @@ public struct StoryListContextState: Equatable {
self.peerReference = peerReference self.peerReference = peerReference
self.items = items self.items = items
self.availableLanguages = availableLanguages self.availableLanguages = availableLanguages
self.availableFolders = availableFolders
self.pinnedIds = pinnedIds self.pinnedIds = pinnedIds
self.totalCount = totalCount self.totalCount = totalCount
self.loadMoreToken = loadMoreToken self.loadMoreToken = loadMoreToken
@ -621,12 +636,13 @@ public protocol StoryListContext: AnyObject {
func loadMore(completion: (() -> Void)?) func loadMore(completion: (() -> Void)?)
} }
public final class PeerStoryListContext: StoryListContext { public final class PeerStoryListContext: StoryListContext {
private final class Impl { private final class Impl {
private let queue: Queue private let queue: Queue
private let account: Account private let account: Account
private let peerId: EnginePeer.Id private let peerId: EnginePeer.Id
private let isArchived: Bool private let isArchived: Bool
private let folderId: Int64?
private let statePromise = Promise<State>() private let statePromise = Promise<State>()
private var stateValue: State { private var stateValue: State {
@ -645,22 +661,40 @@ public final class PeerStoryListContext: StoryListContext {
private var completionCallbacksByToken: [AnyHashable: [() -> Void]] = [:] 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.queue = queue
self.account = account self.account = account
self.peerId = peerId self.peerId = peerId
self.isArchived = isArchived 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) let key = ValueBoxKey(length: 8 + 1)
key.setInt64(0, value: peerId.toInt64()) key.setInt64(0, value: peerId.toInt64())
key.setInt8(8, value: isArchived ? 1 : 0) key.setInt8(8, value: isArchived ? 1 : 0)
let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self) let cachedMain = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self)
guard let cached = cached else { guard let cachedMain else {
return (nil, [], [], 0, [:], false) 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 items: [State.Item] = []
var allEntityFiles: [MediaId: TelegramMediaFile] = [:] var allEntityFiles: [MediaId: TelegramMediaFile] = [:]
for storedItem in cached.items { for storedItem in cached.items {
@ -734,28 +768,30 @@ public final class PeerStoryListContext: StoryListContext {
let peerReference = transaction.getPeer(peerId).flatMap(PeerReference.init) 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 |> deliverOn(self.queue)).start(next: { [weak self] peerReference, items, pinnedIds, totalCount, allEntityFiles, folders, hasCache in
guard let `self` = self else { guard let self else {
return 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) 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)
updatedState.items.sort(by: { lhs, rhs in if self.folderId == nil {
let lhsPinned = updatedState.pinnedIds.firstIndex(of: lhs.storyItem.id) updatedState.items.sort(by: { lhs, rhs in
let rhsPinned = updatedState.pinnedIds.firstIndex(of: rhs.storyItem.id) 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 { if let lhsPinned, let rhsPinned {
return lhsPinned < 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.stateValue = updatedState
self.loadMore(completion: nil) self.loadMore(completion: nil)
@ -791,6 +827,8 @@ public final class PeerStoryListContext: StoryListContext {
let account = self.account let account = self.account
let accountPeerId = account.peerId let accountPeerId = account.peerId
let isArchived = self.isArchived let isArchived = self.isArchived
let folderId = self.folderId
let folders = self.stateValue.availableFolders
self.requestDisposable = (self.account.postbox.transaction { transaction -> Api.InputPeer? in self.requestDisposable = (self.account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer) return transaction.getPeer(peerId).flatMap(apiInputPeer)
} }
@ -822,6 +860,73 @@ public final class PeerStoryListContext: StoryListContext {
var totalCount: Int = 0 var totalCount: Int = 0
var hasMore: Bool = false 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 { switch result {
case let .stories(_, count, stories, pinnedStories, chats, users): case let .stories(_, count, stories, pinnedStories, chats, users):
totalCount = Int(count) totalCount = Int(count)
@ -883,7 +988,7 @@ public final class PeerStoryListContext: StoryListContext {
let key = ValueBoxKey(length: 8 + 1) let key = ValueBoxKey(length: 8 + 1)
key.setInt64(0, value: peerId.toInt64()) key.setInt64(0, value: peerId.toInt64())
key.setInt8(8, value: isArchived ? 1 : 0) 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) 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 self.updatesDisposable = (self.account.stateManager.storyUpdates
|> deliverOn(self.queue)).start(next: { [weak self] updates in |> deliverOn(self.queue)).start(next: { [weak self] updates in
guard let `self` = self else { guard let `self` = self else {
@ -1255,11 +1360,12 @@ public final class PeerStoryListContext: StoryListContext {
let items = finalUpdatedState.items let items = finalUpdatedState.items
let pinnedIds = finalUpdatedState.pinnedIds let pinnedIds = finalUpdatedState.pinnedIds
let totalCount = finalUpdatedState.totalCount let totalCount = finalUpdatedState.totalCount
let folders = finalUpdatedState.availableFolders
let _ = (self.account.postbox.transaction { transaction -> Void in let _ = (self.account.postbox.transaction { transaction -> Void in
let key = ValueBoxKey(length: 8 + 1) let key = ValueBoxKey(length: 8 + 1)
key.setInt64(0, value: peerId.toInt64()) key.setInt64(0, value: peerId.toInt64())
key.setInt8(8, value: isArchived ? 1 : 0) 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) transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key), entry: entry)
} }
}).start() }).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> { public var state: Signal<State, NoError> {
@ -1280,11 +1622,18 @@ public final class PeerStoryListContext: StoryListContext {
private let queue: Queue private let queue: Queue
private let impl: QueueLocalObject<Impl> 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() let queue = Queue.mainQueue()
self.queue = queue self.queue = queue
self.account = account
self.peerId = peerId
self.folderId = folderId
self.impl = QueueLocalObject(queue: queue, generate: { 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) 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 { public final class SearchStoryListContext: StoryListContext {
@ -1332,7 +1786,7 @@ public final class SearchStoryListContext: StoryListContext {
self.account = account self.account = account
self.source = source 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.statePromise.set(.single(self.stateValue))
self.loadMore(completion: nil) self.loadMore(completion: nil)
@ -2149,7 +2603,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
self.isArchived = isArchived 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) let localStateKey: PostboxViewKey = .storiesState(key: .local)
@ -2166,6 +2620,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
peerReference: peer.flatMap(PeerReference.init), peerReference: peer.flatMap(PeerReference.init),
items: [], items: [],
availableLanguages: [], availableLanguages: [],
availableFolders: [],
pinnedIds: [], pinnedIds: [],
totalCount: 0, totalCount: 0,
loadMoreToken: AnyHashable(0), loadMoreToken: AnyHashable(0),
@ -2313,6 +2768,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
peerReference: (peer?._asPeer()).flatMap(PeerReference.init), peerReference: (peer?._asPeer()).flatMap(PeerReference.init),
items: items, items: items,
availableLanguages: availableLanguages, availableLanguages: availableLanguages,
availableFolders: [],
pinnedIds: [], pinnedIds: [],
totalCount: items.count, totalCount: items.count,
loadMoreToken: nil, loadMoreToken: nil,
@ -2413,6 +2869,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
peerReference: PeerReference(peer), peerReference: PeerReference(peer),
items: items, items: items,
availableLanguages: [], availableLanguages: [],
availableFolders: [],
pinnedIds: [], pinnedIds: [],
totalCount: items.count, totalCount: items.count,
loadMoreToken: nil, loadMoreToken: nil,
@ -2592,6 +3049,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
peerReference: self.stateValue.peerReference, peerReference: self.stateValue.peerReference,
items: items, items: items,
availableLanguages: [], availableLanguages: [],
availableFolders: [],
pinnedIds: [], pinnedIds: [],
totalCount: items.count, totalCount: items.count,
loadMoreToken: nil, loadMoreToken: nil,

View file

@ -720,7 +720,7 @@ public extension StarGift {
extension StarGift { extension StarGift {
init?(apiStarGift: Api.StarGift) { init?(apiStarGift: Api.StarGift) {
switch apiStarGift { 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() var flags = StarGift.Gift.Flags()
if (apiFlags & (1 << 2)) != 0 { if (apiFlags & (1 << 2)) != 0 {
flags.insert(.isBirthdayGift) flags.insert(.isBirthdayGift)

View file

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

View file

@ -5,44 +5,62 @@ import ComponentFlow
import MultilineTextComponent import MultilineTextComponent
import TextLoadingEffect import TextLoadingEffect
import ComponentDisplayAdapters import ComponentDisplayAdapters
import TooltipUI
import AccountContext
import UIKitRuntimeUtils
public final class PeerInfoRatingComponent: Component { public final class PeerInfoRatingComponent: Component {
let context: AccountContext
let backgroundColor: UIColor let backgroundColor: UIColor
let foregroundColor: UIColor let foregroundColor: UIColor
let tooltipBackgroundColor: UIColor
let isExpanded: Bool let isExpanded: Bool
let compactLabel: String let compactLabel: String
let fraction: CGFloat let fraction: CGFloat
let label: String let label: String
let nextLabel: String let nextLabel: String
let tooltipLabel: String
let action: () -> Void let action: () -> Void
public init( public init(
context: AccountContext,
backgroundColor: UIColor, backgroundColor: UIColor,
foregroundColor: UIColor, foregroundColor: UIColor,
tooltipBackgroundColor: UIColor,
isExpanded: Bool, isExpanded: Bool,
compactLabel: String, compactLabel: String,
fraction: CGFloat, fraction: CGFloat,
label: String, label: String,
nextLabel: String, nextLabel: String,
tooltipLabel: String,
action: @escaping () -> Void action: @escaping () -> Void
) { ) {
self.context = context
self.backgroundColor = backgroundColor self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor self.foregroundColor = foregroundColor
self.tooltipBackgroundColor = tooltipBackgroundColor
self.isExpanded = isExpanded self.isExpanded = isExpanded
self.compactLabel = compactLabel self.compactLabel = compactLabel
self.fraction = fraction self.fraction = fraction
self.label = label self.label = label
self.nextLabel = nextLabel self.nextLabel = nextLabel
self.tooltipLabel = tooltipLabel
self.action = action self.action = action
} }
public static func ==(lhs: PeerInfoRatingComponent, rhs: PeerInfoRatingComponent) -> Bool { public static func ==(lhs: PeerInfoRatingComponent, rhs: PeerInfoRatingComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.backgroundColor != rhs.backgroundColor { if lhs.backgroundColor != rhs.backgroundColor {
return false return false
} }
if lhs.foregroundColor != rhs.foregroundColor { if lhs.foregroundColor != rhs.foregroundColor {
return false return false
} }
if lhs.tooltipBackgroundColor != rhs.tooltipBackgroundColor {
return false
}
if lhs.isExpanded != rhs.isExpanded { if lhs.isExpanded != rhs.isExpanded {
return false return false
} }
@ -58,6 +76,9 @@ public final class PeerInfoRatingComponent: Component {
if lhs.nextLabel != rhs.nextLabel { if lhs.nextLabel != rhs.nextLabel {
return false return false
} }
if lhs.tooltipLabel != rhs.tooltipLabel {
return false
}
return true return true
} }
@ -77,6 +98,8 @@ public final class PeerInfoRatingComponent: Component {
private var component: PeerInfoRatingComponent? private var component: PeerInfoRatingComponent?
private var tooltipController: TooltipScreen?
override public init(frame: CGRect) { override public init(frame: CGRect) {
self.backgroundView = UIImageView() 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 return size
} }
} }

View file

@ -677,7 +677,7 @@ public func keepPeerInfoScreenDataHot(context: AccountContext, peerId: PeerId, c
if case .user = inputData { if case .user = inputData {
signals.append(Signal { _ in 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) let expiringListContext = PeerExpiringStoryListContext(account: context.account, peerId: peerId)
return ActionDisposable { return ActionDisposable {
@ -829,7 +829,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
) )
|> distinctUntilChanged |> 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 let hasStories: Signal<Bool?, NoError> = storyListContext.state
|> map { state -> Bool? in |> map { state -> Bool? in
if !state.hasCache { 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)) 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 let hasStories: Signal<Bool?, NoError> = storyListContext.state
|> map { state -> Bool? in |> map { state -> Bool? in
if !state.hasCache { if !state.hasCache {
@ -1201,7 +1201,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
let hasStoryArchive: Signal<Bool?, NoError> let hasStoryArchive: Signal<Bool?, NoError>
var storyArchiveListContext: StoryListContext? var storyArchiveListContext: StoryListContext?
if isMyProfile { 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 storyArchiveListContext = storyArchiveListContextValue
hasStoryArchive = storyArchiveListContextValue.state hasStoryArchive = storyArchiveListContextValue.state
|> map { state -> Bool? in |> map { state -> Bool? in
@ -1535,7 +1535,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
let requestsContextPromise = Promise<PeerInvitationImportersContext?>(nil) let requestsContextPromise = Promise<PeerInvitationImportersContext?>(nil)
let requestsStatePromise = Promise<PeerInvitationImportersState?>(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 let hasStories: Signal<Bool?, NoError> = storyListContext.state
|> map { state -> Bool? in |> map { state -> Bool? in
if !state.hasCache { if !state.hasCache {
@ -1857,7 +1857,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
let storyListContext: StoryListContext? let storyListContext: StoryListContext?
let hasStories: Signal<Bool?, NoError> let hasStories: Signal<Bool?, NoError>
if peerId.namespace == Namespaces.Peer.CloudChannel { 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 hasStories = storyListContext!.state
|> map { state -> Bool? in |> map { state -> Bool? in
if !state.hasCache { if !state.hasCache {

View file

@ -1926,22 +1926,47 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.subtitleRating = subtitleRating self.subtitleRating = subtitleRating
} }
let fraction: CGFloat let fraction: CGFloat
let tooltipLabel: String
if let nextLevelStars = starRating.nextLevelStars { if let nextLevelStars = starRating.nextLevelStars {
fraction = CGFloat(starRating.currentLevelStars) / CGFloat(nextLevelStars) fraction = CGFloat(starRating.currentLevelStars) / CGFloat(nextLevelStars)
tooltipLabel = "\(starRating.currentLevelStars) / \(nextLevelStars)"
} else { } else {
fraction = 1.0 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 //TODO:localize
subtitleRatingSize = subtitleRating.update( subtitleRatingSize = subtitleRating.update(
transition: subtitleRatingTransition, transition: subtitleRatingTransition,
component: AnyComponent(PeerInfoRatingComponent( component: AnyComponent(PeerInfoRatingComponent(
backgroundColor: UIColor(white: 1.0, alpha: 0.1), context: self.context,
foregroundColor: UIColor(white: 1.0, alpha: 1.0), backgroundColor: ratingBackgroundColor,
foregroundColor: ratingForegroundColor,
tooltipBackgroundColor: tooltipBackgroundColor,
isExpanded: self.subtitleRatingIsExpanded, isExpanded: self.subtitleRatingIsExpanded,
compactLabel: "\(starRating.level)", compactLabel: "\(starRating.level)",
fraction: fraction, fraction: fraction,
label: "Level \(starRating.level)", label: "Level \(starRating.level)",
nextLabel: starRating.nextLevelStars != nil ? "\(starRating.level + 1)" : "", nextLabel: starRating.nextLevelStars != nil ? "\(starRating.level + 1)" : "",
tooltipLabel: tooltipLabel,
action: { [weak self] in action: { [weak self] in
guard let self else { guard let self else {
return 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) 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 self.subtitleNodeRawContainer.frame = rawSubtitleFrame
transition.updateFrameAdditiveToCenter(node: self.subtitleNodeContainer, frame: CGRect(origin: rawSubtitleFrame.center, size: CGSize())) 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.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.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize()))
transition.updateSublayerTransformScale(node: self.titleNodeContainer, scale: titleScale) transition.updateSublayerTransformScale(node: self.titleNodeContainer, scale: titleScale)
@ -2006,6 +2031,17 @@ final class PeerInfoHeaderNode: ASDisplayNode {
transition.updateFrameAdditive(view: subtitleBadgeView, frame: subtitleBadgeFrame) transition.updateFrameAdditive(view: subtitleBadgeView, frame: subtitleBadgeFrame)
transition.updateAlpha(layer: subtitleBadgeView.layer, alpha: (1.0 - transitionFraction)) 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 { } else {
let titleScale: CGFloat let titleScale: CGFloat
let subtitleScale: CGFloat let subtitleScale: CGFloat
@ -2048,7 +2084,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
usernameCenter.x = rawTitleFrame.center.x + (usernameCenter.x - rawTitleFrame.center.x) * subtitleScale 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.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.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.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize()))
transition.updateSublayerTransformScaleAdditive(node: self.titleNodeContainer, scale: titleScale) 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(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 self.isAvatarExpanded = isAvatarExpanded
if isAvatarExpanded { if isAvatarExpanded {
self.avatarListNode.listContainerNode.selectFirstItem() self.avatarListNode.listContainerNode.selectFirstItem()
self.subtitleRatingIsExpanded = false
} }
if case .animated = transition, !isAvatarExpanded { if case .animated = transition, !isAvatarExpanded {
self.avatarListNode.animateAvatarCollapse(transition: transition) self.avatarListNode.animateAvatarCollapse(transition: transition)

View file

@ -11496,26 +11496,150 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
private func displayMediaGalleryContextMenu(source: ContextReferenceContentNode, gesture: ContextGesture?) { private func displayMediaGalleryContextMenu(source: ContextReferenceContentNode, gesture: ContextGesture?) {
let peerId = self.peerId 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 { guard let controller = self.controller else {
return return
} }
guard let pane = self.paneContainerNode.currentPane?.node as? PeerInfoStoryPaneNode else { guard let pane = self.paneContainerNode.currentPane?.node as? PeerInfoStoryPaneNode else {
return 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] = [] 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 {
let strings = self.presentationData.strings return
}
var ignoreNextActions = false
var items: [ContextMenuItem] = []
if pane.canAddMoreBotPreviews() {
items.append(.action(ContextMenuActionItem(text: strings.BotPreviews_MenuAddPreview, icon: { theme in let strings = self.presentationData.strings
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
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 }, action: { [weak self] _, a in
if ignoreNextActions { if ignoreNextActions {
return return
@ -11527,92 +11651,96 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
self.headerNode.navigationButtonContainer.performAction?(.postStory, nil, nil) 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 { if pane.canReorder() {
self.toggleStorySelection(ids: [], isSelected: true) 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 {
if let language = pane.currentBotPreviewLanguage { return
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) ignoreNextActions = true
}, action: { [weak pane] _, a in a(.default)
if ignoreNextActions {
return if let pane {
} pane.beginReordering()
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) if let folder = pane.currentStoryFolder {
guard let localResult = strongSelf.hitTest(localPoint, with: nil) else { 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) return .dismiss(consume: true, result: nil)
} }
self.mediaGalleryContextMenu = contextController
var testView: UIView? = localResult controller.presentInGlobalOverlay(contextController)
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 { } else {
let _ = (self.context.engine.data.get(EngineDataMap([ let _ = (self.context.engine.data.get(EngineDataMap([
TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: peerId, threadId: self.chatLocation.threadId, tag: .photo), 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) { 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)) 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: case .gifts:
//if let data = self.data, let channel = data.peer as? TelegramChannel, case .broadcast = channel.info { //if let data = self.data, let channel = data.peer as? TelegramChannel, case .broadcast = channel.info {
rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .sort, isForExpandedView: true)) rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .sort, isForExpandedView: true))

View file

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

View file

@ -44,6 +44,8 @@ import MultilineTextComponent
import LocationUI import LocationUI
import TabSelectorComponent import TabSelectorComponent
import LanguageSelectionScreen import LanguageSelectionScreen
import PromptUI
import BottomButtonPanelComponent
private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6) private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6)
private let mediaBadgeTextColor = UIColor.white private let mediaBadgeTextColor = UIColor.white
@ -1543,7 +1545,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
} }
private let context: AccountContext private let context: AccountContext
private let scope: Scope public let scope: Scope
private let isProfileEmbedded: Bool private let isProfileEmbedded: Bool
private let canManageStories: Bool private let canManageStories: Bool
@ -1568,7 +1570,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
private var mapInfoNode: LocationInfoListItemNode? private var mapInfoNode: LocationInfoListItemNode?
private var searchHeader: ComponentView<Empty>? private var searchHeader: ComponentView<Empty>?
private var botPreviewLanguageTab: ComponentView<Empty>? private var folderTab: ComponentView<Empty>?
private var botPreviewFooter: ComponentView<Empty>? private var botPreviewFooter: ComponentView<Empty>?
private var barBackgroundLayer: SimpleLayer? private var barBackgroundLayer: SimpleLayer?
@ -1583,6 +1585,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
private var didUpdateItemsOnce: Bool = false private var didUpdateItemsOnce: Bool = false
private var selectionPanel: ComponentView<Empty>? private var selectionPanel: ComponentView<Empty>?
private var actionPanel: ComponentView<Empty>?
private var isDeceleratingAfterTracking = false private var isDeceleratingAfterTracking = false
@ -1627,6 +1630,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
public var isEmptyUpdated: (Bool) -> Void = { _ in } public var isEmptyUpdated: (Bool) -> Void = { _ in }
public var shouldOpenItemsWhileInSelectionMode: Bool = true
public private(set) var isSelectionModeActive: Bool 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)? 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 { public var tabBarOffset: CGFloat {
if case .botPreview = self.scope { if case .botPreview = self.scope {
return 0.0 return 0.0
} else if case let .peer(peerId, _, isArchived) = self.scope, peerId == self.context.account.peerId, !isArchived, self.isProfileEmbedded {
return 0.0
} else { } else {
return self.itemGrid.coveringInsetOffset return self.itemGrid.coveringInsetOffset
} }
@ -1660,6 +1666,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
private var currentBotPreviewLanguages: [StoryListContext.State.Language] = [] private var currentBotPreviewLanguages: [StoryListContext.State.Language] = []
private var removedBotPreviewLanguages = Set<String>() private var removedBotPreviewLanguages = Set<String>()
private var currentStoryFolders: [StoryListContext.State.Folder] = []
private var removedStoryFolders = Set<Int64>()
private var numberOfItemsToRequest: Int = 50 private var numberOfItemsToRequest: Int = 50
private var isRequestingView: Bool = false private var isRequestingView: Bool = false
private var isFirstHistoryView: Bool = true private var isFirstHistoryView: Bool = true
@ -1674,7 +1683,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
private let maxBotPreviewCount: Int private let maxBotPreviewCount: Int
private let defaultListSource: StoryListContext private let defaultListSource: StoryListContext
private var cachedListSources: [String: StoryListContext] = [:] private var cachedListSources: [AnyHashable: StoryListContext] = [:]
public var currentBotPreviewLanguage: (id: String, name: String)? { public var currentBotPreviewLanguage: (id: String, name: String)? {
guard let listSource = self.listSource as? BotPreviewStoryListContext else { guard let listSource = self.listSource as? BotPreviewStoryListContext else {
@ -1688,6 +1697,19 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
} }
return (language.id, language.name) 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 openCurrentDate: (() -> Void)?
public var paneDidScroll: (() -> Void)? public var paneDidScroll: (() -> Void)?
@ -1749,7 +1771,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
} else { } else {
switch self.scope { switch self.scope {
case let .peer(id, _, isArchived): 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): case let .search(peerId, query):
self.listSource = SearchStoryListContext(account: context.account, source: .hashtag(peerId, query)) self.listSource = SearchStoryListContext(account: context.account, source: .hashtag(peerId, query))
case let .location(coordinates, venue): case let .location(coordinates, venue):
@ -1798,7 +1820,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
return return
} }
if self.isProfileEmbedded { if self.isProfileEmbedded || !self.shouldOpenItemsWhileInSelectionMode {
if let selectedIds = self.itemInteraction.selectedIds { if let selectedIds = self.itemInteraction.selectedIds {
self.itemInteraction.toggleSelection(item.story.id, !selectedIds.contains(item.story.id)) self.itemInteraction.toggleSelection(item.story.id, !selectedIds.contains(item.story.id))
return return
@ -2275,7 +2297,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: false) self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: false)
if case let .peer(id, _, isArchived) = self.scope, id == context.account.peerId, !isArchived { 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 { if case let .location(_, venue) = scope, let mapNode = self.mapNode {
@ -2447,6 +2469,122 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
var items: [ContextMenuItem] = [] var items: [ContextMenuItem] = []
if canManage, case let .peer(peerId, _, isArchived) = self.scope { 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 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 { guard let self else {
f(.default) f(.default)
@ -2784,6 +2922,13 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
} }
botPreviewLanguages.sort(by: { $0.name < $1.name }) 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 var hadLocalItems = false
if let currentListState = self.currentListState { if let currentListState = self.currentListState {
for item in currentListState.items { 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) 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.currentBotPreviewLanguages = botPreviewLanguages
self.currentStoryFolders = storyFolders
if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { 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) 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 isReorderable = !item.storyItem.isPending
case let .peer(id, _, _): case let .peer(id, _, _):
if id == self.context.account.peerId { 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, _): case let .search(peerId, _):
if peerId != nil { if peerId != nil {
@ -2963,11 +3113,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
if case .botPreview = self.scope { if case .botPreview = self.scope {
} else if case let .peer(id, _, _) = self.scope { } else if case let .peer(id, _, _) = self.scope {
if id == self.context.account.peerId { if id == self.context.account.peerId {
let maxPinnedIndex = items.items.lastIndex(where: { ($0 as? VisualMediaItem)?.isPinned == true }) if self.currentStoryFolder != nil {
if let maxPinnedIndex {
toIndex = min(toIndex, maxPinnedIndex)
} else { } else {
return let maxPinnedIndex = items.items.lastIndex(where: { ($0 as? VisualMediaItem)?.isPinned == true })
if let maxPinnedIndex {
toIndex = min(toIndex, maxPinnedIndex)
} else {
return
}
} }
} }
} else { } else {
@ -3379,8 +3532,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
if let _ = self.mapNode { if let _ = self.mapNode {
self.updateMapLayout(size: currentParams.size, topInset: currentParams.topInset, bottomInset: currentParams.bottomInset, deviceMetrics: currentParams.deviceMetrics, transition: transition) self.updateMapLayout(size: currentParams.size, topInset: currentParams.topInset, bottomInset: currentParams.bottomInset, deviceMetrics: currentParams.deviceMetrics, transition: transition)
} }
if case .botPreview = self.scope, self.canManageStories { if case let .peer(peerId, _, isArchived) = self.scope, peerId == self.context.account.peerId, !isArchived, self.isProfileEmbedded {
self.updateBotPreviewLanguageTab(size: currentParams.size, topInset: currentParams.topInset, transition: transition) 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) 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) { private func updateFolderTab(size: CGSize, topInset: CGFloat, transition: ContainedViewLayoutTransition) {
guard case .botPreview = self.scope, self.canManageStories else { 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 return
} }
let botPreviewLanguageTab: ComponentView<Empty> let folderTab: ComponentView<Empty>
if let current = self.botPreviewLanguageTab { if let current = self.folderTab {
botPreviewLanguageTab = current folderTab = current
} else { } else {
botPreviewLanguageTab = ComponentView() folderTab = ComponentView()
self.botPreviewLanguageTab = botPreviewLanguageTab self.folderTab = folderTab
} }
var languageItems: [TabSelectorComponent.Item] = [] var folderItems: [TabSelectorComponent.Item] = []
languageItems.append(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"), id: AnyHashable("_main"),
title: self.presentationData.strings.BotPreviews_LanguageTab_Main title: mainTitle
)) ))
for language in self.currentBotPreviewLanguages {
languageItems.append(TabSelectorComponent.Item( if case .botPreview = self.scope {
id: AnyHashable(language.id), for language in self.currentBotPreviewLanguages {
title: language.name 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"), 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 { 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), transition: ComponentTransition(transition),
component: AnyComponent(TabSelectorComponent( component: AnyComponent(TabSelectorComponent(
colors: TabSelectorComponent.Colors( colors: TabSelectorComponent.Colors(
@ -3581,39 +3765,53 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
spacing: 9.0, spacing: 9.0,
verticalInset: 11.0 verticalInset: 11.0
), ),
items: languageItems, items: folderItems,
selectedId: AnyHashable(selectedLanguageId), selectedId: selectedId,
setSelectedId: { [weak self] id in setSelectedId: { [weak self] id in
guard let self, let id = id.base as? String else { guard let self else {
return return
} }
if id == "_add" { if let id = id.base as? String {
self.presentAddBotPreviewLanguage() if id == "_add" {
} else if id == "_main" { if case .botPreview = self.scope {
self.setBotPreviewLanguage(id: nil, assumeEmpty: false) self.presentAddBotPreviewLanguage()
} else if let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) { } else {
self.setBotPreviewLanguage(id: language.id, assumeEmpty: false) 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: {}, environment: {},
containerSize: CGSize(width: size.width, height: 44.0) 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 let effectiveScrollingOffset: CGFloat
effectiveScrollingOffset = self.itemGrid.scrollingOffset effectiveScrollingOffset = self.itemGrid.scrollingOffset
botPreviewLanguageTabFrame.origin.y -= effectiveScrollingOffset folderTabFrame.origin.y -= effectiveScrollingOffset
let isSelectingOrReordering = self.isReordering || self.itemInteraction.selectedIds != nil let isSelectingOrReordering = self.isReordering || self.itemInteraction.selectedIds != nil
if let botPreviewLanguageTabView = botPreviewLanguageTab.view { if let folderTabView = folderTab.view {
if botPreviewLanguageTabView.superview == nil { if folderTabView.superview == nil {
self.view.addSubview(botPreviewLanguageTabView) self.view.addSubview(folderTabView)
} }
transition.updateFrame(view: botPreviewLanguageTabView, frame: botPreviewLanguageTabFrame) transition.updateFrame(view: folderTabView, frame: folderTabFrame)
transition.updateAlpha(layer: botPreviewLanguageTabView.layer, alpha: isSelectingOrReordering ? 0.5 : 1.0) transition.updateAlpha(layer: folderTabView.layer, alpha: isSelectingOrReordering ? 0.5 : 1.0)
botPreviewLanguageTabView.isUserInteractionEnabled = !isSelectingOrReordering folderTabView.isUserInteractionEnabled = !isSelectingOrReordering
} }
} }
@ -3742,15 +3940,24 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
var listBottomInset = bottomInset var listBottomInset = bottomInset
var bottomInset = bottomInset var bottomInset = bottomInset
var displayFolderTab = false
if case .botPreview = self.scope, self.canManageStories { 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 gridTopInset += 50.0
updateBotPreviewFooter(size: size, bottomInset: 0.0, transition: transition) if case .botPreview = self.scope {
if let botPreviewFooterView = self.botPreviewFooter?.view { updateBotPreviewFooter(size: size, bottomInset: 0.0, transition: transition)
if let botPreviewFooterView = self.botPreviewFooter?.view {
listBottomInset += 18.0 + botPreviewFooterView.bounds.height listBottomInset += 18.0 + botPreviewFooterView.bounds.height
} }
} }
}
if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories, case let .peer(peerId, _, isArchived) = self.scope { if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories, case let .peer(peerId, _, isArchived) = self.scope {
let selectionPanel: ComponentView<Empty> let selectionPanel: ComponentView<Empty>
@ -3930,13 +4137,200 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
} else if let selectionPanel = self.selectionPanel { } else if let selectionPanel = self.selectionPanel {
self.selectionPanel = nil self.selectionPanel = nil
if let selectionPanelView = selectionPanel.view { 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))) 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> let emptyStateView: ComponentView<Empty>
var emptyStateTransition = ComponentTransition(transition) var emptyStateTransition = ComponentTransition(transition)
if let current = self.emptyStateView { if let current = self.emptyStateView {
@ -3946,29 +4340,44 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
emptyStateView = ComponentView() emptyStateView = ComponentView()
self.emptyStateView = emptyStateView self.emptyStateView = emptyStateView
} }
var isMainLanguage = true
if let listSource = self.listSource as? BotPreviewStoryListContext, let _ = listSource.language {
isMainLanguage = false
}
let emptyStateSize = emptyStateView.update( let emptyStateSize = emptyStateView.update(
transition: emptyStateTransition, transition: emptyStateTransition,
component: AnyComponent(EmptyStateIndicatorComponent( component: AnyComponent(EmptyStateIndicatorComponent(
context: self.context, context: self.context,
theme: presentationData.theme, theme: presentationData.theme,
fitToHeight: self.isProfileEmbedded, fitToHeight: self.isProfileEmbedded,
animationName: "StoryListEmpty", animationName: nil,
title: isArchived ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyPosts_Title, title: presentationData.strings.BotPreviews_Empty_Title,
text: isArchived ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyPosts_Text, text: presentationData.strings.BotPreviews_Empty_Text(Int32(self.maxBotPreviewCount)),
actionTitle: isArchived ? nil : presentationData.strings.StoryList_SavedAddAction, actionTitle: self.canManageStories ? presentationData.strings.BotPreviews_Empty_Add : nil,
action: { [weak self] in action: { [weak self] in
guard let self else { guard let self else {
return 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 additionalAction: { [weak self] in
guard let self else { guard let self else {
return return
} }
self.additionalEmptyAction?() if isMainLanguage {
} self.presentAddBotPreviewLanguage()
} else {
self.presentDeleteBotPreviewLanguage()
}
},
additionalActionSeparator: self.canManageStories ? presentationData.strings.BotPreviews_Empty_Separator : nil
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: size.width, height: size.height - gridTopInset - bottomInset) containerSize: CGSize(width: size.width, height: size.height - gridTopInset - bottomInset)
@ -3976,7 +4385,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
let emptyStateFrame: CGRect let emptyStateFrame: CGRect
if self.isProfileEmbedded { 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 { } else {
emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: gridTopInset), size: emptyStateSize) 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 let backgroundColor: UIColor
if self.isProfileEmbedded { if self.isProfileEmbedded, case .botPreview = self.scope {
backgroundColor = presentationData.theme.list.plainBackgroundColor backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else if self.isProfileEmbedded {
backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else { } else {
backgroundColor = presentationData.theme.list.blocksBackgroundColor backgroundColor = presentationData.theme.list.blocksBackgroundColor
} }
@ -4003,7 +4414,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
} else { } else {
self.view.backgroundColor = backgroundColor 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> let emptyStateView: ComponentView<Empty>
var emptyStateTransition = ComponentTransition(transition) var emptyStateTransition = ComponentTransition(transition)
if let current = self.emptyStateView { if let current = self.emptyStateView {
@ -4073,6 +4484,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
let backgroundColor: UIColor let backgroundColor: UIColor
if self.isProfileEmbedded, case .botPreview = self.scope { if self.isProfileEmbedded, case .botPreview = self.scope {
backgroundColor = presentationData.theme.list.blocksBackgroundColor 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 { } else if self.isProfileEmbedded {
backgroundColor = presentationData.theme.list.blocksBackgroundColor backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else { } else {
@ -4101,6 +4514,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
if self.isProfileEmbedded, case .botPreview = self.scope { if self.isProfileEmbedded, case .botPreview = self.scope {
subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor) 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 { } else if self.isProfileEmbedded {
subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.plainBackgroundColor) subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.plainBackgroundColor)
} else { } else {
@ -4109,6 +4524,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
} else { } else {
if self.isProfileEmbedded, case .botPreview = self.scope { if self.isProfileEmbedded, case .botPreview = self.scope {
self.view.backgroundColor = presentationData.theme.list.blocksBackgroundColor 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 { } else {
if case let .search(peerId, _) = self.scope, peerId != nil { if case let .search(peerId, _) = self.scope, peerId != nil {
@ -4132,6 +4549,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
var adjustForSmallCount = true var adjustForSmallCount = true
if case .botPreview = self.scope { if case .botPreview = self.scope {
adjustForSmallCount = false adjustForSmallCount = false
} else if self.currentStoryFolder != nil {
adjustForSmallCount = false
} }
self.itemGrid.pinchEnabled = items.count > 2 && !self.isReordering self.itemGrid.pinchEnabled = items.count > 2 && !self.isReordering
@ -4234,10 +4653,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
} }
public func canReorder() -> Bool { public func canReorder() -> Bool {
guard let items = self.items else { if case .botPreview = self.scope {
return false 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() { 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() { public func presentUnableToAddMorePreviewsAlert() {
self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.BotPreviews_AlertTooManyPreviews(Int32(self.maxBotPreviewCount)), actions: [ 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: { 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 id {
if let cachedListSource = self.cachedListSources[id] { if let cachedListSource = self.cachedListSources[AnyHashable(id)] {
self.listSource = cachedListSource self.listSource = cachedListSource
} else { } else {
let listSource = BotPreviewStoryListContext(account: self.context.account, engine: self.context.engine, peerId: peerId, language: id, assumeEmpty: assumeEmpty) let listSource = BotPreviewStoryListContext(account: self.context.account, engine: self.context.engine, peerId: peerId, language: id, assumeEmpty: assumeEmpty)
self.listSource = listSource 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 { } else {
self.listSource = self.defaultListSource self.listSource = self.defaultListSource
@ -4388,20 +4910,26 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
listSource.reorderItems(media: reorderedMedia) listSource.reorderItems(media: reorderedMedia)
} }
} else if case let .peer(id, _, _) = self.scope, id == self.context.account.peerId, let items = self.items { } else if case let .peer(id, _, _) = self.scope, id == self.context.account.peerId, let items = self.items {
var updatedPinnedIds: [Int32] = [] if let _ = self.currentStoryFolder {
for id in reorderedIds { if let listSource = self.listSource as? PeerStoryListContext {
inner: for item in items.items { listSource.reorderItemsInFolder(itemIds: reorderedIds.map { $0.id })
if let item = item as? VisualMediaItem { }
if item.storyId == id { } else {
if item.isPinned { var updatedPinnedIds: [Int32] = []
updatedPinnedIds.append(id.id) for id in reorderedIds {
break inner 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/InteractiveTextComponent",
"//submodules/TelegramUI/Components/SaveProgressScreen", "//submodules/TelegramUI/Components/SaveProgressScreen",
"//submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController", "//submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController",
"//submodules/DirectMediaImageCache",
"//submodules/PromptUI",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View file

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

View file

@ -1688,6 +1688,39 @@ private final class StoryContainerScreenComponent: Component {
performReorderAction?() 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 controller: { [weak self] in
return self?.environment?.controller() return self?.environment?.controller()
}, },

View file

@ -44,6 +44,8 @@ import StoryFooterPanelComponent
import TelegramNotices import TelegramNotices
import SliderContextItem import SliderContextItem
import SaveProgressScreen import SaveProgressScreen
import DirectMediaImageCache
import PromptUI
public final class StoryAvailableReactions: Equatable { public final class StoryAvailableReactions: Equatable {
let reactionItems: [ReactionItem] let reactionItems: [ReactionItem]
@ -120,6 +122,8 @@ public final class StoryItemSetContainerComponent: Component {
public let delete: () -> Void public let delete: () -> Void
public let markAsSeen: (StoryId) -> Void public let markAsSeen: (StoryId) -> Void
public let reorder: () -> Void public let reorder: () -> Void
public let createToFolder: (String, [EngineStoryItem]) -> Void
public let addToFolder: (Int64) -> Void
public let controller: () -> ViewController? public let controller: () -> ViewController?
public let toggleAmbientMode: () -> Void public let toggleAmbientMode: () -> Void
public let keyboardInputData: Signal<ChatEntityKeyboardInputNode.InputData, NoError> public let keyboardInputData: Signal<ChatEntityKeyboardInputNode.InputData, NoError>
@ -157,6 +161,8 @@ public final class StoryItemSetContainerComponent: Component {
delete: @escaping () -> Void, delete: @escaping () -> Void,
markAsSeen: @escaping (StoryId) -> Void, markAsSeen: @escaping (StoryId) -> Void,
reorder: @escaping () -> Void, reorder: @escaping () -> Void,
createToFolder: @escaping (String, [EngineStoryItem]) -> Void,
addToFolder: @escaping (Int64) -> Void,
controller: @escaping () -> ViewController?, controller: @escaping () -> ViewController?,
toggleAmbientMode: @escaping () -> Void, toggleAmbientMode: @escaping () -> Void,
keyboardInputData: Signal<ChatEntityKeyboardInputNode.InputData, NoError>, keyboardInputData: Signal<ChatEntityKeyboardInputNode.InputData, NoError>,
@ -193,6 +199,8 @@ public final class StoryItemSetContainerComponent: Component {
self.delete = delete self.delete = delete
self.markAsSeen = markAsSeen self.markAsSeen = markAsSeen
self.reorder = reorder self.reorder = reorder
self.createToFolder = createToFolder
self.addToFolder = addToFolder
self.controller = controller self.controller = controller
self.toggleAmbientMode = toggleAmbientMode self.toggleAmbientMode = toggleAmbientMode
self.keyboardInputData = keyboardInputData self.keyboardInputData = keyboardInputData
@ -6101,6 +6109,102 @@ public final class StoryItemSetContainerComponent: Component {
var items: [ContextMenuItem] = [] 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 { if case .file = component.slice.item.storyItem.media {
var speedValue: String = presentationData.strings.PlaybackSpeed_Normal var speedValue: String = presentationData.strings.PlaybackSpeed_Normal
var speedIconText: String = "1x" 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() { func displayMutedVideoTooltip() {
guard let component = self.component else { guard let component = self.component else {
return return

View file

@ -2546,6 +2546,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return PeerInfoStoryGridScreen(context: context, peerId: context.account.peerId, scope: isArchive ? .archive : .saved) 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 { public func makeArchiveSettingsController(context: AccountContext) -> ViewController {
return archiveSettingsController(context: context) return archiveSettingsController(context: context)
} }

View file

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

View file

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

1
third-party/XcodeGen vendored Submodule

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