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 { public protocol SharedAccountContext: AnyObject {
var sharedContainerPath: String { get } var sharedContainerPath: String { get }
var basePath: String { get } var basePath: String { get }
@ -999,7 +1002,7 @@ public protocol SharedAccountContext: AnyObject {
selectedMessages: Signal<Set<MessageId>?, NoError>, selectedMessages: Signal<Set<MessageId>?, NoError>,
mode: ChatHistoryListMode mode: ChatHistoryListMode
) -> ChatHistoryListNode ) -> 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 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 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? 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 makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> 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 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? func makeDebugSettingsController(context: AccountContext?) -> ViewController?

View File

@ -58,7 +58,7 @@ final class BotCheckoutWebInteractionController: ViewController {
} }
override func loadDisplayNode() { 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) { override func viewDidAppear(_ animated: Bool) {

View File

@ -4,6 +4,7 @@ import Display
import AsyncDisplayKit import AsyncDisplayKit
import WebKit import WebKit
import TelegramPresentationData import TelegramPresentationData
import AccountContext
private class WeakPaymentScriptMessageHandler: NSObject, WKScriptMessageHandler { private class WeakPaymentScriptMessageHandler: NSObject, WKScriptMessageHandler {
private let f: (WKScriptMessage) -> () private let f: (WKScriptMessage) -> ()
@ -20,12 +21,14 @@ private class WeakPaymentScriptMessageHandler: NSObject, WKScriptMessageHandler
} }
final class BotCheckoutWebInteractionControllerNode: ViewControllerTracingNode, WKNavigationDelegate { final class BotCheckoutWebInteractionControllerNode: ViewControllerTracingNode, WKNavigationDelegate {
private let context: AccountContext
private var presentationData: PresentationData private var presentationData: PresentationData
private let intent: BotCheckoutWebInteractionControllerIntent private let intent: BotCheckoutWebInteractionControllerIntent
private var webView: WKWebView? 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.presentationData = presentationData
self.intent = intent self.intent = intent
@ -146,6 +149,14 @@ final class BotCheckoutWebInteractionControllerNode: ViewControllerTracingNode,
decisionHandler(.allow) decisionHandler(.allow)
} }
} else { } 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) decisionHandler(.allow)
} }
} }

View File

@ -3543,9 +3543,19 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
} else if case .apps = key { } else if case .apps = key {
if let navigationController = self.navigationController { if let navigationController = self.navigationController {
if isRecommended { 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) { 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) navigationController.pushViewController(peerInfoScreen)
} }
#endif
} else if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.parentController { } else if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.parentController {
self.context.sharedContext.openWebApp( self.context.sharedContext.openWebApp(
context: self.context, context: self.context,
@ -3560,7 +3570,6 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
skipTermsOfService: true skipTermsOfService: true
) )
} else { } else {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
navigationController: navigationController, navigationController: navigationController,
context: self.context, 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?) { func setGlobalPortal(view: GlobalPortalView?) {
if let globalPortalView = self.globalPortalView { if let globalPortalView = self.globalPortalView {
self.globalPortalView = nil self.globalPortalView = nil

View File

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

View File

@ -551,6 +551,10 @@ public final class SparseItemGrid: ASDisplayNode {
return self.scrollView.contentOffset.y return self.scrollView.contentOffset.y
} }
var contentBottomOffset: CGFloat {
return -self.scrollView.contentOffset.y + self.scrollView.contentSize.height
}
let coveringOffsetUpdated: (Viewport, ContainedViewLayoutTransition) -> Void let coveringOffsetUpdated: (Viewport, ContainedViewLayoutTransition) -> Void
let offsetUpdated: (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 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 { var offset: CGFloat {
return self.fromViewport.offset * (1.0 - self.currentProgress) + self.toViewport.offset * self.currentProgress 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 { public var scrollingOffset: CGFloat {
if let currentViewportTransition = self.currentViewportTransition { if let currentViewportTransition = self.currentViewportTransition {
return currentViewportTransition.offset return currentViewportTransition.offset

View File

@ -105,6 +105,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-944407322] = { return Api.BotMenuButton.parse_botMenuButton($0) } dict[-944407322] = { return Api.BotMenuButton.parse_botMenuButton($0) }
dict[1113113093] = { return Api.BotMenuButton.parse_botMenuButtonCommands($0) } dict[1113113093] = { return Api.BotMenuButton.parse_botMenuButtonCommands($0) }
dict[1966318984] = { return Api.BotMenuButton.parse_botMenuButtonDefault($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[-2076642874] = { return Api.BroadcastRevenueBalances.parse_broadcastRevenueBalances($0) }
dict[1434332356] = { return Api.BroadcastRevenueTransaction.parse_broadcastRevenueTransactionProceeds($0) } dict[1434332356] = { return Api.BroadcastRevenueTransaction.parse_broadcastRevenueTransactionProceeds($0) }
dict[1121127726] = { return Api.BroadcastRevenueTransaction.parse_broadcastRevenueTransactionRefund($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[340088945] = { return Api.MediaArea.parse_mediaAreaSuggestedReaction($0) }
dict[926421125] = { return Api.MediaArea.parse_mediaAreaUrl($0) } dict[926421125] = { return Api.MediaArea.parse_mediaAreaUrl($0) }
dict[-1098720356] = { return Api.MediaArea.parse_mediaAreaVenue($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[-808853502] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) }
dict[-1808510398] = { return Api.Message.parse_message($0) } dict[-1808510398] = { return Api.Message.parse_message($0) }
dict[-1868117372] = { return Api.Message.parse_messageEmpty($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[-1542017919] = { return Api.auth.SentCodeType.parse_sentCodeTypeSmsWord($0) }
dict[-391678544] = { return Api.bots.BotInfo.parse_botInfo($0) } dict[-391678544] = { return Api.bots.BotInfo.parse_botInfo($0) }
dict[428978491] = { return Api.bots.PopularAppBots.parse_popularAppBots($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[-309659827] = { return Api.channels.AdminLogResults.parse_adminLogResults($0) }
dict[-541588713] = { return Api.channels.ChannelParticipant.parse_channelParticipant($0) } dict[-541588713] = { return Api.channels.ChannelParticipant.parse_channelParticipant($0) }
dict[-1699676497] = { return Api.channels.ChannelParticipants.parse_channelParticipants($0) } dict[-1699676497] = { return Api.channels.ChannelParticipants.parse_channelParticipants($0) }
@ -1493,6 +1495,8 @@ public extension Api {
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.BotMenuButton: case let _1 as Api.BotMenuButton:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.BotPreviewMedia:
_1.serialize(buffer, boxed)
case let _1 as Api.BroadcastRevenueBalances: case let _1 as Api.BroadcastRevenueBalances:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.BroadcastRevenueTransaction: case let _1 as Api.BroadcastRevenueTransaction:
@ -2151,6 +2155,8 @@ public extension Api {
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.bots.PopularAppBots: case let _1 as Api.bots.PopularAppBots:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.bots.PreviewInfo:
_1.serialize(buffer, boxed)
case let _1 as Api.channels.AdminLogResults: case let _1 as Api.channels.AdminLogResults:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.channels.ChannelParticipant: 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 mediaAreaSuggestedReaction(flags: Int32, coordinates: Api.MediaAreaCoordinates, reaction: Api.Reaction)
case mediaAreaUrl(coordinates: Api.MediaAreaCoordinates, url: String) 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 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) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
@ -295,14 +295,14 @@ public extension Api {
serializeString(venueId, buffer: buffer, boxed: false) serializeString(venueId, buffer: buffer, boxed: false)
serializeString(venueType, buffer: buffer, boxed: false) serializeString(venueType, buffer: buffer, boxed: false)
break break
case .mediaAreaWeather(let flags, let coordinates, let emoji, let temperatureC): case .mediaAreaWeather(let coordinates, let emoji, let temperatureC, let color):
if boxed { if boxed {
buffer.appendInt32(1132918857) buffer.appendInt32(1235637404)
} }
serializeInt32(flags, buffer: buffer, boxed: false)
coordinates.serialize(buffer, true) coordinates.serialize(buffer, true)
serializeString(emoji, buffer: buffer, boxed: false) serializeString(emoji, buffer: buffer, boxed: false)
serializeDouble(temperatureC, buffer: buffer, boxed: false) serializeDouble(temperatureC, buffer: buffer, boxed: false)
serializeInt32(color, buffer: buffer, boxed: false)
break break
} }
} }
@ -323,8 +323,8 @@ public extension Api {
return ("mediaAreaUrl", [("coordinates", coordinates as Any), ("url", url as Any)]) 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): 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)]) 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): case .mediaAreaWeather(let coordinates, let emoji, let temperatureC, let color):
return ("mediaAreaWeather", [("flags", flags as Any), ("coordinates", coordinates as Any), ("emoji", emoji as Any), ("temperatureC", temperatureC as Any)]) 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? { public static func parse_mediaAreaWeather(_ reader: BufferReader) -> MediaArea? {
var _1: Int32? var _1: Api.MediaAreaCoordinates?
_1 = reader.readInt32()
var _2: Api.MediaAreaCoordinates?
if let signature = reader.readInt32() { 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? var _2: String?
_3 = parseString(reader) _2 = parseString(reader)
var _4: Double? var _3: Double?
_4 = reader.readDouble() _3 = reader.readDouble()
var _4: Int32?
_4 = reader.readInt32()
let _c1 = _1 != nil let _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = _3 != nil
let _c4 = _4 != nil let _c4 = _4 != nil
if _c1 && _c2 && _c3 && _c4 { 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 { else {
return nil 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 { public extension Api {
enum BroadcastRevenueBalances: TypeConstructorDescription { enum BroadcastRevenueBalances: TypeConstructorDescription {
case broadcastRevenueBalances(currentBalance: Int64, availableBalance: Int64, overallRevenue: Int64) 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 { public extension Api.channels {
enum AdminLogResults: TypeConstructorDescription { enum AdminLogResults: TypeConstructorDescription {
case adminLogResults(events: [Api.ChannelAdminLogEvent], chats: [Api.Chat], users: [Api.User]) 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 { public extension Api {
enum BusinessLocation: TypeConstructorDescription { enum BusinessLocation: TypeConstructorDescription {
case businessLocation(flags: Int32, geoPoint: Api.GeoPoint?, address: String) 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 { public extension Api.help {
enum CountryCode: TypeConstructorDescription { enum CountryCode: TypeConstructorDescription {
case countryCode(flags: Int32, countryCode: String, prefixes: [String]?, patterns: [String]?) 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 { 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() let buffer = Buffer()
buffer.appendInt32(1633332331) buffer.appendInt32(397326170)
bot.serialize(buffer, true) bot.serialize(buffer, true)
serializeString(langCode, buffer: buffer, boxed: false)
media.serialize(buffer, true) 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) let reader = BufferReader(buffer)
var result: Api.MessageMedia? var result: Api.BotPreviewMedia?
if let signature = reader.readInt32() { 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 return result
}) })
@ -2263,16 +2264,17 @@ public extension Api.functions.bots {
} }
} }
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() let buffer = Buffer()
buffer.appendInt32(481471475) buffer.appendInt32(755054003)
bot.serialize(buffer, true) bot.serialize(buffer, true)
serializeString(langCode, buffer: buffer, boxed: false)
buffer.appendInt32(481674261) buffer.appendInt32(481674261)
buffer.appendInt32(Int32(media.count)) buffer.appendInt32(Int32(media.count))
for item in media { for item in media {
item.serialize(buffer, true) 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) let reader = BufferReader(buffer)
var result: Api.Bool? var result: Api.Bool?
if let signature = reader.readInt32() { if let signature = reader.readInt32() {
@ -2283,17 +2285,18 @@ public extension Api.functions.bots {
} }
} }
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() let buffer = Buffer()
buffer.appendInt32(-1436441263) buffer.appendInt32(-2061148049)
bot.serialize(buffer, true) bot.serialize(buffer, true)
serializeString(langCode, buffer: buffer, boxed: false)
media.serialize(buffer, true) media.serialize(buffer, true)
newMedia.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) let reader = BufferReader(buffer)
var result: Api.MessageMedia? var result: Api.BotPreviewMedia?
if let signature = reader.readInt32() { 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 return result
}) })
@ -2364,15 +2367,31 @@ public extension Api.functions.bots {
} }
} }
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() let buffer = Buffer()
buffer.appendInt32(1720252591) buffer.appendInt32(1111143341)
bot.serialize(buffer, true) 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) 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() { 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 return result
}) })
@ -2396,16 +2415,17 @@ public extension Api.functions.bots {
} }
} }
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() let buffer = Buffer()
buffer.appendInt32(-1472444656) buffer.appendInt32(-1238895702)
bot.serialize(buffer, true) bot.serialize(buffer, true)
serializeString(langCode, buffer: buffer, boxed: false)
buffer.appendInt32(481674261) buffer.appendInt32(481674261)
buffer.appendInt32(Int32(order.count)) buffer.appendInt32(Int32(order.count))
for item in order { for item in order {
item.serialize(buffer, true) 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) let reader = BufferReader(buffer)
var result: Api.Bool? var result: Api.Bool?
if let signature = reader.readInt32() { if let signature = reader.readInt32() {

View File

@ -522,9 +522,9 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? {
return .link(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), url: url) return .link(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), url: url)
case let .mediaAreaChannelPost(coordinates, channelId, messageId): 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)) 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() var parsedFlags = MediaArea.WeatherFlags()
if (flags & (1 << 0)) != 0 { if color != 0 {
parsedFlags.insert(.isDark) parsedFlags.insert(.isDark)
} }
return .weather(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), emoji: emoji, temperature: temperatureC, flags: parsedFlags) 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): case let .link(_, url):
apiMediaAreas.append(.mediaAreaUrl(coordinates: inputCoordinates, url: url)) apiMediaAreas.append(.mediaAreaUrl(coordinates: inputCoordinates, url: url))
case let .weather(_, emoji, temperature, flags): case let .weather(_, emoji, temperature, flags):
var apiFlags: Int32 = 0 apiMediaAreas.append(.mediaAreaWeather(coordinates: inputCoordinates, emoji: emoji, temperatureC: temperature, color: flags.contains(.isDark) ? 1 : 0))
if flags.contains(.isDark) {
apiFlags |= (1 << 0)
}
apiMediaAreas.append(.mediaAreaWeather(flags: apiFlags, coordinates: inputCoordinates, emoji: emoji, temperatureC: temperature))
} }
} }
return apiMediaAreas return apiMediaAreas

View File

@ -287,6 +287,11 @@ public final class AccountStateManager {
return self.storyUpdatesPipe.signal() 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 updatedWebpageContexts: [MediaId: UpdatedWebpageSubscriberContext] = [:]
private var updatedPeersNearbyContext = UpdatedPeersNearbySubscriberContext() private var updatedPeersNearbyContext = UpdatedPeersNearbySubscriberContext()
private var updatedRevenueBalancesContext = UpdatedRevenueBalancesSubscriberContext() 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 updateConfigRequested: (() -> Void)?
var isPremiumUpdated: (() -> Void)? var isPremiumUpdated: (() -> Void)?

View File

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

View File

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

View File

@ -1270,6 +1270,7 @@ func _internal_uploadBotPreviewImpl(
revalidationContext: MediaReferenceRevalidationContext, revalidationContext: MediaReferenceRevalidationContext,
auxiliaryMethods: AccountAuxiliaryMethods, auxiliaryMethods: AccountAuxiliaryMethods,
toPeerId: PeerId, toPeerId: PeerId,
language: String?,
stableId: Int32, stableId: Int32,
media: Media, media: Media,
mediaAreas: [MediaArea], mediaAreas: [MediaArea],
@ -1300,12 +1301,17 @@ func _internal_uploadBotPreviewImpl(
return postbox.transaction { transaction -> Signal<StoryUploadResult, NoError> in return postbox.transaction { transaction -> Signal<StoryUploadResult, NoError> in
switch content.content { switch content.content {
case let .media(inputMedia, _): 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) |> map(Optional.init)
|> `catch` { _ -> Signal<Api.MessageMedia?, NoError> in |> `catch` { _ -> Signal<Api.BotPreviewMedia?, NoError> in
return .single(nil) return .single(nil)
} }
|> mapToSignal { resultMedia -> Signal<StoryUploadResult, NoError> in |> 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 return postbox.transaction { transaction -> StoryUploadResult in
var currentState: Stories.LocalState var currentState: Stories.LocalState
if let value = transaction.getLocalStoryState()?.get(Stories.LocalState.self) { if let value = transaction.getLocalStoryState()?.get(Stories.LocalState.self) {
@ -1321,6 +1327,9 @@ func _internal_uploadBotPreviewImpl(
if let resultMediaValue = textMediaAndExpirationTimerFromApiMedia(resultMedia, toPeerId).media { if let resultMediaValue = textMediaAndExpirationTimerFromApiMedia(resultMedia, toPeerId).media {
applyMediaResourceChanges(from: originalMedia, to: resultMediaValue, postbox: postbox, force: originalMedia is TelegramMediaFile && resultMediaValue is TelegramMediaFile) 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 transaction.updatePeerCachedData(peerIds: Set([toPeerId]), update: { _, current in
guard var current = current as? CachedUserData else { guard var current = current as? CachedUserData else {
return current return current
@ -1328,20 +1337,25 @@ func _internal_uploadBotPreviewImpl(
guard let currentBotPreview = current.botPreview else { guard let currentBotPreview = current.botPreview else {
return current return current
} }
var media = currentBotPreview.media var items = currentBotPreview.items
if let index = media.firstIndex(where: { $0.id == resultMediaValue.id }) { if let index = items.firstIndex(where: { $0.media.id == resultMediaValue.id }) {
media.remove(at: index) items.remove(at: index)
} }
media.insert(resultMediaValue, at: 0) items.insert(addedItem, at: 0)
let botPreview = CachedUserData.BotPreview(media: media) let botPreview = CachedUserData.BotPreview(items: items, alternativeLanguageCodes: currentBotPreview.alternativeLanguageCodes)
current = current.withUpdatedBotPreview(botPreview) current = current.withUpdatedBotPreview(botPreview)
return current return current
}) })
} }
stateManager.injectBotPreviewUpdates(updates: [
.added(peerId: toPeerId, language: language, item: addedItem)
])
}
return .completed(nil) return .completed(nil)
} }
} }
}
default: default:
return .complete() return .complete()
} }
@ -1354,27 +1368,14 @@ 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 return account.postbox.transaction { transaction -> (Api.InputUser?, [Api.InputMedia]) in
guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputUser) else { guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputUser) else {
return (nil, []) return (nil, [])
} }
var inputMedia: [Api.InputMedia] = [] var inputMedia: [Api.InputMedia] = []
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 media = currentBotPreview.media
for item in 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 { 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(.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)) inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
@ -1382,15 +1383,76 @@ func _internal_deleteBotPreviews(account: Account, peerId: PeerId, ids: [MediaId
inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil)) 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
media = media.filter({ item in items = items.filter({ item in
guard let id = item.id else { guard let id = item.media.id else {
return false return false
} }
return !ids.contains(id) return !media.contains(where: { $0.id == id })
}) })
let botPreview = CachedUserData.BotPreview(media: media) 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
}
guard let currentBotPreview = current.botPreview else {
return current
}
var alternativeLanguageCodes = currentBotPreview.alternativeLanguageCodes
alternativeLanguageCodes = alternativeLanguageCodes.filter { item in
return item != language
}
let botPreview = CachedUserData.BotPreview(items: currentBotPreview.items, alternativeLanguageCodes: alternativeLanguageCodes)
current = current.withUpdatedBotPreview(botPreview) current = current.withUpdatedBotPreview(botPreview)
return current return current
}) })
@ -1402,7 +1464,11 @@ func _internal_deleteBotPreviews(account: Account, peerId: PeerId, ids: [MediaId
return .complete() 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 |> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse) return .single(.boolFalse)
} }
@ -1623,8 +1689,8 @@ func _internal_checkStoriesUploadAvailability(account: Account, target: Stories.
return .inputPeerSelf return .inputPeerSelf
case let .peer(peerId): case let .peer(peerId):
return transaction.getPeer(peerId).flatMap(apiInputPeer) return transaction.getPeer(peerId).flatMap(apiInputPeer)
case let .botPreview(peerId): case .botPreview:
return transaction.getPeer(peerId).flatMap(apiInputPeer) return nil
} }
} }
|> mapToSignal { inputPeer -> Signal<StoriesUploadAvailability, NoError> in |> mapToSignal { inputPeer -> Signal<StoriesUploadAvailability, NoError> in

View File

@ -12,6 +12,11 @@ enum InternalStoryUpdate {
case updateMyReaction(peerId: PeerId, id: Int32, reaction: MessageReaction.Reaction?) 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 EngineStoryItem: Equatable {
public final class Views: Equatable { public final class Views: Equatable {
public let seenCount: Int 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 peerReference: PeerReference?
public var items: [Item] public var items: [Item]
public var availableLanguages: [Language]
public var pinnedIds: [Int32] public var pinnedIds: [Int32]
public var totalCount: Int public var totalCount: Int
public var loadMoreToken: AnyHashable? public var loadMoreToken: AnyHashable?
@ -575,6 +591,7 @@ public struct StoryListContextState: Equatable {
public init( public init(
peerReference: PeerReference?, peerReference: PeerReference?,
items: [Item], items: [Item],
availableLanguages: [Language],
pinnedIds: [Int32], pinnedIds: [Int32],
totalCount: Int, totalCount: Int,
loadMoreToken: AnyHashable?, loadMoreToken: AnyHashable?,
@ -585,6 +602,7 @@ public struct StoryListContextState: Equatable {
) { ) {
self.peerReference = peerReference self.peerReference = peerReference
self.items = items self.items = items
self.availableLanguages = availableLanguages
self.pinnedIds = pinnedIds self.pinnedIds = pinnedIds
self.totalCount = totalCount self.totalCount = totalCount
self.loadMoreToken = loadMoreToken self.loadMoreToken = loadMoreToken
@ -633,7 +651,7 @@ public final class PeerStoryListContext: StoryListContext {
self.peerId = peerId self.peerId = peerId
self.isArchived = isArchived 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 _ = (account.postbox.transaction { transaction -> (PeerReference?, [State.Item], [Int32], Int, [MediaId: TelegramMediaFile], Bool) in
let key = ValueBoxKey(length: 8 + 1) let key = ValueBoxKey(length: 8 + 1)
@ -723,7 +741,7 @@ public final class PeerStoryListContext: StoryListContext {
return 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 updatedState.items.sort(by: { lhs, rhs in
let lhsPinned = updatedState.pinnedIds.firstIndex(of: lhs.storyItem.id) let lhsPinned = updatedState.pinnedIds.firstIndex(of: lhs.storyItem.id)
let rhsPinned = updatedState.pinnedIds.firstIndex(of: rhs.storyItem.id) let rhsPinned = updatedState.pinnedIds.firstIndex(of: rhs.storyItem.id)
@ -746,6 +764,7 @@ public final class PeerStoryListContext: StoryListContext {
deinit { deinit {
self.requestDisposable?.dispose() self.requestDisposable?.dispose()
self.updatesDisposable?.dispose()
} }
func loadMore(completion: (() -> Void)?) { func loadMore(completion: (() -> Void)?) {
@ -1313,7 +1332,7 @@ public final class SearchStoryListContext: StoryListContext {
self.account = account self.account = account
self.source = source 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.statePromise.set(.single(self.stateValue))
self.loadMore(completion: nil) self.loadMore(completion: nil)
@ -2078,6 +2097,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
private let account: Account private let account: Account
private let engine: TelegramEngine private let engine: TelegramEngine
private let peerId: EnginePeer.Id private let peerId: EnginePeer.Id
private let language: String?
private let isArchived: Bool private let isArchived: Bool
private let statePromise = Promise<State>() private let statePromise = Promise<State>()
@ -2093,6 +2113,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
private var isLoadingMore: Bool = false private var isLoadingMore: Bool = false
private var requestDisposable: Disposable? private var requestDisposable: Disposable?
private var updatesDisposable: Disposable? private var updatesDisposable: Disposable?
private var eventsDisposable: Disposable?
private let reorderDisposable = MetaDisposable() private let reorderDisposable = MetaDisposable()
private var completionCallbacksByToken: [AnyHashable: [() -> Void]] = [:] private var completionCallbacksByToken: [AnyHashable: [() -> Void]] = [:]
@ -2102,35 +2123,68 @@ public final class BotPreviewStoryListContext: StoryListContext {
private var idMapping: [MediaId: Int32] = [:] private var idMapping: [MediaId: Int32] = [:]
private var reverseIdMapping: [Int32: MediaId] = [:] 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.queue = queue
self.account = account self.account = account
self.engine = engine self.engine = engine
self.peerId = peerId self.peerId = peerId
self.language = language
let isArchived = false let isArchived = false
self.isArchived = isArchived 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) let localStateKey: PostboxViewKey = .storiesState(key: .local)
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, self.requestDisposable = (combineLatest(queue: queue,
engine.data.subscribe( engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
TelegramEngine.EngineData.Item.Peer.BotPreview(id: peerId) TelegramEngine.EngineData.Item.Peer.BotPreview(id: peerId),
TelegramEngine.EngineData.Item.Configuration.LocalizationList()
), ),
account.postbox.combinedView(keys: [localStateKey]) account.postbox.combinedView(keys: [
localStateKey
])
) )
|> deliverOn(self.queue)).start(next: { [weak self] peerAndBotPreview, combinedView in |> deliverOn(self.queue)).start(next: { [weak self] peerAndBotPreview, combinedView in
guard let self else { guard let self else {
return return
} }
let (peer, botPreview) = peerAndBotPreview let (peer, botPreview, localizationList) = peerAndBotPreview
var items: [State.Item] = [] var items: [State.Item] = []
var availableLanguages: [StoryListContextState.Language] = []
if let stateView = combinedView.views[localStateKey] as? StoryStatesView, let localState = stateView.value?.get(Stories.LocalState.self) { if let stateView = combinedView.views[localStateKey] as? StoryStatesView, let localState = stateView.value?.get(Stories.LocalState.self) {
for item in localState.items.reversed() { for item in localState.items.reversed() {
@ -2142,7 +2196,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
self.nextId += 1 self.nextId += 1
self.pendingIdMapping[item.stableId] = mappedId self.pendingIdMapping[item.stableId] = mappedId
} }
if case .botPreview(peerId) = item.target { if case let .botPreview(itemPeerId, itemLanguage) = item.target, itemPeerId == peerId, itemLanguage == language {
items.append(State.Item( items.append(State.Item(
id: StoryId(peerId: peerId, id: mappedId), id: StoryId(peerId: peerId, id: mappedId),
storyItem: EngineStoryItem( storyItem: EngineStoryItem(
@ -2177,8 +2231,8 @@ public final class BotPreviewStoryListContext: StoryListContext {
} }
if let botPreview { if let botPreview {
for media in botPreview.media { for item in botPreview.items {
guard let mediaId = media.id else { guard let mediaId = item.media.id else {
continue continue
} }
@ -2196,9 +2250,120 @@ public final class BotPreviewStoryListContext: StoryListContext {
id: StoryId(peerId: peerId, id: id), id: StoryId(peerId: peerId, id: id),
storyItem: EngineStoryItem( storyItem: EngineStoryItem(
id: id, id: id,
timestamp: 0, timestamp: item.timestamp,
expirationTimestamp: Int32.max, expirationTimestamp: Int32.max,
media: EngineMedia(media), 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, alternativeMedia: nil,
mediaAreas: [], mediaAreas: [],
text: "", text: "",
@ -2224,9 +2389,11 @@ public final class BotPreviewStoryListContext: StoryListContext {
} }
} }
self.remoteItems = items
self.stateValue = State( self.stateValue = State(
peerReference: (peer?._asPeer()).flatMap(PeerReference.init), peerReference: PeerReference(peer),
items: items, items: items,
availableLanguages: [],
pinnedIds: [], pinnedIds: [],
totalCount: items.count, totalCount: items.count,
loadMoreToken: nil, loadMoreToken: nil,
@ -2235,58 +2402,179 @@ public final class BotPreviewStoryListContext: StoryListContext {
allEntityFiles: [:], allEntityFiles: [:],
isLoading: botPreview == nil isLoading: botPreview == nil
) )
if botPreview != nil {
self.beginUpdates(language: language)
}
}) })
} }
deinit { private func beginUpdates(language: String) {
self.requestDisposable?.dispose() let localStateKey: PostboxViewKey = .storiesState(key: .local)
self.updatesDisposable?.dispose() self.updatesDisposable?.dispose()
self.reorderDisposable.dispose() self.updatesDisposable = (self.account.postbox.combinedView(keys: [
localStateKey
])
|> deliverOn(self.queue)).startStrict(next: { [weak self] combinedView in
guard let self else {
return
} }
func loadMore(completion: (() -> Void)?) { 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
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 == self.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
))
}
}
} }
func reorderItems(ids: [StoryId]) { if self.localItems != items {
self.localItems = items
if self.stateValue.peerReference != nil {
self.pushLanguageItems()
}
}
})
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()
}
})
}
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 reorderItems(media: [Media]) {
let peerId = self.peerId let peerId = self.peerId
let idMapping = self.idMapping let language = self.language
let reverseIdMapping = self.reverseIdMapping
let _ = (self.account.postbox.transaction({ transaction -> (Api.InputUser?, [Api.InputMedia]) in let _ = (self.account.postbox.transaction({ transaction -> (Api.InputUser?, [Api.InputMedia]) in
let inputUser = transaction.getPeer(peerId).flatMap(apiInputUser) let inputUser = transaction.getPeer(peerId).flatMap(apiInputUser)
var inputMedia: [Api.InputMedia] = [] var inputMedia: [Api.InputMedia] = []
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 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 { for item in media {
if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource { 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(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
@ -2296,10 +2584,42 @@ public final class BotPreviewStoryListContext: StoryListContext {
} }
} }
let botPreview = CachedUserData.BotPreview(media: media) 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) current = current.withUpdatedBotPreview(botPreview)
return current return current
}) })
}
return (inputUser, inputMedia) return (inputUser, inputMedia)
}) })
@ -2307,7 +2627,37 @@ public final class BotPreviewStoryListContext: StoryListContext {
guard let self, let inputUser else { guard let self, let inputUser else {
return 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()) self.reorderDisposable.set(signal.startStrict())
}) })
} }
@ -2322,11 +2672,15 @@ public final class BotPreviewStoryListContext: StoryListContext {
private let queue: Queue private let queue: Queue
private let impl: QueueLocalObject<Impl> 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() let queue = Queue.mainQueue()
self.queue = queue self.queue = queue
self.impl = QueueLocalObject(queue: queue, generate: { 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 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) 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> { public func deleteBotPreviews(peerId: EnginePeer.Id, language: String?, media: [Media]) -> Signal<Never, NoError> {
return _internal_deleteBotPreviews(account: self.account, peerId: peerId, ids: ids) 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] { 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> let botPreview: Signal<CachedUserData.BotPreview?, NoError>
if let user = maybePeer as? TelegramUser, let _ = user.botInfo { if let user = maybePeer as? TelegramUser, let _ = user.botInfo {
botPreview = network.request(Api.functions.bots.getPreviewMedias(bot: inputUser)) botPreview = _internal_requestBotPreview(network: network, peerId: user.id, inputUser: inputUser, language: nil)
|> `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
})
}
} else { } else {
botPreview = .single(nil) 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/Chat/ChatSendStarsScreen",
"//submodules/TelegramUI/Components/MinimizedContainer", "//submodules/TelegramUI/Components/MinimizedContainer",
"//submodules/TelegramUI/Components/SpaceWarpView", "//submodules/TelegramUI/Components/SpaceWarpView",
"//submodules/TelegramUI/Components/MiniAppListScreen",
] + select({ ] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [], "//build-system:ios_sim_arm64": [],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -306,7 +306,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
if let context = self?.context, let navigationController = self?.getNavigationController() { 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() 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 }, requestMessageActionCallback: { [weak self] messageId, _, _, _ in
guard let self else { guard let self else {
return return

View File

@ -418,7 +418,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
}, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in
}, navigateToThreadMessage: { _, _, _ in }, navigateToThreadMessage: { _, _, _ in
}, tapMessage: { _ in }, tapMessage: { _ in
}, clickThroughMessage: { }, clickThroughMessage: { _, _ in
}, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in
return false 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 }, 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 navigateToMessageStandalone: (MessageId) -> Void
public let navigateToThreadMessage: (PeerId, Int64, MessageId?) -> Void public let navigateToThreadMessage: (PeerId, Int64, MessageId?) -> Void
public let tapMessage: ((Message) -> Void)? public let tapMessage: ((Message) -> Void)?
public let clickThroughMessage: () -> Void public let clickThroughMessage: (UIView?, CGPoint?) -> Void
public let toggleMessagesSelection: ([MessageId], Bool) -> Void public let toggleMessagesSelection: ([MessageId], Bool) -> Void
public let sendCurrentMessage: (Bool, ChatSendMessageEffect?) -> Void public let sendCurrentMessage: (Bool, ChatSendMessageEffect?) -> Void
public let sendMessage: (String) -> Void public let sendMessage: (String) -> Void
@ -309,7 +309,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
navigateToMessageStandalone: @escaping (MessageId) -> Void, navigateToMessageStandalone: @escaping (MessageId) -> Void,
navigateToThreadMessage: @escaping (PeerId, Int64, MessageId?) -> Void, navigateToThreadMessage: @escaping (PeerId, Int64, MessageId?) -> Void,
tapMessage: ((Message) -> Void)?, tapMessage: ((Message) -> Void)?,
clickThroughMessage: @escaping () -> Void, clickThroughMessage: @escaping (UIView?, CGPoint?) -> Void,
toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void,
sendCurrentMessage: @escaping (Bool, ChatSendMessageEffect?) -> Void, sendCurrentMessage: @escaping (Bool, ChatSendMessageEffect?) -> Void,
sendMessage: @escaping (String) -> Void, sendMessage: @escaping (String) -> Void,

View File

@ -13,25 +13,27 @@ public final class EmptyStateIndicatorComponent: Component {
public let context: AccountContext public let context: AccountContext
public let theme: PresentationTheme public let theme: PresentationTheme
public let animationName: String? public let animationName: String?
public let title: String public let title: String?
public let text: String public let text: String
public let actionTitle: String? public let actionTitle: String?
public let fitToHeight: Bool public let fitToHeight: Bool
public let action: () -> Void public let action: () -> Void
public let additionalActionTitle: String? public let additionalActionTitle: String?
public let additionalAction: () -> Void public let additionalAction: () -> Void
public let additionalActionSeparator: String?
public init( public init(
context: AccountContext, context: AccountContext,
theme: PresentationTheme, theme: PresentationTheme,
fitToHeight: Bool, fitToHeight: Bool,
animationName: String?, animationName: String?,
title: String, title: String?,
text: String, text: String,
actionTitle: String?, actionTitle: String?,
action: @escaping () -> Void, action: @escaping () -> Void,
additionalActionTitle: String?, additionalActionTitle: String?,
additionalAction: @escaping () -> Void additionalAction: @escaping () -> Void,
additionalActionSeparator: String? = nil
) { ) {
self.context = context self.context = context
self.theme = theme self.theme = theme
@ -43,6 +45,7 @@ public final class EmptyStateIndicatorComponent: Component {
self.action = action self.action = action
self.additionalActionTitle = additionalActionTitle self.additionalActionTitle = additionalActionTitle
self.additionalAction = additionalAction self.additionalAction = additionalAction
self.additionalActionSeparator = additionalActionSeparator
} }
public static func ==(lhs: EmptyStateIndicatorComponent, rhs: EmptyStateIndicatorComponent) -> Bool { public static func ==(lhs: EmptyStateIndicatorComponent, rhs: EmptyStateIndicatorComponent) -> Bool {
@ -70,6 +73,9 @@ public final class EmptyStateIndicatorComponent: Component {
if lhs.additionalActionTitle != rhs.additionalActionTitle { if lhs.additionalActionTitle != rhs.additionalActionTitle {
return false return false
} }
if lhs.additionalActionSeparator != rhs.additionalActionSeparator {
return false
}
return true return true
} }
@ -82,6 +88,9 @@ public final class EmptyStateIndicatorComponent: Component {
private let text = ComponentView<Empty>() private let text = ComponentView<Empty>()
private var button: ComponentView<Empty>? private var button: ComponentView<Empty>?
private var additionalButton: 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) { override public init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
@ -108,16 +117,20 @@ public final class EmptyStateIndicatorComponent: Component {
containerSize: CGSize(width: 120.0, height: 120.0) containerSize: CGSize(width: 120.0, height: 120.0)
) )
} }
let titleSize = self.title.update(
var titleSize: CGSize?
if let title = component.title {
titleSize = self.title.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(MultilineTextComponent( component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)), text: .plain(NSAttributedString(string: title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)),
horizontalAlignment: .center, horizontalAlignment: .center,
maximumNumberOfLines: 0 maximumNumberOfLines: 0
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0) containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0)
) )
}
let textSize = self.text.update( let textSize = self.text.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(BalancedTextComponent( 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 animationSpacing: CGFloat = 11.0
let titleSpacing: CGFloat = 17.0 let titleSpacing: CGFloat = 17.0
let buttonSpacing: CGFloat = 21.0 let buttonSpacing: CGFloat = 21.0
let additionalSeparatorHeight: CGFloat = 31.0
var totalHeight: CGFloat = 0.0 var totalHeight: CGFloat = 0.0
if let animationSize { if let animationSize {
totalHeight += animationSize.height + animationSpacing totalHeight += animationSize.height + animationSpacing
} }
totalHeight += titleSize.height + titleSpacing + textSize.height if let titleSize {
totalHeight += titleSize.height + titleSpacing
}
totalHeight += textSize.height
if let buttonSize { if let buttonSize {
totalHeight += buttonSpacing + buttonSize.height totalHeight += buttonSpacing + buttonSize.height
} }
if let _ = additionalSeparatorTextSize {
totalHeight += additionalSeparatorHeight
}
if let additionalButtonSize { if let additionalButtonSize {
totalHeight += buttonSpacing + additionalButtonSize.height 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)) transition.setFrame(view: animationView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - animationSize.width) * 0.5), y: contentY), size: animationSize))
contentY += animationSize.height + animationSpacing contentY += animationSize.height + animationSpacing
} }
if let titleView = self.title.view { if let titleSize, let titleView = self.title.view {
if titleView.superview == nil { if titleView.superview == nil {
self.addSubview(titleView) 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)) transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) * 0.5), y: contentY), size: buttonSize))
contentY += buttonSize.height + buttonSpacing 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 let additionalButtonSize, let additionalButtonView = self.additionalButton?.view {
if additionalButtonView.superview == nil { if additionalButtonView.superview == nil {
self.addSubview(additionalButtonView) 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? var botPreviewStoryListContext: StoryListContext?
let hasBotPreviewItems: Signal<Bool, NoError> let hasBotPreviewItems: Signal<Bool, NoError>
if case .bot = kind { 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 botPreviewStoryListContext = botPreviewStoryListContextValue
hasBotPreviewItems = botPreviewStoryListContextValue.state hasBotPreviewItems = botPreviewStoryListContextValue.state
|> map { state in |> 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) { 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) 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) availablePanes?.insert(.botPreview, at: 0)
} }
} }

View File

@ -3287,7 +3287,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, navigateToMessage: { _, _, _ in }, navigateToMessage: { _, _, _ in
}, navigateToMessageStandalone: { _ in }, navigateToMessageStandalone: { _ in
}, navigateToThreadMessage: { _, _, _ in }, navigateToThreadMessage: { _, _, _ in
}, tapMessage: nil, clickThroughMessage: { }, tapMessage: nil, clickThroughMessage: { _, _ in
}, toggleMessagesSelection: { [weak self] ids, value in }, toggleMessagesSelection: { [weak self] ids, value in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
@ -9864,7 +9864,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
guard let self else { guard let self else {
return 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: {}, dismissed: {},
groupsPresented: {} 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) 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 contextController.passthroughTouchEvent = { [weak self] sourceView, point in
guard let strongSelf = self else { guard let strongSelf = self else {

View File

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

View File

@ -41,6 +41,8 @@ import Geocoding
import ItemListUI import ItemListUI
import MultilineTextComponent import MultilineTextComponent
import LocationUI import LocationUI
import TabSelectorComponent
import LanguageSelectionScreen
private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6) private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6)
private let mediaBadgeTextColor = UIColor.white private let mediaBadgeTextColor = UIColor.white
@ -1565,6 +1567,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
private var mapInfoNode: LocationInfoListItemNode? private var mapInfoNode: LocationInfoListItemNode?
private var searchHeader: ComponentView<Empty>? private var searchHeader: ComponentView<Empty>?
private var botPreviewLanguageTab: ComponentView<Empty>?
private var botPreviewFooter: ComponentView<Empty>?
private var barBackgroundLayer: SimpleLayer? private var barBackgroundLayer: SimpleLayer?
private let itemGrid: SparseItemGrid private let itemGrid: SparseItemGrid
@ -1638,14 +1643,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
public var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? public var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
public var tabBarOffset: CGFloat { public var tabBarOffset: CGFloat {
if case .botPreview = self.scope {
return 0.0
} else {
return self.itemGrid.coveringInsetOffset return self.itemGrid.coveringInsetOffset
} }
}
private var currentListState: StoryListContext.State? private var currentListState: StoryListContext.State?
private var listDisposable: Disposable? private var listDisposable: Disposable?
private var hiddenMediaDisposable: Disposable? private var hiddenMediaDisposable: Disposable?
private let updateDisposable = MetaDisposable() private let updateDisposable = MetaDisposable()
private var currentBotPreviewLanguages: [StoryListContext.State.Language] = []
private var removedBotPreviewLanguages = Set<String>()
private var numberOfItemsToRequest: Int = 50 private var numberOfItemsToRequest: Int = 50
private var isRequestingView: Bool = false private var isRequestingView: Bool = false
private var isFirstHistoryView: Bool = true private var isFirstHistoryView: Bool = true
@ -1657,6 +1669,24 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
public private(set) var calendarSource: SparseMessageCalendar? public private(set) var calendarSource: SparseMessageCalendar?
private var listSource: StoryListContext 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 openCurrentDate: (() -> Void)?
public var paneDidScroll: (() -> Void)? public var paneDidScroll: (() -> Void)?
public var emptyAction: (() -> Void)? public var emptyAction: (() -> Void)?
@ -1723,11 +1753,18 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
case let .location(coordinates, venue): case let .location(coordinates, venue):
self.listSource = SearchStoryListContext(account: context.account, source: .mediaArea(.venue(coordinates: coordinates, venue: venue))) self.listSource = SearchStoryListContext(account: context.account, source: .mediaArea(.venue(coordinates: coordinates, venue: venue)))
case let .botPreview(id): 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 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() super.init()
if case .peer = self.scope { if case .peer = self.scope {
@ -2690,6 +2727,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
self.listDisposable?.dispose() self.listDisposable?.dispose()
self.listDisposable = nil self.listDisposable = nil
if reloadAtTop {
self.didUpdateItemsOnce = false
}
self.listDisposable = (state self.listDisposable = (state
|> deliverOn(queue)).startStrict(next: { [weak self] state in |> deliverOn(queue)).startStrict(next: { [weak self] state in
guard let self else { guard let self else {
@ -2700,7 +2741,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
if state.totalCount == 0 { if state.totalCount == 0 {
if case .botPreview = self.scope { if case .botPreview = self.scope {
//TODO:localize //TODO:localize
if state.isLoading {
title = "loading"
} else {
title = "no preview added" title = "no preview added"
}
} else { } else {
title = "" title = ""
} }
@ -2738,9 +2783,24 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
return 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.currentListState = state
self.updateItemsFromState(state: state, firstTime: firstTime, reloadAtTop: reloadAtTop, synchronous: synchronous, animated: false) self.updateItemsFromState(state: state, firstTime: firstTime, reloadAtTop: reloadAtTop, synchronous: synchronous, animated: false)
if self.currentBotPreviewLanguages != botPreviewLanguages || reloadAtTop {
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 firstTime = false
self.isRequestingView = 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 { if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams {
var gridSnapshot: UIView? var gridSnapshot: UIView?
if reloadAtTop { if case .botPreview = scope {
} else if reloadAtTop {
gridSnapshot = self.itemGrid.view.snapshotView(afterScreenUpdates: false) 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) 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?))? { public func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
return nil 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? { public func extractPendingStoryTransitionView() -> UIView? {
@ -3294,29 +3318,29 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
controller?.dismissAnimated() controller?.dismissAnimated()
} }
var mappedItemIds: [MediaId] = [] var mappedMedia: [Media] = []
if let items = self.items { 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 { guard let item = item as? VisualMediaItem else {
return nil return nil
} }
if ids.contains(item.story.id) { if ids.contains(item.story.id) {
return item.story.media.id return item.story.media._asMedia()
} else { } else {
return nil return nil
} }
} }
} }
if mappedItemIds.isEmpty { if mappedMedia.isEmpty {
return return
} }
//TODO:localize //TODO:localize
let title: String let title: String
if mappedItemIds.count == 1 { if mappedMedia.count == 1 {
title = "Delete 1 Preview?" title = "Delete 1 Preview?"
} else { } else {
title = "Delete \(mappedItemIds.count) Previews?" title = "Delete \(mappedMedia.count) Previews?"
} }
controller.setItemGroups([ controller.setItemGroups([
@ -3328,12 +3352,15 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
guard let self else { guard let self else {
return return
} }
guard let listSource = self.listSource as? BotPreviewStoryListContext else {
return
}
if let parentController = self.parentController as? PeerInfoScreen { if let parentController = self.parentController as? PeerInfoScreen {
parentController.cancelItemSelection() 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() })]) ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
@ -3349,9 +3376,15 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
} }
private func gridScrollingOffsetUpdated(transition: ContainedViewLayoutTransition) { private func gridScrollingOffsetUpdated(transition: ContainedViewLayoutTransition) {
if let _ = self.mapNode, let currentParams = self.currentParams { 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) 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)
}
}
} }
private var effectiveMapHeight: CGFloat = 0.0 private var effectiveMapHeight: CGFloat = 0.0
@ -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) { 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) 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))) transition.updateFrame(layer: barBackgroundLayer, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: gridTopInset)))
} }
let defaultBottomInset = bottomInset
var bottomInset = 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 { if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories, case let .peer(peerId, _, isArchived) = self.scope {
let selectionPanel: ComponentView<Empty> let selectionPanel: ComponentView<Empty>
var selectionPanelTransition = ComponentTransition(transition) var selectionPanelTransition = ComponentTransition(transition)
@ -3816,6 +4005,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
self.emptyStateView = emptyStateView self.emptyStateView = emptyStateView
} }
//TODO:localize //TODO:localize
var isMainLanguage = true
if let listSource = self.listSource as? BotPreviewStoryListContext, let _ = listSource.language {
isMainLanguage = false
}
let emptyStateSize = emptyStateView.update( let emptyStateSize = emptyStateView.update(
transition: emptyStateTransition, transition: emptyStateTransition,
component: AnyComponent(EmptyStateIndicatorComponent( component: AnyComponent(EmptyStateIndicatorComponent(
@ -3824,7 +4019,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
fitToHeight: self.isProfileEmbedded, fitToHeight: self.isProfileEmbedded,
animationName: nil, animationName: nil,
title: "No Preview", 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, actionTitle: self.canManageStories ? "Add Preview" : nil,
action: { [weak self] in action: { [weak self] in
guard let self else { guard let self else {
@ -3832,8 +4027,15 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
} }
self.emptyAction?() self.emptyAction?()
}, },
additionalActionTitle: nil, additionalActionTitle: self.canManageStories ? (isMainLanguage ? "Create a Translation" : "Delete this Translation") : nil,
additionalAction: {} additionalAction: {
if isMainLanguage {
self.presentAddBotPreviewLanguage()
} else {
self.presentDeleteBotPreviewLanguage()
}
},
additionalActionSeparator: self.canManageStories ? "or" : nil
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: size.width, height: size.height - gridTopInset - bottomInset) 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 { if self.isProfileEmbedded, case .botPreview = self.scope {
backgroundColor = presentationData.theme.list.blocksBackgroundColor backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else if self.isProfileEmbedded { } else if self.isProfileEmbedded {
backgroundColor = presentationData.theme.list.plainBackgroundColor backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else { } else {
backgroundColor = presentationData.theme.list.blocksBackgroundColor backgroundColor = presentationData.theme.list.blocksBackgroundColor
} }
@ -3876,20 +4078,30 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
self.emptyStateView = nil self.emptyStateView = nil
if let emptyStateComponentView = emptyStateView.view { if let emptyStateComponentView = emptyStateView.view {
if self.didUpdateItemsOnce {
subTransition.setAlpha(view: emptyStateComponentView, alpha: 0.0, completion: { [weak emptyStateComponentView] _ in subTransition.setAlpha(view: emptyStateComponentView, alpha: 0.0, completion: { [weak emptyStateComponentView] _ in
emptyStateComponentView?.removeFromSuperview() 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) subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.plainBackgroundColor)
} else { } else {
subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor) subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor)
} }
} else {
if self.isProfileEmbedded, case .botPreview = self.scope {
self.view.backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else { } else {
self.view.backgroundColor = .clear self.view.backgroundColor = .clear
} }
} }
}
transition.updateFrame(node: self.itemGrid, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) transition.updateFrame(node: self.itemGrid, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
if let items = self.items { if let items = self.items {
@ -3904,6 +4116,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
self.itemGrid.pinchEnabled = items.count > 2 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) 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? { public func currentTopTimestamp() -> Int32? {
@ -3992,12 +4208,109 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
return false return false
} }
var maxCount = 10 return items.count < self.maxBotPreviewCount
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["bot_preview_medias_max"] as? Double {
maxCount = Int(value)
} }
return items.count < maxCount 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)
}
}
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() { public func beginReordering() {
@ -4027,7 +4340,17 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
if !isReordering, let reorderedIds = self.reorderedIds { if !isReordering, let reorderedIds = self.reorderedIds {
self.reorderedIds = nil self.reorderedIds = nil
if case .botPreview = self.scope, let listSource = self.listSource as? BotPreviewStoryListContext { 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 { } else if case let .peer(id, _, _) = self.scope, id == self.context.account.peerId, let items = self.items {
var updatedPinnedIds: [Int32] = [] var updatedPinnedIds: [Int32] = []
for id in reorderedIds { 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 state.displayPatternPanel = false
return state return state
}, animated: true) }, animated: true)
}, clickThroughMessage: { }, clickThroughMessage: { _, _ in
}, backgroundNode: self.backgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false) }, backgroundNode: self.backgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)
return item return item
} }

View File

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

View File

@ -2,7 +2,429 @@ import Foundation
import UIKit import UIKit
import Display import Display
import AsyncDisplayKit 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 { 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 case let .user(user) = slice.peer, user.botInfo != nil {
if let id = slice.item.storyItem.media.id { //TODO:localize
let _ = component.context.engine.messages.deleteBotPreviews(peerId: slice.peer.id, ids: [id]).startStandalone() let _ = component.context.engine.messages.deleteBotPreviews(peerId: slice.peer.id, language: nil, media: [slice.item.storyItem.media._asMedia()]).startStandalone()
}
} else { } else {
let _ = component.context.engine.messages.deleteStories(peerId: slice.peer.id, ids: [slice.item.storyItem.id]).startStandalone() 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 font: UIFont
public var spacing: CGFloat public var spacing: CGFloat
public var lineSelection: Bool 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.font = font
self.spacing = spacing self.spacing = spacing
self.lineSelection = lineSelection 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 var component: TabSelectorComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
@ -104,6 +106,14 @@ public final class TabSelectorComponent: Component {
super.init(frame: frame) 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) self.addSubview(self.selectionView)
} }
@ -114,6 +124,10 @@ public final class TabSelectorComponent: Component {
deinit { 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 { func update(component: TabSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let selectionColorUpdated = component.colors.selection != self.component?.colors.selection let selectionColorUpdated = component.colors.selection != self.component?.colors.selection
@ -121,6 +135,12 @@ public final class TabSelectorComponent: Component {
self.state = state self.state = state
let baseHeight: CGFloat = 28.0 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 innerInset: CGFloat = 12.0
let spacing: CGFloat = component.customLayout?.spacing ?? 2.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 previousBackgroundRect: CGRect?
var selectedBackgroundRect: CGRect? var selectedBackgroundRect: CGRect?
var nextBackgroundRect: CGRect? var nextBackgroundRect: CGRect?
@ -213,8 +233,8 @@ public final class TabSelectorComponent: Component {
if !contentWidth.isZero { if !contentWidth.isZero {
contentWidth += spacing contentWidth += spacing
} }
let itemTitleFrame = CGRect(origin: CGPoint(x: contentWidth + innerInset, y: floor((baseHeight - itemSize.height) * 0.5)), size: itemSize) 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: 0.0), size: CGSize(width: innerInset + itemSize.width + innerInset, height: baseHeight)) let itemBackgroundRect = CGRect(origin: CGPoint(x: contentWidth, y: verticalInset), size: CGSize(width: innerInset + itemSize.width + innerInset, height: baseHeight))
contentWidth = itemBackgroundRect.maxX contentWidth = itemBackgroundRect.maxX
if item.id == component.selectedId { if item.id == component.selectedId {
@ -237,6 +257,7 @@ public final class TabSelectorComponent: Component {
} }
index += 1 index += 1
} }
contentWidth += spacing
var removeIds: [AnyHashable] = [] var removeIds: [AnyHashable] = []
for (id, itemView) in self.visibleItems { for (id, itemView) in self.visibleItems {
@ -277,7 +298,14 @@ public final class TabSelectorComponent: Component {
self.selectionView.alpha = 0.0 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 { 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() 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 }, tapMessage: nil, clickThroughMessage: { [weak self] view, location in
self?.chatDisplayNode.dismissInput() self?.chatDisplayNode.dismissInput(view: view, location: location)
}, toggleMessagesSelection: { [weak self] ids, value in }, toggleMessagesSelection: { [weak self] ids, value in
guard let strongSelf = self, strongSelf.isNodeLoaded else { guard let strongSelf = self, strongSelf.isNodeLoaded else {
return return

View File

@ -43,6 +43,7 @@ import ChatInlineSearchResultsListComponent
import ComponentDisplayAdapters import ComponentDisplayAdapters
import ComponentFlow import ComponentFlow
import ChatEmptyNode import ChatEmptyNode
import SpaceWarpView
final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem { final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem {
let itemNode: OverlayMediaItemNode let itemNode: OverlayMediaItemNode
@ -86,6 +87,41 @@ private struct ChatControllerNodeDerivedLayoutState {
var upperInputPositionBound: CGFloat? 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 { class HistoryNodeContainer: ASDisplayNode {
var isSecret: Bool { var isSecret: Bool {
didSet { 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) { init(isSecret: Bool) {
self.contentNodeImpl = ASDisplayNode()
self.isSecret = isSecret self.isSecret = isSecret
super.init() super.init()
@ -103,6 +151,23 @@ class HistoryNodeContainer: ASDisplayNode {
if self.isSecret { if self.isSecret {
setLayerDisableScreenshots(self.layer, 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 contentDimNode: ASDisplayNode
let backgroundNode: WallpaperBackgroundNode let backgroundNode: WallpaperBackgroundNode
let historyNode: ChatHistoryListNodeImpl let historyNode: ChatHistoryListNodeImpl
var blurredHistoryNode: ASImageNode? var blurredHistoryNode: ASImageNode?
let historyNodeContainer: ASDisplayNode let historyNodeContainer: HistoryNodeContainer
let loadingNode: ChatLoadingNode let loadingNode: ChatLoadingNode
private(set) var loadingPlaceholderNode: ChatLoadingPlaceholderNode? private(set) var loadingPlaceholderNode: ChatLoadingPlaceholderNode?
@ -379,7 +444,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.backgroundNode = backgroundNode self.backgroundNode = backgroundNode
self.contentContainerNode = ASDisplayNode() self.contentContainerNode = ChatNodeContainer()
self.contentDimNode = ASDisplayNode() self.contentDimNode = ASDisplayNode()
self.contentDimNode.isUserInteractionEnabled = false self.contentDimNode.isUserInteractionEnabled = false
self.contentDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.2) 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 = HistoryNodeContainer(isSecret: chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat)
self.historyNodeContainer.addSubnode(self.historyNode) self.historyNodeContainer.contentNode.addSubnode(self.historyNode)
var getContentAreaInScreenSpaceImpl: (() -> CGRect)? var getContentAreaInScreenSpaceImpl: (() -> CGRect)?
var onTransitionEventImpl: ((ContainedViewLayoutTransition) -> Void)? var onTransitionEventImpl: ((ContainedViewLayoutTransition) -> Void)?
@ -787,11 +852,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.historyNode.enableExtractedBackgrounds = true self.historyNode.enableExtractedBackgrounds = true
self.addSubnode(self.contentContainerNode) self.addSubnode(self.contentContainerNode)
self.contentContainerNode.addSubnode(self.backgroundNode) self.contentContainerNode.contentNode.addSubnode(self.backgroundNode)
self.contentContainerNode.addSubnode(self.historyNodeContainer) self.contentContainerNode.contentNode.addSubnode(self.historyNodeContainer)
if let navigationBar = self.navigationBar { if let navigationBar = self.navigationBar {
self.contentContainerNode.addSubnode(navigationBar) self.contentContainerNode.contentNode.addSubnode(navigationBar)
} }
self.inputPanelContainerNode.expansionUpdated = { [weak self] transition in self.inputPanelContainerNode.expansionUpdated = { [weak self] transition in
@ -817,9 +882,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.inputPanelBackgroundNode.addSubnode(self.inputPanelBottomBackgroundSeparatorNode) self.inputPanelBackgroundNode.addSubnode(self.inputPanelBottomBackgroundSeparatorNode)
self.addSubnode(self.messageTransitionNode) self.addSubnode(self.messageTransitionNode)
self.contentContainerNode.addSubnode(self.navigateButtons) self.contentContainerNode.contentNode.addSubnode(self.navigateButtons)
self.addSubnode(self.presentationContextMarker) self.addSubnode(self.presentationContextMarker)
self.contentContainerNode.addSubnode(self.contentDimNode) self.contentContainerNode.contentNode.addSubnode(self.contentDimNode)
self.navigationBar?.additionalContentNode.addSubnode(self.titleAccessoryPanelContainer) self.navigationBar?.additionalContentNode.addSubnode(self.titleAccessoryPanelContainer)
@ -1004,9 +1069,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.emptyNode = emptyNode self.emptyNode = emptyNode
if let inlineSearchResultsView = self.inlineSearchResults?.view { if let inlineSearchResultsView = self.inlineSearchResults?.view {
self.contentContainerNode.view.insertSubview(emptyNode.view, belowSubview: inlineSearchResultsView) self.contentContainerNode.contentNode.view.insertSubview(emptyNode.view, belowSubview: inlineSearchResultsView)
} else { } else {
self.contentContainerNode.insertSubnode(emptyNode, aboveSubnode: self.historyNodeContainer) self.contentContainerNode.contentNode.insertSubnode(emptyNode, aboveSubnode: self.historyNodeContainer)
} }
if let (size, insets) = self.validEmptyNodeLayout { if let (size, insets) = self.validEmptyNodeLayout {
@ -1081,13 +1146,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
} }
} }
if let historyNodeContainer = self.historyNodeContainer as? HistoryNodeContainer {
let isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat let isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat
if historyNodeContainer.isSecret != isSecret { if self.historyNodeContainer.isSecret != isSecret {
historyNodeContainer.isSecret = isSecret self.historyNodeContainer.isSecret = isSecret
setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, isSecret) setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, isSecret)
} }
}
var previousListBottomInset: CGFloat? var previousListBottomInset: CGFloat?
if !self.historyNode.frame.isEmpty { if !self.historyNode.frame.isEmpty {
@ -1097,6 +1160,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.messageTransitionNode.frame = CGRect(origin: CGPoint(), size: layout.size) self.messageTransitionNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.contentContainerNode.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 let isOverlay: Bool
switch self.chatPresentationInterfaceState.mode { switch self.chatPresentationInterfaceState.mode {
@ -1239,10 +1303,10 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
if let containerNode = self.containerNode { if let containerNode = self.containerNode {
self.containerNode = nil self.containerNode = nil
containerNode.removeFromSupernode() containerNode.removeFromSupernode()
self.contentContainerNode.insertSubnode(self.backgroundNode, at: 0) self.contentContainerNode.contentNode.insertSubnode(self.backgroundNode, at: 0)
self.contentContainerNode.insertSubnode(self.historyNodeContainer, aboveSubnode: self.backgroundNode) self.contentContainerNode.contentNode.insertSubnode(self.historyNodeContainer, aboveSubnode: self.backgroundNode)
if let restrictedNode = self.restrictedNode { if let restrictedNode = self.restrictedNode {
self.contentContainerNode.insertSubnode(restrictedNode, aboveSubnode: self.historyNodeContainer) self.contentContainerNode.contentNode.insertSubnode(restrictedNode, aboveSubnode: self.historyNodeContainer)
} }
self.navigationBar?.isHidden = false self.navigationBar?.isHidden = false
} }
@ -1392,7 +1456,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
if self.chatImportStatusPanel != importStatusPanelNode { if self.chatImportStatusPanel != importStatusPanelNode {
dismissedImportStatusPanelNode = self.chatImportStatusPanel dismissedImportStatusPanelNode = self.chatImportStatusPanel
self.chatImportStatusPanel = importStatusPanelNode 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) 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.updateBounds(node: self.historyNodeContainer, bounds: contentBounds)
transition.updatePosition(node: self.historyNodeContainer, position: contentBounds.center) 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.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)) 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 dismissedOverlayContextPanelNode = self.overlayContextPanelNode
self.overlayContextPanelNode = overlayContextPanelNode self.overlayContextPanelNode = overlayContextPanelNode
self.contentContainerNode.addSubnode(overlayContextPanelNode) self.contentContainerNode.contentNode.addSubnode(overlayContextPanelNode)
immediatelyLayoutOverlayContextPanelAndAnimateAppearance = true immediatelyLayoutOverlayContextPanelAndAnimateAppearance = true
} }
} else if let overlayContextPanelNode = self.overlayContextPanelNode { } else if let overlayContextPanelNode = self.overlayContextPanelNode {
@ -1999,7 +2064,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
expandedInputDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) expandedInputDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
expandedInputDimNode.alpha = 0.0 expandedInputDimNode.alpha = 0.0
self.expandedInputDimNode = expandedInputDimNode 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) transition.updateAlpha(node: expandedInputDimNode, alpha: 1.0)
expandedInputDimNode.frame = exandedFrame expandedInputDimNode.frame = exandedFrame
transition.animatePositionAdditive(node: expandedInputDimNode, offset: CGPoint(x: 0.0, y: previousInputPanelOrigin.y - inputPanelOrigin)) transition.animatePositionAdditive(node: expandedInputDimNode, offset: CGPoint(x: 0.0, y: previousInputPanelOrigin.y - inputPanelOrigin))
@ -2831,9 +2896,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.skippedShowSearchResultsAsListAnimationOnce = true self.skippedShowSearchResultsAsListAnimationOnce = true
inlineSearchResultsView.layer.allowsGroupOpacity = true inlineSearchResultsView.layer.allowsGroupOpacity = true
if let emptyNode = self.emptyNode { if let emptyNode = self.emptyNode {
self.contentContainerNode.view.insertSubview(inlineSearchResultsView, aboveSubview: emptyNode.view) self.contentContainerNode.contentNode.view.insertSubview(inlineSearchResultsView, aboveSubview: emptyNode.view)
} else { } 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)) inlineSearchResultsTransition.setFrame(view: inlineSearchResultsView, frame: CGRect(origin: CGPoint(), size: layout.size))
@ -3269,15 +3334,20 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) { @objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if recognizer.state == .ended { 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 { if let _ = self.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState {
return 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 { switch self.chatPresentationInterfaceState.inputMode {
case .none: case .none:
break break
@ -3739,7 +3809,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
let dropDimNode = ASDisplayNode() let dropDimNode = ASDisplayNode()
dropDimNode.backgroundColor = self.chatPresentationInterfaceState.theme.chatList.backgroundColor.withAlphaComponent(0.35) dropDimNode.backgroundColor = self.chatPresentationInterfaceState.theme.chatList.backgroundColor.withAlphaComponent(0.35)
self.dropDimNode = dropDimNode self.dropDimNode = dropDimNode
self.contentContainerNode.addSubnode(dropDimNode) self.contentContainerNode.contentNode.addSubnode(dropDimNode)
if let (layout, _) = self.validLayout { if let (layout, _) = self.validLayout {
dropDimNode.frame = CGRect(origin: CGPoint(), size: layout.size) dropDimNode.frame = CGRect(origin: CGPoint(), size: layout.size)
dropDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) 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 }, navigateToMessage: { _, _, _ in
}, navigateToMessageStandalone: { _ in }, navigateToMessageStandalone: { _ in
}, navigateToThreadMessage: { _, _, _ in }, navigateToThreadMessage: { _, _, _ in
}, tapMessage: nil, clickThroughMessage: { }, tapMessage: nil, clickThroughMessage: { _, _ in
}, toggleMessagesSelection: { _, _ in }, toggleMessagesSelection: { _, _ in
}, sendCurrentMessage: { _, _ in }, sendCurrentMessage: { _, _ in
}, sendMessage: { _ in }, sendMessage: { _ in

View File

@ -69,6 +69,7 @@ import StarsPurchaseScreen
import StarsTransferScreen import StarsTransferScreen
import StarsTransactionScreen import StarsTransactionScreen
import StarsWithdrawalScreen import StarsWithdrawalScreen
import MiniAppListScreen
private final class AccountUserInterfaceInUseContext { private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>() 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) 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 let controllerInteraction: ChatControllerInteraction
controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in
@ -1711,8 +1712,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, navigateToThreadMessage: { _, _, _ in }, navigateToThreadMessage: { _, _, _ in
}, tapMessage: { message in }, tapMessage: { message in
tapMessage?(message) tapMessage?(message)
}, clickThroughMessage: { }, clickThroughMessage: { view, location in
clickThroughMessage?() clickThroughMessage?(view, location)
}, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in
return false 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 }, 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: {}) 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) { 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) 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 return nil
case let .peer(id): case let .peer(id):
return id return id
case let .botPreview(id): case let .botPreview(id, _):
return id return id
} }
} }

View File

@ -37,10 +37,10 @@ public class LocalizationListItem: ListViewItem, ItemListItem {
public let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
let alwaysPlain: Bool let alwaysPlain: Bool
let action: () -> Void let action: () -> Void
let setItemWithRevealedOptions: (String?, String?) -> Void let setItemWithRevealedOptions: ((String?, String?) -> Void)?
let removeItem: (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.presentationData = presentationData
self.id = id self.id = id
self.title = title self.title = title
@ -368,7 +368,7 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode {
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) 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)])) 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 { } else {
strongSelf.setRevealOptions((left: [], right: [])) strongSelf.setRevealOptions((left: [], right: []))
@ -491,13 +491,13 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode {
override func revealOptionsInteractivelyOpened() { override func revealOptionsInteractivelyOpened() {
if let item = self.item { if let item = self.item {
item.setItemWithRevealedOptions(item.id, nil) item.setItemWithRevealedOptions?(item.id, nil)
} }
} }
override func revealOptionsInteractivelyClosed() { override func revealOptionsInteractivelyClosed() {
if let item = self.item { if let item = self.item {
item.setItemWithRevealedOptions(nil, item.id) item.setItemWithRevealedOptions?(nil, item.id)
} }
} }
@ -506,7 +506,7 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode {
self.revealOptionsInteractivelyClosed() self.revealOptionsInteractivelyClosed()
if let item = self.item { if let item = self.item {
item.removeItem(item.id) item.removeItem?(item.id)
} }
} }
} }