Bot previews

This commit is contained in:
Isaac 2024-07-24 01:56:34 +08:00
parent f604bc114f
commit 42a6f6e8bc
52 changed files with 3806 additions and 523 deletions

View File

@ -932,6 +932,9 @@ public final class BotPreviewEditorTransitionOut {
}
}
public protocol MiniAppListScreenInitialData: AnyObject {
}
public protocol SharedAccountContext: AnyObject {
var sharedContainerPath: String { get }
var basePath: String { get }
@ -999,7 +1002,7 @@ public protocol SharedAccountContext: AnyObject {
selectedMessages: Signal<Set<MessageId>?, NoError>,
mode: ChatHistoryListMode
) -> ChatHistoryListNode
func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)?, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool, isStandalone: Bool) -> ListViewItem
func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: ((UIView?, CGPoint?) -> Void)?, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool, isStandalone: Bool) -> ListViewItem
func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader
func makeChatMessageAvatarHeaderItem(context: AccountContext, timestamp: Int32, peer: Peer, message: Message, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader
func makePeerSharedMediaController(context: AccountContext, peerId: PeerId) -> ViewController?
@ -1100,6 +1103,10 @@ public protocol SharedAccountContext: AnyObject {
func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController
func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError>
func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController
func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool)
func makeDebugSettingsController(context: AccountContext?) -> ViewController?

View File

@ -58,7 +58,7 @@ final class BotCheckoutWebInteractionController: ViewController {
}
override func loadDisplayNode() {
self.displayNode = BotCheckoutWebInteractionControllerNode(presentationData: self.presentationData, url: self.url, intent: self.intent)
self.displayNode = BotCheckoutWebInteractionControllerNode(context: self.context, presentationData: self.presentationData, url: self.url, intent: self.intent)
}
override func viewDidAppear(_ animated: Bool) {

View File

@ -4,6 +4,7 @@ import Display
import AsyncDisplayKit
import WebKit
import TelegramPresentationData
import AccountContext
private class WeakPaymentScriptMessageHandler: NSObject, WKScriptMessageHandler {
private let f: (WKScriptMessage) -> ()
@ -20,12 +21,14 @@ private class WeakPaymentScriptMessageHandler: NSObject, WKScriptMessageHandler
}
final class BotCheckoutWebInteractionControllerNode: ViewControllerTracingNode, WKNavigationDelegate {
private let context: AccountContext
private var presentationData: PresentationData
private let intent: BotCheckoutWebInteractionControllerIntent
private var webView: WKWebView?
init(presentationData: PresentationData, url: String, intent: BotCheckoutWebInteractionControllerIntent) {
init(context: AccountContext, presentationData: PresentationData, url: String, intent: BotCheckoutWebInteractionControllerIntent) {
self.context = context
self.presentationData = presentationData
self.intent = intent
@ -146,6 +149,14 @@ final class BotCheckoutWebInteractionControllerNode: ViewControllerTracingNode,
decisionHandler(.allow)
}
} else {
if let url = navigationAction.request.url, let scheme = url.scheme {
let defaultSchemes: [String] = ["http", "https"]
if !defaultSchemes.contains(scheme) {
decisionHandler(.cancel)
self.context.sharedContext.applicationBindings.openUrl(url.absoluteString)
return
}
}
decisionHandler(.allow)
}
}

View File

@ -3543,9 +3543,19 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
} else if case .apps = key {
if let navigationController = self.navigationController {
if isRecommended {
#if DEBUG
let _ = (self.context.sharedContext.makeMiniAppListScreenInitialData(context: self.context)
|> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in
guard let self, let navigationController = self.navigationController else {
return
}
navigationController.pushViewController(self.context.sharedContext.makeMiniAppListScreen(context: self.context, initialData: initialData))
})
#else
if let peerInfoScreen = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
navigationController.pushViewController(peerInfoScreen)
}
#endif
} else if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.parentController {
self.context.sharedContext.openWebApp(
context: self.context,
@ -3560,7 +3570,6 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
skipTermsOfService: true
)
} else {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
navigationController: navigationController,
context: self.context,

View File

@ -47,6 +47,13 @@ open class PortalSourceView: UIView {
}
}
public func removePortal(view: PortalView) {
if let index = self.portalReferences.firstIndex(where: { $0.portalView === view }) {
self.portalReferences.remove(at: index)
}
view.disablePortal()
}
func setGlobalPortal(view: GlobalPortalView?) {
if let globalPortalView = self.globalPortalView {
self.globalPortalView = nil

View File

@ -23,6 +23,11 @@ public class PortalView {
}
}
func disablePortal() {
self.view.sourceView = nil
self.sourceView = nil
}
public func reloadPortal() {
if let sourceView = self.sourceView as? PortalSourceView {
self.reloadPortal(sourceView: sourceView)

View File

@ -550,6 +550,10 @@ public final class SparseItemGrid: ASDisplayNode {
var offset: CGFloat {
return self.scrollView.contentOffset.y
}
var contentBottomOffset: CGFloat {
return -self.scrollView.contentOffset.y + self.scrollView.contentSize.height
}
let coveringOffsetUpdated: (Viewport, ContainedViewLayoutTransition) -> Void
let offsetUpdated: (Viewport, ContainedViewLayoutTransition) -> Void
@ -1442,6 +1446,10 @@ public final class SparseItemGrid: ASDisplayNode {
return self.fromViewport.coveringInsetOffset * (1.0 - self.currentProgress) + self.toViewport.coveringInsetOffset * self.currentProgress
}
var contentBottomOffset: CGFloat {
return self.fromViewport.contentBottomOffset * (1.0 - self.currentProgress) + self.toViewport.contentBottomOffset * self.currentProgress
}
var offset: CGFloat {
return self.fromViewport.offset * (1.0 - self.currentProgress) + self.toViewport.offset * self.currentProgress
}
@ -1632,6 +1640,16 @@ public final class SparseItemGrid: ASDisplayNode {
}
}
public var contentBottomOffset: CGFloat {
if let currentViewportTransition = self.currentViewportTransition {
return currentViewportTransition.contentBottomOffset
} else if let currentViewport = self.currentViewport {
return currentViewport.contentBottomOffset
} else {
return 0.0
}
}
public var scrollingOffset: CGFloat {
if let currentViewportTransition = self.currentViewportTransition {
return currentViewportTransition.offset

View File

@ -105,6 +105,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-944407322] = { return Api.BotMenuButton.parse_botMenuButton($0) }
dict[1113113093] = { return Api.BotMenuButton.parse_botMenuButtonCommands($0) }
dict[1966318984] = { return Api.BotMenuButton.parse_botMenuButtonDefault($0) }
dict[602479523] = { return Api.BotPreviewMedia.parse_botPreviewMedia($0) }
dict[-2076642874] = { return Api.BroadcastRevenueBalances.parse_broadcastRevenueBalances($0) }
dict[1434332356] = { return Api.BroadcastRevenueTransaction.parse_broadcastRevenueTransactionProceeds($0) }
dict[1121127726] = { return Api.BroadcastRevenueTransaction.parse_broadcastRevenueTransactionRefund($0) }
@ -520,7 +521,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[340088945] = { return Api.MediaArea.parse_mediaAreaSuggestedReaction($0) }
dict[926421125] = { return Api.MediaArea.parse_mediaAreaUrl($0) }
dict[-1098720356] = { return Api.MediaArea.parse_mediaAreaVenue($0) }
dict[1132918857] = { return Api.MediaArea.parse_mediaAreaWeather($0) }
dict[1235637404] = { return Api.MediaArea.parse_mediaAreaWeather($0) }
dict[-808853502] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) }
dict[-1808510398] = { return Api.Message.parse_message($0) }
dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) }
@ -1170,6 +1171,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1542017919] = { return Api.auth.SentCodeType.parse_sentCodeTypeSmsWord($0) }
dict[-391678544] = { return Api.bots.BotInfo.parse_botInfo($0) }
dict[428978491] = { return Api.bots.PopularAppBots.parse_popularAppBots($0) }
dict[212278628] = { return Api.bots.PreviewInfo.parse_previewInfo($0) }
dict[-309659827] = { return Api.channels.AdminLogResults.parse_adminLogResults($0) }
dict[-541588713] = { return Api.channels.ChannelParticipant.parse_channelParticipant($0) }
dict[-1699676497] = { return Api.channels.ChannelParticipants.parse_channelParticipants($0) }
@ -1493,6 +1495,8 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.BotMenuButton:
_1.serialize(buffer, boxed)
case let _1 as Api.BotPreviewMedia:
_1.serialize(buffer, boxed)
case let _1 as Api.BroadcastRevenueBalances:
_1.serialize(buffer, boxed)
case let _1 as Api.BroadcastRevenueTransaction:
@ -2151,6 +2155,8 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.bots.PopularAppBots:
_1.serialize(buffer, boxed)
case let _1 as Api.bots.PreviewInfo:
_1.serialize(buffer, boxed)
case let _1 as Api.channels.AdminLogResults:
_1.serialize(buffer, boxed)
case let _1 as Api.channels.ChannelParticipant:

View File

@ -231,7 +231,7 @@ public extension Api {
case mediaAreaSuggestedReaction(flags: Int32, coordinates: Api.MediaAreaCoordinates, reaction: Api.Reaction)
case mediaAreaUrl(coordinates: Api.MediaAreaCoordinates, url: String)
case mediaAreaVenue(coordinates: Api.MediaAreaCoordinates, geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String)
case mediaAreaWeather(flags: Int32, coordinates: Api.MediaAreaCoordinates, emoji: String, temperatureC: Double)
case mediaAreaWeather(coordinates: Api.MediaAreaCoordinates, emoji: String, temperatureC: Double, color: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
@ -295,14 +295,14 @@ public extension Api {
serializeString(venueId, buffer: buffer, boxed: false)
serializeString(venueType, buffer: buffer, boxed: false)
break
case .mediaAreaWeather(let flags, let coordinates, let emoji, let temperatureC):
case .mediaAreaWeather(let coordinates, let emoji, let temperatureC, let color):
if boxed {
buffer.appendInt32(1132918857)
buffer.appendInt32(1235637404)
}
serializeInt32(flags, buffer: buffer, boxed: false)
coordinates.serialize(buffer, true)
serializeString(emoji, buffer: buffer, boxed: false)
serializeDouble(temperatureC, buffer: buffer, boxed: false)
serializeInt32(color, buffer: buffer, boxed: false)
break
}
}
@ -323,8 +323,8 @@ public extension Api {
return ("mediaAreaUrl", [("coordinates", coordinates as Any), ("url", url as Any)])
case .mediaAreaVenue(let coordinates, let geo, let title, let address, let provider, let venueId, let venueType):
return ("mediaAreaVenue", [("coordinates", coordinates as Any), ("geo", geo as Any), ("title", title as Any), ("address", address as Any), ("provider", provider as Any), ("venueId", venueId as Any), ("venueType", venueType as Any)])
case .mediaAreaWeather(let flags, let coordinates, let emoji, let temperatureC):
return ("mediaAreaWeather", [("flags", flags as Any), ("coordinates", coordinates as Any), ("emoji", emoji as Any), ("temperatureC", temperatureC as Any)])
case .mediaAreaWeather(let coordinates, let emoji, let temperatureC, let color):
return ("mediaAreaWeather", [("coordinates", coordinates as Any), ("emoji", emoji as Any), ("temperatureC", temperatureC as Any), ("color", color as Any)])
}
}
@ -484,22 +484,22 @@ public extension Api {
}
}
public static func parse_mediaAreaWeather(_ reader: BufferReader) -> MediaArea? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Api.MediaAreaCoordinates?
var _1: Api.MediaAreaCoordinates?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.MediaAreaCoordinates
_1 = Api.parse(reader, signature: signature) as? Api.MediaAreaCoordinates
}
var _3: String?
_3 = parseString(reader)
var _4: Double?
_4 = reader.readDouble()
var _2: String?
_2 = parseString(reader)
var _3: Double?
_3 = reader.readDouble()
var _4: Int32?
_4 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.MediaArea.mediaAreaWeather(flags: _1!, coordinates: _2!, emoji: _3!, temperatureC: _4!)
return Api.MediaArea.mediaAreaWeather(coordinates: _1!, emoji: _2!, temperatureC: _3!, color: _4!)
}
else {
return nil

View File

@ -724,6 +724,48 @@ public extension Api {
}
}
public extension Api {
indirect enum BotPreviewMedia: TypeConstructorDescription {
case botPreviewMedia(date: Int32, media: Api.MessageMedia)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .botPreviewMedia(let date, let media):
if boxed {
buffer.appendInt32(602479523)
}
serializeInt32(date, buffer: buffer, boxed: false)
media.serialize(buffer, true)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .botPreviewMedia(let date, let media):
return ("botPreviewMedia", [("date", date as Any), ("media", media as Any)])
}
}
public static func parse_botPreviewMedia(_ reader: BufferReader) -> BotPreviewMedia? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Api.MessageMedia?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.MessageMedia
}
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.BotPreviewMedia.botPreviewMedia(date: _1!, media: _2!)
}
else {
return nil
}
}
}
}
public extension Api {
enum BroadcastRevenueBalances: TypeConstructorDescription {
case broadcastRevenueBalances(currentBalance: Int64, availableBalance: Int64, overallRevenue: Int64)
@ -1160,53 +1202,3 @@ public extension Api {
}
}
public extension Api {
enum BusinessIntro: TypeConstructorDescription {
case businessIntro(flags: Int32, title: String, description: String, sticker: Api.Document?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .businessIntro(let flags, let title, let description, let sticker):
if boxed {
buffer.appendInt32(1510606445)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(title, buffer: buffer, boxed: false)
serializeString(description, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {sticker!.serialize(buffer, true)}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .businessIntro(let flags, let title, let description, let sticker):
return ("businessIntro", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("sticker", sticker as Any)])
}
}
public static func parse_businessIntro(_ reader: BufferReader) -> BusinessIntro? {
var _1: Int32?
_1 = reader.readInt32()
var _2: String?
_2 = parseString(reader)
var _3: String?
_3 = parseString(reader)
var _4: Api.Document?
if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() {
_4 = Api.parse(reader, signature: signature) as? Api.Document
} }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.BusinessIntro.businessIntro(flags: _1!, title: _2!, description: _3!, sticker: _4)
}
else {
return nil
}
}
}
}

View File

@ -1,3 +1,55 @@
public extension Api.bots {
enum PreviewInfo: TypeConstructorDescription {
case previewInfo(media: [Api.BotPreviewMedia], langCodes: [String])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .previewInfo(let media, let langCodes):
if boxed {
buffer.appendInt32(212278628)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(media.count))
for item in media {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(langCodes.count))
for item in langCodes {
serializeString(item, buffer: buffer, boxed: false)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .previewInfo(let media, let langCodes):
return ("previewInfo", [("media", media as Any), ("langCodes", langCodes as Any)])
}
}
public static func parse_previewInfo(_ reader: BufferReader) -> PreviewInfo? {
var _1: [Api.BotPreviewMedia]?
if let _ = reader.readInt32() {
_1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotPreviewMedia.self)
}
var _2: [String]?
if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.bots.PreviewInfo.previewInfo(media: _1!, langCodes: _2!)
}
else {
return nil
}
}
}
}
public extension Api.channels {
enum AdminLogResults: TypeConstructorDescription {
case adminLogResults(events: [Api.ChannelAdminLogEvent], chats: [Api.Chat], users: [Api.User])
@ -1404,61 +1456,3 @@ public extension Api.help {
}
}
public extension Api.help {
enum Country: TypeConstructorDescription {
case country(flags: Int32, iso2: String, defaultName: String, name: String?, countryCodes: [Api.help.CountryCode])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .country(let flags, let iso2, let defaultName, let name, let countryCodes):
if boxed {
buffer.appendInt32(-1014526429)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(iso2, buffer: buffer, boxed: false)
serializeString(defaultName, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {serializeString(name!, buffer: buffer, boxed: false)}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(countryCodes.count))
for item in countryCodes {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .country(let flags, let iso2, let defaultName, let name, let countryCodes):
return ("country", [("flags", flags as Any), ("iso2", iso2 as Any), ("defaultName", defaultName as Any), ("name", name as Any), ("countryCodes", countryCodes as Any)])
}
}
public static func parse_country(_ reader: BufferReader) -> Country? {
var _1: Int32?
_1 = reader.readInt32()
var _2: String?
_2 = parseString(reader)
var _3: String?
_3 = parseString(reader)
var _4: String?
if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) }
var _5: [Api.help.CountryCode]?
if let _ = reader.readInt32() {
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.help.CountryCode.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil
let _c5 = _5 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 {
return Api.help.Country.country(flags: _1!, iso2: _2!, defaultName: _3!, name: _4, countryCodes: _5!)
}
else {
return nil
}
}
}
}

View File

@ -1,3 +1,53 @@
public extension Api {
enum BusinessIntro: TypeConstructorDescription {
case businessIntro(flags: Int32, title: String, description: String, sticker: Api.Document?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .businessIntro(let flags, let title, let description, let sticker):
if boxed {
buffer.appendInt32(1510606445)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(title, buffer: buffer, boxed: false)
serializeString(description, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {sticker!.serialize(buffer, true)}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .businessIntro(let flags, let title, let description, let sticker):
return ("businessIntro", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("sticker", sticker as Any)])
}
}
public static func parse_businessIntro(_ reader: BufferReader) -> BusinessIntro? {
var _1: Int32?
_1 = reader.readInt32()
var _2: String?
_2 = parseString(reader)
var _3: String?
_3 = parseString(reader)
var _4: Api.Document?
if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() {
_4 = Api.parse(reader, signature: signature) as? Api.Document
} }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.BusinessIntro.businessIntro(flags: _1!, title: _2!, description: _3!, sticker: _4)
}
else {
return nil
}
}
}
}
public extension Api {
enum BusinessLocation: TypeConstructorDescription {
case businessLocation(flags: Int32, geoPoint: Api.GeoPoint?, address: String)

View File

@ -1,3 +1,61 @@
public extension Api.help {
enum Country: TypeConstructorDescription {
case country(flags: Int32, iso2: String, defaultName: String, name: String?, countryCodes: [Api.help.CountryCode])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .country(let flags, let iso2, let defaultName, let name, let countryCodes):
if boxed {
buffer.appendInt32(-1014526429)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(iso2, buffer: buffer, boxed: false)
serializeString(defaultName, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {serializeString(name!, buffer: buffer, boxed: false)}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(countryCodes.count))
for item in countryCodes {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .country(let flags, let iso2, let defaultName, let name, let countryCodes):
return ("country", [("flags", flags as Any), ("iso2", iso2 as Any), ("defaultName", defaultName as Any), ("name", name as Any), ("countryCodes", countryCodes as Any)])
}
}
public static func parse_country(_ reader: BufferReader) -> Country? {
var _1: Int32?
_1 = reader.readInt32()
var _2: String?
_2 = parseString(reader)
var _3: String?
_3 = parseString(reader)
var _4: String?
if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) }
var _5: [Api.help.CountryCode]?
if let _ = reader.readInt32() {
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.help.CountryCode.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil
let _c5 = _5 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 {
return Api.help.Country.country(flags: _1!, iso2: _2!, defaultName: _3!, name: _4, countryCodes: _5!)
}
else {
return nil
}
}
}
}
public extension Api.help {
enum CountryCode: TypeConstructorDescription {
case countryCode(flags: Int32, countryCode: String, prefixes: [String]?, patterns: [String]?)

View File

@ -2201,16 +2201,17 @@ public extension Api.functions.auth {
}
}
public extension Api.functions.bots {
static func addPreviewMedia(bot: Api.InputUser, media: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.MessageMedia>) {
static func addPreviewMedia(bot: Api.InputUser, langCode: String, media: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.BotPreviewMedia>) {
let buffer = Buffer()
buffer.appendInt32(1633332331)
buffer.appendInt32(397326170)
bot.serialize(buffer, true)
serializeString(langCode, buffer: buffer, boxed: false)
media.serialize(buffer, true)
return (FunctionDescription(name: "bots.addPreviewMedia", parameters: [("bot", String(describing: bot)), ("media", String(describing: media))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.MessageMedia? in
return (FunctionDescription(name: "bots.addPreviewMedia", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode)), ("media", String(describing: media))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.BotPreviewMedia? in
let reader = BufferReader(buffer)
var result: Api.MessageMedia?
var result: Api.BotPreviewMedia?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.MessageMedia
result = Api.parse(reader, signature: signature) as? Api.BotPreviewMedia
}
return result
})
@ -2263,16 +2264,17 @@ public extension Api.functions.bots {
}
}
public extension Api.functions.bots {
static func deletePreviewMedia(bot: Api.InputUser, media: [Api.InputMedia]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
static func deletePreviewMedia(bot: Api.InputUser, langCode: String, media: [Api.InputMedia]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(481471475)
buffer.appendInt32(755054003)
bot.serialize(buffer, true)
serializeString(langCode, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(media.count))
for item in media {
item.serialize(buffer, true)
}
return (FunctionDescription(name: "bots.deletePreviewMedia", parameters: [("bot", String(describing: bot)), ("media", String(describing: media))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
return (FunctionDescription(name: "bots.deletePreviewMedia", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode)), ("media", String(describing: media))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {
@ -2283,17 +2285,18 @@ public extension Api.functions.bots {
}
}
public extension Api.functions.bots {
static func editPreviewMedia(bot: Api.InputUser, media: Api.InputMedia, newMedia: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.MessageMedia>) {
static func editPreviewMedia(bot: Api.InputUser, langCode: String, media: Api.InputMedia, newMedia: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.BotPreviewMedia>) {
let buffer = Buffer()
buffer.appendInt32(-1436441263)
buffer.appendInt32(-2061148049)
bot.serialize(buffer, true)
serializeString(langCode, buffer: buffer, boxed: false)
media.serialize(buffer, true)
newMedia.serialize(buffer, true)
return (FunctionDescription(name: "bots.editPreviewMedia", parameters: [("bot", String(describing: bot)), ("media", String(describing: media)), ("newMedia", String(describing: newMedia))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.MessageMedia? in
return (FunctionDescription(name: "bots.editPreviewMedia", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode)), ("media", String(describing: media)), ("newMedia", String(describing: newMedia))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.BotPreviewMedia? in
let reader = BufferReader(buffer)
var result: Api.MessageMedia?
var result: Api.BotPreviewMedia?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.MessageMedia
result = Api.parse(reader, signature: signature) as? Api.BotPreviewMedia
}
return result
})
@ -2364,15 +2367,31 @@ public extension Api.functions.bots {
}
}
public extension Api.functions.bots {
static func getPreviewMedias(bot: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.MessageMedia]>) {
static func getPreviewInfo(bot: Api.InputUser, langCode: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.bots.PreviewInfo>) {
let buffer = Buffer()
buffer.appendInt32(1720252591)
buffer.appendInt32(1111143341)
bot.serialize(buffer, true)
return (FunctionDescription(name: "bots.getPreviewMedias", parameters: [("bot", String(describing: bot))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Api.MessageMedia]? in
serializeString(langCode, buffer: buffer, boxed: false)
return (FunctionDescription(name: "bots.getPreviewInfo", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.bots.PreviewInfo? in
let reader = BufferReader(buffer)
var result: [Api.MessageMedia]?
var result: Api.bots.PreviewInfo?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.bots.PreviewInfo
}
return result
})
}
}
public extension Api.functions.bots {
static func getPreviewMedias(bot: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.BotPreviewMedia]>) {
let buffer = Buffer()
buffer.appendInt32(-1566222003)
bot.serialize(buffer, true)
return (FunctionDescription(name: "bots.getPreviewMedias", parameters: [("bot", String(describing: bot))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Api.BotPreviewMedia]? in
let reader = BufferReader(buffer)
var result: [Api.BotPreviewMedia]?
if let _ = reader.readInt32() {
result = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageMedia.self)
result = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotPreviewMedia.self)
}
return result
})
@ -2396,16 +2415,17 @@ public extension Api.functions.bots {
}
}
public extension Api.functions.bots {
static func reorderPreviewMedias(bot: Api.InputUser, order: [Api.InputMedia]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
static func reorderPreviewMedias(bot: Api.InputUser, langCode: String, order: [Api.InputMedia]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(-1472444656)
buffer.appendInt32(-1238895702)
bot.serialize(buffer, true)
serializeString(langCode, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(order.count))
for item in order {
item.serialize(buffer, true)
}
return (FunctionDescription(name: "bots.reorderPreviewMedias", parameters: [("bot", String(describing: bot)), ("order", String(describing: order))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
return (FunctionDescription(name: "bots.reorderPreviewMedias", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode)), ("order", String(describing: order))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {

View File

@ -522,9 +522,9 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? {
return .link(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), url: url)
case let .mediaAreaChannelPost(coordinates, channelId, messageId):
return .channelMessage(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), messageId: EngineMessage.Id(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId))
case let .mediaAreaWeather(flags, coordinates, emoji, temperatureC):
case let .mediaAreaWeather(coordinates, emoji, temperatureC, color):
var parsedFlags = MediaArea.WeatherFlags()
if (flags & (1 << 0)) != 0 {
if color != 0 {
parsedFlags.insert(.isDark)
}
return .weather(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), emoji: emoji, temperature: temperatureC, flags: parsedFlags)
@ -581,11 +581,7 @@ func apiMediaAreasFromMediaAreas(_ mediaAreas: [MediaArea], transaction: Transac
case let .link(_, url):
apiMediaAreas.append(.mediaAreaUrl(coordinates: inputCoordinates, url: url))
case let .weather(_, emoji, temperature, flags):
var apiFlags: Int32 = 0
if flags.contains(.isDark) {
apiFlags |= (1 << 0)
}
apiMediaAreas.append(.mediaAreaWeather(flags: apiFlags, coordinates: inputCoordinates, emoji: emoji, temperatureC: temperature))
apiMediaAreas.append(.mediaAreaWeather(coordinates: inputCoordinates, emoji: emoji, temperatureC: temperature, color: flags.contains(.isDark) ? 1 : 0))
}
}
return apiMediaAreas

View File

@ -287,6 +287,11 @@ public final class AccountStateManager {
return self.storyUpdatesPipe.signal()
}
fileprivate let botPreviewUpdatesPipe = ValuePipe<[InternalBotPreviewUpdate]>()
public var botPreviewUpdates: Signal<[InternalBotPreviewUpdate], NoError> {
return self.botPreviewUpdatesPipe.signal()
}
private var updatedWebpageContexts: [MediaId: UpdatedWebpageSubscriberContext] = [:]
private var updatedPeersNearbyContext = UpdatedPeersNearbySubscriberContext()
private var updatedRevenueBalancesContext = UpdatedRevenueBalancesSubscriberContext()
@ -1856,6 +1861,18 @@ public final class AccountStateManager {
}
}
var botPreviewUpdates: Signal<[InternalBotPreviewUpdate], NoError> {
return self.impl.signalWith { impl, subscriber in
return impl.botPreviewUpdates.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)
}
}
func injectBotPreviewUpdates(updates: [InternalBotPreviewUpdate]) {
self.impl.with { impl in
impl.botPreviewUpdatesPipe.putNext(updates)
}
}
var updateConfigRequested: (() -> Void)?
var isPremiumUpdated: (() -> Void)?

View File

@ -627,40 +627,87 @@ extension TelegramBusinessChatLinks {
public final class CachedUserData: CachedPeerData {
public final class BotPreview: Codable, Equatable {
private enum CodingKeys: String, CodingKey {
case media
case items
case alternativeLanguageCodes
}
public let media: [Media]
public final class Item: Codable, Equatable {
private enum CodingKeys: String, CodingKey {
case media = "m"
case timestamp = "t"
}
public let media: Media
public let timestamp: Int32
public init(media: Media, timestamp: Int32) {
self.media = media
self.timestamp = timestamp
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let mediaData = try container.decode(Data.self, forKey: .media)
guard let media = PostboxDecoder(buffer: MemoryBuffer(data: mediaData)).decodeRootObject() as? Media else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "media"))
}
self.media = media
self.timestamp = try container.decode(Int32.self, forKey: .timestamp)
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let encoder = PostboxEncoder()
encoder.encodeRootObject(media)
try container.encode(encoder.makeData(), forKey: .media)
try container.encode(self.timestamp, forKey: .timestamp)
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs === rhs {
return true
}
if !lhs.media.isEqual(to: rhs.media) {
return false
}
return true
}
}
public init(media: [Media]) {
self.media = media
public let items: [Item]
public let alternativeLanguageCodes: [String]
public init(items: [Item], alternativeLanguageCodes: [String]) {
self.items = items
self.alternativeLanguageCodes = alternativeLanguageCodes
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let mediaData = try container.decode([Data].self, forKey: .media)
self.media = mediaData.compactMap { data -> Media? in
return PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? Media
}
self.items = try container.decode([Item].self, forKey: .items)
self.alternativeLanguageCodes = try container.decode([String].self, forKey: .alternativeLanguageCodes)
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let mediaData = self.media.map { media -> Data in
let encoder = PostboxEncoder()
encoder.encodeRootObject(media)
return encoder.makeData()
}
try container.encode(mediaData, forKey: .media)
try container.encode(self.items, forKey: .items)
try container.encode(self.alternativeLanguageCodes, forKey: .alternativeLanguageCodes)
}
public static func ==(lhs: BotPreview, rhs: BotPreview) -> Bool {
if lhs === rhs {
return true
}
if !areMediaArraysEqual(lhs.media, rhs.media) {
if lhs.items != rhs.items {
return false
}
if lhs.alternativeLanguageCodes != rhs.alternativeLanguageCodes {
return false
}
return true

View File

@ -8,11 +8,12 @@ public extension Stories {
private enum CodingKeys: String, CodingKey {
case discriminator = "tt"
case peerId = "peerId"
case language = "language"
}
case myStories
case peer(PeerId)
case botPreview(PeerId)
case botPreview(id: PeerId, language: String?)
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
@ -23,7 +24,7 @@ public extension Stories {
case 1:
self = .peer(try container.decode(PeerId.self, forKey: .peerId))
case 2:
self = .botPreview(try container.decode(PeerId.self, forKey: .peerId))
self = .botPreview(id: try container.decode(PeerId.self, forKey: .peerId), language: try container.decodeIfPresent(String.self, forKey: .language))
default:
self = .myStories
}
@ -38,9 +39,10 @@ public extension Stories {
case let .peer(peerId):
try container.encode(1 as Int32, forKey: .discriminator)
try container.encode(peerId, forKey: .peerId)
case let .botPreview(peerId):
case let .botPreview(peerId, language):
try container.encode(2 as Int32, forKey: .discriminator)
try container.encode(peerId, forKey: .peerId)
try container.encodeIfPresent(language, forKey: .language)
}
}
}
@ -406,13 +408,15 @@ final class PendingStoryManager {
let toPeerId: PeerId
var isBotPreview = false
var botPreviewLanguage: String?
switch firstItem.target {
case .myStories:
toPeerId = self.accountPeerId
case let .peer(peerId):
toPeerId = peerId
case let .botPreview(peerId):
case let .botPreview(peerId, language):
toPeerId = peerId
botPreviewLanguage = language
isBotPreview = true
}
@ -427,6 +431,7 @@ final class PendingStoryManager {
revalidationContext: self.revalidationContext,
auxiliaryMethods: self.auxiliaryMethods,
toPeerId: toPeerId,
language: botPreviewLanguage,
stableId: stableId,
media: firstItem.media,
mediaAreas: firstItem.mediaAreas,

View File

@ -1270,6 +1270,7 @@ func _internal_uploadBotPreviewImpl(
revalidationContext: MediaReferenceRevalidationContext,
auxiliaryMethods: AccountAuxiliaryMethods,
toPeerId: PeerId,
language: String?,
stableId: Int32,
media: Media,
mediaAreas: [MediaArea],
@ -1300,46 +1301,59 @@ func _internal_uploadBotPreviewImpl(
return postbox.transaction { transaction -> Signal<StoryUploadResult, NoError> in
switch content.content {
case let .media(inputMedia, _):
return network.request(Api.functions.bots.addPreviewMedia(bot: inputUser, media: inputMedia))
return network.request(Api.functions.bots.addPreviewMedia(bot: inputUser, langCode: language ?? "", media: inputMedia))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.MessageMedia?, NoError> in
|> `catch` { _ -> Signal<Api.BotPreviewMedia?, NoError> in
return .single(nil)
}
|> mapToSignal { resultMedia -> Signal<StoryUploadResult, NoError> in
return postbox.transaction { transaction -> StoryUploadResult in
var currentState: Stories.LocalState
if let value = transaction.getLocalStoryState()?.get(Stories.LocalState.self) {
currentState = value
} else {
currentState = Stories.LocalState(items: [])
}
if let index = currentState.items.firstIndex(where: { $0.stableId == stableId }) {
currentState.items.remove(at: index)
transaction.setLocalStoryState(state: CodableEntry(currentState))
}
if let resultMediaValue = textMediaAndExpirationTimerFromApiMedia(resultMedia, toPeerId).media {
applyMediaResourceChanges(from: originalMedia, to: resultMediaValue, postbox: postbox, force: originalMedia is TelegramMediaFile && resultMediaValue is TelegramMediaFile)
|> mapToSignal { resultPreviewMedia -> Signal<StoryUploadResult, NoError> in
guard let resultPreviewMedia else {
return .single(.completed(nil))
}
switch resultPreviewMedia {
case let .botPreviewMedia(date, resultMedia):
return postbox.transaction { transaction -> StoryUploadResult in
var currentState: Stories.LocalState
if let value = transaction.getLocalStoryState()?.get(Stories.LocalState.self) {
currentState = value
} else {
currentState = Stories.LocalState(items: [])
}
if let index = currentState.items.firstIndex(where: { $0.stableId == stableId }) {
currentState.items.remove(at: index)
transaction.setLocalStoryState(state: CodableEntry(currentState))
}
transaction.updatePeerCachedData(peerIds: Set([toPeerId]), update: { _, current in
guard var current = current as? CachedUserData else {
return current
if let resultMediaValue = textMediaAndExpirationTimerFromApiMedia(resultMedia, toPeerId).media {
applyMediaResourceChanges(from: originalMedia, to: resultMediaValue, postbox: postbox, force: originalMedia is TelegramMediaFile && resultMediaValue is TelegramMediaFile)
let addedItem = CachedUserData.BotPreview.Item(media: resultMediaValue, timestamp: date)
if language == nil {
transaction.updatePeerCachedData(peerIds: Set([toPeerId]), update: { _, current in
guard var current = current as? CachedUserData else {
return current
}
guard let currentBotPreview = current.botPreview else {
return current
}
var items = currentBotPreview.items
if let index = items.firstIndex(where: { $0.media.id == resultMediaValue.id }) {
items.remove(at: index)
}
items.insert(addedItem, at: 0)
let botPreview = CachedUserData.BotPreview(items: items, alternativeLanguageCodes: currentBotPreview.alternativeLanguageCodes)
current = current.withUpdatedBotPreview(botPreview)
return current
})
}
guard let currentBotPreview = current.botPreview else {
return current
}
var media = currentBotPreview.media
if let index = media.firstIndex(where: { $0.id == resultMediaValue.id }) {
media.remove(at: index)
}
media.insert(resultMediaValue, at: 0)
let botPreview = CachedUserData.BotPreview(media: media)
current = current.withUpdatedBotPreview(botPreview)
return current
})
stateManager.injectBotPreviewUpdates(updates: [
.added(peerId: toPeerId, language: language, item: addedItem)
])
}
return .completed(nil)
}
return .completed(nil)
}
}
default:
@ -1354,13 +1368,79 @@ func _internal_uploadBotPreviewImpl(
}
}
func _internal_deleteBotPreviews(account: Account, peerId: PeerId, ids: [MediaId]) -> Signal<Never, NoError> {
func _internal_deleteBotPreviews(account: Account, peerId: PeerId, language: String?, media: [Media]) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> (Api.InputUser?, [Api.InputMedia]) in
guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputUser) else {
return (nil, [])
}
var inputMedia: [Api.InputMedia] = []
for item in media {
if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource {
inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
} else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource {
inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil))
}
}
if language == nil {
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current -> CachedPeerData? in
guard var current = current as? CachedUserData else {
return current
}
guard let currentBotPreview = current.botPreview else {
return current
}
var items = currentBotPreview.items
items = items.filter({ item in
guard let id = item.media.id else {
return false
}
return !media.contains(where: { $0.id == id })
})
let botPreview = CachedUserData.BotPreview(items: items, alternativeLanguageCodes: currentBotPreview.alternativeLanguageCodes)
current = current.withUpdatedBotPreview(botPreview)
return current
})
}
return (inputPeer, inputMedia)
}
|> mapToSignal { inputPeer, inputMedia -> Signal<Never, NoError> in
guard let inputPeer else {
return .complete()
}
account.stateManager.injectBotPreviewUpdates(updates: [
.deleted(peerId: peerId, language: language, ids: media.compactMap(\.id))
])
return account.network.request(Api.functions.bots.deletePreviewMedia(bot: inputPeer, langCode: language ?? "", media: inputMedia))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Never, NoError> in
return .complete()
}
}
}
func _internal_deleteBotPreviewsLanguage(account: Account, peerId: PeerId, language: String, media: [Media]) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> (Api.InputUser?, [Api.InputMedia]) in
guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputUser) else {
return (nil, [])
}
var inputMedia: [Api.InputMedia] = []
for item in media {
if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource {
inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
} else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource {
inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil))
}
}
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current -> CachedPeerData? in
guard var current = current as? CachedUserData else {
return current
@ -1368,29 +1448,11 @@ func _internal_deleteBotPreviews(account: Account, peerId: PeerId, ids: [MediaId
guard let currentBotPreview = current.botPreview else {
return current
}
var media = currentBotPreview.media
for item in media {
guard let id = item.id else {
continue
}
if ids.contains(id) {
if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource {
inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
} else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource {
inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil))
}
}
var alternativeLanguageCodes = currentBotPreview.alternativeLanguageCodes
alternativeLanguageCodes = alternativeLanguageCodes.filter { item in
return item != language
}
media = media.filter({ item in
guard let id = item.id else {
return false
}
return !ids.contains(id)
})
let botPreview = CachedUserData.BotPreview(media: media)
let botPreview = CachedUserData.BotPreview(items: currentBotPreview.items, alternativeLanguageCodes: alternativeLanguageCodes)
current = current.withUpdatedBotPreview(botPreview)
return current
})
@ -1402,7 +1464,11 @@ func _internal_deleteBotPreviews(account: Account, peerId: PeerId, ids: [MediaId
return .complete()
}
return account.network.request(Api.functions.bots.deletePreviewMedia(bot: inputPeer, media: inputMedia))
account.stateManager.injectBotPreviewUpdates(updates: [
.deleted(peerId: peerId, language: language, ids: media.compactMap(\.id))
])
return account.network.request(Api.functions.bots.deletePreviewMedia(bot: inputPeer, langCode: language, media: inputMedia))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
@ -1623,8 +1689,8 @@ func _internal_checkStoriesUploadAvailability(account: Account, target: Stories.
return .inputPeerSelf
case let .peer(peerId):
return transaction.getPeer(peerId).flatMap(apiInputPeer)
case let .botPreview(peerId):
return transaction.getPeer(peerId).flatMap(apiInputPeer)
case .botPreview:
return nil
}
}
|> mapToSignal { inputPeer -> Signal<StoriesUploadAvailability, NoError> in

View File

@ -12,6 +12,11 @@ enum InternalStoryUpdate {
case updateMyReaction(peerId: PeerId, id: Int32, reaction: MessageReaction.Reaction?)
}
enum InternalBotPreviewUpdate {
case added(peerId: PeerId, language: String?, item: CachedUserData.BotPreview.Item)
case deleted(peerId: PeerId, language: String?, ids: [MediaId])
}
public final class EngineStoryItem: Equatable {
public final class Views: Equatable {
public let seenCount: Int
@ -563,8 +568,19 @@ public struct StoryListContextState: Equatable {
}
}
public struct Language: Equatable {
public let id: String
public let name: String
public init(id: String, name: String) {
self.id = id
self.name = name
}
}
public var peerReference: PeerReference?
public var items: [Item]
public var availableLanguages: [Language]
public var pinnedIds: [Int32]
public var totalCount: Int
public var loadMoreToken: AnyHashable?
@ -575,6 +591,7 @@ public struct StoryListContextState: Equatable {
public init(
peerReference: PeerReference?,
items: [Item],
availableLanguages: [Language],
pinnedIds: [Int32],
totalCount: Int,
loadMoreToken: AnyHashable?,
@ -585,6 +602,7 @@ public struct StoryListContextState: Equatable {
) {
self.peerReference = peerReference
self.items = items
self.availableLanguages = availableLanguages
self.pinnedIds = pinnedIds
self.totalCount = totalCount
self.loadMoreToken = loadMoreToken
@ -633,7 +651,7 @@ public final class PeerStoryListContext: StoryListContext {
self.peerId = peerId
self.isArchived = isArchived
self.stateValue = State(peerReference: nil, items: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false)
self.stateValue = State(peerReference: nil, items: [], availableLanguages: [], 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 key = ValueBoxKey(length: 8 + 1)
@ -723,7 +741,7 @@ public final class PeerStoryListContext: StoryListContext {
return
}
var updatedState = State(peerReference: peerReference, items: items, 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: [], pinnedIds: pinnedIds, totalCount: totalCount, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: hasCache, allEntityFiles: allEntityFiles, isLoading: false)
updatedState.items.sort(by: { lhs, rhs in
let lhsPinned = updatedState.pinnedIds.firstIndex(of: lhs.storyItem.id)
let rhsPinned = updatedState.pinnedIds.firstIndex(of: rhs.storyItem.id)
@ -746,6 +764,7 @@ public final class PeerStoryListContext: StoryListContext {
deinit {
self.requestDisposable?.dispose()
self.updatesDisposable?.dispose()
}
func loadMore(completion: (() -> Void)?) {
@ -1313,7 +1332,7 @@ public final class SearchStoryListContext: StoryListContext {
self.account = account
self.source = source
self.stateValue = State(peerReference: nil, items: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(""), isCached: false, hasCache: false, allEntityFiles: [:], isLoading: false)
self.stateValue = State(peerReference: nil, items: [], availableLanguages: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(""), isCached: false, hasCache: false, allEntityFiles: [:], isLoading: false)
self.statePromise.set(.single(self.stateValue))
self.loadMore(completion: nil)
@ -2078,6 +2097,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
private let account: Account
private let engine: TelegramEngine
private let peerId: EnginePeer.Id
private let language: String?
private let isArchived: Bool
private let statePromise = Promise<State>()
@ -2093,6 +2113,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
private var isLoadingMore: Bool = false
private var requestDisposable: Disposable?
private var updatesDisposable: Disposable?
private var eventsDisposable: Disposable?
private let reorderDisposable = MetaDisposable()
private var completionCallbacksByToken: [AnyHashable: [() -> Void]] = [:]
@ -2102,36 +2123,305 @@ public final class BotPreviewStoryListContext: StoryListContext {
private var idMapping: [MediaId: Int32] = [:]
private var reverseIdMapping: [Int32: MediaId] = [:]
init(queue: Queue, account: Account, engine: TelegramEngine, peerId: EnginePeer.Id) {
private var localItems: [State.Item] = []
private var remoteItems: [State.Item] = []
init(queue: Queue, account: Account, engine: TelegramEngine, peerId: EnginePeer.Id, language: String?, assumeEmpty: Bool) {
self.queue = queue
self.account = account
self.engine = engine
self.peerId = peerId
self.language = language
let isArchived = false
self.isArchived = isArchived
self.stateValue = State(peerReference: nil, items: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false)
self.stateValue = State(peerReference: nil, items: [], availableLanguages: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false)
let localStateKey: PostboxViewKey = .storiesState(key: .local)
self.requestDisposable = (combineLatest(queue: queue,
engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
TelegramEngine.EngineData.Item.Peer.BotPreview(id: peerId)
),
account.postbox.combinedView(keys: [localStateKey])
)
|> deliverOn(self.queue)).start(next: { [weak self] peerAndBotPreview, combinedView in
if let language {
let _ = (account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peerId)
}
|> deliverOn(self.queue)).start(next: { [weak self] peer in
guard let self else {
return
}
self.stateValue = State(
peerReference: peer.flatMap(PeerReference.init),
items: [],
availableLanguages: [],
pinnedIds: [],
totalCount: 0,
loadMoreToken: AnyHashable(0),
isCached: assumeEmpty,
hasCache: assumeEmpty,
allEntityFiles: [:],
isLoading: !assumeEmpty
)
self.loadLanguage(language: language, assumeEmpty: assumeEmpty)
})
} else {
self.requestDisposable = (combineLatest(queue: queue,
engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
TelegramEngine.EngineData.Item.Peer.BotPreview(id: peerId),
TelegramEngine.EngineData.Item.Configuration.LocalizationList()
),
account.postbox.combinedView(keys: [
localStateKey
])
)
|> deliverOn(self.queue)).start(next: { [weak self] peerAndBotPreview, combinedView in
guard let self else {
return
}
let (peer, botPreview, localizationList) = peerAndBotPreview
var items: [State.Item] = []
var availableLanguages: [StoryListContextState.Language] = []
if let stateView = combinedView.views[localStateKey] as? StoryStatesView, let localState = stateView.value?.get(Stories.LocalState.self) {
for item in localState.items.reversed() {
let mappedId: Int32
if let current = self.pendingIdMapping[item.stableId] {
mappedId = current
} else {
mappedId = self.nextId
self.nextId += 1
self.pendingIdMapping[item.stableId] = mappedId
}
if case let .botPreview(itemPeerId, itemLanguage) = item.target, itemPeerId == peerId, itemLanguage == language {
items.append(State.Item(
id: StoryId(peerId: peerId, id: mappedId),
storyItem: EngineStoryItem(
id: mappedId,
timestamp: 0,
expirationTimestamp: Int32.max,
media: EngineMedia(item.media),
alternativeMedia: nil,
mediaAreas: [],
text: "",
entities: [],
views: nil,
privacy: nil,
isPinned: false,
isExpired: false,
isPublic: false,
isPending: true,
isCloseFriends: false,
isContacts: false,
isSelectedContacts: false,
isForwardingDisabled: false,
isEdited: false,
isMy: false,
myReaction: nil,
forwardInfo: nil,
author: nil
),
peer: nil
))
}
}
}
if let botPreview {
for item in botPreview.items {
guard let mediaId = item.media.id else {
continue
}
let id: Int32
if let current = self.idMapping[mediaId] {
id = current
} else {
id = self.nextId
self.nextId += 1
self.idMapping[mediaId] = id
self.reverseIdMapping[id] = mediaId
}
items.append(State.Item(
id: StoryId(peerId: peerId, id: id),
storyItem: EngineStoryItem(
id: id,
timestamp: item.timestamp,
expirationTimestamp: Int32.max,
media: EngineMedia(item.media),
alternativeMedia: nil,
mediaAreas: [],
text: "",
entities: [],
views: nil,
privacy: nil,
isPinned: false,
isExpired: false,
isPublic: false,
isPending: false,
isCloseFriends: false,
isContacts: false,
isSelectedContacts: false,
isForwardingDisabled: false,
isEdited: false,
isMy: false,
myReaction: nil,
forwardInfo: nil,
author: nil
),
peer: nil
))
}
for id in botPreview.alternativeLanguageCodes {
inner: for localization in localizationList.availableOfficialLocalizations {
if localization.languageCode == id {
availableLanguages.append(StoryListContextState.Language(
id: localization.languageCode,
name: localization.title
))
break inner
}
}
}
}
self.stateValue = State(
peerReference: (peer?._asPeer()).flatMap(PeerReference.init),
items: items,
availableLanguages: availableLanguages,
pinnedIds: [],
totalCount: items.count,
loadMoreToken: nil,
isCached: botPreview != nil,
hasCache: botPreview != nil,
allEntityFiles: [:],
isLoading: botPreview == nil
)
})
}
}
deinit {
self.requestDisposable?.dispose()
self.updatesDisposable?.dispose()
self.eventsDisposable?.dispose()
self.reorderDisposable.dispose()
}
func loadMore(completion: (() -> Void)?) {
}
private func loadLanguage(language: String, assumeEmpty: Bool) {
let account = self.account
let peerId = self.peerId
let signal: Signal<(CachedUserData.BotPreview?, Peer?), NoError> = (self.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peerId)
}
|> mapToSignal { peer -> Signal<(CachedUserData.BotPreview?, Peer?), NoError> in
guard let peer, let inputUser = apiInputUser(peer) else {
return .single((nil, nil))
}
return _internal_requestBotPreview(network: account.network, peerId: peerId, inputUser: inputUser, language: language)
|> map { botPreview in
return (botPreview, peer)
}
})
self.requestDisposable?.dispose()
self.requestDisposable = (signal
|> deliverOn(self.queue)).startStrict(next: { [weak self] botPreview, peer in
guard let self, let peer else {
return
}
var items: [State.Item] = []
if let botPreview {
for item in botPreview.items {
guard let mediaId = item.media.id else {
continue
}
let id: Int32
if let current = self.idMapping[mediaId] {
id = current
} else {
id = self.nextId
self.nextId += 1
self.idMapping[mediaId] = id
self.reverseIdMapping[id] = mediaId
}
items.append(State.Item(
id: StoryId(peerId: peerId, id: id),
storyItem: EngineStoryItem(
id: id,
timestamp: item.timestamp,
expirationTimestamp: Int32.max,
media: EngineMedia(item.media),
alternativeMedia: nil,
mediaAreas: [],
text: "",
entities: [],
views: nil,
privacy: nil,
isPinned: false,
isExpired: false,
isPublic: false,
isPending: false,
isCloseFriends: false,
isContacts: false,
isSelectedContacts: false,
isForwardingDisabled: false,
isEdited: false,
isMy: false,
myReaction: nil,
forwardInfo: nil,
author: nil
),
peer: nil
))
}
}
self.remoteItems = items
self.stateValue = State(
peerReference: PeerReference(peer),
items: items,
availableLanguages: [],
pinnedIds: [],
totalCount: items.count,
loadMoreToken: nil,
isCached: botPreview != nil,
hasCache: botPreview != nil,
allEntityFiles: [:],
isLoading: botPreview == nil
)
if botPreview != nil {
self.beginUpdates(language: language)
}
})
}
private func beginUpdates(language: String) {
let localStateKey: PostboxViewKey = .storiesState(key: .local)
self.updatesDisposable?.dispose()
self.updatesDisposable = (self.account.postbox.combinedView(keys: [
localStateKey
])
|> deliverOn(self.queue)).startStrict(next: { [weak self] combinedView in
guard let self else {
return
}
let (peer, botPreview) = peerAndBotPreview
var items: [State.Item] = []
if let stateView = combinedView.views[localStateKey] as? StoryStatesView, let localState = stateView.value?.get(Stories.LocalState.self) {
for item in localState.items.reversed() {
let mappedId: Int32
@ -2142,7 +2432,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
self.nextId += 1
self.pendingIdMapping[item.stableId] = mappedId
}
if case .botPreview(peerId) = item.target {
if case let .botPreview(itemPeerId, itemLanguage) = item.target, itemPeerId == self.peerId, itemLanguage == language {
items.append(State.Item(
id: StoryId(peerId: peerId, id: mappedId),
storyItem: EngineStoryItem(
@ -2176,130 +2466,160 @@ public final class BotPreviewStoryListContext: StoryListContext {
}
}
if let botPreview {
for media in botPreview.media {
guard let mediaId = media.id else {
continue
}
let id: Int32
if let current = self.idMapping[mediaId] {
id = current
} else {
id = self.nextId
self.nextId += 1
self.idMapping[mediaId] = id
self.reverseIdMapping[id] = mediaId
}
items.append(State.Item(
id: StoryId(peerId: peerId, id: id),
storyItem: EngineStoryItem(
id: id,
timestamp: 0,
expirationTimestamp: Int32.max,
media: EngineMedia(media),
alternativeMedia: nil,
mediaAreas: [],
text: "",
entities: [],
views: nil,
privacy: nil,
isPinned: false,
isExpired: false,
isPublic: false,
isPending: false,
isCloseFriends: false,
isContacts: false,
isSelectedContacts: false,
isForwardingDisabled: false,
isEdited: false,
isMy: false,
myReaction: nil,
forwardInfo: nil,
author: nil
),
peer: nil
))
if self.localItems != items {
self.localItems = items
if self.stateValue.peerReference != nil {
self.pushLanguageItems()
}
}
self.stateValue = State(
peerReference: (peer?._asPeer()).flatMap(PeerReference.init),
items: items,
pinnedIds: [],
totalCount: items.count,
loadMoreToken: nil,
isCached: botPreview != nil,
hasCache: botPreview != nil,
allEntityFiles: [:],
isLoading: botPreview == nil
)
})
self.eventsDisposable?.dispose()
self.eventsDisposable = (self.account.stateManager.botPreviewUpdates
|> deliverOn(self.queue)).startStrict(next: { [weak self] events in
guard let self else {
return
}
var remoteItems = self.remoteItems
for event in events {
switch event {
case let .added(peerId, language, item):
if let mediaId = item.media.id, self.peerId == peerId, self.language == language {
let id: Int32
if let current = self.idMapping[mediaId] {
id = current
} else {
id = self.nextId
self.nextId += 1
self.idMapping[mediaId] = id
self.reverseIdMapping[id] = mediaId
}
let mappedItem = State.Item(
id: StoryId(peerId: peerId, id: id),
storyItem: EngineStoryItem(
id: id,
timestamp: item.timestamp,
expirationTimestamp: Int32.max,
media: EngineMedia(item.media),
alternativeMedia: nil,
mediaAreas: [],
text: "",
entities: [],
views: nil,
privacy: nil,
isPinned: false,
isExpired: false,
isPublic: false,
isPending: false,
isCloseFriends: false,
isContacts: false,
isSelectedContacts: false,
isForwardingDisabled: false,
isEdited: false,
isMy: false,
myReaction: nil,
forwardInfo: nil,
author: nil
),
peer: nil
)
if let index = remoteItems.firstIndex(where: { $0.storyItem.media.id == item.media.id }) {
remoteItems[index] = mappedItem
} else {
remoteItems.insert(mappedItem, at: 0)
}
}
case let .deleted(peerId, language, ids):
if self.peerId == peerId && self.language == language {
remoteItems = remoteItems.filter { item in
guard let id = item.storyItem.media.id else {
return false
}
return !ids.contains(id)
}
}
}
}
if self.remoteItems != remoteItems {
self.remoteItems = remoteItems
self.pushLanguageItems()
}
})
}
deinit {
self.requestDisposable?.dispose()
self.updatesDisposable?.dispose()
self.reorderDisposable.dispose()
private func pushLanguageItems() {
var items = self.localItems
items.append(contentsOf: self.remoteItems)
self.stateValue = State(
peerReference: self.stateValue.peerReference,
items: items,
availableLanguages: [],
pinnedIds: [],
totalCount: items.count,
loadMoreToken: nil,
isCached: true,
hasCache: true,
allEntityFiles: [:],
isLoading: false
)
}
func loadMore(completion: (() -> Void)?) {
}
func reorderItems(ids: [StoryId]) {
func reorderItems(media: [Media]) {
let peerId = self.peerId
let idMapping = self.idMapping
let reverseIdMapping = self.reverseIdMapping
let language = self.language
let _ = (self.account.postbox.transaction({ transaction -> (Api.InputUser?, [Api.InputMedia]) in
let inputUser = transaction.getPeer(peerId).flatMap(apiInputUser)
var inputMedia: [Api.InputMedia] = []
transaction.updatePeerCachedData(peerIds: Set([self.peerId]), update: { _, current in
guard var current = current as? CachedUserData else {
for item in media {
if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource {
inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
} else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource {
inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil))
}
}
if language == nil {
transaction.updatePeerCachedData(peerIds: Set([self.peerId]), update: { _, current in
guard var current = current as? CachedUserData else {
return current
}
guard let currentBotPreview = current.botPreview else {
return current
}
var items: [CachedUserData.BotPreview.Item] = []
var seenIds = Set<MediaId>()
for item in media {
guard let mediaId = item.id else {
continue
}
if let index = currentBotPreview.items.firstIndex(where: { $0.media.id == mediaId }) {
seenIds.insert(mediaId)
items.append(currentBotPreview.items[index])
}
}
for item in currentBotPreview.items {
guard let id = item.media.id else {
continue
}
if !seenIds.contains(id) {
items.append(item)
}
}
let botPreview = CachedUserData.BotPreview(items: items, alternativeLanguageCodes: currentBotPreview.alternativeLanguageCodes)
current = current.withUpdatedBotPreview(botPreview)
return current
}
guard let currentBotPreview = current.botPreview else {
return current
}
var media: [Media] = []
media = []
var seenIds = Set<Int32>()
for id in ids {
guard let mediaId = reverseIdMapping[id.id] else {
continue
}
if let index = currentBotPreview.media.firstIndex(where: { $0.id == mediaId }) {
seenIds.insert(id.id)
media.append(currentBotPreview.media[index])
}
}
for item in currentBotPreview.media {
guard let id = item.id, let storyId = idMapping[id] else {
continue
}
if !seenIds.contains(storyId) {
media.append(item)
}
}
for item in media {
if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource {
inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
} else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource {
inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil))
}
}
let botPreview = CachedUserData.BotPreview(media: media)
current = current.withUpdatedBotPreview(botPreview)
return current
})
})
}
return (inputUser, inputMedia)
})
@ -2307,7 +2627,37 @@ public final class BotPreviewStoryListContext: StoryListContext {
guard let self, let inputUser else {
return
}
let signal = self.account.network.request(Api.functions.bots.reorderPreviewMedias(bot: inputUser, order: inputMedia))
if language != nil {
var updatedItems: [State.Item] = []
var seenIds = Set<MediaId>()
for item in media {
guard let mediaId = item.id else {
continue
}
if let index = self.remoteItems.firstIndex(where: { $0.storyItem.media.id == mediaId }) {
seenIds.insert(mediaId)
updatedItems.append(self.remoteItems[index])
}
}
for item in self.remoteItems {
guard let id = item.storyItem.media.id else {
continue
}
if !seenIds.contains(id) {
updatedItems.append(item)
}
}
if self.remoteItems != updatedItems {
self.remoteItems = updatedItems
self.pushLanguageItems()
}
}
let signal = self.account.network.request(Api.functions.bots.reorderPreviewMedias(bot: inputUser, langCode: language ?? "", order: inputMedia))
self.reorderDisposable.set(signal.startStrict())
})
}
@ -2322,11 +2672,15 @@ public final class BotPreviewStoryListContext: StoryListContext {
private let queue: Queue
private let impl: QueueLocalObject<Impl>
public init(account: Account, engine: TelegramEngine, peerId: EnginePeer.Id) {
public let language: String?
public init(account: Account, engine: TelegramEngine, peerId: EnginePeer.Id, language: String?, assumeEmpty: Bool) {
self.language = language
let queue = Queue.mainQueue()
self.queue = queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, account: account, engine: engine, peerId: peerId)
return Impl(queue: queue, account: account, engine: engine, peerId: peerId, language: language, assumeEmpty: assumeEmpty)
})
}
@ -2336,9 +2690,9 @@ public final class BotPreviewStoryListContext: StoryListContext {
}
}
public func reorderItems(ids: [StoryId]) {
public func reorderItems(media: [Media]) {
self.impl.with { impl in
impl.reorderItems(ids: ids)
impl.reorderItems(media: media)
}
}
}

View File

@ -1373,8 +1373,12 @@ public extension TelegramEngine {
return _internal_getStoryById(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, peerId: peerId, id: id)
}
public func deleteBotPreviews(peerId: EnginePeer.Id, ids: [MediaId]) -> Signal<Never, NoError> {
return _internal_deleteBotPreviews(account: self.account, peerId: peerId, ids: ids)
public func deleteBotPreviews(peerId: EnginePeer.Id, language: String?, media: [Media]) -> Signal<Never, NoError> {
return _internal_deleteBotPreviews(account: self.account, peerId: peerId, language: language, media: media)
}
public func deleteBotPreviewsLanguage(peerId: EnginePeer.Id, language: String, media: [Media]) -> Signal<Never, NoError> {
return _internal_deleteBotPreviewsLanguage(account: self.account, peerId: peerId, language: language, media: media)
}
public func synchronouslyIsMessageDeletedInteractively(ids: [EngineMessage.Id]) -> [EngineMessage.Id] {

View File

@ -199,16 +199,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee
let botPreview: Signal<CachedUserData.BotPreview?, NoError>
if let user = maybePeer as? TelegramUser, let _ = user.botInfo {
botPreview = network.request(Api.functions.bots.getPreviewMedias(bot: inputUser))
|> `catch` { _ -> Signal<[Api.MessageMedia], NoError> in
return .single([])
}
|> map { result -> CachedUserData.BotPreview? in
return CachedUserData.BotPreview(media: result.compactMap { item -> Media? in
let value = textMediaAndExpirationTimerFromApiMedia(item, user.id)
return value.media
})
}
botPreview = _internal_requestBotPreview(network: network, peerId: user.id, inputUser: inputUser, language: nil)
} else {
botPreview = .single(nil)
}
@ -843,3 +834,33 @@ extension CachedPeerAutoremoveTimeout.Value {
}
}
}
func _internal_requestBotPreview(network: Network, peerId: PeerId, inputUser: Api.InputUser, language: String?) -> Signal<CachedUserData.BotPreview?, NoError> {
return network.request(Api.functions.bots.getPreviewInfo(bot: inputUser, langCode: language ?? ""))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.bots.PreviewInfo?, NoError> in
return .single(nil)
}
|> map { result -> CachedUserData.BotPreview? in
guard let result else {
return nil
}
switch result {
case let .previewInfo(media, langCodes):
return CachedUserData.BotPreview(
items: media.compactMap { item -> CachedUserData.BotPreview.Item? in
switch item {
case let .botPreviewMedia(date, media):
let value = textMediaAndExpirationTimerFromApiMedia(media, peerId)
if let media = value.media {
return CachedUserData.BotPreview.Item(media: media, timestamp: date)
} else {
return nil
}
}
},
alternativeLanguageCodes: langCodes
)
}
}
}

View File

@ -458,6 +458,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatSendStarsScreen",
"//submodules/TelegramUI/Components/MinimizedContainer",
"//submodules/TelegramUI/Components/SpaceWarpView",
"//submodules/TelegramUI/Components/MiniAppListScreen",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],

View File

@ -1841,7 +1841,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
}
} else if case .tap = gesture {
item.controllerInteraction.clickThroughMessage()
item.controllerInteraction.clickThroughMessage(self.view, location)
} else if case .doubleTap = gesture {
if canAddMessageReactions(message: item.message) {
item.controllerInteraction.updateMessageReaction(item.message, .default, false, nil)

View File

@ -4576,7 +4576,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
}
} else if case .tap = gesture {
item.controllerInteraction.clickThroughMessage()
item.controllerInteraction.clickThroughMessage(self.view, location)
} else if case .doubleTap = gesture {
if canAddMessageReactions(message: item.message) {
item.controllerInteraction.updateMessageReaction(item.message, .default, false, nil)

View File

@ -961,7 +961,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco
break
}
} else if case .tap = gesture {
self.item?.controllerInteraction.clickThroughMessage()
self.item?.controllerInteraction.clickThroughMessage(self.view, location)
}
}
default:

View File

@ -1586,7 +1586,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
return
}
self.item?.controllerInteraction.clickThroughMessage()
self.item?.controllerInteraction.clickThroughMessage(self.view, location)
case .longTap, .doubleTap, .secondaryTap:
break
case .hold:

View File

@ -1405,7 +1405,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
}
}
} else if case .tap = gesture {
self.item?.controllerInteraction.clickThroughMessage()
self.item?.controllerInteraction.clickThroughMessage(self.view, location)
}
}
default:

View File

@ -306,7 +306,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
if let context = self?.context, let navigationController = self?.getNavigationController() {
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: threadId, messageId: nil, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: false, keepStack: .always).startStandalone()
}
}, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false
}, tapMessage: nil, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false
}, requestMessageActionCallback: { [weak self] messageId, _, _, _ in
guard let self else {
return

View File

@ -418,7 +418,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
}, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in
}, navigateToThreadMessage: { _, _, _ in
}, tapMessage: { _ in
}, clickThroughMessage: {
}, clickThroughMessage: { _, _ in
}, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in
return false
}, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in

View File

@ -180,7 +180,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
public let navigateToMessageStandalone: (MessageId) -> Void
public let navigateToThreadMessage: (PeerId, Int64, MessageId?) -> Void
public let tapMessage: ((Message) -> Void)?
public let clickThroughMessage: () -> Void
public let clickThroughMessage: (UIView?, CGPoint?) -> Void
public let toggleMessagesSelection: ([MessageId], Bool) -> Void
public let sendCurrentMessage: (Bool, ChatSendMessageEffect?) -> Void
public let sendMessage: (String) -> Void
@ -309,7 +309,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
navigateToMessageStandalone: @escaping (MessageId) -> Void,
navigateToThreadMessage: @escaping (PeerId, Int64, MessageId?) -> Void,
tapMessage: ((Message) -> Void)?,
clickThroughMessage: @escaping () -> Void,
clickThroughMessage: @escaping (UIView?, CGPoint?) -> Void,
toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void,
sendCurrentMessage: @escaping (Bool, ChatSendMessageEffect?) -> Void,
sendMessage: @escaping (String) -> Void,

View File

@ -13,25 +13,27 @@ public final class EmptyStateIndicatorComponent: Component {
public let context: AccountContext
public let theme: PresentationTheme
public let animationName: String?
public let title: String
public let title: String?
public let text: String
public let actionTitle: String?
public let fitToHeight: Bool
public let action: () -> Void
public let additionalActionTitle: String?
public let additionalAction: () -> Void
public let additionalActionSeparator: String?
public init(
context: AccountContext,
theme: PresentationTheme,
fitToHeight: Bool,
animationName: String?,
title: String,
title: String?,
text: String,
actionTitle: String?,
action: @escaping () -> Void,
additionalActionTitle: String?,
additionalAction: @escaping () -> Void
additionalAction: @escaping () -> Void,
additionalActionSeparator: String? = nil
) {
self.context = context
self.theme = theme
@ -43,6 +45,7 @@ public final class EmptyStateIndicatorComponent: Component {
self.action = action
self.additionalActionTitle = additionalActionTitle
self.additionalAction = additionalAction
self.additionalActionSeparator = additionalActionSeparator
}
public static func ==(lhs: EmptyStateIndicatorComponent, rhs: EmptyStateIndicatorComponent) -> Bool {
@ -70,6 +73,9 @@ public final class EmptyStateIndicatorComponent: Component {
if lhs.additionalActionTitle != rhs.additionalActionTitle {
return false
}
if lhs.additionalActionSeparator != rhs.additionalActionSeparator {
return false
}
return true
}
@ -82,6 +88,9 @@ public final class EmptyStateIndicatorComponent: Component {
private let text = ComponentView<Empty>()
private var button: ComponentView<Empty>?
private var additionalButton: ComponentView<Empty>?
private var additionalSeparatorLeft: SimpleLayer?
private var additionalSeparatorRight: SimpleLayer?
private var additionalSeparatorText: ComponentView<Empty>?
override public init(frame: CGRect) {
super.init(frame: frame)
@ -108,16 +117,20 @@ public final class EmptyStateIndicatorComponent: Component {
containerSize: CGSize(width: 120.0, height: 120.0)
)
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0)
)
var titleSize: CGSize?
if let title = component.title {
titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0)
)
}
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(BalancedTextComponent(
@ -203,19 +216,80 @@ public final class EmptyStateIndicatorComponent: Component {
}
}
var additionalSeparatorTextSize: CGSize?
if let additionalActionSeparator = component.additionalActionSeparator {
let additionalSeparatorText: ComponentView<Empty>
if let current = self.additionalSeparatorText {
additionalSeparatorText = current
} else {
additionalSeparatorText = ComponentView()
self.additionalSeparatorText = additionalSeparatorText
}
let additionalSeparatorLeft: SimpleLayer
if let current = self.additionalSeparatorLeft {
additionalSeparatorLeft = current
} else {
additionalSeparatorLeft = SimpleLayer()
self.additionalSeparatorLeft = additionalSeparatorLeft
self.layer.addSublayer(additionalSeparatorLeft)
}
let additionalSeparatorRight: SimpleLayer
if let current = self.additionalSeparatorRight {
additionalSeparatorRight = current
} else {
additionalSeparatorRight = SimpleLayer()
self.additionalSeparatorRight = additionalSeparatorRight
self.layer.addSublayer(additionalSeparatorRight)
}
additionalSeparatorLeft.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
additionalSeparatorRight.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
additionalSeparatorTextSize = additionalSeparatorText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: additionalActionSeparator, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 100.0)
)
} else {
if let additionalSeparatorLeft = self.additionalSeparatorLeft {
self.additionalSeparatorLeft = nil
additionalSeparatorLeft.removeFromSuperlayer()
}
if let additionalSeparatorRight = self.additionalSeparatorRight {
self.additionalSeparatorRight = nil
additionalSeparatorRight.removeFromSuperlayer()
}
if let additionalSeparatorText = self.additionalSeparatorText {
self.additionalSeparatorText = nil
additionalSeparatorText.view?.removeFromSuperview()
}
}
let animationSpacing: CGFloat = 11.0
let titleSpacing: CGFloat = 17.0
let buttonSpacing: CGFloat = 21.0
let additionalSeparatorHeight: CGFloat = 31.0
var totalHeight: CGFloat = 0.0
if let animationSize {
totalHeight += animationSize.height + animationSpacing
}
totalHeight += titleSize.height + titleSpacing + textSize.height
if let titleSize {
totalHeight += titleSize.height + titleSpacing
}
totalHeight += textSize.height
if let buttonSize {
totalHeight += buttonSpacing + buttonSize.height
}
if let _ = additionalSeparatorTextSize {
totalHeight += additionalSeparatorHeight
}
if let additionalButtonSize {
totalHeight += buttonSpacing + additionalButtonSize.height
}
@ -234,7 +308,7 @@ public final class EmptyStateIndicatorComponent: Component {
transition.setFrame(view: animationView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - animationSize.width) * 0.5), y: contentY), size: animationSize))
contentY += animationSize.height + animationSpacing
}
if let titleView = self.title.view {
if let titleSize, let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
@ -255,6 +329,25 @@ public final class EmptyStateIndicatorComponent: Component {
transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) * 0.5), y: contentY), size: buttonSize))
contentY += buttonSize.height + buttonSpacing
}
if let additionalSeparatorTextSize, let additionalSeparatorText = self.additionalSeparatorText, let additionalSeparatorLeft = self.additionalSeparatorLeft, let additionalSeparatorRight = self.additionalSeparatorRight {
let additionalSeparatorTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - additionalSeparatorTextSize.width) * 0.5), y: contentY), size: additionalSeparatorTextSize)
if let additionalSeparatorTextView = additionalSeparatorText.view {
if additionalSeparatorTextView.superview == nil {
self.addSubview(additionalSeparatorTextView)
}
transition.setFrame(view: additionalSeparatorTextView, frame: additionalSeparatorTextFrame)
}
let separatorWidth: CGFloat = 72.0
let separatorSpacing: CGFloat = 10.0
transition.setFrame(layer: additionalSeparatorLeft, frame: CGRect(origin: CGPoint(x: additionalSeparatorTextFrame.minX - separatorSpacing - separatorWidth, y: additionalSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel)))
transition.setFrame(layer: additionalSeparatorRight, frame: CGRect(origin: CGPoint(x: additionalSeparatorTextFrame.maxX + separatorSpacing, y: additionalSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel)))
contentY += additionalSeparatorHeight
}
if let additionalButtonSize, let additionalButtonView = self.additionalButton?.view {
if additionalButtonView.superview == nil {
self.addSubview(additionalButtonView)

View File

@ -0,0 +1,39 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "MiniAppListScreen",
module_name = "MiniAppListScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/PresentationDataUtils",
"//submodules/AccountContext",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/MergeLists",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/ItemListUI",
"//submodules/ChatListUI",
"//submodules/ItemListPeerItem",
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/SearchBarNode",
"//submodules/Components/BalancedTextComponent",
"//submodules/ChatListSearchItemHeader",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,811 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import PresentationDataUtils
import AccountContext
import ComponentFlow
import ViewControllerComponent
import MergeLists
import ComponentDisplayAdapters
import ItemListPeerItem
import ItemListUI
import ChatListHeaderComponent
import PlainButtonComponent
import MultilineTextComponent
import SearchBarNode
import BalancedTextComponent
import ChatListSearchItemHeader
final class MiniAppListScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let initialData: MiniAppListScreen.InitialData
init(
context: AccountContext,
initialData: MiniAppListScreen.InitialData
) {
self.context = context
self.initialData = initialData
}
static func ==(lhs: MiniAppListScreenComponent, rhs: MiniAppListScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
private enum ContentEntry: Comparable, Identifiable {
enum Id: Hashable {
case item(EnginePeer.Id)
}
var stableId: Id {
switch self {
case let .item(peer, _):
return .item(peer.id)
}
}
case item(peer: EnginePeer, sortIndex: Int)
static func <(lhs: ContentEntry, rhs: ContentEntry) -> Bool {
switch lhs {
case let .item(lhsPeer, lhsSortIndex):
switch rhs {
case let .item(rhsPeer, rhsSortIndex):
if lhsSortIndex != rhsSortIndex {
return lhsSortIndex < rhsSortIndex
}
return lhsPeer.id < rhsPeer.id
}
}
}
func item(listNode: ContentListNode) -> ListViewItem {
switch self {
case let .item(peer, _):
let text: ItemListPeerItemText
if case let .user(user) = peer, let subscriberCount = user.subscriberCount {
text = .text(listNode.presentationData.strings.Conversation_StatusBotSubscribers(subscriberCount), .secondary)
} else {
text = .none
}
return ItemListPeerItem(
presentationData: ItemListPresentationData(listNode.presentationData),
dateTimeFormat: listNode.presentationData.dateTimeFormat,
nameDisplayOrder: listNode.presentationData.nameDisplayOrder,
context: listNode.context,
peer: peer,
presence: nil,
text: text,
label: .none,
editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: nil),
enabled: true,
selectable: true,
sectionId: 0,
action: { [weak listNode] in
guard let listNode else {
return
}
if let view = listNode.parentView {
view.openItem(peer: peer)
}
},
setPeerIdWithRevealedOptions: { _, _ in
},
removePeer: { _ in
},
noInsets: true,
header: nil
)
}
}
}
private final class ContentListNode: ListView {
weak var parentView: View?
let context: AccountContext
var presentationData: PresentationData
private var currentEntries: [ContentEntry] = []
private var originalEntries: [ContentEntry] = []
init(parentView: View, context: AccountContext) {
self.parentView = parentView
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with({ $0 })
super.init()
}
func update(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) {
let (listViewDuration, listViewCurve) = listViewAnimationDurationAndCurve(transition: transition.containedViewLayoutTransition)
self.transaction(
deleteIndices: [],
insertIndicesAndItems: [],
updateIndicesAndItems: [],
options: [.Synchronous, .LowLatency, .PreferSynchronousResourceLoading],
additionalScrollDistance: 0.0,
updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: listViewDuration, curve: listViewCurve),
updateOpaqueState: nil
)
}
func setEntries(entries: [ContentEntry], animated: Bool) {
self.originalEntries = entries
let entries = entries
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.currentEntries, rightList: entries)
self.currentEntries = entries
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(listNode: self), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(listNode: self), directionHint: nil) }
var options: ListViewDeleteAndInsertOptions = [.Synchronous, .LowLatency]
if animated {
options.insert(.AnimateInsertion)
} else {
options.insert(.PreferSynchronousResourceLoading)
}
self.transaction(
deleteIndices: deletions,
insertIndicesAndItems: insertions,
updateIndicesAndItems: updates,
options: options,
scrollToItem: nil,
stationaryItemRange: nil,
updateOpaqueState: nil,
completion: { _ in
}
)
}
}
final class View: UIView {
private var contentListNode: ContentListNode?
private var ignoreVisibleContentOffsetChanged: Bool = false
private var emptySearchState: ComponentView<Empty>?
private let navigationBarView = ComponentView<Empty>()
private var navigationHeight: CGFloat?
private let sectionHeader = ComponentView<Empty>()
private var searchBarNode: SearchBarNode?
private var isUpdating: Bool = false
private var component: MiniAppListScreenComponent?
private(set) weak var state: EmptyComponentState?
private var environment: EnvironmentType?
private var recommendedAppPeers: [EnginePeer]?
private var recommendedAppPeersDisposable: Disposable?
private var keepUpdatedDisposable: Disposable?
private var isSearchDisplayControllerActive: Bool = false
private var searchQuery: String = ""
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.recommendedAppPeersDisposable?.dispose()
self.keepUpdatedDisposable?.dispose()
}
func scrollToTop() {
}
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
return true
}
func openItem(peer: EnginePeer) {
guard let component = self.component else {
return
}
guard let environment = self.environment, let controller = environment.controller() else {
return
}
if let peerInfoScreen = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
peerInfoScreen.navigationPresentation = .modal
controller.push(peerInfoScreen)
}
}
private func updateNavigationBar(
component: MiniAppListScreenComponent,
theme: PresentationTheme,
strings: PresentationStrings,
size: CGSize,
insets: UIEdgeInsets,
statusBarHeight: CGFloat,
isModal: Bool,
transition: ComponentTransition,
deferScrollApplication: Bool
) -> CGFloat {
let rightButtons: [AnyComponentWithIdentity<NavigationButtonComponentEnvironment>] = []
//TODO:localize
let titleText: String = "Examples"
let closeTitle: String = strings.Common_Close
let headerContent: ChatListHeaderComponent.Content? = ChatListHeaderComponent.Content(
title: titleText,
navigationBackTitle: nil,
titleComponent: nil,
chatListTitle: nil,
leftButton: isModal ? AnyComponentWithIdentity(id: "close", component: AnyComponent(NavigationButtonComponent(
content: .text(title: closeTitle, isBold: false),
pressed: { [weak self] _ in
guard let self else {
return
}
if self.attemptNavigation(complete: {}) {
self.environment?.controller()?.dismiss()
}
}
))) : nil,
rightButtons: rightButtons,
backTitle: isModal ? nil : strings.Common_Back,
backPressed: { [weak self] in
guard let self else {
return
}
if self.attemptNavigation(complete: {}) {
self.environment?.controller()?.dismiss()
}
}
)
let navigationBarSize = self.navigationBarView.update(
transition: transition,
component: AnyComponent(ChatListNavigationBar(
context: component.context,
theme: theme,
strings: strings,
statusBarHeight: statusBarHeight,
sideInset: insets.left,
isSearchActive: self.isSearchDisplayControllerActive,
isSearchEnabled: true,
primaryContent: headerContent,
secondaryContent: nil,
secondaryTransition: 0.0,
storySubscriptions: nil,
storiesIncludeHidden: false,
uploadProgress: [:],
tabsNode: nil,
tabsNodeIsSearch: false,
accessoryPanelContainer: nil,
accessoryPanelContainerHeight: 0.0,
activateSearch: { [weak self] _ in
guard let self else {
return
}
self.isSearchDisplayControllerActive = true
self.state?.updated(transition: .spring(duration: 0.4))
},
openStatusSetup: { _ in
},
allowAutomaticOrder: {
}
)),
environment: {},
containerSize: size
)
//TODO:localize
let sectionHeaderSize = self.sectionHeader.update(
transition: transition,
component: AnyComponent(ListHeaderComponent(
theme: theme,
title: "APPS THAT ACCEPT STARS"
)),
environment: {},
containerSize: CGSize(width: size.width, height: 1000.0)
)
if let sectionHeaderView = self.sectionHeader.view {
if sectionHeaderView.superview == nil {
sectionHeaderView.layer.anchorPoint = CGPoint()
self.addSubview(sectionHeaderView)
}
transition.setBounds(view: sectionHeaderView, bounds: CGRect(origin: CGPoint(), size: sectionHeaderSize))
}
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
if deferScrollApplication {
navigationBarComponentView.deferScrollApplication = true
}
if navigationBarComponentView.superview == nil {
self.addSubview(navigationBarComponentView)
}
transition.setFrame(view: navigationBarComponentView, frame: CGRect(origin: CGPoint(), size: navigationBarSize))
return navigationBarSize.height
} else {
return 0.0
}
}
private func updateNavigationScrolling(navigationHeight: CGFloat, transition: ComponentTransition) {
var mainOffset: CGFloat
if let recommendedAppPeers = self.recommendedAppPeers, !recommendedAppPeers.isEmpty {
if let contentListNode = self.contentListNode {
switch contentListNode.visibleContentOffset() {
case .none:
mainOffset = 0.0
case .unknown:
mainOffset = navigationHeight
case let .known(value):
mainOffset = value
}
} else {
mainOffset = navigationHeight
}
} else {
mainOffset = navigationHeight
}
mainOffset = min(mainOffset, ChatListNavigationBar.searchScrollHeight)
if abs(mainOffset) < 0.1 {
mainOffset = 0.0
}
let resultingOffset = mainOffset
var offset = resultingOffset
if self.isSearchDisplayControllerActive {
offset = 0.0
}
if let sectionHeaderView = self.sectionHeader.view {
transition.setPosition(view: sectionHeaderView, position: CGPoint(x: 0.0, y: navigationHeight - offset))
}
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: false, forceUpdate: false, transition: transition.withUserData(ChatListNavigationBar.AnimationHint(
disableStoriesAnimations: false,
crossfadeStoryPeers: false
)))
}
}
func update(component: MiniAppListScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
if self.component == nil {
self.recommendedAppPeers = component.initialData.recommendedAppPeers
/*self.shortcutMessageListDisposable = (component.context.engine.accountData.shortcutMessageList(onlyRemote: false)
|> deliverOnMainQueue).startStrict(next: { [weak self] shortcutMessageList in
guard let self else {
return
}
self.shortcutMessageList = shortcutMessageList
if !self.isUpdating {
self.state?.updated(transition: .immediate)
}
})*/
self.keepUpdatedDisposable = component.context.engine.peers.requestRecommendedAppsIfNeeded().startStrict()
}
let environment = environment[EnvironmentType.self].value
let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment
self.component = component
self.state = state
if themeUpdated {
self.backgroundColor = environment.theme.list.plainBackgroundColor
}
var isModal = false
if let controller = environment.controller(), controller.navigationPresentation == .modal {
isModal = true
}
var statusBarHeight = environment.statusBarHeight
if isModal {
statusBarHeight = max(statusBarHeight, 1.0)
}
let listBottomInset = environment.safeInsets.bottom + environment.additionalInsets.bottom
let navigationHeight = self.updateNavigationBar(
component: component,
theme: environment.theme,
strings: environment.strings,
size: availableSize,
insets: environment.safeInsets,
statusBarHeight: statusBarHeight,
isModal: isModal,
transition: transition,
deferScrollApplication: true
)
self.navigationHeight = navigationHeight
var removedSearchBar: SearchBarNode?
if self.isSearchDisplayControllerActive {
let searchBarNode: SearchBarNode
var searchBarTransition = transition
if let current = self.searchBarNode {
searchBarNode = current
} else {
searchBarTransition = .immediate
let searchBarTheme = SearchBarNodeTheme(theme: environment.theme, hasSeparator: false)
searchBarNode = SearchBarNode(
theme: searchBarTheme,
strings: environment.strings,
fieldStyle: .modern,
displayBackground: false
)
searchBarNode.placeholderString = NSAttributedString(string: environment.strings.Common_Search, font: Font.regular(17.0), textColor: searchBarTheme.placeholder)
self.searchBarNode = searchBarNode
searchBarNode.cancel = { [weak self] in
guard let self else {
return
}
self.isSearchDisplayControllerActive = false
self.state?.updated(transition: .spring(duration: 0.4))
}
searchBarNode.textUpdated = { [weak self] query, _ in
guard let self else {
return
}
if self.searchQuery != query {
self.searchQuery = query.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
self.state?.updated(transition: .immediate)
}
}
DispatchQueue.main.async { [weak self, weak searchBarNode] in
guard let self, let searchBarNode, self.searchBarNode === searchBarNode else {
return
}
searchBarNode.activate()
}
}
var searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight - 54.0 + 2.0), size: CGSize(width: availableSize.width, height: 54.0))
if isModal {
searchBarFrame.origin.y += 2.0
}
searchBarNode.updateLayout(boundingSize: searchBarFrame.size, leftInset: environment.safeInsets.left + 6.0, rightInset: environment.safeInsets.right, transition: searchBarTransition.containedViewLayoutTransition)
searchBarTransition.setFrame(view: searchBarNode.view, frame: searchBarFrame)
if searchBarNode.view.superview == nil {
self.addSubview(searchBarNode.view)
if case let .curve(duration, curve) = transition.animation, let navigationBarView = self.navigationBarView.view as? ChatListNavigationBar.View, let placeholderNode = navigationBarView.searchContentNode?.placeholderNode {
let timingFunction: String
switch curve {
case .easeInOut:
timingFunction = CAMediaTimingFunctionName.easeOut.rawValue
case .linear:
timingFunction = CAMediaTimingFunctionName.linear.rawValue
case .spring:
timingFunction = kCAMediaTimingFunctionSpring
case .custom:
timingFunction = kCAMediaTimingFunctionSpring
}
searchBarNode.animateIn(from: placeholderNode, duration: duration, timingFunction: timingFunction)
}
}
} else {
self.searchQuery = ""
if let searchBarNode = self.searchBarNode {
self.searchBarNode = nil
removedSearchBar = searchBarNode
}
}
let contentListNode: ContentListNode
if let current = self.contentListNode {
contentListNode = current
} else {
contentListNode = ContentListNode(parentView: self, context: component.context)
self.contentListNode = contentListNode
contentListNode.visibleContentOffsetChanged = { [weak self] offset in
guard let self else {
return
}
guard let navigationHeight = self.navigationHeight else {
return
}
if self.ignoreVisibleContentOffsetChanged {
return
}
self.updateNavigationScrolling(navigationHeight: navigationHeight, transition: .immediate)
}
if let sectionHeaderView = self.sectionHeader.view {
self.insertSubview(contentListNode.view, belowSubview: sectionHeaderView)
} else if let navigationBarComponentView = self.navigationBarView.view {
self.insertSubview(contentListNode.view, belowSubview: navigationBarComponentView)
} else {
self.addSubview(contentListNode.view)
}
}
var contentTopInset = navigationHeight
if let sectionHeaderView = self.sectionHeader.view {
contentTopInset += sectionHeaderView.bounds.height
}
transition.setFrame(view: contentListNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
self.ignoreVisibleContentOffsetChanged = true
contentListNode.update(size: availableSize, insets: UIEdgeInsets(top: contentTopInset, left: environment.safeInsets.left, bottom: listBottomInset, right: environment.safeInsets.right), transition: transition)
self.ignoreVisibleContentOffsetChanged = false
var entries: [ContentEntry] = []
if let recommendedAppPeers = self.recommendedAppPeers {
let normalizedSearchQuery = self.searchQuery.lowercased().trimmingTrailingSpaces()
for peer in recommendedAppPeers {
if !self.searchQuery.isEmpty {
var matches = false
if peer.indexName.matchesByTokens(normalizedSearchQuery) {
matches = true
}
if !matches {
continue
}
}
entries.append(.item(peer: peer, sortIndex: entries.count))
}
}
contentListNode.setEntries(entries: entries, animated: !transition.animation.isImmediate)
if let sectionHeaderView = self.sectionHeader.view {
sectionHeaderView.isHidden = entries.isEmpty
}
if !self.searchQuery.isEmpty && entries.isEmpty {
var emptySearchStateTransition = transition
let emptySearchState: ComponentView<Empty>
if let current = self.emptySearchState {
emptySearchState = current
} else {
emptySearchStateTransition = emptySearchStateTransition.withAnimation(.none)
emptySearchState = ComponentView()
self.emptySearchState = emptySearchState
}
let emptySearchStateSize = emptySearchState.update(
transition: .immediate,
component: AnyComponent(BalancedTextComponent(
text: .plain(NSAttributedString(string: environment.strings.Conversation_SearchNoResults, font: Font.regular(17.0), textColor: environment.theme.list.freeTextColor, paragraphAlignment: .center)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: availableSize.height)
)
var emptySearchStateBottomInset = listBottomInset
emptySearchStateBottomInset = max(emptySearchStateBottomInset, environment.inputHeight)
let emptySearchStateFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - emptySearchStateSize.width) * 0.5), y: navigationHeight + floor((availableSize.height - emptySearchStateBottomInset - navigationHeight) * 0.5)), size: emptySearchStateSize)
if let emptySearchStateView = emptySearchState.view {
if emptySearchStateView.superview == nil {
if let navigationBarComponentView = self.navigationBarView.view {
self.insertSubview(emptySearchStateView, belowSubview: navigationBarComponentView)
} else {
self.addSubview(emptySearchStateView)
}
}
emptySearchStateTransition.containedViewLayoutTransition.updatePosition(layer: emptySearchStateView.layer, position: emptySearchStateFrame.center)
emptySearchStateView.bounds = CGRect(origin: CGPoint(), size: emptySearchStateFrame.size)
}
} else if let emptySearchState = self.emptySearchState {
self.emptySearchState = nil
emptySearchState.view?.removeFromSuperview()
}
if let recommendedAppPeers = self.recommendedAppPeers, !recommendedAppPeers.isEmpty {
contentListNode.isHidden = false
} else {
contentListNode.isHidden = true
}
self.updateNavigationScrolling(navigationHeight: navigationHeight, transition: transition)
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
navigationBarComponentView.deferScrollApplication = false
navigationBarComponentView.applyCurrentScroll(transition: transition)
}
if let removedSearchBar {
if !transition.animation.isImmediate, let navigationBarView = self.navigationBarView.view as? ChatListNavigationBar.View, let placeholderNode =
navigationBarView.searchContentNode?.placeholderNode {
removedSearchBar.transitionOut(to: placeholderNode, transition: transition.containedViewLayoutTransition, completion: { [weak removedSearchBar] in
removedSearchBar?.view.removeFromSuperview()
})
} else {
removedSearchBar.view.removeFromSuperview()
}
}
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class MiniAppListScreen: ViewControllerComponentContainer {
public final class InitialData: MiniAppListScreenInitialData {
let recommendedAppPeers: [EnginePeer]
init(
recommendedAppPeers: [EnginePeer]
) {
self.recommendedAppPeers = recommendedAppPeers
}
}
private let context: AccountContext
public init(context: AccountContext, initialData: InitialData) {
self.context = context
super.init(context: context, component: MiniAppListScreenComponent(
context: context,
initialData: initialData
), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil)
self.navigationPresentation = .modal
self.scrollToTop = { [weak self] in
guard let self, let componentView = self.node.hostView.componentView as? MiniAppListScreenComponent.View else {
return
}
componentView.scrollToTop()
}
self.attemptNavigation = { [weak self] complete in
guard let self, let componentView = self.node.hostView.componentView as? MiniAppListScreenComponent.View else {
return true
}
return componentView.attemptNavigation(complete: complete)
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
@objc private func cancelPressed() {
self.dismiss()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
public static func initialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError> {
let recommendedAppPeers = context.engine.peers.recommendedAppPeerIds()
|> take(1)
|> mapToSignal { peerIds -> Signal<[EnginePeer], NoError> in
guard let peerIds else {
return .single([])
}
return context.engine.data.get(
EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
)
|> map { peers -> [EnginePeer] in
return peers.compactMap { $0 }
}
}
return recommendedAppPeers
|> map { recommendedAppPeers -> MiniAppListScreenInitialData in
return InitialData(
recommendedAppPeers: recommendedAppPeers
)
}
}
}
private final class ListHeaderComponent: Component {
let theme: PresentationTheme
let title: String
init(
theme: PresentationTheme,
title: String
) {
self.theme = theme
self.title = title
}
static func ==(lhs: ListHeaderComponent, rhs: ListHeaderComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.title != rhs.title {
return false
}
return true
}
final class View: UIView {
private let title = ComponentView<Empty>()
private var component: ListHeaderComponent?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ListHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
if self.component?.theme !== component.theme {
self.backgroundColor = component.theme.chatList.sectionHeaderFillColor
}
let insets = UIEdgeInsets(top: 7.0, left: 16.0, bottom: 7.0, right: 16.0)
let titleString = component.title
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titleString, font: Font.regular(13.0), textColor: component.theme.chatList.sectionHeaderTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - insets.left - insets.right, height: 100.0)
)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
titleView.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: titleSize)
}
return CGSize(width: availableSize.width, height: titleSize.height + insets.top + insets.bottom)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -1148,7 +1148,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
var botPreviewStoryListContext: StoryListContext?
let hasBotPreviewItems: Signal<Bool, NoError>
if case .bot = kind {
let botPreviewStoryListContextValue = BotPreviewStoryListContext(account: context.account, engine: context.engine, peerId: peerId)
let botPreviewStoryListContextValue = BotPreviewStoryListContext(account: context.account, engine: context.engine, peerId: peerId, language: nil, assumeEmpty: false)
botPreviewStoryListContext = botPreviewStoryListContextValue
hasBotPreviewItems = botPreviewStoryListContextValue.state
|> map { state in
@ -1307,7 +1307,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
if let user = peerView.peers[peerView.peerId] as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), botInfo.flags.contains(.canEdit) {
availablePanes?.insert(.botPreview, at: 0)
} else if let cachedData = peerView.cachedData as? CachedUserData, let botPreview = cachedData.botPreview, !botPreview.media.isEmpty {
} else if let cachedData = peerView.cachedData as? CachedUserData, let botPreview = cachedData.botPreview, !botPreview.items.isEmpty {
availablePanes?.insert(.botPreview, at: 0)
}
}

View File

@ -3287,7 +3287,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, navigateToMessage: { _, _, _ in
}, navigateToMessageStandalone: { _ in
}, navigateToThreadMessage: { _, _, _ in
}, tapMessage: nil, clickThroughMessage: {
}, tapMessage: nil, clickThroughMessage: { _, _ in
}, toggleMessagesSelection: { [weak self] ids, value in
guard let strongSelf = self else {
return
@ -9864,7 +9864,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
guard let self else {
return
}
self.openBotPreviewEditor(target: .botPreview(self.peerId), source: result, transitionIn: (transitionView, transitionRect, transitionImage))
guard let pane = self.paneContainerNode.currentPane?.node as? PeerInfoStoryPaneNode else {
return
}
self.openBotPreviewEditor(target: .botPreview(id: self.peerId, language: pane.currentBotPreviewLanguage?.id), source: result, transitionIn: (transitionView, transitionRect, transitionImage))
},
dismissed: {},
groupsPresented: {}
@ -10939,6 +10944,23 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
})))
if let language = pane.currentBotPreviewLanguage {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Delete \(language.name)", 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 {

View File

@ -46,6 +46,8 @@ swift_library(
"//submodules/TelegramUI/Components/MediaEditorScreen",
"//submodules/LocationUI",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/Settings/LanguageSelectionScreen",
],
visibility = [
"//visibility:public",

View File

@ -41,6 +41,8 @@ import Geocoding
import ItemListUI
import MultilineTextComponent
import LocationUI
import TabSelectorComponent
import LanguageSelectionScreen
private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6)
private let mediaBadgeTextColor = UIColor.white
@ -1565,6 +1567,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
private var mapInfoNode: LocationInfoListItemNode?
private var searchHeader: ComponentView<Empty>?
private var botPreviewLanguageTab: ComponentView<Empty>?
private var botPreviewFooter: ComponentView<Empty>?
private var barBackgroundLayer: SimpleLayer?
private let itemGrid: SparseItemGrid
@ -1638,7 +1643,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
public var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
public var tabBarOffset: CGFloat {
return self.itemGrid.coveringInsetOffset
if case .botPreview = self.scope {
return 0.0
} else {
return self.itemGrid.coveringInsetOffset
}
}
private var currentListState: StoryListContext.State?
@ -1646,6 +1655,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
private var hiddenMediaDisposable: Disposable?
private let updateDisposable = MetaDisposable()
private var currentBotPreviewLanguages: [StoryListContext.State.Language] = []
private var removedBotPreviewLanguages = Set<String>()
private var numberOfItemsToRequest: Int = 50
private var isRequestingView: Bool = false
private var isFirstHistoryView: Bool = true
@ -1656,6 +1668,24 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
public private(set) var calendarSource: SparseMessageCalendar?
private var listSource: StoryListContext
private let maxBotPreviewCount: Int
private let defaultListSource: StoryListContext
private var cachedListSources: [String: StoryListContext] = [:]
public var currentBotPreviewLanguage: (id: String, name: String)? {
guard let listSource = self.listSource as? BotPreviewStoryListContext else {
return nil
}
guard let id = listSource.language else {
return nil
}
guard let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) else {
return nil
}
return (language.id, language.name)
}
public var openCurrentDate: (() -> Void)?
public var paneDidScroll: (() -> Void)?
@ -1723,11 +1753,18 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
case let .location(coordinates, venue):
self.listSource = SearchStoryListContext(account: context.account, source: .mediaArea(.venue(coordinates: coordinates, venue: venue)))
case let .botPreview(id):
self.listSource = BotPreviewStoryListContext(account: context.account, engine: context.engine, peerId: id)
self.listSource = BotPreviewStoryListContext(account: context.account, engine: context.engine, peerId: id, language: nil, assumeEmpty: false)
}
}
self.defaultListSource = self.listSource
self.calendarSource = nil
var maxBotPreviewCount = 10
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["bot_preview_medias_max"] as? Double {
maxBotPreviewCount = Int(value)
}
self.maxBotPreviewCount = maxBotPreviewCount
super.init()
if case .peer = self.scope {
@ -2689,6 +2726,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
self.listDisposable?.dispose()
self.listDisposable = nil
if reloadAtTop {
self.didUpdateItemsOnce = false
}
self.listDisposable = (state
|> deliverOn(queue)).startStrict(next: { [weak self] state in
@ -2700,7 +2741,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
if state.totalCount == 0 {
if case .botPreview = self.scope {
//TODO:localize
title = "no preview added"
if state.isLoading {
title = "loading"
} else {
title = "no preview added"
}
} else {
title = ""
}
@ -2738,9 +2783,24 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
return
}
var botPreviewLanguages = self.currentBotPreviewLanguages
for language in state.availableLanguages {
if !botPreviewLanguages.contains(where: { $0.id == language.id }) && !self.removedBotPreviewLanguages.contains(language.id) {
botPreviewLanguages.append(language)
}
}
botPreviewLanguages.sort(by: { $0.name < $1.name })
self.currentListState = state
self.updateItemsFromState(state: state, firstTime: firstTime, reloadAtTop: reloadAtTop, synchronous: synchronous, animated: false)
if self.currentBotPreviewLanguages != botPreviewLanguages || reloadAtTop {
self.currentBotPreviewLanguages = botPreviewLanguages
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)
}
}
firstTime = false
self.isRequestingView = false
}
@ -2853,7 +2913,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams {
var gridSnapshot: UIView?
if reloadAtTop {
if case .botPreview = scope {
} else if reloadAtTop {
gridSnapshot = self.itemGrid.view.snapshotView(afterScreenUpdates: false)
}
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: transition, animateGridItems: animated)
@ -2971,43 +3032,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
public func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
return nil
/*var foundItemLayer: SparseItemGridLayer?
self.itemGrid.forEachVisibleItem { item in
guard let itemLayer = item.layer as? ItemLayer else {
return
}
if let item = itemLayer.item, item.message.id == messageId {
foundItemLayer = itemLayer
}
}
if let itemLayer = foundItemLayer {
let itemFrame = self.view.convert(self.itemGrid.frameForItem(layer: itemLayer), from: self.itemGrid.view)
let proxyNode = ASDisplayNode()
proxyNode.frame = itemFrame
if let contents = itemLayer.getContents() {
if let image = contents as? UIImage {
proxyNode.contents = image.cgImage
} else {
proxyNode.contents = contents
}
}
proxyNode.isHidden = true
self.addSubnode(proxyNode)
let escapeNotification = EscapeNotification {
proxyNode.removeFromSupernode()
}
return (proxyNode, proxyNode.bounds, {
let view = UIView()
view.frame = proxyNode.frame
view.layer.contents = proxyNode.layer.contents
escapeNotification.keep()
return (view, nil)
})
}
return nil*/
}
public func extractPendingStoryTransitionView() -> UIView? {
@ -3294,29 +3318,29 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
controller?.dismissAnimated()
}
var mappedItemIds: [MediaId] = []
var mappedMedia: [Media] = []
if let items = self.items {
mappedItemIds = items.items.compactMap { item -> MediaId? in
mappedMedia = items.items.compactMap { item -> Media? in
guard let item = item as? VisualMediaItem else {
return nil
}
if ids.contains(item.story.id) {
return item.story.media.id
return item.story.media._asMedia()
} else {
return nil
}
}
}
if mappedItemIds.isEmpty {
if mappedMedia.isEmpty {
return
}
//TODO:localize
let title: String
if mappedItemIds.count == 1 {
if mappedMedia.count == 1 {
title = "Delete 1 Preview?"
} else {
title = "Delete \(mappedItemIds.count) Previews?"
title = "Delete \(mappedMedia.count) Previews?"
}
controller.setItemGroups([
@ -3328,12 +3352,15 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
guard let self else {
return
}
guard let listSource = self.listSource as? BotPreviewStoryListContext else {
return
}
if let parentController = self.parentController as? PeerInfoScreen {
parentController.cancelItemSelection()
}
let _ = self.context.engine.messages.deleteBotPreviews(peerId: peerId, ids: mappedItemIds).startStandalone()
let _ = self.context.engine.messages.deleteBotPreviews(peerId: peerId, language: listSource.language, media: mappedMedia).startStandalone()
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
@ -3349,8 +3376,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
}
private func gridScrollingOffsetUpdated(transition: ContainedViewLayoutTransition) {
if let _ = self.mapNode, let currentParams = self.currentParams {
self.updateMapLayout(size: currentParams.size, topInset: currentParams.topInset, bottomInset: currentParams.bottomInset, deviceMetrics: currentParams.deviceMetrics, transition: transition)
if let currentParams = self.currentParams {
if let _ = self.mapNode {
self.updateMapLayout(size: currentParams.size, topInset: currentParams.topInset, bottomInset: currentParams.bottomInset, deviceMetrics: currentParams.deviceMetrics, transition: transition)
}
if case .botPreview = self.scope, self.canManageStories {
self.updateBotPreviewLanguageTab(size: currentParams.size, topInset: currentParams.topInset, transition: transition)
self.updateBotPreviewFooter(size: currentParams.size, bottomInset: currentParams.bottomInset, transition: transition)
}
}
}
@ -3504,6 +3537,150 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
}
}
private func updateBotPreviewLanguageTab(size: CGSize, topInset: CGFloat, transition: ContainedViewLayoutTransition) {
guard case .botPreview = self.scope, self.canManageStories else {
return
}
let botPreviewLanguageTab: ComponentView<Empty>
if let current = self.botPreviewLanguageTab {
botPreviewLanguageTab = current
} else {
botPreviewLanguageTab = ComponentView()
self.botPreviewLanguageTab = botPreviewLanguageTab
}
//TODO:localize
var languageItems: [TabSelectorComponent.Item] = []
languageItems.append(TabSelectorComponent.Item(
id: AnyHashable("_main"),
title: "Main"
))
for language in self.currentBotPreviewLanguages {
languageItems.append(TabSelectorComponent.Item(
id: AnyHashable(language.id),
title: language.name
))
}
languageItems.append(TabSelectorComponent.Item(
id: AnyHashable("_add"),
title: "+ Add Language"
))
var selectedLanguageId = "_main"
if let listSource = self.listSource as? BotPreviewStoryListContext, let language = listSource.language {
selectedLanguageId = language
}
let botPreviewLanguageTabSize = botPreviewLanguageTab.update(
transition: ComponentTransition(transition),
component: AnyComponent(TabSelectorComponent(
colors: TabSelectorComponent.Colors(
foreground: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.8),
selection: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05)
),
customLayout: TabSelectorComponent.CustomLayout(
font: Font.medium(14.0),
spacing: 9.0,
verticalInset: 11.0
),
items: languageItems,
selectedId: AnyHashable(selectedLanguageId),
setSelectedId: { [weak self] id in
guard let self, let id = id.base as? String else {
return
}
if id == "_add" {
self.presentAddBotPreviewLanguage()
} else if id == "_main" {
self.setBotPreviewLanguage(id: nil, assumeEmpty: false)
} else if let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) {
self.setBotPreviewLanguage(id: language.id, assumeEmpty: false)
}
}
)),
environment: {},
containerSize: CGSize(width: size.width, height: 44.0)
)
var botPreviewLanguageTabFrame = CGRect(origin: CGPoint(x: floor((size.width - botPreviewLanguageTabSize.width) * 0.5), y: topInset - 11.0), size: botPreviewLanguageTabSize)
let effectiveScrollingOffset: CGFloat
effectiveScrollingOffset = self.itemGrid.scrollingOffset
botPreviewLanguageTabFrame.origin.y -= effectiveScrollingOffset
if let botPreviewLanguageTabView = botPreviewLanguageTab.view {
if botPreviewLanguageTabView.superview == nil {
self.view.addSubview(botPreviewLanguageTabView)
}
transition.updateFrame(view: botPreviewLanguageTabView, frame: botPreviewLanguageTabFrame)
}
}
private func updateBotPreviewFooter(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
if let items = self.items, !items.items.isEmpty {
var botPreviewFooterTransition = ComponentTransition(transition)
let botPreviewFooter: ComponentView<Empty>
if let current = self.botPreviewFooter {
botPreviewFooter = current
} else {
botPreviewFooterTransition = .immediate
botPreviewFooter = ComponentView()
self.botPreviewFooter = botPreviewFooter
}
var isMainLanguage = true
let text: String
if let listSource = self.listSource as? BotPreviewStoryListContext, let id = listSource.language, let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) {
isMainLanguage = false
text = "This preview will be displayed for all users who have \(language.name) set as their language."
} else {
text = "This preview will be shown by default. You can also add translations into specific languages."
}
let botPreviewFooterSize = botPreviewFooter.update(
transition: botPreviewFooterTransition,
component: AnyComponent(EmptyStateIndicatorComponent(
context: self.context,
theme: self.presentationData.theme,
fitToHeight: true,
animationName: nil,
title: nil,
text: text,
actionTitle: "Add Preview",
action: { [weak self] in
guard let self else {
return
}
self.emptyAction?()
},
additionalActionTitle: isMainLanguage ? "Create a Translation" : nil,
additionalAction: { [weak self] in
guard let self else {
return
}
if isMainLanguage {
self.presentAddBotPreviewLanguage()
}
},
additionalActionSeparator: isMainLanguage ? "or" : nil
)),
environment: {},
containerSize: CGSize(width: size.width, height: 1000.0)
)
let botPreviewFooterFrame = CGRect(origin: CGPoint(x: floor((size.width - botPreviewFooterSize.width) * 0.5), y: self.itemGrid.contentBottomOffset - botPreviewFooterSize.height - bottomInset), size: botPreviewFooterSize)
if let botPreviewFooterView = botPreviewFooter.view {
if botPreviewFooterView.superview == nil {
self.view.addSubview(botPreviewFooterView)
}
botPreviewFooterTransition.setFrame(view: botPreviewFooterView, frame: botPreviewFooterFrame)
}
} else {
if let botPreviewFooter = self.botPreviewFooter {
self.botPreviewFooter = nil
botPreviewFooter.view?.removeFromSuperview()
}
}
}
public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
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: transition, animateGridItems: false)
}
@ -3555,7 +3732,19 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
transition.updateFrame(layer: barBackgroundLayer, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: gridTopInset)))
}
let defaultBottomInset = bottomInset
var bottomInset = bottomInset
if case .botPreview = self.scope, self.canManageStories {
updateBotPreviewLanguageTab(size: size, topInset: topInset, transition: transition)
gridTopInset += 50.0
updateBotPreviewFooter(size: size, bottomInset: defaultBottomInset, transition: transition)
if let botPreviewFooterView = self.botPreviewFooter?.view {
bottomInset += 18.0 + botPreviewFooterView.bounds.height
}
}
if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories, case let .peer(peerId, _, isArchived) = self.scope {
let selectionPanel: ComponentView<Empty>
var selectionPanelTransition = ComponentTransition(transition)
@ -3816,6 +4005,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
self.emptyStateView = emptyStateView
}
//TODO:localize
var isMainLanguage = true
if let listSource = self.listSource as? BotPreviewStoryListContext, let _ = listSource.language {
isMainLanguage = false
}
let emptyStateSize = emptyStateView.update(
transition: emptyStateTransition,
component: AnyComponent(EmptyStateIndicatorComponent(
@ -3824,7 +4019,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
fitToHeight: self.isProfileEmbedded,
animationName: nil,
title: "No Preview",
text: "Upload screenshots and video demos for your Mini App that will be visible for your users here.",
text: "Upload up to \(self.maxBotPreviewCount) screenshots and video demos for your mini app.",
actionTitle: self.canManageStories ? "Add Preview" : nil,
action: { [weak self] in
guard let self else {
@ -3832,8 +4027,15 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
}
self.emptyAction?()
},
additionalActionTitle: nil,
additionalAction: {}
additionalActionTitle: self.canManageStories ? (isMainLanguage ? "Create a Translation" : "Delete this Translation") : nil,
additionalAction: {
if isMainLanguage {
self.presentAddBotPreviewLanguage()
} else {
self.presentDeleteBotPreviewLanguage()
}
},
additionalActionSeparator: self.canManageStories ? "or" : nil
)),
environment: {},
containerSize: CGSize(width: size.width, height: size.height - gridTopInset - bottomInset)
@ -3860,7 +4062,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
if self.isProfileEmbedded, case .botPreview = self.scope {
backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else if self.isProfileEmbedded {
backgroundColor = presentationData.theme.list.plainBackgroundColor
backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else {
backgroundColor = presentationData.theme.list.blocksBackgroundColor
}
@ -3876,18 +4078,28 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
self.emptyStateView = nil
if let emptyStateComponentView = emptyStateView.view {
subTransition.setAlpha(view: emptyStateComponentView, alpha: 0.0, completion: { [weak emptyStateComponentView] _ in
emptyStateComponentView?.removeFromSuperview()
})
if self.didUpdateItemsOnce {
subTransition.setAlpha(view: emptyStateComponentView, alpha: 0.0, completion: { [weak emptyStateComponentView] _ in
emptyStateComponentView?.removeFromSuperview()
})
} else {
emptyStateComponentView.removeFromSuperview()
}
}
if self.isProfileEmbedded {
if self.isProfileEmbedded, case .botPreview = self.scope {
subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor)
} else if self.isProfileEmbedded {
subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.plainBackgroundColor)
} else {
subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor)
}
} else {
self.view.backgroundColor = .clear
if self.isProfileEmbedded, case .botPreview = self.scope {
self.view.backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else {
self.view.backgroundColor = .clear
}
}
}
@ -3904,6 +4116,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
self.itemGrid.pinchEnabled = items.count > 2
self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none, transition: animateGridItems ? .spring(duration: 0.35) : .immediate)
}
if case .botPreview = self.scope, self.canManageStories {
updateBotPreviewFooter(size: size, bottomInset: defaultBottomInset, transition: transition)
}
}
public func currentTopTimestamp() -> Int32? {
@ -3992,12 +4208,109 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
return false
}
var maxCount = 10
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["bot_preview_medias_max"] as? Double {
maxCount = Int(value)
return items.count < self.maxBotPreviewCount
}
private func presentAddBotPreviewLanguage() {
self.parentController?.push(LanguageSelectionScreen(context: self.context, selectLocalization: { [weak self] info in
guard let self else {
return
}
self.addBotPreviewLanguage(language: StoryListContext.State.Language(id: info.languageCode, name: info.title))
}))
}
public func presentDeleteBotPreviewLanguage() {
//TODO:localize
self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: "Delete Translation", text: "Are you sure you want to delete this translation?", actions: [
TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {
}),
TextAlertAction(type: .destructiveAction, title: "OK", action: { [weak self] in
guard let self else {
return
}
if let listSource = self.listSource as? BotPreviewStoryListContext, let language = listSource.language {
self.deleteBotPreviewLanguage(id: language)
}
})
], parseMarkdown: true), in: .window(.root))
}
private func addBotPreviewLanguage(language: StoryListContext.State.Language) {
var botPreviewLanguages = self.currentBotPreviewLanguages
var assumeEmpty = false
if !botPreviewLanguages.contains(where: { $0.id == language.id}) {
botPreviewLanguages.append(language)
assumeEmpty = true
}
botPreviewLanguages.sort(by: { $0.name < $1.name })
self.removedBotPreviewLanguages.remove(language.id)
if self.currentBotPreviewLanguages != botPreviewLanguages {
self.currentBotPreviewLanguages = botPreviewLanguages
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)
}
}
return items.count < maxCount
self.setBotPreviewLanguage(id: language.id, assumeEmpty: assumeEmpty)
}
private func deleteBotPreviewLanguage(id: String) {
var botPreviewLanguages = self.currentBotPreviewLanguages
if let index = botPreviewLanguages.firstIndex(where: { $0.id == id}) {
botPreviewLanguages.remove(at: index)
}
self.removedBotPreviewLanguages.insert(id)
guard case let .botPreview(peerId) = self.scope else {
return
}
var mappedMedia: [Media] = []
if let items = self.items {
mappedMedia = items.items.compactMap { item -> Media? in
guard let item = item as? VisualMediaItem else {
return nil
}
return item.story.media._asMedia()
}
}
let _ = self.context.engine.messages.deleteBotPreviewsLanguage(peerId: peerId, language: id, media: mappedMedia).startStandalone()
if self.currentBotPreviewLanguages != botPreviewLanguages {
self.currentBotPreviewLanguages = botPreviewLanguages
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)
}
}
self.setBotPreviewLanguage(id: nil, assumeEmpty: false)
}
private func setBotPreviewLanguage(id: String?, assumeEmpty: Bool) {
guard case let .botPreview(peerId) = self.scope else {
return
}
if let listSource = self.listSource as? BotPreviewStoryListContext, listSource.language == id {
return
}
if let id {
if let cachedListSource = self.cachedListSources[id] {
self.listSource = cachedListSource
} else {
let listSource = BotPreviewStoryListContext(account: self.context.account, engine: self.context.engine, peerId: peerId, language: id, assumeEmpty: assumeEmpty)
self.listSource = listSource
self.cachedListSources[id] = listSource
}
} else {
self.listSource = self.defaultListSource
}
self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: true)
}
public func beginReordering() {
@ -4027,7 +4340,17 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
if !isReordering, let reorderedIds = self.reorderedIds {
self.reorderedIds = nil
if case .botPreview = self.scope, let listSource = self.listSource as? BotPreviewStoryListContext {
listSource.reorderItems(ids: reorderedIds)
if let items = self.items {
var reorderedMedia: [Media] = []
for id in reorderedIds {
if let item = items.items.first(where: { ($0 as? VisualMediaItem)?.storyId == id }) as? VisualMediaItem {
reorderedMedia.append(item.story.media._asMedia())
}
}
listSource.reorderItems(media: reorderedMedia)
}
} else if case let .peer(id, _, _) = self.scope, id == self.context.account.peerId, let items = self.items {
var updatedPinnedIds: [Int32] = []
for id in reorderedIds {

View File

@ -0,0 +1,31 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "LanguageSelectionScreen",
module_name = "LanguageSelectionScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/MergeLists",
"//submodules/ItemListUI",
"//submodules/PresentationDataUtils",
"//submodules/AccountContext",
"//submodules/SearchBarNode",
"//submodules/SearchUI",
"//submodules/TelegramUIPreferences",
"//submodules/TranslateUI",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,177 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import AccountContext
import SearchUI
public class LanguageSelectionScreen: ViewController {
private let context: AccountContext
private let selectLocalization: (LocalizationInfo) -> Void
private var controllerNode: LanguageSelectionScreenNode {
return self.displayNode as! LanguageSelectionScreenNode
}
private var _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private var searchContentNode: NavigationBarSearchContentNode?
private var previousContentOffset: ListViewVisibleContentOffset?
public init(context: AccountContext, selectLocalization: @escaping (LocalizationInfo) -> Void) {
self.context = context
self.selectLocalization = selectLocalization
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.navigationPresentation = .modal
//TODO:localize
self.title = "Add a Translation"
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.scrollToTop = { [weak self] in
if let strongSelf = self {
if let searchContentNode = strongSelf.searchContentNode {
searchContentNode.updateExpansionProgress(1.0, animated: true)
}
strongSelf.controllerNode.scrollToTop()
}
}
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.updateThemeAndStrings()
}
}
})
self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search, inline: true, activate: { [weak self] in
self?.activateSearch()
})
self.navigationBar?.setContentNode(self.searchContentNode, animated: false)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
private func updateThemeAndStrings() {
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
self.searchContentNode?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search)
self.title = self.presentationData.strings.Settings_AppLanguage
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.controllerNode.updatePresentationData(self.presentationData)
}
override public func loadDisplayNode() {
self.displayNode = LanguageSelectionScreenNode(context: self.context, presentationData: self.presentationData, navigationBar: self.navigationBar!, requestActivateSearch: { [weak self] in
self?.activateSearch()
}, requestDeactivateSearch: { [weak self] in
self?.deactivateSearch()
}, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}, push: { [weak self] c in
self?.push(c)
}, selectLocalization: { [weak self] info in
guard let self else {
return
}
self.selectLocalization(info)
self.dismiss()
})
self.controllerNode.listNode.visibleContentOffsetChanged = { [weak self] offset in
if let strongSelf = self {
if let searchContentNode = strongSelf.searchContentNode {
searchContentNode.updateListVisibleContentOffset(offset)
}
var previousContentOffsetValue: CGFloat?
if let previousContentOffset = strongSelf.previousContentOffset, case let .known(value) = previousContentOffset {
previousContentOffsetValue = value
}
switch offset {
case let .known(value):
let transition: ContainedViewLayoutTransition
if let previousContentOffsetValue = previousContentOffsetValue, value <= 0.0, previousContentOffsetValue > 30.0 {
transition = .animated(duration: 0.2, curve: .easeInOut)
} else {
transition = .immediate
}
strongSelf.navigationBar?.updateBackgroundAlpha(min(30.0, max(0.0, value - 54.0)) / 30.0, transition: transition)
case .unknown, .none:
strongSelf.navigationBar?.updateBackgroundAlpha(1.0, transition: .immediate)
}
strongSelf.previousContentOffset = offset
}
}
self.controllerNode.listNode.didEndScrolling = { [weak self] _ in
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode {
let _ = fixNavigationSearchableListNodeScrolling(strongSelf.controllerNode.listNode, searchNode: searchContentNode)
}
}
self._ready.set(self.controllerNode._ready.get())
self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
self.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, transition: transition)
}
private func activateSearch() {
if self.displayNavigationBar {
if let scrollToTop = self.scrollToTop {
scrollToTop()
}
if let searchContentNode = self.searchContentNode {
self.controllerNode.activateSearch(placeholderNode: searchContentNode.placeholderNode)
}
self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring))
}
}
private func deactivateSearch() {
if !self.displayNavigationBar {
self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring))
if let searchContentNode = self.searchContentNode {
self.controllerNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode)
}
}
}
}

View File

@ -0,0 +1,568 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import MergeLists
import ItemListUI
import PresentationDataUtils
import AccountContext
import SearchBarNode
import SearchUI
import TelegramUIPreferences
import TranslateUI
private enum LanguageListSection: ItemListSectionId {
case official
case unofficial
}
private enum LanguageListEntryId: Hashable {
case localizationTitle
case localization(String)
}
private enum LanguageListEntryType {
case official
case unofficial
}
private enum LanguageListEntry: Comparable, Identifiable {
case localizationTitle(text: String, section: ItemListSectionId)
case localization(index: Int, info: LocalizationInfo?, type: LanguageListEntryType)
var stableId: LanguageListEntryId {
switch self {
case .localizationTitle:
return .localizationTitle
case let .localization(index, info, _):
return .localization(info?.languageCode ?? "\(index)")
}
}
private func index() -> Int {
switch self {
case .localizationTitle:
return 1000
case let .localization(index, _, _):
return 1001 + index
}
}
static func <(lhs: LanguageListEntry, rhs: LanguageListEntry) -> Bool {
return lhs.index() < rhs.index()
}
func item(presentationData: PresentationData, searchMode: Bool, openSearch: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void) -> ListViewItem {
switch self {
case let .localizationTitle(text, section):
return ItemListSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), text: text, sectionId: section)
case let .localization(_, info, type):
return LocalizationListItem(presentationData: ItemListPresentationData(presentationData), id: info?.languageCode ?? "", title: info?.title ?? " ", subtitle: info?.localizedTitle ?? " ", checked: false, activity: false, loading: info == nil, editing: LocalizationListItemEditing(editable: false, editing: false, revealed: false, reorderable: false), sectionId: type == .official ? LanguageListSection.official.rawValue : LanguageListSection.unofficial.rawValue, alwaysPlain: searchMode, action: {
if let info {
selectLocalization(info)
}
}, setItemWithRevealedOptions: nil, removeItem: nil)
}
}
}
private struct LocalizationListSearchContainerTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let isSearching: Bool
}
private func preparedLanguageListSearchContainerTransition(presentationData: PresentationData, from fromEntries: [LanguageListEntry], to toEntries: [LanguageListEntry], selectLocalization: @escaping (LocalizationInfo) -> Void, isSearching: Bool, forceUpdate: Bool) -> LocalizationListSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, openSearch: {}, selectLocalization: selectLocalization), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, openSearch: {}, selectLocalization: selectLocalization), directionHint: nil) }
return LocalizationListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching)
}
private final class LocalizationListSearchContainerNode: SearchDisplayControllerContentNode {
private let dimNode: ASDisplayNode
private let listNode: ListView
private var enqueuedTransitions: [LocalizationListSearchContainerTransition] = []
private var hasValidLayout = false
private let searchQuery = Promise<String?>()
private let searchDisposable = MetaDisposable()
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let presentationDataPromise: Promise<PresentationData>
public override var hasDim: Bool {
return true
}
init(context: AccountContext, listState: LocalizationListState, selectLocalization: @escaping (LocalizationInfo) -> Void) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
self.presentationDataPromise = Promise(self.presentationData)
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5)
self.listNode = ListView()
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
super.init()
self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.listNode.isHidden = true
self.addSubnode(self.dimNode)
self.addSubnode(self.listNode)
let foundItems = self.searchQuery.get()
|> mapToSignal { query -> Signal<[LocalizationInfo]?, NoError> in
if let query = query, !query.isEmpty {
let normalizedQuery = query.lowercased()
var result: [LocalizationInfo] = []
var uniqueIds = Set<String>()
for info in listState.availableSavedLocalizations + listState.availableOfficialLocalizations {
if info.title.lowercased().hasPrefix(normalizedQuery) || info.localizedTitle.lowercased().hasPrefix(normalizedQuery) {
if uniqueIds.contains(info.languageCode) {
continue
}
uniqueIds.insert(info.languageCode)
result.append(info)
}
}
return .single(result)
} else {
return .single(nil)
}
}
let previousEntriesHolder = Atomic<([LanguageListEntry], PresentationTheme, PresentationStrings)?>(value: nil)
self.searchDisposable.set(combineLatest(queue: .mainQueue(), foundItems, self.presentationDataPromise.get()).start(next: { [weak self] items, presentationData in
guard let strongSelf = self else {
return
}
var entries: [LanguageListEntry] = []
if let items = items {
for item in items {
entries.append(.localization(index: entries.count, info: item, type: .official))
}
}
let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings))
let transition = preparedLanguageListSearchContainerTransition(presentationData: presentationData, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, selectLocalization: selectLocalization, isSearching: items != nil, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings)
strongSelf.enqueueTransition(transition)
}))
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
strongSelf.presentationDataPromise.set(.single(presentationData))
}
}
})
self.listNode.beganInteractiveDragging = { [weak self] _ in
self?.dismissInput?()
}
}
deinit {
self.searchDisposable.dispose()
self.presentationDataDisposable?.dispose()
}
override func didLoad() {
super.didLoad()
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.listNode.backgroundColor = theme.chatList.backgroundColor
}
override func searchTextUpdated(text: String) {
if text.isEmpty {
self.searchQuery.set(.single(nil))
} else {
self.searchQuery.set(.single(text))
}
}
private func enqueueTransition(_ transition: LocalizationListSearchContainerTransition) {
self.enqueuedTransitions.append(transition)
if self.hasValidLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransitions()
}
}
}
private func dequeueTransitions() {
if let transition = self.enqueuedTransitions.first {
self.enqueuedTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
options.insert(.PreferSynchronousDrawing)
let isSearching = transition.isSearching
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
self?.listNode.isHidden = !isSearching
self?.dimNode.isHidden = isSearching
})
}
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
let topInset = navigationBarHeight
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset)))
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if !self.hasValidLayout {
self.hasValidLayout = true
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransitions()
}
}
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.cancel?()
}
}
}
private struct LanguageListNodeTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let firstTime: Bool
let isLoading: Bool
let animated: Bool
let crossfade: Bool
}
private func preparedLanguageListNodeTransition(
presentationData: PresentationData,
from fromEntries: [LanguageListEntry],
to toEntries: [LanguageListEntry],
openSearch: @escaping () -> Void,
selectLocalization: @escaping (LocalizationInfo) -> Void,
firstTime: Bool,
isLoading: Bool,
forceUpdate: Bool,
animated: Bool,
crossfade: Bool
) -> LanguageListNodeTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization), directionHint: nil) }
return LanguageListNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, isLoading: isLoading, animated: animated, crossfade: crossfade)
}
final class LanguageSelectionScreenNode: ViewControllerTracingNode {
private let context: AccountContext
private var presentationData: PresentationData
private weak var navigationBar: NavigationBar?
private let requestActivateSearch: () -> Void
private let requestDeactivateSearch: () -> Void
private let present: (ViewController, Any?) -> Void
private let push: (ViewController) -> Void
private let selectLocalization: (LocalizationInfo) -> Void
private var didSetReady = false
let _ready = ValuePromise<Bool>()
private var containerLayout: (ContainerViewLayout, CGFloat)?
let listNode: ListView
private let leftOverlayNode: ASDisplayNode
private let rightOverlayNode: ASDisplayNode
private var queuedTransitions: [LanguageListNodeTransition] = []
private var searchDisplayController: SearchDisplayController?
private let presentationDataValue = Promise<PresentationData>()
private var updatedDisposable: Disposable?
private var listDisposable: Disposable?
private var currentListState: LocalizationListState?
init(context: AccountContext, presentationData: PresentationData, navigationBar: NavigationBar, requestActivateSearch: @escaping () -> Void, requestDeactivateSearch: @escaping () -> Void, present: @escaping (ViewController, Any?) -> Void, push: @escaping (ViewController) -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void) {
self.context = context
self.presentationData = presentationData
self.presentationDataValue.set(.single(presentationData))
self.navigationBar = navigationBar
self.requestActivateSearch = requestActivateSearch
self.requestDeactivateSearch = requestDeactivateSearch
self.present = present
self.push = push
self.selectLocalization = selectLocalization
self.listNode = ListView()
self.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.list.blocksBackgroundColor, direction: true)
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
self.leftOverlayNode = ASDisplayNode()
self.leftOverlayNode.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
self.rightOverlayNode = ASDisplayNode()
self.rightOverlayNode.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
super.init()
self.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.addSubnode(self.listNode)
let openSearch: () -> Void = {
requestActivateSearch()
}
let previousState = Atomic<LocalizationListState?>(value: nil)
let previousEntriesHolder = Atomic<([LanguageListEntry], PresentationTheme, PresentationStrings)?>(value: nil)
self.listDisposable = combineLatest(
queue: .mainQueue(),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.LocalizationList()),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)),
context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.localizationSettings, ApplicationSpecificSharedDataKeys.translationSettings]),
self.presentationDataValue.get()
).start(next: { [weak self] localizationListState, peer, sharedData, presentationData in
guard let strongSelf = self else {
return
}
var entries: [LanguageListEntry] = []
var existingIds = Set<String>()
if !localizationListState.availableOfficialLocalizations.isEmpty {
strongSelf.currentListState = localizationListState
let availableSavedLocalizations = localizationListState.availableSavedLocalizations.filter({ info in !localizationListState.availableOfficialLocalizations.contains(where: { $0.languageCode == info.languageCode }) })
if !availableSavedLocalizations.isEmpty {
//entries.append(.localizationTitle(text: presentationData.strings.Localization_InterfaceLanguage.uppercased(), section: LanguageListSection.unofficial.rawValue))
for info in availableSavedLocalizations {
if existingIds.contains(info.languageCode) {
continue
}
existingIds.insert(info.languageCode)
entries.append(.localization(index: entries.count, info: info, type: .unofficial))
}
} else {
//entries.append(.localizationTitle(text: presentationData.strings.Localization_InterfaceLanguage.uppercased(), section: LanguageListSection.official.rawValue))
}
for info in localizationListState.availableOfficialLocalizations {
if existingIds.contains(info.languageCode) {
continue
}
existingIds.insert(info.languageCode)
entries.append(.localization(index: entries.count, info: info, type: .official))
}
} else {
for _ in 0 ..< 15 {
entries.append(.localization(index: entries.count, info: nil, type: .official))
}
}
let previousState = previousState.swap(localizationListState)
let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings))
let transition = preparedLanguageListNodeTransition(
presentationData: presentationData,
from: previousEntriesAndPresentationData?.0 ?? [],
to: entries,
openSearch: openSearch,
selectLocalization: { [weak self] info in
self?.selectLocalization(info)
},
firstTime: previousEntriesAndPresentationData == nil,
isLoading: entries.isEmpty,
forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings,
animated: (previousEntriesAndPresentationData?.0.count ?? 0) != entries.count,
crossfade: (previousState == nil || previousState!.availableOfficialLocalizations.isEmpty) != localizationListState.availableOfficialLocalizations.isEmpty
)
strongSelf.enqueueTransition(transition)
})
self.updatedDisposable = context.engine.localization.synchronizedLocalizationListState().start()
self.listNode.itemNodeHitTest = { [weak self] point in
if let strongSelf = self {
return point.x > strongSelf.leftOverlayNode.frame.maxX && point.x < strongSelf.rightOverlayNode.frame.minX
} else {
return true
}
}
}
deinit {
self.listDisposable?.dispose()
self.updatedDisposable?.dispose()
}
func updatePresentationData(_ presentationData: PresentationData) {
let stringsUpdated = self.presentationData.strings !== presentationData.strings
self.presentationData = presentationData
if stringsUpdated {
if let snapshotView = self.view.snapshotView(afterScreenUpdates: false) {
self.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
}
self.presentationDataValue.set(.single(presentationData))
self.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.list.blocksBackgroundColor, direction: true)
self.searchDisplayController?.updatePresentationData(presentationData)
self.leftOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.rightOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let hadValidLayout = self.containerLayout != nil
self.containerLayout = (layout, navigationBarHeight)
var listInsets = layout.insets(options: [.input])
listInsets.top += navigationBarHeight
if layout.size.width >= 375.0 {
let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
listInsets.left += inset
listInsets.right += inset
} else {
listInsets.left += layout.safeInsets.left
listInsets.right += layout.safeInsets.right
}
self.leftOverlayNode.frame = CGRect(x: 0.0, y: 0.0, width: listInsets.left, height: layout.size.height)
self.rightOverlayNode.frame = CGRect(x: layout.size.width - listInsets.right, y: 0.0, width: listInsets.right, height: layout.size.height)
if self.leftOverlayNode.supernode == nil {
self.insertSubnode(self.leftOverlayNode, aboveSubnode: self.listNode)
}
if self.rightOverlayNode.supernode == nil {
self.insertSubnode(self.rightOverlayNode, aboveSubnode: self.listNode)
}
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
}
self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: listInsets, duration: duration, curve: curve)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if !hadValidLayout {
self.dequeueTransitions()
}
}
private func enqueueTransition(_ transition: LanguageListNodeTransition) {
self.queuedTransitions.append(transition)
if self.containerLayout != nil {
self.dequeueTransitions()
}
}
private func dequeueTransitions() {
guard let _ = self.containerLayout else {
return
}
while !self.queuedTransitions.isEmpty {
let transition = self.queuedTransitions.removeFirst()
var options = ListViewDeleteAndInsertOptions()
if transition.firstTime {
options.insert(.Synchronous)
options.insert(.LowLatency)
} else if transition.crossfade {
options.insert(.AnimateCrossfade)
} else if transition.animated {
options.insert(.AnimateInsertion)
}
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in
if let strongSelf = self {
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(true)
}
}
})
}
}
func activateSearch(placeholderNode: SearchBarPlaceholderNode) {
guard let (containerLayout, navigationBarHeight) = self.containerLayout, self.searchDisplayController == nil else {
return
}
self.searchDisplayController = SearchDisplayController(
presentationData: self.presentationData,
contentNode: LocalizationListSearchContainerNode(
context: self.context,
listState: self.currentListState ?? LocalizationListState.defaultSettings,
selectLocalization: { [weak self] info in
self?.selectLocalization(info)
}),
inline: true,
cancel: { [weak self] in
self?.requestDeactivateSearch()
}
)
self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate)
self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in
if let strongSelf = self, let strongPlaceholderNode = placeholderNode {
if isSearchBar {
strongPlaceholderNode.supernode?.insertSubnode(subnode, aboveSubnode: strongPlaceholderNode)
} else if let navigationBar = strongSelf.navigationBar {
strongSelf.insertSubnode(subnode, belowSubnode: navigationBar)
}
}
}, placeholder: placeholderNode)
}
func deactivateSearch(placeholderNode: SearchBarPlaceholderNode) {
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.deactivate(placeholder: placeholderNode)
self.searchDisplayController = nil
}
}
func scrollToTop() {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
}

View File

@ -1093,7 +1093,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate
state.displayPatternPanel = false
return state
}, animated: true)
}, clickThroughMessage: {
}, clickThroughMessage: { _, _ in
}, backgroundNode: self.backgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)
return item
}

View File

@ -12,6 +12,7 @@ swift_library(
deps = [
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/ComponentFlow",
],
visibility = [
"//visibility:public",

View File

@ -2,7 +2,429 @@ import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
/*open class SpaceWarpView: UIView {
private final class WarpPartView: UIView {
let cloneView: PortalView
init?(contentView: PortalSourceView) {
guard let cloneView = PortalView(matchPosition: false) else {
return nil
}
self.cloneView = cloneView
super.init(frame: CGRect())
self.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0)
self.clipsToBounds = true
self.addSubview(cloneView.view)
contentView.addPortal(view: cloneView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(containerSize: CGSize, rect: CGRect, transition: ComponentTransition) {
transition.setFrame(view: self.cloneView.view, frame: CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: CGSize(width: containerSize.width, height: containerSize.height)))
}
}
public var contentView: UIView {
return self.contentViewImpl
}
let contentViewImpl: PortalSourceView
private var warpViews: [WarpPartView] = []
override public init(frame: CGRect) {
self.contentViewImpl = PortalSourceView()
super.init(frame: frame)
self.addSubview(self.contentView)
self.contentView.alpha = 0.1
for _ in 0 ..< 8 {
if let warpView = WarpPartView(contentView: self.contentViewImpl) {
self.warpViews.append(warpView)
self.addSubview(warpView)
}
}
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(size: CGSize, warpHeight: CGFloat, transition: ComponentTransition) {
transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: size))
let allItemsHeight = warpHeight * 0.5
for i in 0 ..< self.warpViews.count {
let itemHeight = warpHeight / CGFloat(self.warpViews.count)
let itemFraction = CGFloat(i + 1) / CGFloat(self.warpViews.count)
let _ = itemHeight
let da = CGFloat.pi * 0.5 / CGFloat(self.warpViews.count)
let alpha = CGFloat.pi * 0.5 - itemFraction * CGFloat.pi * 0.5
let endPoint = CGPoint(x: cos(alpha), y: sin(alpha))
let prevAngle = alpha + da
let prevPt = CGPoint(x: cos(prevAngle), y: sin(prevAngle))
var angle: CGFloat
angle = -atan2(endPoint.y - prevPt.y, endPoint.x - prevPt.x)
let itemLengthVector = CGPoint(x: endPoint.x - prevPt.x, y: endPoint.y - prevPt.y)
let itemLength = sqrt(itemLengthVector.x * itemLengthVector.x + itemLengthVector.y * itemLengthVector.y) * warpHeight * 0.5
let _ = itemLength
var transform: CATransform3D
transform = CATransform3DIdentity
transform.m34 = 1.0 / 240.0
transform = CATransform3DTranslate(transform, 0.0, prevPt.x * allItemsHeight, (1.0 - prevPt.y) * allItemsHeight)
transform = CATransform3DRotate(transform, angle, 1.0, 0.0, 0.0)
let positionY = size.height - allItemsHeight + 4.0 + CGFloat(i) * itemLength
let rect = CGRect(origin: CGPoint(x: 0.0, y: positionY), size: CGSize(width: size.width, height: itemLength))
transition.setPosition(view: self.warpViews[i], position: CGPoint(x: rect.midX, y: 4.0))
transition.setBounds(view: self.warpViews[i], bounds: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: itemLength)))
transition.setTransform(view: self.warpViews[i], transform: transform)
self.warpViews[i].update(containerSize: size, rect: rect, transition: transition)
}
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return self.contentView.hitTest(point, with: event)
}
}*/
private extension CGPoint {
static func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}
static func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
static func *(lhs: CGPoint, rhs: CGFloat) -> CGPoint {
return CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
}
}
private func length(_ v: CGPoint) -> CGFloat {
return sqrt(v.x * v.x + v.y * v.y)
}
private func normalize(_ v: CGPoint) -> CGPoint {
let len = length(v)
return CGPoint(x: v.x / len, y: v.y / len)
}
private struct RippleParams {
var amplitude: CGFloat
var frequency: CGFloat
var decay: CGFloat
var speed: CGFloat
init(amplitude: CGFloat, frequency: CGFloat, decay: CGFloat, speed: CGFloat) {
self.amplitude = amplitude
self.frequency = frequency
self.decay = decay
self.speed = speed
}
}
private func transformCoordinate(
position: CGPoint,
origin: CGPoint,
time: CGFloat,
params: RippleParams
) -> CGPoint {
// The distance of the current pixel position from `origin`.
let distance = length(position - origin)
if distance < 10.0 {
return position
}
// The amount of time it takes for the ripple to arrive at the current pixel position.
let delay = distance / params.speed
// Adjust for delay, clamp to 0.
var time = time
time -= delay
time = max(0.0, time)
// The ripple is a sine wave that Metal scales by an exponential decay
// function.
let rippleAmount = params.amplitude * sin(params.frequency * time) * exp(-params.decay * time)
// A vector of length `amplitude` that points away from position.
let n = normalize(position - origin)
// Scale `n` by the ripple amount at the current pixel position and add it
// to the current pixel position.
//
// This new position moves toward or away from `origin` based on the
// sign and magnitude of `rippleAmount`.
let newPosition = position + n * rippleAmount
return newPosition
}
private func rectToQuad(
rect: CGRect,
quadTL: CGPoint,
quadTR: CGPoint,
quadBL: CGPoint,
quadBR: CGPoint
) -> CATransform3D {
let x1a = quadTL.x
let y1a = quadTL.y
let x2a = quadTR.x
let y2a = quadTR.y
let x3a = quadBL.x
let y3a = quadBL.y
let x4a = quadBR.x
let y4a = quadBR.y
let X = rect.origin.x
let Y = rect.origin.y
let W = rect.size.width
let H = rect.size.height
let y21 = y2a - y1a
let y32 = y3a - y2a
let y43 = y4a - y3a
let y14 = y1a - y4a
let y31 = y3a - y1a
let y42 = y4a - y2a
let a = -H*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42)
let b = W*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43)
let c = H*X*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42) - H*W*x1a*(x4a*y32 - x3a*y42 + x2a*y43) - W*Y*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43)
let d = H*(-x4a*y21*y3a + x2a*y1a*y43 - x1a*y2a*y43 - x3a*y1a*y4a + x3a*y2a*y4a)
let e = W*(x4a*y2a*y31 - x3a*y1a*y42 - x2a*y31*y4a + x1a*y3a*y42)
let f = -(W*(x4a*(Y*y2a*y31 + H*y1a*y32) - x3a*(H + Y)*y1a*y42 + H*x2a*y1a*y43 + x2a*Y*(y1a - y3a)*y4a + x1a*Y*y3a*(-y2a + y4a)) - H*X*(x4a*y21*y3a - x2a*y1a*y43 + x3a*(y1a - y2a)*y4a + x1a*y2a*(-y3a + y4a)))
let g = H*(x3a*y21 - x4a*y21 + (-x1a + x2a)*y43)
let h = W*(-x2a*y31 + x4a*y31 + (x1a - x3a)*y42)
var i = W*Y*(x2a*y31 - x4a*y31 - x1a*y42 + x3a*y42) + H*(X*(-(x3a*y21) + x4a*y21 + x1a*y43 - x2a*y43) + W*(-(x3a*y2a) + x4a*y2a + x2a*y3a - x4a*y3a - x2a*y4a + x3a*y4a))
let kEpsilon = 0.0001
if fabs(i) < kEpsilon {
i = kEpsilon * (i > 0 ? 1.0 : -1.0)
}
//CATransform3D transform = {a/i, d/i, 0, g/i, b/i, e/i, 0, h/i, 0, 0, 1, 0, c/i, f/i, 0, 1.0}
let transform = CATransform3D(m11: a/i, m12: d/i, m13: 0, m14: g/i, m21: b/i, m22: e/i, m23: 0, m24: h/i, m31: 0, m32: 0, m33: 1, m34: 0, m41: c/i, m42: f/i, m43: 0, m44: 1.0)
return transform
}
open class SpaceWarpView: UIView {
private final class GridView: UIView {
let cloneView: PortalView
let gridPosition: CGPoint
init?(contentView: PortalSourceView, gridPosition: CGPoint) {
self.gridPosition = gridPosition
guard let cloneView = PortalView(matchPosition: false) else {
return nil
}
self.cloneView = cloneView
super.init(frame: CGRect())
self.layer.anchorPoint = CGPoint(x: 0.0, y: 0.0)
self.clipsToBounds = true
self.isUserInteractionEnabled = false
self.addSubview(cloneView.view)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateIsActive(contentView: PortalSourceView, isActive: Bool) {
if isActive {
contentView.addPortal(view: self.cloneView)
} else {
contentView.removePortal(view: self.cloneView)
}
}
func update(containerSize: CGSize, rect: CGRect, transition: ComponentTransition) {
transition.setFrame(view: self.cloneView.view, frame: CGRect(origin: CGPoint(x: -rect.minX - containerSize.width * 0.5, y: -rect.minY - containerSize.height * 0.5), size: CGSize(width: containerSize.width, height: containerSize.height)))
}
}
private var gridViews: [GridView] = []
public var contentView: UIView {
return self.contentViewImpl
}
let contentViewImpl: PortalSourceView
private var link: SharedDisplayLinkDriver.Link?
private var startPoint: CGPoint?
private var timeValue: CGFloat = 0.0
private var currentActiveViews: Int = 0
private var resolution: (x: Int, y: Int)?
private var size: CGSize?
override public init(frame: CGRect) {
self.contentViewImpl = PortalSourceView()
super.init(frame: frame)
self.addSubview(self.contentView)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func trigger(at point: CGPoint) {
self.startPoint = point
self.timeValue = 0.0
if self.link == nil {
self.link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in
guard let self else {
return
}
self.timeValue += deltaTime * (1.0 / CGFloat(UIView.animationDurationFactor()))
if let size = self.size {
self.update(size: size, transition: .immediate)
}
})
}
}
private func updateGrid(resolutionX: Int, resolutionY: Int) {
if let resolution = self.resolution, resolution.x == resolutionX, resolution.y == resolutionY {
return
}
self.resolution = (resolutionX, resolutionY)
for gridView in self.gridViews {
gridView.removeFromSuperview()
}
var gridViews: [GridView] = []
for y in 0 ..< resolutionY {
for x in 0 ..< resolutionX {
if let gridView = GridView(contentView: self.contentViewImpl, gridPosition: CGPoint(x: CGFloat(x) / CGFloat(resolutionX), y: CGFloat(y) / CGFloat(resolutionY))) {
gridView.isUserInteractionEnabled = false
gridViews.append(gridView)
self.addSubview(gridView)
}
}
}
self.gridViews = gridViews
}
public func update(size: CGSize, transition: ComponentTransition) {
self.size = size
if size.width <= 0.0 || size.height <= 0.0 {
return
}
self.updateGrid(resolutionX: max(2, Int(size.width / 100.0)), resolutionY: max(2, Int(size.height / 100.0)))
guard let resolution = self.resolution else {
return
}
//let pixelStep = CGPoint(x: CGFloat(resolution.x) * 0.33, y: CGFloat(resolution.y) * 0.33)
let pixelStep = CGPoint()
let itemSize = CGSize(width: size.width / CGFloat(resolution.x), height: size.height / CGFloat(resolution.y))
let params = RippleParams(amplitude: 22.0, frequency: 15.0, decay: 8.0, speed: 1400.0)
var activeViews = 0
for gridView in self.gridViews {
let sourceRect = CGRect(origin: CGPoint(x: gridView.gridPosition.x * (size.width + pixelStep.x), y: gridView.gridPosition.y * (size.height + pixelStep.y)), size: itemSize)
gridView.bounds = CGRect(origin: CGPoint(), size: sourceRect.size)
gridView.update(containerSize: size, rect: sourceRect, transition: transition)
let initialTopLeft = CGPoint(x: sourceRect.minX, y: sourceRect.minY)
let initialTopRight = CGPoint(x: sourceRect.maxX, y: sourceRect.minY)
let initialBottomLeft = CGPoint(x: sourceRect.minX, y: sourceRect.maxY)
let initialBottomRight = CGPoint(x: sourceRect.maxX, y: sourceRect.maxY)
var topLeft = initialTopLeft
var topRight = initialTopRight
var bottomLeft = initialBottomLeft
var bottomRight = initialBottomRight
if let startPoint = self.startPoint {
topLeft = transformCoordinate(position: topLeft, origin: startPoint, time: self.timeValue, params: params)
topRight = transformCoordinate(position: topRight, origin: startPoint, time: self.timeValue, params: params)
bottomLeft = transformCoordinate(position: bottomLeft, origin: startPoint, time: self.timeValue, params: params)
bottomRight = transformCoordinate(position: bottomRight, origin: startPoint, time: self.timeValue, params: params)
}
let distanceTopLeft = length(topLeft - initialTopLeft)
let distanceTopRight = length(topRight - initialTopRight)
let distanceBottomLeft = length(bottomLeft - initialBottomLeft)
let distanceBottomRight = length(bottomRight - initialBottomRight)
var maxDistance = max(distanceTopLeft, distanceTopRight)
maxDistance = max(maxDistance, distanceBottomLeft)
maxDistance = max(maxDistance, distanceBottomRight)
let isActive: Bool
if maxDistance <= 0.5 {
gridView.layer.transform = CATransform3DIdentity
isActive = false
} else {
let transform = rectToQuad(rect: CGRect(origin: CGPoint(), size: itemSize), quadTL: topLeft, quadTR: topRight, quadBL: bottomLeft, quadBR: bottomRight)
gridView.layer.transform = transform
isActive = true
activeViews += 1
}
if gridView.isHidden != !isActive {
gridView.isHidden = !isActive
gridView.updateIsActive(contentView: self.contentViewImpl, isActive: isActive)
}
}
if self.currentActiveViews != activeViews {
self.currentActiveViews = activeViews
#if DEBUG
print("SpaceWarpView: activeViews = \(activeViews)")
#endif
}
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.alpha.isZero || self.isHidden || !self.isUserInteractionEnabled {
return nil
}
for view in self.contentView.subviews.reversed() {
if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled {
return result
}
}
let result = super.hitTest(point, with: event)
if result != self {
return result
} else {
return nil
}
}
}

View File

@ -1655,9 +1655,8 @@ private final class StoryContainerScreenComponent: Component {
}
if case let .user(user) = slice.peer, user.botInfo != nil {
if let id = slice.item.storyItem.media.id {
let _ = component.context.engine.messages.deleteBotPreviews(peerId: slice.peer.id, ids: [id]).startStandalone()
}
//TODO:localize
let _ = component.context.engine.messages.deleteBotPreviews(peerId: slice.peer.id, language: nil, media: [slice.item.storyItem.media._asMedia()]).startStandalone()
} else {
let _ = component.context.engine.messages.deleteStories(peerId: slice.peer.id, ids: [slice.item.storyItem.id]).startStandalone()
}

View File

@ -22,11 +22,13 @@ public final class TabSelectorComponent: Component {
public var font: UIFont
public var spacing: CGFloat
public var lineSelection: Bool
public var verticalInset: CGFloat
public init(font: UIFont, spacing: CGFloat, lineSelection: Bool = false) {
public init(font: UIFont, spacing: CGFloat, lineSelection: Bool = false, verticalInset: CGFloat = 0.0) {
self.font = font
self.spacing = spacing
self.lineSelection = lineSelection
self.verticalInset = verticalInset
}
}
@ -92,7 +94,7 @@ public final class TabSelectorComponent: Component {
}
}
public final class View: UIView {
public final class View: UIScrollView {
private var component: TabSelectorComponent?
private weak var state: EmptyComponentState?
@ -104,6 +106,14 @@ public final class TabSelectorComponent: Component {
super.init(frame: frame)
self.showsVerticalScrollIndicator = false
self.showsHorizontalScrollIndicator = false
self.scrollsToTop = false
self.delaysContentTouches = false
self.canCancelContentTouches = true
self.contentInsetAdjustmentBehavior = .never
self.alwaysBounceVertical = false
self.addSubview(self.selectionView)
}
@ -114,6 +124,10 @@ public final class TabSelectorComponent: Component {
deinit {
}
override public func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
func update(component: TabSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let selectionColorUpdated = component.colors.selection != self.component?.colors.selection
@ -121,6 +135,12 @@ public final class TabSelectorComponent: Component {
self.state = state
let baseHeight: CGFloat = 28.0
var verticalInset: CGFloat = 0.0
if let customLayout = component.customLayout {
verticalInset = customLayout.verticalInset * 2.0
}
let innerInset: CGFloat = 12.0
let spacing: CGFloat = component.customLayout?.spacing ?? 2.0
@ -148,7 +168,7 @@ public final class TabSelectorComponent: Component {
}
}
var contentWidth: CGFloat = 0.0
var contentWidth: CGFloat = spacing
var previousBackgroundRect: CGRect?
var selectedBackgroundRect: CGRect?
var nextBackgroundRect: CGRect?
@ -213,8 +233,8 @@ public final class TabSelectorComponent: Component {
if !contentWidth.isZero {
contentWidth += spacing
}
let itemTitleFrame = CGRect(origin: CGPoint(x: contentWidth + innerInset, y: floor((baseHeight - itemSize.height) * 0.5)), size: itemSize)
let itemBackgroundRect = CGRect(origin: CGPoint(x: contentWidth, y: 0.0), size: CGSize(width: innerInset + itemSize.width + innerInset, height: baseHeight))
let itemTitleFrame = CGRect(origin: CGPoint(x: contentWidth + innerInset, y: verticalInset + floor((baseHeight - itemSize.height) * 0.5)), size: itemSize)
let itemBackgroundRect = CGRect(origin: CGPoint(x: contentWidth, y: verticalInset), size: CGSize(width: innerInset + itemSize.width + innerInset, height: baseHeight))
contentWidth = itemBackgroundRect.maxX
if item.id == component.selectedId {
@ -237,6 +257,7 @@ public final class TabSelectorComponent: Component {
}
index += 1
}
contentWidth += spacing
var removeIds: [AnyHashable] = []
for (id, itemView) in self.visibleItems {
@ -277,7 +298,14 @@ public final class TabSelectorComponent: Component {
self.selectionView.alpha = 0.0
}
return CGSize(width: contentWidth, height: baseHeight)
self.contentSize = CGSize(width: contentWidth, height: baseHeight + verticalInset * 2.0)
self.disablesInteractiveTransitionGestureRecognizer = contentWidth > availableSize.width
if let selectedBackgroundRect {
self.scrollRectToVisible(selectedBackgroundRect.insetBy(dx: -spacing, dy: 0.0), animated: false)
}
return CGSize(width: min(contentWidth, availableSize.width), height: baseHeight + verticalInset * 2.0)
}
}

View File

@ -1816,8 +1816,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let context = self?.context, let navigationController = self?.effectiveNavigationController {
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: threadId, messageId: messageId, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: false, keepStack: .always).startStandalone()
}
}, tapMessage: nil, clickThroughMessage: { [weak self] in
self?.chatDisplayNode.dismissInput()
}, tapMessage: nil, clickThroughMessage: { [weak self] view, location in
self?.chatDisplayNode.dismissInput(view: view, location: location)
}, toggleMessagesSelection: { [weak self] ids, value in
guard let strongSelf = self, strongSelf.isNodeLoaded else {
return

View File

@ -43,6 +43,7 @@ import ChatInlineSearchResultsListComponent
import ComponentDisplayAdapters
import ComponentFlow
import ChatEmptyNode
import SpaceWarpView
final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem {
let itemNode: OverlayMediaItemNode
@ -86,6 +87,41 @@ private struct ChatControllerNodeDerivedLayoutState {
var upperInputPositionBound: CGFloat?
}
class ChatNodeContainer: ASDisplayNode {
private let contentNodeImpl: ASDisplayNode
var contentNode: ASDisplayNode {
if self.view is SpaceWarpView {
return self.contentNodeImpl
} else {
return self
}
}
override init() {
self.contentNodeImpl = ASDisplayNode()
super.init()
#if DEBUG && false
self.setViewBlock({
return SpaceWarpView(frame: CGRect())
})
#endif
(self.view as? SpaceWarpView)?.contentView.addSubnode(self.contentNodeImpl)
}
func triggerRipple(at point: CGPoint) {
(self.view as? SpaceWarpView)?.trigger(at: point)
}
func update(size: CGSize, transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.contentNodeImpl, frame: CGRect(origin: CGPoint(), size: size))
(self.view as? SpaceWarpView)?.update(size: size, transition: ComponentTransition(transition))
}
}
class HistoryNodeContainer: ASDisplayNode {
var isSecret: Bool {
didSet {
@ -95,7 +131,19 @@ class HistoryNodeContainer: ASDisplayNode {
}
}
private let contentNodeImpl: ASDisplayNode
var contentNode: ASDisplayNode {
if self.view is SpaceWarpView {
return self.contentNodeImpl
} else {
return self
}
}
init(isSecret: Bool) {
self.contentNodeImpl = ASDisplayNode()
self.isSecret = isSecret
super.init()
@ -103,6 +151,23 @@ class HistoryNodeContainer: ASDisplayNode {
if self.isSecret {
setLayerDisableScreenshots(self.layer, self.isSecret)
}
#if DEBUG && false
self.setViewBlock({
return SpaceWarpView(frame: CGRect())
})
#endif
(self.view as? SpaceWarpView)?.contentView.addSubnode(self.contentNodeImpl)
}
func triggerRipple(at point: CGPoint) {
(self.view as? SpaceWarpView)?.trigger(at: point)
}
func update(size: CGSize, transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.contentNodeImpl, frame: CGRect(origin: CGPoint(), size: size))
(self.view as? SpaceWarpView)?.update(size: size, transition: ComponentTransition(transition))
}
}
@ -127,12 +192,12 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
}
let contentContainerNode: ASDisplayNode
let contentContainerNode: ChatNodeContainer
let contentDimNode: ASDisplayNode
let backgroundNode: WallpaperBackgroundNode
let historyNode: ChatHistoryListNodeImpl
var blurredHistoryNode: ASImageNode?
let historyNodeContainer: ASDisplayNode
let historyNodeContainer: HistoryNodeContainer
let loadingNode: ChatLoadingNode
private(set) var loadingPlaceholderNode: ChatLoadingPlaceholderNode?
@ -379,7 +444,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.backgroundNode = backgroundNode
self.contentContainerNode = ASDisplayNode()
self.contentContainerNode = ChatNodeContainer()
self.contentDimNode = ASDisplayNode()
self.contentDimNode.isUserInteractionEnabled = false
self.contentDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.2)
@ -633,7 +698,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.historyNodeContainer = HistoryNodeContainer(isSecret: chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat)
self.historyNodeContainer.addSubnode(self.historyNode)
self.historyNodeContainer.contentNode.addSubnode(self.historyNode)
var getContentAreaInScreenSpaceImpl: (() -> CGRect)?
var onTransitionEventImpl: ((ContainedViewLayoutTransition) -> Void)?
@ -787,11 +852,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.historyNode.enableExtractedBackgrounds = true
self.addSubnode(self.contentContainerNode)
self.contentContainerNode.addSubnode(self.backgroundNode)
self.contentContainerNode.addSubnode(self.historyNodeContainer)
self.contentContainerNode.contentNode.addSubnode(self.backgroundNode)
self.contentContainerNode.contentNode.addSubnode(self.historyNodeContainer)
if let navigationBar = self.navigationBar {
self.contentContainerNode.addSubnode(navigationBar)
self.contentContainerNode.contentNode.addSubnode(navigationBar)
}
self.inputPanelContainerNode.expansionUpdated = { [weak self] transition in
@ -817,9 +882,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.inputPanelBackgroundNode.addSubnode(self.inputPanelBottomBackgroundSeparatorNode)
self.addSubnode(self.messageTransitionNode)
self.contentContainerNode.addSubnode(self.navigateButtons)
self.contentContainerNode.contentNode.addSubnode(self.navigateButtons)
self.addSubnode(self.presentationContextMarker)
self.contentContainerNode.addSubnode(self.contentDimNode)
self.contentContainerNode.contentNode.addSubnode(self.contentDimNode)
self.navigationBar?.additionalContentNode.addSubnode(self.titleAccessoryPanelContainer)
@ -1004,9 +1069,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.emptyNode = emptyNode
if let inlineSearchResultsView = self.inlineSearchResults?.view {
self.contentContainerNode.view.insertSubview(emptyNode.view, belowSubview: inlineSearchResultsView)
self.contentContainerNode.contentNode.view.insertSubview(emptyNode.view, belowSubview: inlineSearchResultsView)
} else {
self.contentContainerNode.insertSubnode(emptyNode, aboveSubnode: self.historyNodeContainer)
self.contentContainerNode.contentNode.insertSubnode(emptyNode, aboveSubnode: self.historyNodeContainer)
}
if let (size, insets) = self.validEmptyNodeLayout {
@ -1081,12 +1146,10 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
}
if let historyNodeContainer = self.historyNodeContainer as? HistoryNodeContainer {
let isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat
if historyNodeContainer.isSecret != isSecret {
historyNodeContainer.isSecret = isSecret
setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, isSecret)
}
let isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat
if self.historyNodeContainer.isSecret != isSecret {
self.historyNodeContainer.isSecret = isSecret
setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, isSecret)
}
var previousListBottomInset: CGFloat?
@ -1097,6 +1160,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.messageTransitionNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.contentContainerNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.contentContainerNode.update(size: layout.size, transition: transition)
let isOverlay: Bool
switch self.chatPresentationInterfaceState.mode {
@ -1239,10 +1303,10 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
if let containerNode = self.containerNode {
self.containerNode = nil
containerNode.removeFromSupernode()
self.contentContainerNode.insertSubnode(self.backgroundNode, at: 0)
self.contentContainerNode.insertSubnode(self.historyNodeContainer, aboveSubnode: self.backgroundNode)
self.contentContainerNode.contentNode.insertSubnode(self.backgroundNode, at: 0)
self.contentContainerNode.contentNode.insertSubnode(self.historyNodeContainer, aboveSubnode: self.backgroundNode)
if let restrictedNode = self.restrictedNode {
self.contentContainerNode.insertSubnode(restrictedNode, aboveSubnode: self.historyNodeContainer)
self.contentContainerNode.contentNode.insertSubnode(restrictedNode, aboveSubnode: self.historyNodeContainer)
}
self.navigationBar?.isHidden = false
}
@ -1392,7 +1456,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
if self.chatImportStatusPanel != importStatusPanelNode {
dismissedImportStatusPanelNode = self.chatImportStatusPanel
self.chatImportStatusPanel = importStatusPanelNode
self.contentContainerNode.addSubnode(importStatusPanelNode)
self.contentContainerNode.contentNode.addSubnode(importStatusPanelNode)
}
importStatusPanelHeight = importStatusPanelNode.update(context: self.context, progress: CGFloat(importState.progress), presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: self.chatPresentationInterfaceState.theme, wallpaper: self.chatPresentationInterfaceState.chatWallpaper), fontSize: self.chatPresentationInterfaceState.fontSize, strings: self.chatPresentationInterfaceState.strings, dateTimeFormat: self.chatPresentationInterfaceState.dateTimeFormat, nameDisplayOrder: self.chatPresentationInterfaceState.nameDisplayOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: false)), width: layout.size.width)
@ -1732,6 +1796,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
transition.updateBounds(node: self.historyNodeContainer, bounds: contentBounds)
transition.updatePosition(node: self.historyNodeContainer, position: contentBounds.center)
self.historyNodeContainer.update(size: contentBounds.size, transition: transition)
transition.updateBounds(node: self.historyNode, bounds: CGRect(origin: CGPoint(), size: contentBounds.size))
transition.updatePosition(node: self.historyNode, position: CGPoint(x: contentBounds.size.width / 2.0, y: contentBounds.size.height / 2.0))
@ -1779,7 +1844,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
dismissedOverlayContextPanelNode = self.overlayContextPanelNode
self.overlayContextPanelNode = overlayContextPanelNode
self.contentContainerNode.addSubnode(overlayContextPanelNode)
self.contentContainerNode.contentNode.addSubnode(overlayContextPanelNode)
immediatelyLayoutOverlayContextPanelAndAnimateAppearance = true
}
} else if let overlayContextPanelNode = self.overlayContextPanelNode {
@ -1999,7 +2064,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
expandedInputDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
expandedInputDimNode.alpha = 0.0
self.expandedInputDimNode = expandedInputDimNode
self.contentContainerNode.insertSubnode(expandedInputDimNode, aboveSubnode: self.historyNodeContainer)
self.contentContainerNode.contentNode.insertSubnode(expandedInputDimNode, aboveSubnode: self.historyNodeContainer)
transition.updateAlpha(node: expandedInputDimNode, alpha: 1.0)
expandedInputDimNode.frame = exandedFrame
transition.animatePositionAdditive(node: expandedInputDimNode, offset: CGPoint(x: 0.0, y: previousInputPanelOrigin.y - inputPanelOrigin))
@ -2831,9 +2896,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.skippedShowSearchResultsAsListAnimationOnce = true
inlineSearchResultsView.layer.allowsGroupOpacity = true
if let emptyNode = self.emptyNode {
self.contentContainerNode.view.insertSubview(inlineSearchResultsView, aboveSubview: emptyNode.view)
self.contentContainerNode.contentNode.view.insertSubview(inlineSearchResultsView, aboveSubview: emptyNode.view)
} else {
self.contentContainerNode.view.insertSubview(inlineSearchResultsView, aboveSubview: self.historyNodeContainer.view)
self.contentContainerNode.contentNode.view.insertSubview(inlineSearchResultsView, aboveSubview: self.historyNodeContainer.view)
}
}
inlineSearchResultsTransition.setFrame(view: inlineSearchResultsView, frame: CGRect(origin: CGPoint(), size: layout.size))
@ -3269,15 +3334,20 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if recognizer.state == .ended {
self.dismissInput()
self.dismissInput(view: self.view, location: recognizer.location(in: self.contentContainerNode.view))
}
}
func dismissInput() {
func dismissInput(view: UIView? = nil, location: CGPoint? = nil) {
if let _ = self.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState {
return
}
if let view, let location {
self.contentContainerNode.triggerRipple(at: self.contentContainerNode.view.convert(location, from: view))
self.historyNodeContainer.triggerRipple(at: self.historyNodeContainer.view.convert(location, from: view))
}
switch self.chatPresentationInterfaceState.inputMode {
case .none:
break
@ -3739,7 +3809,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
let dropDimNode = ASDisplayNode()
dropDimNode.backgroundColor = self.chatPresentationInterfaceState.theme.chatList.backgroundColor.withAlphaComponent(0.35)
self.dropDimNode = dropDimNode
self.contentContainerNode.addSubnode(dropDimNode)
self.contentContainerNode.contentNode.addSubnode(dropDimNode)
if let (layout, _) = self.validLayout {
dropDimNode.frame = CGRect(origin: CGPoint(), size: layout.size)
dropDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)

View File

@ -84,7 +84,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}, navigateToMessage: { _, _, _ in
}, navigateToMessageStandalone: { _ in
}, navigateToThreadMessage: { _, _, _ in
}, tapMessage: nil, clickThroughMessage: {
}, tapMessage: nil, clickThroughMessage: { _, _ in
}, toggleMessagesSelection: { _, _ in
}, sendCurrentMessage: { _, _ in
}, sendMessage: { _ in

View File

@ -69,6 +69,7 @@ import StarsPurchaseScreen
import StarsTransferScreen
import StarsTransactionScreen
import StarsWithdrawalScreen
import MiniAppListScreen
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
@ -1701,7 +1702,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return presentAddMembersImpl(context: context, updatedPresentationData: updatedPresentationData, parentController: parentController, groupPeer: groupPeer, selectAddMemberDisposable: selectAddMemberDisposable, addMemberDisposable: addMemberDisposable)
}
public func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)? = nil, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool, isStandalone: Bool) -> ListViewItem {
public func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: ((UIView?, CGPoint?) -> Void)? = nil, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool, isStandalone: Bool) -> ListViewItem {
let controllerInteraction: ChatControllerInteraction
controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in
@ -1711,8 +1712,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, navigateToThreadMessage: { _, _, _ in
}, tapMessage: { message in
tapMessage?(message)
}, clickThroughMessage: {
clickThroughMessage?()
}, clickThroughMessage: { view, location in
clickThroughMessage?(view, location)
}, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in
return false
}, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
@ -2727,6 +2728,14 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return StarsTransactionScreen(context: context, subject: .gift(message), action: {})
}
public func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError> {
return MiniAppListScreen.initialData(context: context)
}
public func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController {
return MiniAppListScreen(context: context, initialData: initialData as! MiniAppListScreen.InitialData)
}
public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) {
openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, peer: peer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService)
}

View File

@ -393,7 +393,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
return nil
case let .peer(id):
return id
case let .botPreview(id):
case let .botPreview(id, _):
return id
}
}

View File

@ -37,10 +37,10 @@ public class LocalizationListItem: ListViewItem, ItemListItem {
public let sectionId: ItemListSectionId
let alwaysPlain: Bool
let action: () -> Void
let setItemWithRevealedOptions: (String?, String?) -> Void
let removeItem: (String) -> Void
let setItemWithRevealedOptions: ((String?, String?) -> Void)?
let removeItem: ((String) -> Void)?
public init(presentationData: ItemListPresentationData, id: String, title: String, subtitle: String, checked: Bool, activity: Bool, loading: Bool, editing: LocalizationListItemEditing, enabled: Bool = true, sectionId: ItemListSectionId, alwaysPlain: Bool, action: @escaping () -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) {
public init(presentationData: ItemListPresentationData, id: String, title: String, subtitle: String, checked: Bool, activity: Bool, loading: Bool, editing: LocalizationListItemEditing, enabled: Bool = true, sectionId: ItemListSectionId, alwaysPlain: Bool, action: @escaping () -> Void, setItemWithRevealedOptions: ((String?, String?) -> Void)?, removeItem: ((String) -> Void)?) {
self.presentationData = presentationData
self.id = id
self.title = title
@ -368,7 +368,7 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode {
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
if item.editing.editable {
if item.editing.editable, item.removeItem != nil {
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
} else {
strongSelf.setRevealOptions((left: [], right: []))
@ -491,13 +491,13 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode {
override func revealOptionsInteractivelyOpened() {
if let item = self.item {
item.setItemWithRevealedOptions(item.id, nil)
item.setItemWithRevealedOptions?(item.id, nil)
}
}
override func revealOptionsInteractivelyClosed() {
if let item = self.item {
item.setItemWithRevealedOptions(nil, item.id)
item.setItemWithRevealedOptions?(nil, item.id)
}
}
@ -506,7 +506,7 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode {
self.revealOptionsInteractivelyClosed()
if let item = self.item {
item.removeItem(item.id)
item.removeItem?(item.id)
}
}
}