mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Bot previews
This commit is contained in:
parent
f604bc114f
commit
42a6f6e8bc
@ -932,6 +932,9 @@ public final class BotPreviewEditorTransitionOut {
|
||||
}
|
||||
}
|
||||
|
||||
public protocol MiniAppListScreenInitialData: AnyObject {
|
||||
}
|
||||
|
||||
public protocol SharedAccountContext: AnyObject {
|
||||
var sharedContainerPath: String { get }
|
||||
var basePath: String { get }
|
||||
@ -999,7 +1002,7 @@ public protocol SharedAccountContext: AnyObject {
|
||||
selectedMessages: Signal<Set<MessageId>?, NoError>,
|
||||
mode: ChatHistoryListMode
|
||||
) -> ChatHistoryListNode
|
||||
func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)?, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool, isStandalone: Bool) -> ListViewItem
|
||||
func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: ((UIView?, CGPoint?) -> Void)?, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool, isStandalone: Bool) -> ListViewItem
|
||||
func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader
|
||||
func makeChatMessageAvatarHeaderItem(context: AccountContext, timestamp: Int32, peer: Peer, message: Message, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader
|
||||
func makePeerSharedMediaController(context: AccountContext, peerId: PeerId) -> ViewController?
|
||||
@ -1100,6 +1103,10 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController
|
||||
func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController
|
||||
func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController
|
||||
|
||||
func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError>
|
||||
func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController
|
||||
|
||||
func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool)
|
||||
|
||||
func makeDebugSettingsController(context: AccountContext?) -> ViewController?
|
||||
|
@ -58,7 +58,7 @@ final class BotCheckoutWebInteractionController: ViewController {
|
||||
}
|
||||
|
||||
override func loadDisplayNode() {
|
||||
self.displayNode = BotCheckoutWebInteractionControllerNode(presentationData: self.presentationData, url: self.url, intent: self.intent)
|
||||
self.displayNode = BotCheckoutWebInteractionControllerNode(context: self.context, presentationData: self.presentationData, url: self.url, intent: self.intent)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
|
@ -4,6 +4,7 @@ import Display
|
||||
import AsyncDisplayKit
|
||||
import WebKit
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
|
||||
private class WeakPaymentScriptMessageHandler: NSObject, WKScriptMessageHandler {
|
||||
private let f: (WKScriptMessage) -> ()
|
||||
@ -20,12 +21,14 @@ private class WeakPaymentScriptMessageHandler: NSObject, WKScriptMessageHandler
|
||||
}
|
||||
|
||||
final class BotCheckoutWebInteractionControllerNode: ViewControllerTracingNode, WKNavigationDelegate {
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
private let intent: BotCheckoutWebInteractionControllerIntent
|
||||
|
||||
private var webView: WKWebView?
|
||||
|
||||
init(presentationData: PresentationData, url: String, intent: BotCheckoutWebInteractionControllerIntent) {
|
||||
init(context: AccountContext, presentationData: PresentationData, url: String, intent: BotCheckoutWebInteractionControllerIntent) {
|
||||
self.context = context
|
||||
self.presentationData = presentationData
|
||||
self.intent = intent
|
||||
|
||||
@ -146,6 +149,14 @@ final class BotCheckoutWebInteractionControllerNode: ViewControllerTracingNode,
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
} else {
|
||||
if let url = navigationAction.request.url, let scheme = url.scheme {
|
||||
let defaultSchemes: [String] = ["http", "https"]
|
||||
if !defaultSchemes.contains(scheme) {
|
||||
decisionHandler(.cancel)
|
||||
self.context.sharedContext.applicationBindings.openUrl(url.absoluteString)
|
||||
return
|
||||
}
|
||||
}
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
}
|
||||
|
@ -3543,9 +3543,19 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
} else if case .apps = key {
|
||||
if let navigationController = self.navigationController {
|
||||
if isRecommended {
|
||||
#if DEBUG
|
||||
let _ = (self.context.sharedContext.makeMiniAppListScreenInitialData(context: self.context)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in
|
||||
guard let self, let navigationController = self.navigationController else {
|
||||
return
|
||||
}
|
||||
navigationController.pushViewController(self.context.sharedContext.makeMiniAppListScreen(context: self.context, initialData: initialData))
|
||||
})
|
||||
#else
|
||||
if let peerInfoScreen = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
|
||||
navigationController.pushViewController(peerInfoScreen)
|
||||
}
|
||||
#endif
|
||||
} else if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.parentController {
|
||||
self.context.sharedContext.openWebApp(
|
||||
context: self.context,
|
||||
@ -3560,7 +3570,6 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
skipTermsOfService: true
|
||||
)
|
||||
} else {
|
||||
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
|
||||
navigationController: navigationController,
|
||||
context: self.context,
|
||||
|
@ -47,6 +47,13 @@ open class PortalSourceView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
public func removePortal(view: PortalView) {
|
||||
if let index = self.portalReferences.firstIndex(where: { $0.portalView === view }) {
|
||||
self.portalReferences.remove(at: index)
|
||||
}
|
||||
view.disablePortal()
|
||||
}
|
||||
|
||||
func setGlobalPortal(view: GlobalPortalView?) {
|
||||
if let globalPortalView = self.globalPortalView {
|
||||
self.globalPortalView = nil
|
||||
|
@ -23,6 +23,11 @@ public class PortalView {
|
||||
}
|
||||
}
|
||||
|
||||
func disablePortal() {
|
||||
self.view.sourceView = nil
|
||||
self.sourceView = nil
|
||||
}
|
||||
|
||||
public func reloadPortal() {
|
||||
if let sourceView = self.sourceView as? PortalSourceView {
|
||||
self.reloadPortal(sourceView: sourceView)
|
||||
|
@ -550,6 +550,10 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
var offset: CGFloat {
|
||||
return self.scrollView.contentOffset.y
|
||||
}
|
||||
|
||||
var contentBottomOffset: CGFloat {
|
||||
return -self.scrollView.contentOffset.y + self.scrollView.contentSize.height
|
||||
}
|
||||
|
||||
let coveringOffsetUpdated: (Viewport, ContainedViewLayoutTransition) -> Void
|
||||
let offsetUpdated: (Viewport, ContainedViewLayoutTransition) -> Void
|
||||
@ -1442,6 +1446,10 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
return self.fromViewport.coveringInsetOffset * (1.0 - self.currentProgress) + self.toViewport.coveringInsetOffset * self.currentProgress
|
||||
}
|
||||
|
||||
var contentBottomOffset: CGFloat {
|
||||
return self.fromViewport.contentBottomOffset * (1.0 - self.currentProgress) + self.toViewport.contentBottomOffset * self.currentProgress
|
||||
}
|
||||
|
||||
var offset: CGFloat {
|
||||
return self.fromViewport.offset * (1.0 - self.currentProgress) + self.toViewport.offset * self.currentProgress
|
||||
}
|
||||
@ -1632,6 +1640,16 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
public var contentBottomOffset: CGFloat {
|
||||
if let currentViewportTransition = self.currentViewportTransition {
|
||||
return currentViewportTransition.contentBottomOffset
|
||||
} else if let currentViewport = self.currentViewport {
|
||||
return currentViewport.contentBottomOffset
|
||||
} else {
|
||||
return 0.0
|
||||
}
|
||||
}
|
||||
|
||||
public var scrollingOffset: CGFloat {
|
||||
if let currentViewportTransition = self.currentViewportTransition {
|
||||
return currentViewportTransition.offset
|
||||
|
@ -105,6 +105,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-944407322] = { return Api.BotMenuButton.parse_botMenuButton($0) }
|
||||
dict[1113113093] = { return Api.BotMenuButton.parse_botMenuButtonCommands($0) }
|
||||
dict[1966318984] = { return Api.BotMenuButton.parse_botMenuButtonDefault($0) }
|
||||
dict[602479523] = { return Api.BotPreviewMedia.parse_botPreviewMedia($0) }
|
||||
dict[-2076642874] = { return Api.BroadcastRevenueBalances.parse_broadcastRevenueBalances($0) }
|
||||
dict[1434332356] = { return Api.BroadcastRevenueTransaction.parse_broadcastRevenueTransactionProceeds($0) }
|
||||
dict[1121127726] = { return Api.BroadcastRevenueTransaction.parse_broadcastRevenueTransactionRefund($0) }
|
||||
@ -520,7 +521,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[340088945] = { return Api.MediaArea.parse_mediaAreaSuggestedReaction($0) }
|
||||
dict[926421125] = { return Api.MediaArea.parse_mediaAreaUrl($0) }
|
||||
dict[-1098720356] = { return Api.MediaArea.parse_mediaAreaVenue($0) }
|
||||
dict[1132918857] = { return Api.MediaArea.parse_mediaAreaWeather($0) }
|
||||
dict[1235637404] = { return Api.MediaArea.parse_mediaAreaWeather($0) }
|
||||
dict[-808853502] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) }
|
||||
dict[-1808510398] = { return Api.Message.parse_message($0) }
|
||||
dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) }
|
||||
@ -1170,6 +1171,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-1542017919] = { return Api.auth.SentCodeType.parse_sentCodeTypeSmsWord($0) }
|
||||
dict[-391678544] = { return Api.bots.BotInfo.parse_botInfo($0) }
|
||||
dict[428978491] = { return Api.bots.PopularAppBots.parse_popularAppBots($0) }
|
||||
dict[212278628] = { return Api.bots.PreviewInfo.parse_previewInfo($0) }
|
||||
dict[-309659827] = { return Api.channels.AdminLogResults.parse_adminLogResults($0) }
|
||||
dict[-541588713] = { return Api.channels.ChannelParticipant.parse_channelParticipant($0) }
|
||||
dict[-1699676497] = { return Api.channels.ChannelParticipants.parse_channelParticipants($0) }
|
||||
@ -1493,6 +1495,8 @@ public extension Api {
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.BotMenuButton:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.BotPreviewMedia:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.BroadcastRevenueBalances:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.BroadcastRevenueTransaction:
|
||||
@ -2151,6 +2155,8 @@ public extension Api {
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.bots.PopularAppBots:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.bots.PreviewInfo:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.channels.AdminLogResults:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.channels.ChannelParticipant:
|
||||
|
@ -231,7 +231,7 @@ public extension Api {
|
||||
case mediaAreaSuggestedReaction(flags: Int32, coordinates: Api.MediaAreaCoordinates, reaction: Api.Reaction)
|
||||
case mediaAreaUrl(coordinates: Api.MediaAreaCoordinates, url: String)
|
||||
case mediaAreaVenue(coordinates: Api.MediaAreaCoordinates, geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String)
|
||||
case mediaAreaWeather(flags: Int32, coordinates: Api.MediaAreaCoordinates, emoji: String, temperatureC: Double)
|
||||
case mediaAreaWeather(coordinates: Api.MediaAreaCoordinates, emoji: String, temperatureC: Double, color: Int32)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
@ -295,14 +295,14 @@ public extension Api {
|
||||
serializeString(venueId, buffer: buffer, boxed: false)
|
||||
serializeString(venueType, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .mediaAreaWeather(let flags, let coordinates, let emoji, let temperatureC):
|
||||
case .mediaAreaWeather(let coordinates, let emoji, let temperatureC, let color):
|
||||
if boxed {
|
||||
buffer.appendInt32(1132918857)
|
||||
buffer.appendInt32(1235637404)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
coordinates.serialize(buffer, true)
|
||||
serializeString(emoji, buffer: buffer, boxed: false)
|
||||
serializeDouble(temperatureC, buffer: buffer, boxed: false)
|
||||
serializeInt32(color, buffer: buffer, boxed: false)
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -323,8 +323,8 @@ public extension Api {
|
||||
return ("mediaAreaUrl", [("coordinates", coordinates as Any), ("url", url as Any)])
|
||||
case .mediaAreaVenue(let coordinates, let geo, let title, let address, let provider, let venueId, let venueType):
|
||||
return ("mediaAreaVenue", [("coordinates", coordinates as Any), ("geo", geo as Any), ("title", title as Any), ("address", address as Any), ("provider", provider as Any), ("venueId", venueId as Any), ("venueType", venueType as Any)])
|
||||
case .mediaAreaWeather(let flags, let coordinates, let emoji, let temperatureC):
|
||||
return ("mediaAreaWeather", [("flags", flags as Any), ("coordinates", coordinates as Any), ("emoji", emoji as Any), ("temperatureC", temperatureC as Any)])
|
||||
case .mediaAreaWeather(let coordinates, let emoji, let temperatureC, let color):
|
||||
return ("mediaAreaWeather", [("coordinates", coordinates as Any), ("emoji", emoji as Any), ("temperatureC", temperatureC as Any), ("color", color as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
@ -484,22 +484,22 @@ public extension Api {
|
||||
}
|
||||
}
|
||||
public static func parse_mediaAreaWeather(_ reader: BufferReader) -> MediaArea? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Api.MediaAreaCoordinates?
|
||||
var _1: Api.MediaAreaCoordinates?
|
||||
if let signature = reader.readInt32() {
|
||||
_2 = Api.parse(reader, signature: signature) as? Api.MediaAreaCoordinates
|
||||
_1 = Api.parse(reader, signature: signature) as? Api.MediaAreaCoordinates
|
||||
}
|
||||
var _3: String?
|
||||
_3 = parseString(reader)
|
||||
var _4: Double?
|
||||
_4 = reader.readDouble()
|
||||
var _2: String?
|
||||
_2 = parseString(reader)
|
||||
var _3: Double?
|
||||
_3 = reader.readDouble()
|
||||
var _4: Int32?
|
||||
_4 = reader.readInt32()
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = _4 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 {
|
||||
return Api.MediaArea.mediaAreaWeather(flags: _1!, coordinates: _2!, emoji: _3!, temperatureC: _4!)
|
||||
return Api.MediaArea.mediaAreaWeather(coordinates: _1!, emoji: _2!, temperatureC: _3!, color: _4!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
|
@ -724,6 +724,48 @@ public extension Api {
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api {
|
||||
indirect enum BotPreviewMedia: TypeConstructorDescription {
|
||||
case botPreviewMedia(date: Int32, media: Api.MessageMedia)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .botPreviewMedia(let date, let media):
|
||||
if boxed {
|
||||
buffer.appendInt32(602479523)
|
||||
}
|
||||
serializeInt32(date, buffer: buffer, boxed: false)
|
||||
media.serialize(buffer, true)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .botPreviewMedia(let date, let media):
|
||||
return ("botPreviewMedia", [("date", date as Any), ("media", media as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_botPreviewMedia(_ reader: BufferReader) -> BotPreviewMedia? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Api.MessageMedia?
|
||||
if let signature = reader.readInt32() {
|
||||
_2 = Api.parse(reader, signature: signature) as? Api.MessageMedia
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
if _c1 && _c2 {
|
||||
return Api.BotPreviewMedia.botPreviewMedia(date: _1!, media: _2!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api {
|
||||
enum BroadcastRevenueBalances: TypeConstructorDescription {
|
||||
case broadcastRevenueBalances(currentBalance: Int64, availableBalance: Int64, overallRevenue: Int64)
|
||||
@ -1160,53 +1202,3 @@ public extension Api {
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api {
|
||||
enum BusinessIntro: TypeConstructorDescription {
|
||||
case businessIntro(flags: Int32, title: String, description: String, sticker: Api.Document?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .businessIntro(let flags, let title, let description, let sticker):
|
||||
if boxed {
|
||||
buffer.appendInt32(1510606445)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeString(title, buffer: buffer, boxed: false)
|
||||
serializeString(description, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 0) != 0 {sticker!.serialize(buffer, true)}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .businessIntro(let flags, let title, let description, let sticker):
|
||||
return ("businessIntro", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("sticker", sticker as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_businessIntro(_ reader: BufferReader) -> BusinessIntro? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: String?
|
||||
_2 = parseString(reader)
|
||||
var _3: String?
|
||||
_3 = parseString(reader)
|
||||
var _4: Api.Document?
|
||||
if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() {
|
||||
_4 = Api.parse(reader, signature: signature) as? Api.Document
|
||||
} }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 {
|
||||
return Api.BusinessIntro.businessIntro(flags: _1!, title: _2!, description: _3!, sticker: _4)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,55 @@
|
||||
public extension Api.bots {
|
||||
enum PreviewInfo: TypeConstructorDescription {
|
||||
case previewInfo(media: [Api.BotPreviewMedia], langCodes: [String])
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .previewInfo(let media, let langCodes):
|
||||
if boxed {
|
||||
buffer.appendInt32(212278628)
|
||||
}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(media.count))
|
||||
for item in media {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(langCodes.count))
|
||||
for item in langCodes {
|
||||
serializeString(item, buffer: buffer, boxed: false)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .previewInfo(let media, let langCodes):
|
||||
return ("previewInfo", [("media", media as Any), ("langCodes", langCodes as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_previewInfo(_ reader: BufferReader) -> PreviewInfo? {
|
||||
var _1: [Api.BotPreviewMedia]?
|
||||
if let _ = reader.readInt32() {
|
||||
_1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotPreviewMedia.self)
|
||||
}
|
||||
var _2: [String]?
|
||||
if let _ = reader.readInt32() {
|
||||
_2 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
if _c1 && _c2 {
|
||||
return Api.bots.PreviewInfo.previewInfo(media: _1!, langCodes: _2!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.channels {
|
||||
enum AdminLogResults: TypeConstructorDescription {
|
||||
case adminLogResults(events: [Api.ChannelAdminLogEvent], chats: [Api.Chat], users: [Api.User])
|
||||
@ -1404,61 +1456,3 @@ public extension Api.help {
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.help {
|
||||
enum Country: TypeConstructorDescription {
|
||||
case country(flags: Int32, iso2: String, defaultName: String, name: String?, countryCodes: [Api.help.CountryCode])
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .country(let flags, let iso2, let defaultName, let name, let countryCodes):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1014526429)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeString(iso2, buffer: buffer, boxed: false)
|
||||
serializeString(defaultName, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 1) != 0 {serializeString(name!, buffer: buffer, boxed: false)}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(countryCodes.count))
|
||||
for item in countryCodes {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .country(let flags, let iso2, let defaultName, let name, let countryCodes):
|
||||
return ("country", [("flags", flags as Any), ("iso2", iso2 as Any), ("defaultName", defaultName as Any), ("name", name as Any), ("countryCodes", countryCodes as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_country(_ reader: BufferReader) -> Country? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: String?
|
||||
_2 = parseString(reader)
|
||||
var _3: String?
|
||||
_3 = parseString(reader)
|
||||
var _4: String?
|
||||
if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) }
|
||||
var _5: [Api.help.CountryCode]?
|
||||
if let _ = reader.readInt32() {
|
||||
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.help.CountryCode.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil
|
||||
let _c5 = _5 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 {
|
||||
return Api.help.Country.country(flags: _1!, iso2: _2!, defaultName: _3!, name: _4, countryCodes: _5!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,53 @@
|
||||
public extension Api {
|
||||
enum BusinessIntro: TypeConstructorDescription {
|
||||
case businessIntro(flags: Int32, title: String, description: String, sticker: Api.Document?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .businessIntro(let flags, let title, let description, let sticker):
|
||||
if boxed {
|
||||
buffer.appendInt32(1510606445)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeString(title, buffer: buffer, boxed: false)
|
||||
serializeString(description, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 0) != 0 {sticker!.serialize(buffer, true)}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .businessIntro(let flags, let title, let description, let sticker):
|
||||
return ("businessIntro", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("sticker", sticker as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_businessIntro(_ reader: BufferReader) -> BusinessIntro? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: String?
|
||||
_2 = parseString(reader)
|
||||
var _3: String?
|
||||
_3 = parseString(reader)
|
||||
var _4: Api.Document?
|
||||
if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() {
|
||||
_4 = Api.parse(reader, signature: signature) as? Api.Document
|
||||
} }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 {
|
||||
return Api.BusinessIntro.businessIntro(flags: _1!, title: _2!, description: _3!, sticker: _4)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api {
|
||||
enum BusinessLocation: TypeConstructorDescription {
|
||||
case businessLocation(flags: Int32, geoPoint: Api.GeoPoint?, address: String)
|
||||
|
@ -1,3 +1,61 @@
|
||||
public extension Api.help {
|
||||
enum Country: TypeConstructorDescription {
|
||||
case country(flags: Int32, iso2: String, defaultName: String, name: String?, countryCodes: [Api.help.CountryCode])
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .country(let flags, let iso2, let defaultName, let name, let countryCodes):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1014526429)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeString(iso2, buffer: buffer, boxed: false)
|
||||
serializeString(defaultName, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 1) != 0 {serializeString(name!, buffer: buffer, boxed: false)}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(countryCodes.count))
|
||||
for item in countryCodes {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .country(let flags, let iso2, let defaultName, let name, let countryCodes):
|
||||
return ("country", [("flags", flags as Any), ("iso2", iso2 as Any), ("defaultName", defaultName as Any), ("name", name as Any), ("countryCodes", countryCodes as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_country(_ reader: BufferReader) -> Country? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: String?
|
||||
_2 = parseString(reader)
|
||||
var _3: String?
|
||||
_3 = parseString(reader)
|
||||
var _4: String?
|
||||
if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) }
|
||||
var _5: [Api.help.CountryCode]?
|
||||
if let _ = reader.readInt32() {
|
||||
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.help.CountryCode.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil
|
||||
let _c5 = _5 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 {
|
||||
return Api.help.Country.country(flags: _1!, iso2: _2!, defaultName: _3!, name: _4, countryCodes: _5!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.help {
|
||||
enum CountryCode: TypeConstructorDescription {
|
||||
case countryCode(flags: Int32, countryCode: String, prefixes: [String]?, patterns: [String]?)
|
||||
|
@ -2201,16 +2201,17 @@ public extension Api.functions.auth {
|
||||
}
|
||||
}
|
||||
public extension Api.functions.bots {
|
||||
static func addPreviewMedia(bot: Api.InputUser, media: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.MessageMedia>) {
|
||||
static func addPreviewMedia(bot: Api.InputUser, langCode: String, media: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.BotPreviewMedia>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(1633332331)
|
||||
buffer.appendInt32(397326170)
|
||||
bot.serialize(buffer, true)
|
||||
serializeString(langCode, buffer: buffer, boxed: false)
|
||||
media.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "bots.addPreviewMedia", parameters: [("bot", String(describing: bot)), ("media", String(describing: media))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.MessageMedia? in
|
||||
return (FunctionDescription(name: "bots.addPreviewMedia", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode)), ("media", String(describing: media))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.BotPreviewMedia? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.MessageMedia?
|
||||
var result: Api.BotPreviewMedia?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.MessageMedia
|
||||
result = Api.parse(reader, signature: signature) as? Api.BotPreviewMedia
|
||||
}
|
||||
return result
|
||||
})
|
||||
@ -2263,16 +2264,17 @@ public extension Api.functions.bots {
|
||||
}
|
||||
}
|
||||
public extension Api.functions.bots {
|
||||
static func deletePreviewMedia(bot: Api.InputUser, media: [Api.InputMedia]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
static func deletePreviewMedia(bot: Api.InputUser, langCode: String, media: [Api.InputMedia]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(481471475)
|
||||
buffer.appendInt32(755054003)
|
||||
bot.serialize(buffer, true)
|
||||
serializeString(langCode, buffer: buffer, boxed: false)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(media.count))
|
||||
for item in media {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
return (FunctionDescription(name: "bots.deletePreviewMedia", parameters: [("bot", String(describing: bot)), ("media", String(describing: media))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||
return (FunctionDescription(name: "bots.deletePreviewMedia", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode)), ("media", String(describing: media))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Bool?
|
||||
if let signature = reader.readInt32() {
|
||||
@ -2283,17 +2285,18 @@ public extension Api.functions.bots {
|
||||
}
|
||||
}
|
||||
public extension Api.functions.bots {
|
||||
static func editPreviewMedia(bot: Api.InputUser, media: Api.InputMedia, newMedia: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.MessageMedia>) {
|
||||
static func editPreviewMedia(bot: Api.InputUser, langCode: String, media: Api.InputMedia, newMedia: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.BotPreviewMedia>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-1436441263)
|
||||
buffer.appendInt32(-2061148049)
|
||||
bot.serialize(buffer, true)
|
||||
serializeString(langCode, buffer: buffer, boxed: false)
|
||||
media.serialize(buffer, true)
|
||||
newMedia.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "bots.editPreviewMedia", parameters: [("bot", String(describing: bot)), ("media", String(describing: media)), ("newMedia", String(describing: newMedia))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.MessageMedia? in
|
||||
return (FunctionDescription(name: "bots.editPreviewMedia", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode)), ("media", String(describing: media)), ("newMedia", String(describing: newMedia))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.BotPreviewMedia? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.MessageMedia?
|
||||
var result: Api.BotPreviewMedia?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.MessageMedia
|
||||
result = Api.parse(reader, signature: signature) as? Api.BotPreviewMedia
|
||||
}
|
||||
return result
|
||||
})
|
||||
@ -2364,15 +2367,31 @@ public extension Api.functions.bots {
|
||||
}
|
||||
}
|
||||
public extension Api.functions.bots {
|
||||
static func getPreviewMedias(bot: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.MessageMedia]>) {
|
||||
static func getPreviewInfo(bot: Api.InputUser, langCode: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.bots.PreviewInfo>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(1720252591)
|
||||
buffer.appendInt32(1111143341)
|
||||
bot.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "bots.getPreviewMedias", parameters: [("bot", String(describing: bot))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Api.MessageMedia]? in
|
||||
serializeString(langCode, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "bots.getPreviewInfo", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.bots.PreviewInfo? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: [Api.MessageMedia]?
|
||||
var result: Api.bots.PreviewInfo?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.bots.PreviewInfo
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.bots {
|
||||
static func getPreviewMedias(bot: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.BotPreviewMedia]>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-1566222003)
|
||||
bot.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "bots.getPreviewMedias", parameters: [("bot", String(describing: bot))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Api.BotPreviewMedia]? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: [Api.BotPreviewMedia]?
|
||||
if let _ = reader.readInt32() {
|
||||
result = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageMedia.self)
|
||||
result = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotPreviewMedia.self)
|
||||
}
|
||||
return result
|
||||
})
|
||||
@ -2396,16 +2415,17 @@ public extension Api.functions.bots {
|
||||
}
|
||||
}
|
||||
public extension Api.functions.bots {
|
||||
static func reorderPreviewMedias(bot: Api.InputUser, order: [Api.InputMedia]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
static func reorderPreviewMedias(bot: Api.InputUser, langCode: String, order: [Api.InputMedia]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-1472444656)
|
||||
buffer.appendInt32(-1238895702)
|
||||
bot.serialize(buffer, true)
|
||||
serializeString(langCode, buffer: buffer, boxed: false)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(order.count))
|
||||
for item in order {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
return (FunctionDescription(name: "bots.reorderPreviewMedias", parameters: [("bot", String(describing: bot)), ("order", String(describing: order))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||
return (FunctionDescription(name: "bots.reorderPreviewMedias", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode)), ("order", String(describing: order))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Bool?
|
||||
if let signature = reader.readInt32() {
|
||||
|
@ -522,9 +522,9 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? {
|
||||
return .link(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), url: url)
|
||||
case let .mediaAreaChannelPost(coordinates, channelId, messageId):
|
||||
return .channelMessage(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), messageId: EngineMessage.Id(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId))
|
||||
case let .mediaAreaWeather(flags, coordinates, emoji, temperatureC):
|
||||
case let .mediaAreaWeather(coordinates, emoji, temperatureC, color):
|
||||
var parsedFlags = MediaArea.WeatherFlags()
|
||||
if (flags & (1 << 0)) != 0 {
|
||||
if color != 0 {
|
||||
parsedFlags.insert(.isDark)
|
||||
}
|
||||
return .weather(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), emoji: emoji, temperature: temperatureC, flags: parsedFlags)
|
||||
@ -581,11 +581,7 @@ func apiMediaAreasFromMediaAreas(_ mediaAreas: [MediaArea], transaction: Transac
|
||||
case let .link(_, url):
|
||||
apiMediaAreas.append(.mediaAreaUrl(coordinates: inputCoordinates, url: url))
|
||||
case let .weather(_, emoji, temperature, flags):
|
||||
var apiFlags: Int32 = 0
|
||||
if flags.contains(.isDark) {
|
||||
apiFlags |= (1 << 0)
|
||||
}
|
||||
apiMediaAreas.append(.mediaAreaWeather(flags: apiFlags, coordinates: inputCoordinates, emoji: emoji, temperatureC: temperature))
|
||||
apiMediaAreas.append(.mediaAreaWeather(coordinates: inputCoordinates, emoji: emoji, temperatureC: temperature, color: flags.contains(.isDark) ? 1 : 0))
|
||||
}
|
||||
}
|
||||
return apiMediaAreas
|
||||
|
@ -287,6 +287,11 @@ public final class AccountStateManager {
|
||||
return self.storyUpdatesPipe.signal()
|
||||
}
|
||||
|
||||
fileprivate let botPreviewUpdatesPipe = ValuePipe<[InternalBotPreviewUpdate]>()
|
||||
public var botPreviewUpdates: Signal<[InternalBotPreviewUpdate], NoError> {
|
||||
return self.botPreviewUpdatesPipe.signal()
|
||||
}
|
||||
|
||||
private var updatedWebpageContexts: [MediaId: UpdatedWebpageSubscriberContext] = [:]
|
||||
private var updatedPeersNearbyContext = UpdatedPeersNearbySubscriberContext()
|
||||
private var updatedRevenueBalancesContext = UpdatedRevenueBalancesSubscriberContext()
|
||||
@ -1856,6 +1861,18 @@ public final class AccountStateManager {
|
||||
}
|
||||
}
|
||||
|
||||
var botPreviewUpdates: Signal<[InternalBotPreviewUpdate], NoError> {
|
||||
return self.impl.signalWith { impl, subscriber in
|
||||
return impl.botPreviewUpdates.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)
|
||||
}
|
||||
}
|
||||
|
||||
func injectBotPreviewUpdates(updates: [InternalBotPreviewUpdate]) {
|
||||
self.impl.with { impl in
|
||||
impl.botPreviewUpdatesPipe.putNext(updates)
|
||||
}
|
||||
}
|
||||
|
||||
var updateConfigRequested: (() -> Void)?
|
||||
var isPremiumUpdated: (() -> Void)?
|
||||
|
||||
|
@ -627,40 +627,87 @@ extension TelegramBusinessChatLinks {
|
||||
public final class CachedUserData: CachedPeerData {
|
||||
public final class BotPreview: Codable, Equatable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case media
|
||||
case items
|
||||
case alternativeLanguageCodes
|
||||
}
|
||||
|
||||
public let media: [Media]
|
||||
public final class Item: Codable, Equatable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case media = "m"
|
||||
case timestamp = "t"
|
||||
}
|
||||
|
||||
public let media: Media
|
||||
public let timestamp: Int32
|
||||
|
||||
public init(media: Media, timestamp: Int32) {
|
||||
self.media = media
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
|
||||
public init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
let mediaData = try container.decode(Data.self, forKey: .media)
|
||||
guard let media = PostboxDecoder(buffer: MemoryBuffer(data: mediaData)).decodeRootObject() as? Media else {
|
||||
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "media"))
|
||||
}
|
||||
self.media = media
|
||||
|
||||
self.timestamp = try container.decode(Int32.self, forKey: .timestamp)
|
||||
}
|
||||
|
||||
public func encode(to encoder: any Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
let encoder = PostboxEncoder()
|
||||
encoder.encodeRootObject(media)
|
||||
try container.encode(encoder.makeData(), forKey: .media)
|
||||
|
||||
try container.encode(self.timestamp, forKey: .timestamp)
|
||||
}
|
||||
|
||||
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
if lhs === rhs {
|
||||
return true
|
||||
}
|
||||
if !lhs.media.isEqual(to: rhs.media) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public init(media: [Media]) {
|
||||
self.media = media
|
||||
public let items: [Item]
|
||||
public let alternativeLanguageCodes: [String]
|
||||
|
||||
public init(items: [Item], alternativeLanguageCodes: [String]) {
|
||||
self.items = items
|
||||
self.alternativeLanguageCodes = alternativeLanguageCodes
|
||||
}
|
||||
|
||||
public init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
let mediaData = try container.decode([Data].self, forKey: .media)
|
||||
self.media = mediaData.compactMap { data -> Media? in
|
||||
return PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? Media
|
||||
}
|
||||
self.items = try container.decode([Item].self, forKey: .items)
|
||||
self.alternativeLanguageCodes = try container.decode([String].self, forKey: .alternativeLanguageCodes)
|
||||
}
|
||||
|
||||
public func encode(to encoder: any Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
let mediaData = self.media.map { media -> Data in
|
||||
let encoder = PostboxEncoder()
|
||||
encoder.encodeRootObject(media)
|
||||
return encoder.makeData()
|
||||
}
|
||||
try container.encode(mediaData, forKey: .media)
|
||||
try container.encode(self.items, forKey: .items)
|
||||
try container.encode(self.alternativeLanguageCodes, forKey: .alternativeLanguageCodes)
|
||||
}
|
||||
|
||||
public static func ==(lhs: BotPreview, rhs: BotPreview) -> Bool {
|
||||
if lhs === rhs {
|
||||
return true
|
||||
}
|
||||
if !areMediaArraysEqual(lhs.media, rhs.media) {
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
if lhs.alternativeLanguageCodes != rhs.alternativeLanguageCodes {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
@ -8,11 +8,12 @@ public extension Stories {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case discriminator = "tt"
|
||||
case peerId = "peerId"
|
||||
case language = "language"
|
||||
}
|
||||
|
||||
case myStories
|
||||
case peer(PeerId)
|
||||
case botPreview(PeerId)
|
||||
case botPreview(id: PeerId, language: String?)
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
@ -23,7 +24,7 @@ public extension Stories {
|
||||
case 1:
|
||||
self = .peer(try container.decode(PeerId.self, forKey: .peerId))
|
||||
case 2:
|
||||
self = .botPreview(try container.decode(PeerId.self, forKey: .peerId))
|
||||
self = .botPreview(id: try container.decode(PeerId.self, forKey: .peerId), language: try container.decodeIfPresent(String.self, forKey: .language))
|
||||
default:
|
||||
self = .myStories
|
||||
}
|
||||
@ -38,9 +39,10 @@ public extension Stories {
|
||||
case let .peer(peerId):
|
||||
try container.encode(1 as Int32, forKey: .discriminator)
|
||||
try container.encode(peerId, forKey: .peerId)
|
||||
case let .botPreview(peerId):
|
||||
case let .botPreview(peerId, language):
|
||||
try container.encode(2 as Int32, forKey: .discriminator)
|
||||
try container.encode(peerId, forKey: .peerId)
|
||||
try container.encodeIfPresent(language, forKey: .language)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -406,13 +408,15 @@ final class PendingStoryManager {
|
||||
|
||||
let toPeerId: PeerId
|
||||
var isBotPreview = false
|
||||
var botPreviewLanguage: String?
|
||||
switch firstItem.target {
|
||||
case .myStories:
|
||||
toPeerId = self.accountPeerId
|
||||
case let .peer(peerId):
|
||||
toPeerId = peerId
|
||||
case let .botPreview(peerId):
|
||||
case let .botPreview(peerId, language):
|
||||
toPeerId = peerId
|
||||
botPreviewLanguage = language
|
||||
isBotPreview = true
|
||||
}
|
||||
|
||||
@ -427,6 +431,7 @@ final class PendingStoryManager {
|
||||
revalidationContext: self.revalidationContext,
|
||||
auxiliaryMethods: self.auxiliaryMethods,
|
||||
toPeerId: toPeerId,
|
||||
language: botPreviewLanguage,
|
||||
stableId: stableId,
|
||||
media: firstItem.media,
|
||||
mediaAreas: firstItem.mediaAreas,
|
||||
|
@ -1270,6 +1270,7 @@ func _internal_uploadBotPreviewImpl(
|
||||
revalidationContext: MediaReferenceRevalidationContext,
|
||||
auxiliaryMethods: AccountAuxiliaryMethods,
|
||||
toPeerId: PeerId,
|
||||
language: String?,
|
||||
stableId: Int32,
|
||||
media: Media,
|
||||
mediaAreas: [MediaArea],
|
||||
@ -1300,46 +1301,59 @@ func _internal_uploadBotPreviewImpl(
|
||||
return postbox.transaction { transaction -> Signal<StoryUploadResult, NoError> in
|
||||
switch content.content {
|
||||
case let .media(inputMedia, _):
|
||||
return network.request(Api.functions.bots.addPreviewMedia(bot: inputUser, media: inputMedia))
|
||||
return network.request(Api.functions.bots.addPreviewMedia(bot: inputUser, langCode: language ?? "", media: inputMedia))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.MessageMedia?, NoError> in
|
||||
|> `catch` { _ -> Signal<Api.BotPreviewMedia?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { resultMedia -> Signal<StoryUploadResult, NoError> in
|
||||
return postbox.transaction { transaction -> StoryUploadResult in
|
||||
var currentState: Stories.LocalState
|
||||
if let value = transaction.getLocalStoryState()?.get(Stories.LocalState.self) {
|
||||
currentState = value
|
||||
} else {
|
||||
currentState = Stories.LocalState(items: [])
|
||||
}
|
||||
if let index = currentState.items.firstIndex(where: { $0.stableId == stableId }) {
|
||||
currentState.items.remove(at: index)
|
||||
transaction.setLocalStoryState(state: CodableEntry(currentState))
|
||||
}
|
||||
|
||||
if let resultMediaValue = textMediaAndExpirationTimerFromApiMedia(resultMedia, toPeerId).media {
|
||||
applyMediaResourceChanges(from: originalMedia, to: resultMediaValue, postbox: postbox, force: originalMedia is TelegramMediaFile && resultMediaValue is TelegramMediaFile)
|
||||
|> mapToSignal { resultPreviewMedia -> Signal<StoryUploadResult, NoError> in
|
||||
guard let resultPreviewMedia else {
|
||||
return .single(.completed(nil))
|
||||
}
|
||||
switch resultPreviewMedia {
|
||||
case let .botPreviewMedia(date, resultMedia):
|
||||
return postbox.transaction { transaction -> StoryUploadResult in
|
||||
var currentState: Stories.LocalState
|
||||
if let value = transaction.getLocalStoryState()?.get(Stories.LocalState.self) {
|
||||
currentState = value
|
||||
} else {
|
||||
currentState = Stories.LocalState(items: [])
|
||||
}
|
||||
if let index = currentState.items.firstIndex(where: { $0.stableId == stableId }) {
|
||||
currentState.items.remove(at: index)
|
||||
transaction.setLocalStoryState(state: CodableEntry(currentState))
|
||||
}
|
||||
|
||||
transaction.updatePeerCachedData(peerIds: Set([toPeerId]), update: { _, current in
|
||||
guard var current = current as? CachedUserData else {
|
||||
return current
|
||||
if let resultMediaValue = textMediaAndExpirationTimerFromApiMedia(resultMedia, toPeerId).media {
|
||||
applyMediaResourceChanges(from: originalMedia, to: resultMediaValue, postbox: postbox, force: originalMedia is TelegramMediaFile && resultMediaValue is TelegramMediaFile)
|
||||
|
||||
let addedItem = CachedUserData.BotPreview.Item(media: resultMediaValue, timestamp: date)
|
||||
|
||||
if language == nil {
|
||||
transaction.updatePeerCachedData(peerIds: Set([toPeerId]), update: { _, current in
|
||||
guard var current = current as? CachedUserData else {
|
||||
return current
|
||||
}
|
||||
guard let currentBotPreview = current.botPreview else {
|
||||
return current
|
||||
}
|
||||
var items = currentBotPreview.items
|
||||
if let index = items.firstIndex(where: { $0.media.id == resultMediaValue.id }) {
|
||||
items.remove(at: index)
|
||||
}
|
||||
items.insert(addedItem, at: 0)
|
||||
let botPreview = CachedUserData.BotPreview(items: items, alternativeLanguageCodes: currentBotPreview.alternativeLanguageCodes)
|
||||
current = current.withUpdatedBotPreview(botPreview)
|
||||
return current
|
||||
})
|
||||
}
|
||||
guard let currentBotPreview = current.botPreview else {
|
||||
return current
|
||||
}
|
||||
var media = currentBotPreview.media
|
||||
if let index = media.firstIndex(where: { $0.id == resultMediaValue.id }) {
|
||||
media.remove(at: index)
|
||||
}
|
||||
media.insert(resultMediaValue, at: 0)
|
||||
let botPreview = CachedUserData.BotPreview(media: media)
|
||||
current = current.withUpdatedBotPreview(botPreview)
|
||||
return current
|
||||
})
|
||||
stateManager.injectBotPreviewUpdates(updates: [
|
||||
.added(peerId: toPeerId, language: language, item: addedItem)
|
||||
])
|
||||
}
|
||||
|
||||
return .completed(nil)
|
||||
}
|
||||
|
||||
return .completed(nil)
|
||||
}
|
||||
}
|
||||
default:
|
||||
@ -1354,13 +1368,79 @@ func _internal_uploadBotPreviewImpl(
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_deleteBotPreviews(account: Account, peerId: PeerId, ids: [MediaId]) -> Signal<Never, NoError> {
|
||||
func _internal_deleteBotPreviews(account: Account, peerId: PeerId, language: String?, media: [Media]) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> (Api.InputUser?, [Api.InputMedia]) in
|
||||
guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputUser) else {
|
||||
return (nil, [])
|
||||
}
|
||||
|
||||
var inputMedia: [Api.InputMedia] = []
|
||||
for item in media {
|
||||
if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource {
|
||||
inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
|
||||
inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
|
||||
} else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource {
|
||||
inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil))
|
||||
}
|
||||
}
|
||||
if language == nil {
|
||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current -> CachedPeerData? in
|
||||
guard var current = current as? CachedUserData else {
|
||||
return current
|
||||
}
|
||||
guard let currentBotPreview = current.botPreview else {
|
||||
return current
|
||||
}
|
||||
var items = currentBotPreview.items
|
||||
|
||||
items = items.filter({ item in
|
||||
guard let id = item.media.id else {
|
||||
return false
|
||||
}
|
||||
return !media.contains(where: { $0.id == id })
|
||||
})
|
||||
let botPreview = CachedUserData.BotPreview(items: items, alternativeLanguageCodes: currentBotPreview.alternativeLanguageCodes)
|
||||
current = current.withUpdatedBotPreview(botPreview)
|
||||
return current
|
||||
})
|
||||
}
|
||||
|
||||
return (inputPeer, inputMedia)
|
||||
}
|
||||
|> mapToSignal { inputPeer, inputMedia -> Signal<Never, NoError> in
|
||||
guard let inputPeer else {
|
||||
return .complete()
|
||||
}
|
||||
|
||||
account.stateManager.injectBotPreviewUpdates(updates: [
|
||||
.deleted(peerId: peerId, language: language, ids: media.compactMap(\.id))
|
||||
])
|
||||
|
||||
return account.network.request(Api.functions.bots.deletePreviewMedia(bot: inputPeer, langCode: language ?? "", media: inputMedia))
|
||||
|> `catch` { _ -> Signal<Api.Bool, NoError> in
|
||||
return .single(.boolFalse)
|
||||
}
|
||||
|> mapToSignal { _ -> Signal<Never, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_deleteBotPreviewsLanguage(account: Account, peerId: PeerId, language: String, media: [Media]) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> (Api.InputUser?, [Api.InputMedia]) in
|
||||
guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputUser) else {
|
||||
return (nil, [])
|
||||
}
|
||||
|
||||
var inputMedia: [Api.InputMedia] = []
|
||||
for item in media {
|
||||
if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource {
|
||||
inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
|
||||
inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
|
||||
} else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource {
|
||||
inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil))
|
||||
}
|
||||
}
|
||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current -> CachedPeerData? in
|
||||
guard var current = current as? CachedUserData else {
|
||||
return current
|
||||
@ -1368,29 +1448,11 @@ func _internal_deleteBotPreviews(account: Account, peerId: PeerId, ids: [MediaId
|
||||
guard let currentBotPreview = current.botPreview else {
|
||||
return current
|
||||
}
|
||||
var media = currentBotPreview.media
|
||||
|
||||
for item in media {
|
||||
guard let id = item.id else {
|
||||
continue
|
||||
}
|
||||
if ids.contains(id) {
|
||||
if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource {
|
||||
inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
|
||||
inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
|
||||
} else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource {
|
||||
inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil))
|
||||
}
|
||||
}
|
||||
var alternativeLanguageCodes = currentBotPreview.alternativeLanguageCodes
|
||||
alternativeLanguageCodes = alternativeLanguageCodes.filter { item in
|
||||
return item != language
|
||||
}
|
||||
|
||||
media = media.filter({ item in
|
||||
guard let id = item.id else {
|
||||
return false
|
||||
}
|
||||
return !ids.contains(id)
|
||||
})
|
||||
let botPreview = CachedUserData.BotPreview(media: media)
|
||||
let botPreview = CachedUserData.BotPreview(items: currentBotPreview.items, alternativeLanguageCodes: alternativeLanguageCodes)
|
||||
current = current.withUpdatedBotPreview(botPreview)
|
||||
return current
|
||||
})
|
||||
@ -1402,7 +1464,11 @@ func _internal_deleteBotPreviews(account: Account, peerId: PeerId, ids: [MediaId
|
||||
return .complete()
|
||||
}
|
||||
|
||||
return account.network.request(Api.functions.bots.deletePreviewMedia(bot: inputPeer, media: inputMedia))
|
||||
account.stateManager.injectBotPreviewUpdates(updates: [
|
||||
.deleted(peerId: peerId, language: language, ids: media.compactMap(\.id))
|
||||
])
|
||||
|
||||
return account.network.request(Api.functions.bots.deletePreviewMedia(bot: inputPeer, langCode: language, media: inputMedia))
|
||||
|> `catch` { _ -> Signal<Api.Bool, NoError> in
|
||||
return .single(.boolFalse)
|
||||
}
|
||||
@ -1623,8 +1689,8 @@ func _internal_checkStoriesUploadAvailability(account: Account, target: Stories.
|
||||
return .inputPeerSelf
|
||||
case let .peer(peerId):
|
||||
return transaction.getPeer(peerId).flatMap(apiInputPeer)
|
||||
case let .botPreview(peerId):
|
||||
return transaction.getPeer(peerId).flatMap(apiInputPeer)
|
||||
case .botPreview:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|> mapToSignal { inputPeer -> Signal<StoriesUploadAvailability, NoError> in
|
||||
|
@ -12,6 +12,11 @@ enum InternalStoryUpdate {
|
||||
case updateMyReaction(peerId: PeerId, id: Int32, reaction: MessageReaction.Reaction?)
|
||||
}
|
||||
|
||||
enum InternalBotPreviewUpdate {
|
||||
case added(peerId: PeerId, language: String?, item: CachedUserData.BotPreview.Item)
|
||||
case deleted(peerId: PeerId, language: String?, ids: [MediaId])
|
||||
}
|
||||
|
||||
public final class EngineStoryItem: Equatable {
|
||||
public final class Views: Equatable {
|
||||
public let seenCount: Int
|
||||
@ -563,8 +568,19 @@ public struct StoryListContextState: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct Language: Equatable {
|
||||
public let id: String
|
||||
public let name: String
|
||||
|
||||
public init(id: String, name: String) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
public var peerReference: PeerReference?
|
||||
public var items: [Item]
|
||||
public var availableLanguages: [Language]
|
||||
public var pinnedIds: [Int32]
|
||||
public var totalCount: Int
|
||||
public var loadMoreToken: AnyHashable?
|
||||
@ -575,6 +591,7 @@ public struct StoryListContextState: Equatable {
|
||||
public init(
|
||||
peerReference: PeerReference?,
|
||||
items: [Item],
|
||||
availableLanguages: [Language],
|
||||
pinnedIds: [Int32],
|
||||
totalCount: Int,
|
||||
loadMoreToken: AnyHashable?,
|
||||
@ -585,6 +602,7 @@ public struct StoryListContextState: Equatable {
|
||||
) {
|
||||
self.peerReference = peerReference
|
||||
self.items = items
|
||||
self.availableLanguages = availableLanguages
|
||||
self.pinnedIds = pinnedIds
|
||||
self.totalCount = totalCount
|
||||
self.loadMoreToken = loadMoreToken
|
||||
@ -633,7 +651,7 @@ public final class PeerStoryListContext: StoryListContext {
|
||||
self.peerId = peerId
|
||||
self.isArchived = isArchived
|
||||
|
||||
self.stateValue = State(peerReference: nil, items: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false)
|
||||
self.stateValue = State(peerReference: nil, items: [], availableLanguages: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false)
|
||||
|
||||
let _ = (account.postbox.transaction { transaction -> (PeerReference?, [State.Item], [Int32], Int, [MediaId: TelegramMediaFile], Bool) in
|
||||
let key = ValueBoxKey(length: 8 + 1)
|
||||
@ -723,7 +741,7 @@ public final class PeerStoryListContext: StoryListContext {
|
||||
return
|
||||
}
|
||||
|
||||
var updatedState = State(peerReference: peerReference, items: items, pinnedIds: pinnedIds, totalCount: totalCount, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: hasCache, allEntityFiles: allEntityFiles, isLoading: false)
|
||||
var updatedState = State(peerReference: peerReference, items: items, availableLanguages: [], pinnedIds: pinnedIds, totalCount: totalCount, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: hasCache, allEntityFiles: allEntityFiles, isLoading: false)
|
||||
updatedState.items.sort(by: { lhs, rhs in
|
||||
let lhsPinned = updatedState.pinnedIds.firstIndex(of: lhs.storyItem.id)
|
||||
let rhsPinned = updatedState.pinnedIds.firstIndex(of: rhs.storyItem.id)
|
||||
@ -746,6 +764,7 @@ public final class PeerStoryListContext: StoryListContext {
|
||||
|
||||
deinit {
|
||||
self.requestDisposable?.dispose()
|
||||
self.updatesDisposable?.dispose()
|
||||
}
|
||||
|
||||
func loadMore(completion: (() -> Void)?) {
|
||||
@ -1313,7 +1332,7 @@ public final class SearchStoryListContext: StoryListContext {
|
||||
self.account = account
|
||||
self.source = source
|
||||
|
||||
self.stateValue = State(peerReference: nil, items: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(""), isCached: false, hasCache: false, allEntityFiles: [:], isLoading: false)
|
||||
self.stateValue = State(peerReference: nil, items: [], availableLanguages: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(""), isCached: false, hasCache: false, allEntityFiles: [:], isLoading: false)
|
||||
self.statePromise.set(.single(self.stateValue))
|
||||
|
||||
self.loadMore(completion: nil)
|
||||
@ -2078,6 +2097,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
|
||||
private let account: Account
|
||||
private let engine: TelegramEngine
|
||||
private let peerId: EnginePeer.Id
|
||||
private let language: String?
|
||||
private let isArchived: Bool
|
||||
|
||||
private let statePromise = Promise<State>()
|
||||
@ -2093,6 +2113,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
|
||||
private var isLoadingMore: Bool = false
|
||||
private var requestDisposable: Disposable?
|
||||
private var updatesDisposable: Disposable?
|
||||
private var eventsDisposable: Disposable?
|
||||
private let reorderDisposable = MetaDisposable()
|
||||
|
||||
private var completionCallbacksByToken: [AnyHashable: [() -> Void]] = [:]
|
||||
@ -2102,36 +2123,305 @@ public final class BotPreviewStoryListContext: StoryListContext {
|
||||
private var idMapping: [MediaId: Int32] = [:]
|
||||
private var reverseIdMapping: [Int32: MediaId] = [:]
|
||||
|
||||
init(queue: Queue, account: Account, engine: TelegramEngine, peerId: EnginePeer.Id) {
|
||||
private var localItems: [State.Item] = []
|
||||
private var remoteItems: [State.Item] = []
|
||||
|
||||
init(queue: Queue, account: Account, engine: TelegramEngine, peerId: EnginePeer.Id, language: String?, assumeEmpty: Bool) {
|
||||
self.queue = queue
|
||||
self.account = account
|
||||
self.engine = engine
|
||||
self.peerId = peerId
|
||||
self.language = language
|
||||
|
||||
let isArchived = false
|
||||
|
||||
self.isArchived = isArchived
|
||||
|
||||
self.stateValue = State(peerReference: nil, items: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false)
|
||||
self.stateValue = State(peerReference: nil, items: [], availableLanguages: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false)
|
||||
|
||||
let localStateKey: PostboxViewKey = .storiesState(key: .local)
|
||||
|
||||
self.requestDisposable = (combineLatest(queue: queue,
|
||||
engine.data.subscribe(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.BotPreview(id: peerId)
|
||||
),
|
||||
account.postbox.combinedView(keys: [localStateKey])
|
||||
)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] peerAndBotPreview, combinedView in
|
||||
if let language {
|
||||
let _ = (account.postbox.transaction { transaction -> Peer? in
|
||||
return transaction.getPeer(peerId)
|
||||
}
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] peer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.stateValue = State(
|
||||
peerReference: peer.flatMap(PeerReference.init),
|
||||
items: [],
|
||||
availableLanguages: [],
|
||||
pinnedIds: [],
|
||||
totalCount: 0,
|
||||
loadMoreToken: AnyHashable(0),
|
||||
isCached: assumeEmpty,
|
||||
hasCache: assumeEmpty,
|
||||
allEntityFiles: [:],
|
||||
isLoading: !assumeEmpty
|
||||
)
|
||||
|
||||
self.loadLanguage(language: language, assumeEmpty: assumeEmpty)
|
||||
})
|
||||
} else {
|
||||
self.requestDisposable = (combineLatest(queue: queue,
|
||||
engine.data.subscribe(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.BotPreview(id: peerId),
|
||||
TelegramEngine.EngineData.Item.Configuration.LocalizationList()
|
||||
),
|
||||
account.postbox.combinedView(keys: [
|
||||
localStateKey
|
||||
])
|
||||
)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] peerAndBotPreview, combinedView in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let (peer, botPreview, localizationList) = peerAndBotPreview
|
||||
|
||||
var items: [State.Item] = []
|
||||
var availableLanguages: [StoryListContextState.Language] = []
|
||||
|
||||
if let stateView = combinedView.views[localStateKey] as? StoryStatesView, let localState = stateView.value?.get(Stories.LocalState.self) {
|
||||
for item in localState.items.reversed() {
|
||||
let mappedId: Int32
|
||||
if let current = self.pendingIdMapping[item.stableId] {
|
||||
mappedId = current
|
||||
} else {
|
||||
mappedId = self.nextId
|
||||
self.nextId += 1
|
||||
self.pendingIdMapping[item.stableId] = mappedId
|
||||
}
|
||||
if case let .botPreview(itemPeerId, itemLanguage) = item.target, itemPeerId == peerId, itemLanguage == language {
|
||||
items.append(State.Item(
|
||||
id: StoryId(peerId: peerId, id: mappedId),
|
||||
storyItem: EngineStoryItem(
|
||||
id: mappedId,
|
||||
timestamp: 0,
|
||||
expirationTimestamp: Int32.max,
|
||||
media: EngineMedia(item.media),
|
||||
alternativeMedia: nil,
|
||||
mediaAreas: [],
|
||||
text: "",
|
||||
entities: [],
|
||||
views: nil,
|
||||
privacy: nil,
|
||||
isPinned: false,
|
||||
isExpired: false,
|
||||
isPublic: false,
|
||||
isPending: true,
|
||||
isCloseFriends: false,
|
||||
isContacts: false,
|
||||
isSelectedContacts: false,
|
||||
isForwardingDisabled: false,
|
||||
isEdited: false,
|
||||
isMy: false,
|
||||
myReaction: nil,
|
||||
forwardInfo: nil,
|
||||
author: nil
|
||||
),
|
||||
peer: nil
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let botPreview {
|
||||
for item in botPreview.items {
|
||||
guard let mediaId = item.media.id else {
|
||||
continue
|
||||
}
|
||||
|
||||
let id: Int32
|
||||
if let current = self.idMapping[mediaId] {
|
||||
id = current
|
||||
} else {
|
||||
id = self.nextId
|
||||
self.nextId += 1
|
||||
self.idMapping[mediaId] = id
|
||||
self.reverseIdMapping[id] = mediaId
|
||||
}
|
||||
|
||||
items.append(State.Item(
|
||||
id: StoryId(peerId: peerId, id: id),
|
||||
storyItem: EngineStoryItem(
|
||||
id: id,
|
||||
timestamp: item.timestamp,
|
||||
expirationTimestamp: Int32.max,
|
||||
media: EngineMedia(item.media),
|
||||
alternativeMedia: nil,
|
||||
mediaAreas: [],
|
||||
text: "",
|
||||
entities: [],
|
||||
views: nil,
|
||||
privacy: nil,
|
||||
isPinned: false,
|
||||
isExpired: false,
|
||||
isPublic: false,
|
||||
isPending: false,
|
||||
isCloseFriends: false,
|
||||
isContacts: false,
|
||||
isSelectedContacts: false,
|
||||
isForwardingDisabled: false,
|
||||
isEdited: false,
|
||||
isMy: false,
|
||||
myReaction: nil,
|
||||
forwardInfo: nil,
|
||||
author: nil
|
||||
),
|
||||
peer: nil
|
||||
))
|
||||
}
|
||||
|
||||
for id in botPreview.alternativeLanguageCodes {
|
||||
inner: for localization in localizationList.availableOfficialLocalizations {
|
||||
if localization.languageCode == id {
|
||||
availableLanguages.append(StoryListContextState.Language(
|
||||
id: localization.languageCode,
|
||||
name: localization.title
|
||||
))
|
||||
break inner
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.stateValue = State(
|
||||
peerReference: (peer?._asPeer()).flatMap(PeerReference.init),
|
||||
items: items,
|
||||
availableLanguages: availableLanguages,
|
||||
pinnedIds: [],
|
||||
totalCount: items.count,
|
||||
loadMoreToken: nil,
|
||||
isCached: botPreview != nil,
|
||||
hasCache: botPreview != nil,
|
||||
allEntityFiles: [:],
|
||||
isLoading: botPreview == nil
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.requestDisposable?.dispose()
|
||||
self.updatesDisposable?.dispose()
|
||||
self.eventsDisposable?.dispose()
|
||||
self.reorderDisposable.dispose()
|
||||
}
|
||||
|
||||
func loadMore(completion: (() -> Void)?) {
|
||||
}
|
||||
|
||||
private func loadLanguage(language: String, assumeEmpty: Bool) {
|
||||
let account = self.account
|
||||
let peerId = self.peerId
|
||||
let signal: Signal<(CachedUserData.BotPreview?, Peer?), NoError> = (self.account.postbox.transaction { transaction -> Peer? in
|
||||
return transaction.getPeer(peerId)
|
||||
}
|
||||
|> mapToSignal { peer -> Signal<(CachedUserData.BotPreview?, Peer?), NoError> in
|
||||
guard let peer, let inputUser = apiInputUser(peer) else {
|
||||
return .single((nil, nil))
|
||||
}
|
||||
return _internal_requestBotPreview(network: account.network, peerId: peerId, inputUser: inputUser, language: language)
|
||||
|> map { botPreview in
|
||||
return (botPreview, peer)
|
||||
}
|
||||
})
|
||||
|
||||
self.requestDisposable?.dispose()
|
||||
self.requestDisposable = (signal
|
||||
|> deliverOn(self.queue)).startStrict(next: { [weak self] botPreview, peer in
|
||||
guard let self, let peer else {
|
||||
return
|
||||
}
|
||||
|
||||
var items: [State.Item] = []
|
||||
|
||||
if let botPreview {
|
||||
for item in botPreview.items {
|
||||
guard let mediaId = item.media.id else {
|
||||
continue
|
||||
}
|
||||
|
||||
let id: Int32
|
||||
if let current = self.idMapping[mediaId] {
|
||||
id = current
|
||||
} else {
|
||||
id = self.nextId
|
||||
self.nextId += 1
|
||||
self.idMapping[mediaId] = id
|
||||
self.reverseIdMapping[id] = mediaId
|
||||
}
|
||||
|
||||
items.append(State.Item(
|
||||
id: StoryId(peerId: peerId, id: id),
|
||||
storyItem: EngineStoryItem(
|
||||
id: id,
|
||||
timestamp: item.timestamp,
|
||||
expirationTimestamp: Int32.max,
|
||||
media: EngineMedia(item.media),
|
||||
alternativeMedia: nil,
|
||||
mediaAreas: [],
|
||||
text: "",
|
||||
entities: [],
|
||||
views: nil,
|
||||
privacy: nil,
|
||||
isPinned: false,
|
||||
isExpired: false,
|
||||
isPublic: false,
|
||||
isPending: false,
|
||||
isCloseFriends: false,
|
||||
isContacts: false,
|
||||
isSelectedContacts: false,
|
||||
isForwardingDisabled: false,
|
||||
isEdited: false,
|
||||
isMy: false,
|
||||
myReaction: nil,
|
||||
forwardInfo: nil,
|
||||
author: nil
|
||||
),
|
||||
peer: nil
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
self.remoteItems = items
|
||||
self.stateValue = State(
|
||||
peerReference: PeerReference(peer),
|
||||
items: items,
|
||||
availableLanguages: [],
|
||||
pinnedIds: [],
|
||||
totalCount: items.count,
|
||||
loadMoreToken: nil,
|
||||
isCached: botPreview != nil,
|
||||
hasCache: botPreview != nil,
|
||||
allEntityFiles: [:],
|
||||
isLoading: botPreview == nil
|
||||
)
|
||||
|
||||
if botPreview != nil {
|
||||
self.beginUpdates(language: language)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func beginUpdates(language: String) {
|
||||
let localStateKey: PostboxViewKey = .storiesState(key: .local)
|
||||
|
||||
self.updatesDisposable?.dispose()
|
||||
self.updatesDisposable = (self.account.postbox.combinedView(keys: [
|
||||
localStateKey
|
||||
])
|
||||
|> deliverOn(self.queue)).startStrict(next: { [weak self] combinedView in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let (peer, botPreview) = peerAndBotPreview
|
||||
|
||||
var items: [State.Item] = []
|
||||
|
||||
if let stateView = combinedView.views[localStateKey] as? StoryStatesView, let localState = stateView.value?.get(Stories.LocalState.self) {
|
||||
for item in localState.items.reversed() {
|
||||
let mappedId: Int32
|
||||
@ -2142,7 +2432,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
|
||||
self.nextId += 1
|
||||
self.pendingIdMapping[item.stableId] = mappedId
|
||||
}
|
||||
if case .botPreview(peerId) = item.target {
|
||||
if case let .botPreview(itemPeerId, itemLanguage) = item.target, itemPeerId == self.peerId, itemLanguage == language {
|
||||
items.append(State.Item(
|
||||
id: StoryId(peerId: peerId, id: mappedId),
|
||||
storyItem: EngineStoryItem(
|
||||
@ -2176,130 +2466,160 @@ public final class BotPreviewStoryListContext: StoryListContext {
|
||||
}
|
||||
}
|
||||
|
||||
if let botPreview {
|
||||
for media in botPreview.media {
|
||||
guard let mediaId = media.id else {
|
||||
continue
|
||||
}
|
||||
|
||||
let id: Int32
|
||||
if let current = self.idMapping[mediaId] {
|
||||
id = current
|
||||
} else {
|
||||
id = self.nextId
|
||||
self.nextId += 1
|
||||
self.idMapping[mediaId] = id
|
||||
self.reverseIdMapping[id] = mediaId
|
||||
}
|
||||
|
||||
items.append(State.Item(
|
||||
id: StoryId(peerId: peerId, id: id),
|
||||
storyItem: EngineStoryItem(
|
||||
id: id,
|
||||
timestamp: 0,
|
||||
expirationTimestamp: Int32.max,
|
||||
media: EngineMedia(media),
|
||||
alternativeMedia: nil,
|
||||
mediaAreas: [],
|
||||
text: "",
|
||||
entities: [],
|
||||
views: nil,
|
||||
privacy: nil,
|
||||
isPinned: false,
|
||||
isExpired: false,
|
||||
isPublic: false,
|
||||
isPending: false,
|
||||
isCloseFriends: false,
|
||||
isContacts: false,
|
||||
isSelectedContacts: false,
|
||||
isForwardingDisabled: false,
|
||||
isEdited: false,
|
||||
isMy: false,
|
||||
myReaction: nil,
|
||||
forwardInfo: nil,
|
||||
author: nil
|
||||
),
|
||||
peer: nil
|
||||
))
|
||||
if self.localItems != items {
|
||||
self.localItems = items
|
||||
|
||||
if self.stateValue.peerReference != nil {
|
||||
self.pushLanguageItems()
|
||||
}
|
||||
}
|
||||
|
||||
self.stateValue = State(
|
||||
peerReference: (peer?._asPeer()).flatMap(PeerReference.init),
|
||||
items: items,
|
||||
pinnedIds: [],
|
||||
totalCount: items.count,
|
||||
loadMoreToken: nil,
|
||||
isCached: botPreview != nil,
|
||||
hasCache: botPreview != nil,
|
||||
allEntityFiles: [:],
|
||||
isLoading: botPreview == nil
|
||||
)
|
||||
})
|
||||
|
||||
self.eventsDisposable?.dispose()
|
||||
self.eventsDisposable = (self.account.stateManager.botPreviewUpdates
|
||||
|> deliverOn(self.queue)).startStrict(next: { [weak self] events in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
var remoteItems = self.remoteItems
|
||||
for event in events {
|
||||
switch event {
|
||||
case let .added(peerId, language, item):
|
||||
if let mediaId = item.media.id, self.peerId == peerId, self.language == language {
|
||||
let id: Int32
|
||||
if let current = self.idMapping[mediaId] {
|
||||
id = current
|
||||
} else {
|
||||
id = self.nextId
|
||||
self.nextId += 1
|
||||
self.idMapping[mediaId] = id
|
||||
self.reverseIdMapping[id] = mediaId
|
||||
}
|
||||
|
||||
let mappedItem = State.Item(
|
||||
id: StoryId(peerId: peerId, id: id),
|
||||
storyItem: EngineStoryItem(
|
||||
id: id,
|
||||
timestamp: item.timestamp,
|
||||
expirationTimestamp: Int32.max,
|
||||
media: EngineMedia(item.media),
|
||||
alternativeMedia: nil,
|
||||
mediaAreas: [],
|
||||
text: "",
|
||||
entities: [],
|
||||
views: nil,
|
||||
privacy: nil,
|
||||
isPinned: false,
|
||||
isExpired: false,
|
||||
isPublic: false,
|
||||
isPending: false,
|
||||
isCloseFriends: false,
|
||||
isContacts: false,
|
||||
isSelectedContacts: false,
|
||||
isForwardingDisabled: false,
|
||||
isEdited: false,
|
||||
isMy: false,
|
||||
myReaction: nil,
|
||||
forwardInfo: nil,
|
||||
author: nil
|
||||
),
|
||||
peer: nil
|
||||
)
|
||||
|
||||
if let index = remoteItems.firstIndex(where: { $0.storyItem.media.id == item.media.id }) {
|
||||
remoteItems[index] = mappedItem
|
||||
} else {
|
||||
remoteItems.insert(mappedItem, at: 0)
|
||||
}
|
||||
}
|
||||
case let .deleted(peerId, language, ids):
|
||||
if self.peerId == peerId && self.language == language {
|
||||
remoteItems = remoteItems.filter { item in
|
||||
guard let id = item.storyItem.media.id else {
|
||||
return false
|
||||
}
|
||||
return !ids.contains(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.remoteItems != remoteItems {
|
||||
self.remoteItems = remoteItems
|
||||
self.pushLanguageItems()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.requestDisposable?.dispose()
|
||||
self.updatesDisposable?.dispose()
|
||||
self.reorderDisposable.dispose()
|
||||
private func pushLanguageItems() {
|
||||
var items = self.localItems
|
||||
items.append(contentsOf: self.remoteItems)
|
||||
self.stateValue = State(
|
||||
peerReference: self.stateValue.peerReference,
|
||||
items: items,
|
||||
availableLanguages: [],
|
||||
pinnedIds: [],
|
||||
totalCount: items.count,
|
||||
loadMoreToken: nil,
|
||||
isCached: true,
|
||||
hasCache: true,
|
||||
allEntityFiles: [:],
|
||||
isLoading: false
|
||||
)
|
||||
}
|
||||
|
||||
func loadMore(completion: (() -> Void)?) {
|
||||
}
|
||||
|
||||
func reorderItems(ids: [StoryId]) {
|
||||
func reorderItems(media: [Media]) {
|
||||
let peerId = self.peerId
|
||||
let idMapping = self.idMapping
|
||||
let reverseIdMapping = self.reverseIdMapping
|
||||
let language = self.language
|
||||
|
||||
let _ = (self.account.postbox.transaction({ transaction -> (Api.InputUser?, [Api.InputMedia]) in
|
||||
let inputUser = transaction.getPeer(peerId).flatMap(apiInputUser)
|
||||
|
||||
var inputMedia: [Api.InputMedia] = []
|
||||
transaction.updatePeerCachedData(peerIds: Set([self.peerId]), update: { _, current in
|
||||
guard var current = current as? CachedUserData else {
|
||||
for item in media {
|
||||
if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource {
|
||||
inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
|
||||
inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
|
||||
} else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource {
|
||||
inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil))
|
||||
}
|
||||
}
|
||||
|
||||
if language == nil {
|
||||
transaction.updatePeerCachedData(peerIds: Set([self.peerId]), update: { _, current in
|
||||
guard var current = current as? CachedUserData else {
|
||||
return current
|
||||
}
|
||||
guard let currentBotPreview = current.botPreview else {
|
||||
return current
|
||||
}
|
||||
|
||||
var items: [CachedUserData.BotPreview.Item] = []
|
||||
|
||||
var seenIds = Set<MediaId>()
|
||||
for item in media {
|
||||
guard let mediaId = item.id else {
|
||||
continue
|
||||
}
|
||||
if let index = currentBotPreview.items.firstIndex(where: { $0.media.id == mediaId }) {
|
||||
seenIds.insert(mediaId)
|
||||
items.append(currentBotPreview.items[index])
|
||||
}
|
||||
}
|
||||
|
||||
for item in currentBotPreview.items {
|
||||
guard let id = item.media.id else {
|
||||
continue
|
||||
}
|
||||
if !seenIds.contains(id) {
|
||||
items.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
let botPreview = CachedUserData.BotPreview(items: items, alternativeLanguageCodes: currentBotPreview.alternativeLanguageCodes)
|
||||
current = current.withUpdatedBotPreview(botPreview)
|
||||
return current
|
||||
}
|
||||
guard let currentBotPreview = current.botPreview else {
|
||||
return current
|
||||
}
|
||||
|
||||
var media: [Media] = []
|
||||
media = []
|
||||
|
||||
var seenIds = Set<Int32>()
|
||||
for id in ids {
|
||||
guard let mediaId = reverseIdMapping[id.id] else {
|
||||
continue
|
||||
}
|
||||
if let index = currentBotPreview.media.firstIndex(where: { $0.id == mediaId }) {
|
||||
seenIds.insert(id.id)
|
||||
media.append(currentBotPreview.media[index])
|
||||
}
|
||||
}
|
||||
|
||||
for item in currentBotPreview.media {
|
||||
guard let id = item.id, let storyId = idMapping[id] else {
|
||||
continue
|
||||
}
|
||||
if !seenIds.contains(storyId) {
|
||||
media.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
for item in media {
|
||||
if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource {
|
||||
inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
|
||||
inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
|
||||
} else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource {
|
||||
inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil))
|
||||
}
|
||||
}
|
||||
|
||||
let botPreview = CachedUserData.BotPreview(media: media)
|
||||
current = current.withUpdatedBotPreview(botPreview)
|
||||
return current
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (inputUser, inputMedia)
|
||||
})
|
||||
@ -2307,7 +2627,37 @@ public final class BotPreviewStoryListContext: StoryListContext {
|
||||
guard let self, let inputUser else {
|
||||
return
|
||||
}
|
||||
let signal = self.account.network.request(Api.functions.bots.reorderPreviewMedias(bot: inputUser, order: inputMedia))
|
||||
|
||||
if language != nil {
|
||||
var updatedItems: [State.Item] = []
|
||||
|
||||
var seenIds = Set<MediaId>()
|
||||
for item in media {
|
||||
guard let mediaId = item.id else {
|
||||
continue
|
||||
}
|
||||
if let index = self.remoteItems.firstIndex(where: { $0.storyItem.media.id == mediaId }) {
|
||||
seenIds.insert(mediaId)
|
||||
updatedItems.append(self.remoteItems[index])
|
||||
}
|
||||
}
|
||||
|
||||
for item in self.remoteItems {
|
||||
guard let id = item.storyItem.media.id else {
|
||||
continue
|
||||
}
|
||||
if !seenIds.contains(id) {
|
||||
updatedItems.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
if self.remoteItems != updatedItems {
|
||||
self.remoteItems = updatedItems
|
||||
self.pushLanguageItems()
|
||||
}
|
||||
}
|
||||
|
||||
let signal = self.account.network.request(Api.functions.bots.reorderPreviewMedias(bot: inputUser, langCode: language ?? "", order: inputMedia))
|
||||
self.reorderDisposable.set(signal.startStrict())
|
||||
})
|
||||
}
|
||||
@ -2322,11 +2672,15 @@ public final class BotPreviewStoryListContext: StoryListContext {
|
||||
private let queue: Queue
|
||||
private let impl: QueueLocalObject<Impl>
|
||||
|
||||
public init(account: Account, engine: TelegramEngine, peerId: EnginePeer.Id) {
|
||||
public let language: String?
|
||||
|
||||
public init(account: Account, engine: TelegramEngine, peerId: EnginePeer.Id, language: String?, assumeEmpty: Bool) {
|
||||
self.language = language
|
||||
|
||||
let queue = Queue.mainQueue()
|
||||
self.queue = queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return Impl(queue: queue, account: account, engine: engine, peerId: peerId)
|
||||
return Impl(queue: queue, account: account, engine: engine, peerId: peerId, language: language, assumeEmpty: assumeEmpty)
|
||||
})
|
||||
}
|
||||
|
||||
@ -2336,9 +2690,9 @@ public final class BotPreviewStoryListContext: StoryListContext {
|
||||
}
|
||||
}
|
||||
|
||||
public func reorderItems(ids: [StoryId]) {
|
||||
public func reorderItems(media: [Media]) {
|
||||
self.impl.with { impl in
|
||||
impl.reorderItems(ids: ids)
|
||||
impl.reorderItems(media: media)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1373,8 +1373,12 @@ public extension TelegramEngine {
|
||||
return _internal_getStoryById(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, peerId: peerId, id: id)
|
||||
}
|
||||
|
||||
public func deleteBotPreviews(peerId: EnginePeer.Id, ids: [MediaId]) -> Signal<Never, NoError> {
|
||||
return _internal_deleteBotPreviews(account: self.account, peerId: peerId, ids: ids)
|
||||
public func deleteBotPreviews(peerId: EnginePeer.Id, language: String?, media: [Media]) -> Signal<Never, NoError> {
|
||||
return _internal_deleteBotPreviews(account: self.account, peerId: peerId, language: language, media: media)
|
||||
}
|
||||
|
||||
public func deleteBotPreviewsLanguage(peerId: EnginePeer.Id, language: String, media: [Media]) -> Signal<Never, NoError> {
|
||||
return _internal_deleteBotPreviewsLanguage(account: self.account, peerId: peerId, language: language, media: media)
|
||||
}
|
||||
|
||||
public func synchronouslyIsMessageDeletedInteractively(ids: [EngineMessage.Id]) -> [EngineMessage.Id] {
|
||||
|
@ -199,16 +199,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee
|
||||
|
||||
let botPreview: Signal<CachedUserData.BotPreview?, NoError>
|
||||
if let user = maybePeer as? TelegramUser, let _ = user.botInfo {
|
||||
botPreview = network.request(Api.functions.bots.getPreviewMedias(bot: inputUser))
|
||||
|> `catch` { _ -> Signal<[Api.MessageMedia], NoError> in
|
||||
return .single([])
|
||||
}
|
||||
|> map { result -> CachedUserData.BotPreview? in
|
||||
return CachedUserData.BotPreview(media: result.compactMap { item -> Media? in
|
||||
let value = textMediaAndExpirationTimerFromApiMedia(item, user.id)
|
||||
return value.media
|
||||
})
|
||||
}
|
||||
botPreview = _internal_requestBotPreview(network: network, peerId: user.id, inputUser: inputUser, language: nil)
|
||||
} else {
|
||||
botPreview = .single(nil)
|
||||
}
|
||||
@ -843,3 +834,33 @@ extension CachedPeerAutoremoveTimeout.Value {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_requestBotPreview(network: Network, peerId: PeerId, inputUser: Api.InputUser, language: String?) -> Signal<CachedUserData.BotPreview?, NoError> {
|
||||
return network.request(Api.functions.bots.getPreviewInfo(bot: inputUser, langCode: language ?? ""))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.bots.PreviewInfo?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> map { result -> CachedUserData.BotPreview? in
|
||||
guard let result else {
|
||||
return nil
|
||||
}
|
||||
switch result {
|
||||
case let .previewInfo(media, langCodes):
|
||||
return CachedUserData.BotPreview(
|
||||
items: media.compactMap { item -> CachedUserData.BotPreview.Item? in
|
||||
switch item {
|
||||
case let .botPreviewMedia(date, media):
|
||||
let value = textMediaAndExpirationTimerFromApiMedia(media, peerId)
|
||||
if let media = value.media {
|
||||
return CachedUserData.BotPreview.Item(media: media, timestamp: date)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
},
|
||||
alternativeLanguageCodes: langCodes
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -458,6 +458,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Chat/ChatSendStarsScreen",
|
||||
"//submodules/TelegramUI/Components/MinimizedContainer",
|
||||
"//submodules/TelegramUI/Components/SpaceWarpView",
|
||||
"//submodules/TelegramUI/Components/MiniAppListScreen",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
"//build-system:ios_sim_arm64": [],
|
||||
|
@ -1841,7 +1841,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
}
|
||||
} else if case .tap = gesture {
|
||||
item.controllerInteraction.clickThroughMessage()
|
||||
item.controllerInteraction.clickThroughMessage(self.view, location)
|
||||
} else if case .doubleTap = gesture {
|
||||
if canAddMessageReactions(message: item.message) {
|
||||
item.controllerInteraction.updateMessageReaction(item.message, .default, false, nil)
|
||||
|
@ -4576,7 +4576,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
}
|
||||
}
|
||||
} else if case .tap = gesture {
|
||||
item.controllerInteraction.clickThroughMessage()
|
||||
item.controllerInteraction.clickThroughMessage(self.view, location)
|
||||
} else if case .doubleTap = gesture {
|
||||
if canAddMessageReactions(message: item.message) {
|
||||
item.controllerInteraction.updateMessageReaction(item.message, .default, false, nil)
|
||||
|
@ -961,7 +961,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco
|
||||
break
|
||||
}
|
||||
} else if case .tap = gesture {
|
||||
self.item?.controllerInteraction.clickThroughMessage()
|
||||
self.item?.controllerInteraction.clickThroughMessage(self.view, location)
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
@ -1586,7 +1586,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
return
|
||||
}
|
||||
|
||||
self.item?.controllerInteraction.clickThroughMessage()
|
||||
self.item?.controllerInteraction.clickThroughMessage(self.view, location)
|
||||
case .longTap, .doubleTap, .secondaryTap:
|
||||
break
|
||||
case .hold:
|
||||
|
@ -1405,7 +1405,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
}
|
||||
} else if case .tap = gesture {
|
||||
self.item?.controllerInteraction.clickThroughMessage()
|
||||
self.item?.controllerInteraction.clickThroughMessage(self.view, location)
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
@ -306,7 +306,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
if let context = self?.context, let navigationController = self?.getNavigationController() {
|
||||
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: threadId, messageId: nil, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: false, keepStack: .always).startStandalone()
|
||||
}
|
||||
}, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false
|
||||
}, tapMessage: nil, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false
|
||||
}, requestMessageActionCallback: { [weak self] messageId, _, _, _ in
|
||||
guard let self else {
|
||||
return
|
||||
|
@ -418,7 +418,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
|
||||
}, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in
|
||||
}, navigateToThreadMessage: { _, _, _ in
|
||||
}, tapMessage: { _ in
|
||||
}, clickThroughMessage: {
|
||||
}, clickThroughMessage: { _, _ in
|
||||
}, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in
|
||||
return false
|
||||
}, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
|
||||
|
@ -180,7 +180,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
||||
public let navigateToMessageStandalone: (MessageId) -> Void
|
||||
public let navigateToThreadMessage: (PeerId, Int64, MessageId?) -> Void
|
||||
public let tapMessage: ((Message) -> Void)?
|
||||
public let clickThroughMessage: () -> Void
|
||||
public let clickThroughMessage: (UIView?, CGPoint?) -> Void
|
||||
public let toggleMessagesSelection: ([MessageId], Bool) -> Void
|
||||
public let sendCurrentMessage: (Bool, ChatSendMessageEffect?) -> Void
|
||||
public let sendMessage: (String) -> Void
|
||||
@ -309,7 +309,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
||||
navigateToMessageStandalone: @escaping (MessageId) -> Void,
|
||||
navigateToThreadMessage: @escaping (PeerId, Int64, MessageId?) -> Void,
|
||||
tapMessage: ((Message) -> Void)?,
|
||||
clickThroughMessage: @escaping () -> Void,
|
||||
clickThroughMessage: @escaping (UIView?, CGPoint?) -> Void,
|
||||
toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void,
|
||||
sendCurrentMessage: @escaping (Bool, ChatSendMessageEffect?) -> Void,
|
||||
sendMessage: @escaping (String) -> Void,
|
||||
|
@ -13,25 +13,27 @@ public final class EmptyStateIndicatorComponent: Component {
|
||||
public let context: AccountContext
|
||||
public let theme: PresentationTheme
|
||||
public let animationName: String?
|
||||
public let title: String
|
||||
public let title: String?
|
||||
public let text: String
|
||||
public let actionTitle: String?
|
||||
public let fitToHeight: Bool
|
||||
public let action: () -> Void
|
||||
public let additionalActionTitle: String?
|
||||
public let additionalAction: () -> Void
|
||||
public let additionalActionSeparator: String?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
fitToHeight: Bool,
|
||||
animationName: String?,
|
||||
title: String,
|
||||
title: String?,
|
||||
text: String,
|
||||
actionTitle: String?,
|
||||
action: @escaping () -> Void,
|
||||
additionalActionTitle: String?,
|
||||
additionalAction: @escaping () -> Void
|
||||
additionalAction: @escaping () -> Void,
|
||||
additionalActionSeparator: String? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
@ -43,6 +45,7 @@ public final class EmptyStateIndicatorComponent: Component {
|
||||
self.action = action
|
||||
self.additionalActionTitle = additionalActionTitle
|
||||
self.additionalAction = additionalAction
|
||||
self.additionalActionSeparator = additionalActionSeparator
|
||||
}
|
||||
|
||||
public static func ==(lhs: EmptyStateIndicatorComponent, rhs: EmptyStateIndicatorComponent) -> Bool {
|
||||
@ -70,6 +73,9 @@ public final class EmptyStateIndicatorComponent: Component {
|
||||
if lhs.additionalActionTitle != rhs.additionalActionTitle {
|
||||
return false
|
||||
}
|
||||
if lhs.additionalActionSeparator != rhs.additionalActionSeparator {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -82,6 +88,9 @@ public final class EmptyStateIndicatorComponent: Component {
|
||||
private let text = ComponentView<Empty>()
|
||||
private var button: ComponentView<Empty>?
|
||||
private var additionalButton: ComponentView<Empty>?
|
||||
private var additionalSeparatorLeft: SimpleLayer?
|
||||
private var additionalSeparatorRight: SimpleLayer?
|
||||
private var additionalSeparatorText: ComponentView<Empty>?
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
@ -108,16 +117,20 @@ public final class EmptyStateIndicatorComponent: Component {
|
||||
containerSize: CGSize(width: 120.0, height: 120.0)
|
||||
)
|
||||
}
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0)
|
||||
)
|
||||
|
||||
var titleSize: CGSize?
|
||||
if let title = component.title {
|
||||
titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0)
|
||||
)
|
||||
}
|
||||
let textSize = self.text.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BalancedTextComponent(
|
||||
@ -203,19 +216,80 @@ public final class EmptyStateIndicatorComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
var additionalSeparatorTextSize: CGSize?
|
||||
if let additionalActionSeparator = component.additionalActionSeparator {
|
||||
let additionalSeparatorText: ComponentView<Empty>
|
||||
if let current = self.additionalSeparatorText {
|
||||
additionalSeparatorText = current
|
||||
} else {
|
||||
additionalSeparatorText = ComponentView()
|
||||
self.additionalSeparatorText = additionalSeparatorText
|
||||
}
|
||||
|
||||
let additionalSeparatorLeft: SimpleLayer
|
||||
if let current = self.additionalSeparatorLeft {
|
||||
additionalSeparatorLeft = current
|
||||
} else {
|
||||
additionalSeparatorLeft = SimpleLayer()
|
||||
self.additionalSeparatorLeft = additionalSeparatorLeft
|
||||
self.layer.addSublayer(additionalSeparatorLeft)
|
||||
}
|
||||
|
||||
let additionalSeparatorRight: SimpleLayer
|
||||
if let current = self.additionalSeparatorRight {
|
||||
additionalSeparatorRight = current
|
||||
} else {
|
||||
additionalSeparatorRight = SimpleLayer()
|
||||
self.additionalSeparatorRight = additionalSeparatorRight
|
||||
self.layer.addSublayer(additionalSeparatorRight)
|
||||
}
|
||||
|
||||
additionalSeparatorLeft.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
|
||||
additionalSeparatorRight.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
|
||||
|
||||
additionalSeparatorTextSize = additionalSeparatorText.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: additionalActionSeparator, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 100.0)
|
||||
)
|
||||
} else {
|
||||
if let additionalSeparatorLeft = self.additionalSeparatorLeft {
|
||||
self.additionalSeparatorLeft = nil
|
||||
additionalSeparatorLeft.removeFromSuperlayer()
|
||||
}
|
||||
if let additionalSeparatorRight = self.additionalSeparatorRight {
|
||||
self.additionalSeparatorRight = nil
|
||||
additionalSeparatorRight.removeFromSuperlayer()
|
||||
}
|
||||
if let additionalSeparatorText = self.additionalSeparatorText {
|
||||
self.additionalSeparatorText = nil
|
||||
additionalSeparatorText.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
let animationSpacing: CGFloat = 11.0
|
||||
let titleSpacing: CGFloat = 17.0
|
||||
let buttonSpacing: CGFloat = 21.0
|
||||
let additionalSeparatorHeight: CGFloat = 31.0
|
||||
|
||||
var totalHeight: CGFloat = 0.0
|
||||
|
||||
if let animationSize {
|
||||
totalHeight += animationSize.height + animationSpacing
|
||||
}
|
||||
totalHeight += titleSize.height + titleSpacing + textSize.height
|
||||
if let titleSize {
|
||||
totalHeight += titleSize.height + titleSpacing
|
||||
}
|
||||
totalHeight += textSize.height
|
||||
if let buttonSize {
|
||||
totalHeight += buttonSpacing + buttonSize.height
|
||||
}
|
||||
if let _ = additionalSeparatorTextSize {
|
||||
totalHeight += additionalSeparatorHeight
|
||||
}
|
||||
if let additionalButtonSize {
|
||||
totalHeight += buttonSpacing + additionalButtonSize.height
|
||||
}
|
||||
@ -234,7 +308,7 @@ public final class EmptyStateIndicatorComponent: Component {
|
||||
transition.setFrame(view: animationView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - animationSize.width) * 0.5), y: contentY), size: animationSize))
|
||||
contentY += animationSize.height + animationSpacing
|
||||
}
|
||||
if let titleView = self.title.view {
|
||||
if let titleSize, let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
@ -255,6 +329,25 @@ public final class EmptyStateIndicatorComponent: Component {
|
||||
transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) * 0.5), y: contentY), size: buttonSize))
|
||||
contentY += buttonSize.height + buttonSpacing
|
||||
}
|
||||
|
||||
if let additionalSeparatorTextSize, let additionalSeparatorText = self.additionalSeparatorText, let additionalSeparatorLeft = self.additionalSeparatorLeft, let additionalSeparatorRight = self.additionalSeparatorRight {
|
||||
let additionalSeparatorTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - additionalSeparatorTextSize.width) * 0.5), y: contentY), size: additionalSeparatorTextSize)
|
||||
if let additionalSeparatorTextView = additionalSeparatorText.view {
|
||||
if additionalSeparatorTextView.superview == nil {
|
||||
self.addSubview(additionalSeparatorTextView)
|
||||
}
|
||||
transition.setFrame(view: additionalSeparatorTextView, frame: additionalSeparatorTextFrame)
|
||||
}
|
||||
|
||||
let separatorWidth: CGFloat = 72.0
|
||||
let separatorSpacing: CGFloat = 10.0
|
||||
|
||||
transition.setFrame(layer: additionalSeparatorLeft, frame: CGRect(origin: CGPoint(x: additionalSeparatorTextFrame.minX - separatorSpacing - separatorWidth, y: additionalSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel)))
|
||||
transition.setFrame(layer: additionalSeparatorRight, frame: CGRect(origin: CGPoint(x: additionalSeparatorTextFrame.maxX + separatorSpacing, y: additionalSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel)))
|
||||
|
||||
contentY += additionalSeparatorHeight
|
||||
}
|
||||
|
||||
if let additionalButtonSize, let additionalButtonView = self.additionalButton?.view {
|
||||
if additionalButtonView.superview == nil {
|
||||
self.addSubview(additionalButtonView)
|
||||
|
39
submodules/TelegramUI/Components/MiniAppListScreen/BUILD
Normal file
39
submodules/TelegramUI/Components/MiniAppListScreen/BUILD
Normal 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",
|
||||
],
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
@ -1148,7 +1148,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
var botPreviewStoryListContext: StoryListContext?
|
||||
let hasBotPreviewItems: Signal<Bool, NoError>
|
||||
if case .bot = kind {
|
||||
let botPreviewStoryListContextValue = BotPreviewStoryListContext(account: context.account, engine: context.engine, peerId: peerId)
|
||||
let botPreviewStoryListContextValue = BotPreviewStoryListContext(account: context.account, engine: context.engine, peerId: peerId, language: nil, assumeEmpty: false)
|
||||
botPreviewStoryListContext = botPreviewStoryListContextValue
|
||||
hasBotPreviewItems = botPreviewStoryListContextValue.state
|
||||
|> map { state in
|
||||
@ -1307,7 +1307,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
|
||||
if let user = peerView.peers[peerView.peerId] as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), botInfo.flags.contains(.canEdit) {
|
||||
availablePanes?.insert(.botPreview, at: 0)
|
||||
} else if let cachedData = peerView.cachedData as? CachedUserData, let botPreview = cachedData.botPreview, !botPreview.media.isEmpty {
|
||||
} else if let cachedData = peerView.cachedData as? CachedUserData, let botPreview = cachedData.botPreview, !botPreview.items.isEmpty {
|
||||
availablePanes?.insert(.botPreview, at: 0)
|
||||
}
|
||||
}
|
||||
|
@ -3287,7 +3287,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}, navigateToMessage: { _, _, _ in
|
||||
}, navigateToMessageStandalone: { _ in
|
||||
}, navigateToThreadMessage: { _, _, _ in
|
||||
}, tapMessage: nil, clickThroughMessage: {
|
||||
}, tapMessage: nil, clickThroughMessage: { _, _ in
|
||||
}, toggleMessagesSelection: { [weak self] ids, value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -9864,7 +9864,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openBotPreviewEditor(target: .botPreview(self.peerId), source: result, transitionIn: (transitionView, transitionRect, transitionImage))
|
||||
|
||||
guard let pane = self.paneContainerNode.currentPane?.node as? PeerInfoStoryPaneNode else {
|
||||
return
|
||||
}
|
||||
|
||||
self.openBotPreviewEditor(target: .botPreview(id: self.peerId, language: pane.currentBotPreviewLanguage?.id), source: result, transitionIn: (transitionView, transitionRect, transitionImage))
|
||||
},
|
||||
dismissed: {},
|
||||
groupsPresented: {}
|
||||
@ -10939,6 +10944,23 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}
|
||||
})))
|
||||
|
||||
if let language = pane.currentBotPreviewLanguage {
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Delete \(language.name)", textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
||||
}, action: { [weak pane] _, a in
|
||||
if ignoreNextActions {
|
||||
return
|
||||
}
|
||||
ignoreNextActions = true
|
||||
a(.default)
|
||||
|
||||
if let pane {
|
||||
pane.presentDeleteBotPreviewLanguage()
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
let contextController = ContextController(presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
contextController.passthroughTouchEvent = { [weak self] sourceView, point in
|
||||
guard let strongSelf = self else {
|
||||
|
@ -46,6 +46,8 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/MediaEditorScreen",
|
||||
"//submodules/LocationUI",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/TelegramUI/Components/TabSelectorComponent",
|
||||
"//submodules/TelegramUI/Components/Settings/LanguageSelectionScreen",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -41,6 +41,8 @@ import Geocoding
|
||||
import ItemListUI
|
||||
import MultilineTextComponent
|
||||
import LocationUI
|
||||
import TabSelectorComponent
|
||||
import LanguageSelectionScreen
|
||||
|
||||
private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6)
|
||||
private let mediaBadgeTextColor = UIColor.white
|
||||
@ -1565,6 +1567,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
private var mapInfoNode: LocationInfoListItemNode?
|
||||
private var searchHeader: ComponentView<Empty>?
|
||||
|
||||
private var botPreviewLanguageTab: ComponentView<Empty>?
|
||||
private var botPreviewFooter: ComponentView<Empty>?
|
||||
|
||||
private var barBackgroundLayer: SimpleLayer?
|
||||
|
||||
private let itemGrid: SparseItemGrid
|
||||
@ -1638,7 +1643,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
|
||||
public var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||
public var tabBarOffset: CGFloat {
|
||||
return self.itemGrid.coveringInsetOffset
|
||||
if case .botPreview = self.scope {
|
||||
return 0.0
|
||||
} else {
|
||||
return self.itemGrid.coveringInsetOffset
|
||||
}
|
||||
}
|
||||
|
||||
private var currentListState: StoryListContext.State?
|
||||
@ -1646,6 +1655,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
private var hiddenMediaDisposable: Disposable?
|
||||
private let updateDisposable = MetaDisposable()
|
||||
|
||||
private var currentBotPreviewLanguages: [StoryListContext.State.Language] = []
|
||||
private var removedBotPreviewLanguages = Set<String>()
|
||||
|
||||
private var numberOfItemsToRequest: Int = 50
|
||||
private var isRequestingView: Bool = false
|
||||
private var isFirstHistoryView: Bool = true
|
||||
@ -1656,6 +1668,24 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
|
||||
public private(set) var calendarSource: SparseMessageCalendar?
|
||||
private var listSource: StoryListContext
|
||||
|
||||
private let maxBotPreviewCount: Int
|
||||
|
||||
private let defaultListSource: StoryListContext
|
||||
private var cachedListSources: [String: StoryListContext] = [:]
|
||||
|
||||
public var currentBotPreviewLanguage: (id: String, name: String)? {
|
||||
guard let listSource = self.listSource as? BotPreviewStoryListContext else {
|
||||
return nil
|
||||
}
|
||||
guard let id = listSource.language else {
|
||||
return nil
|
||||
}
|
||||
guard let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) else {
|
||||
return nil
|
||||
}
|
||||
return (language.id, language.name)
|
||||
}
|
||||
|
||||
public var openCurrentDate: (() -> Void)?
|
||||
public var paneDidScroll: (() -> Void)?
|
||||
@ -1723,11 +1753,18 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
case let .location(coordinates, venue):
|
||||
self.listSource = SearchStoryListContext(account: context.account, source: .mediaArea(.venue(coordinates: coordinates, venue: venue)))
|
||||
case let .botPreview(id):
|
||||
self.listSource = BotPreviewStoryListContext(account: context.account, engine: context.engine, peerId: id)
|
||||
self.listSource = BotPreviewStoryListContext(account: context.account, engine: context.engine, peerId: id, language: nil, assumeEmpty: false)
|
||||
}
|
||||
}
|
||||
self.defaultListSource = self.listSource
|
||||
self.calendarSource = nil
|
||||
|
||||
var maxBotPreviewCount = 10
|
||||
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["bot_preview_medias_max"] as? Double {
|
||||
maxBotPreviewCount = Int(value)
|
||||
}
|
||||
self.maxBotPreviewCount = maxBotPreviewCount
|
||||
|
||||
super.init()
|
||||
|
||||
if case .peer = self.scope {
|
||||
@ -2689,6 +2726,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
|
||||
self.listDisposable?.dispose()
|
||||
self.listDisposable = nil
|
||||
|
||||
if reloadAtTop {
|
||||
self.didUpdateItemsOnce = false
|
||||
}
|
||||
|
||||
self.listDisposable = (state
|
||||
|> deliverOn(queue)).startStrict(next: { [weak self] state in
|
||||
@ -2700,7 +2741,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
if state.totalCount == 0 {
|
||||
if case .botPreview = self.scope {
|
||||
//TODO:localize
|
||||
title = "no preview added"
|
||||
if state.isLoading {
|
||||
title = "loading"
|
||||
} else {
|
||||
title = "no preview added"
|
||||
}
|
||||
} else {
|
||||
title = ""
|
||||
}
|
||||
@ -2738,9 +2783,24 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
return
|
||||
}
|
||||
|
||||
var botPreviewLanguages = self.currentBotPreviewLanguages
|
||||
for language in state.availableLanguages {
|
||||
if !botPreviewLanguages.contains(where: { $0.id == language.id }) && !self.removedBotPreviewLanguages.contains(language.id) {
|
||||
botPreviewLanguages.append(language)
|
||||
}
|
||||
}
|
||||
botPreviewLanguages.sort(by: { $0.name < $1.name })
|
||||
self.currentListState = state
|
||||
|
||||
self.updateItemsFromState(state: state, firstTime: firstTime, reloadAtTop: reloadAtTop, synchronous: synchronous, animated: false)
|
||||
|
||||
if self.currentBotPreviewLanguages != botPreviewLanguages || reloadAtTop {
|
||||
self.currentBotPreviewLanguages = botPreviewLanguages
|
||||
if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams {
|
||||
self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: synchronous, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
firstTime = false
|
||||
self.isRequestingView = false
|
||||
}
|
||||
@ -2853,7 +2913,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
|
||||
if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams {
|
||||
var gridSnapshot: UIView?
|
||||
if reloadAtTop {
|
||||
if case .botPreview = scope {
|
||||
} else if reloadAtTop {
|
||||
gridSnapshot = self.itemGrid.view.snapshotView(afterScreenUpdates: false)
|
||||
}
|
||||
self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: transition, animateGridItems: animated)
|
||||
@ -2971,43 +3032,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
|
||||
public func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
||||
return nil
|
||||
|
||||
/*var foundItemLayer: SparseItemGridLayer?
|
||||
self.itemGrid.forEachVisibleItem { item in
|
||||
guard let itemLayer = item.layer as? ItemLayer else {
|
||||
return
|
||||
}
|
||||
if let item = itemLayer.item, item.message.id == messageId {
|
||||
foundItemLayer = itemLayer
|
||||
}
|
||||
}
|
||||
if let itemLayer = foundItemLayer {
|
||||
let itemFrame = self.view.convert(self.itemGrid.frameForItem(layer: itemLayer), from: self.itemGrid.view)
|
||||
let proxyNode = ASDisplayNode()
|
||||
proxyNode.frame = itemFrame
|
||||
if let contents = itemLayer.getContents() {
|
||||
if let image = contents as? UIImage {
|
||||
proxyNode.contents = image.cgImage
|
||||
} else {
|
||||
proxyNode.contents = contents
|
||||
}
|
||||
}
|
||||
proxyNode.isHidden = true
|
||||
self.addSubnode(proxyNode)
|
||||
|
||||
let escapeNotification = EscapeNotification {
|
||||
proxyNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
return (proxyNode, proxyNode.bounds, {
|
||||
let view = UIView()
|
||||
view.frame = proxyNode.frame
|
||||
view.layer.contents = proxyNode.layer.contents
|
||||
escapeNotification.keep()
|
||||
return (view, nil)
|
||||
})
|
||||
}
|
||||
return nil*/
|
||||
}
|
||||
|
||||
public func extractPendingStoryTransitionView() -> UIView? {
|
||||
@ -3294,29 +3318,29 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
|
||||
var mappedItemIds: [MediaId] = []
|
||||
var mappedMedia: [Media] = []
|
||||
if let items = self.items {
|
||||
mappedItemIds = items.items.compactMap { item -> MediaId? in
|
||||
mappedMedia = items.items.compactMap { item -> Media? in
|
||||
guard let item = item as? VisualMediaItem else {
|
||||
return nil
|
||||
}
|
||||
if ids.contains(item.story.id) {
|
||||
return item.story.media.id
|
||||
return item.story.media._asMedia()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if mappedItemIds.isEmpty {
|
||||
if mappedMedia.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let title: String
|
||||
if mappedItemIds.count == 1 {
|
||||
if mappedMedia.count == 1 {
|
||||
title = "Delete 1 Preview?"
|
||||
} else {
|
||||
title = "Delete \(mappedItemIds.count) Previews?"
|
||||
title = "Delete \(mappedMedia.count) Previews?"
|
||||
}
|
||||
|
||||
controller.setItemGroups([
|
||||
@ -3328,12 +3352,15 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let listSource = self.listSource as? BotPreviewStoryListContext else {
|
||||
return
|
||||
}
|
||||
|
||||
if let parentController = self.parentController as? PeerInfoScreen {
|
||||
parentController.cancelItemSelection()
|
||||
}
|
||||
|
||||
let _ = self.context.engine.messages.deleteBotPreviews(peerId: peerId, ids: mappedItemIds).startStandalone()
|
||||
let _ = self.context.engine.messages.deleteBotPreviews(peerId: peerId, language: listSource.language, media: mappedMedia).startStandalone()
|
||||
})
|
||||
]),
|
||||
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
||||
@ -3349,8 +3376,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
}
|
||||
|
||||
private func gridScrollingOffsetUpdated(transition: ContainedViewLayoutTransition) {
|
||||
if let _ = self.mapNode, let currentParams = self.currentParams {
|
||||
self.updateMapLayout(size: currentParams.size, topInset: currentParams.topInset, bottomInset: currentParams.bottomInset, deviceMetrics: currentParams.deviceMetrics, transition: transition)
|
||||
if let currentParams = self.currentParams {
|
||||
if let _ = self.mapNode {
|
||||
self.updateMapLayout(size: currentParams.size, topInset: currentParams.topInset, bottomInset: currentParams.bottomInset, deviceMetrics: currentParams.deviceMetrics, transition: transition)
|
||||
}
|
||||
if case .botPreview = self.scope, self.canManageStories {
|
||||
self.updateBotPreviewLanguageTab(size: currentParams.size, topInset: currentParams.topInset, transition: transition)
|
||||
self.updateBotPreviewFooter(size: currentParams.size, bottomInset: currentParams.bottomInset, transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3504,6 +3537,150 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
}
|
||||
}
|
||||
|
||||
private func updateBotPreviewLanguageTab(size: CGSize, topInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
guard case .botPreview = self.scope, self.canManageStories else {
|
||||
return
|
||||
}
|
||||
|
||||
let botPreviewLanguageTab: ComponentView<Empty>
|
||||
if let current = self.botPreviewLanguageTab {
|
||||
botPreviewLanguageTab = current
|
||||
} else {
|
||||
botPreviewLanguageTab = ComponentView()
|
||||
self.botPreviewLanguageTab = botPreviewLanguageTab
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
var languageItems: [TabSelectorComponent.Item] = []
|
||||
languageItems.append(TabSelectorComponent.Item(
|
||||
id: AnyHashable("_main"),
|
||||
title: "Main"
|
||||
))
|
||||
for language in self.currentBotPreviewLanguages {
|
||||
languageItems.append(TabSelectorComponent.Item(
|
||||
id: AnyHashable(language.id),
|
||||
title: language.name
|
||||
))
|
||||
}
|
||||
languageItems.append(TabSelectorComponent.Item(
|
||||
id: AnyHashable("_add"),
|
||||
title: "+ Add Language"
|
||||
))
|
||||
var selectedLanguageId = "_main"
|
||||
if let listSource = self.listSource as? BotPreviewStoryListContext, let language = listSource.language {
|
||||
selectedLanguageId = language
|
||||
}
|
||||
|
||||
let botPreviewLanguageTabSize = botPreviewLanguageTab.update(
|
||||
transition: ComponentTransition(transition),
|
||||
component: AnyComponent(TabSelectorComponent(
|
||||
colors: TabSelectorComponent.Colors(
|
||||
foreground: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.8),
|
||||
selection: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05)
|
||||
),
|
||||
customLayout: TabSelectorComponent.CustomLayout(
|
||||
font: Font.medium(14.0),
|
||||
spacing: 9.0,
|
||||
verticalInset: 11.0
|
||||
),
|
||||
items: languageItems,
|
||||
selectedId: AnyHashable(selectedLanguageId),
|
||||
setSelectedId: { [weak self] id in
|
||||
guard let self, let id = id.base as? String else {
|
||||
return
|
||||
}
|
||||
if id == "_add" {
|
||||
self.presentAddBotPreviewLanguage()
|
||||
} else if id == "_main" {
|
||||
self.setBotPreviewLanguage(id: nil, assumeEmpty: false)
|
||||
} else if let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) {
|
||||
self.setBotPreviewLanguage(id: language.id, assumeEmpty: false)
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width, height: 44.0)
|
||||
)
|
||||
var botPreviewLanguageTabFrame = CGRect(origin: CGPoint(x: floor((size.width - botPreviewLanguageTabSize.width) * 0.5), y: topInset - 11.0), size: botPreviewLanguageTabSize)
|
||||
|
||||
let effectiveScrollingOffset: CGFloat
|
||||
effectiveScrollingOffset = self.itemGrid.scrollingOffset
|
||||
botPreviewLanguageTabFrame.origin.y -= effectiveScrollingOffset
|
||||
|
||||
if let botPreviewLanguageTabView = botPreviewLanguageTab.view {
|
||||
if botPreviewLanguageTabView.superview == nil {
|
||||
self.view.addSubview(botPreviewLanguageTabView)
|
||||
}
|
||||
transition.updateFrame(view: botPreviewLanguageTabView, frame: botPreviewLanguageTabFrame)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateBotPreviewFooter(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
if let items = self.items, !items.items.isEmpty {
|
||||
var botPreviewFooterTransition = ComponentTransition(transition)
|
||||
let botPreviewFooter: ComponentView<Empty>
|
||||
if let current = self.botPreviewFooter {
|
||||
botPreviewFooter = current
|
||||
} else {
|
||||
botPreviewFooterTransition = .immediate
|
||||
botPreviewFooter = ComponentView()
|
||||
self.botPreviewFooter = botPreviewFooter
|
||||
}
|
||||
|
||||
var isMainLanguage = true
|
||||
let text: String
|
||||
if let listSource = self.listSource as? BotPreviewStoryListContext, let id = listSource.language, let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) {
|
||||
isMainLanguage = false
|
||||
text = "This preview will be displayed for all users who have \(language.name) set as their language."
|
||||
} else {
|
||||
text = "This preview will be shown by default. You can also add translations into specific languages."
|
||||
}
|
||||
|
||||
let botPreviewFooterSize = botPreviewFooter.update(
|
||||
transition: botPreviewFooterTransition,
|
||||
component: AnyComponent(EmptyStateIndicatorComponent(
|
||||
context: self.context,
|
||||
theme: self.presentationData.theme,
|
||||
fitToHeight: true,
|
||||
animationName: nil,
|
||||
title: nil,
|
||||
text: text,
|
||||
actionTitle: "Add Preview",
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.emptyAction?()
|
||||
},
|
||||
additionalActionTitle: isMainLanguage ? "Create a Translation" : nil,
|
||||
additionalAction: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if isMainLanguage {
|
||||
self.presentAddBotPreviewLanguage()
|
||||
}
|
||||
},
|
||||
additionalActionSeparator: isMainLanguage ? "or" : nil
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width, height: 1000.0)
|
||||
)
|
||||
let botPreviewFooterFrame = CGRect(origin: CGPoint(x: floor((size.width - botPreviewFooterSize.width) * 0.5), y: self.itemGrid.contentBottomOffset - botPreviewFooterSize.height - bottomInset), size: botPreviewFooterSize)
|
||||
if let botPreviewFooterView = botPreviewFooter.view {
|
||||
if botPreviewFooterView.superview == nil {
|
||||
self.view.addSubview(botPreviewFooterView)
|
||||
}
|
||||
botPreviewFooterTransition.setFrame(view: botPreviewFooterView, frame: botPreviewFooterFrame)
|
||||
}
|
||||
} else {
|
||||
if let botPreviewFooter = self.botPreviewFooter {
|
||||
self.botPreviewFooter = nil
|
||||
botPreviewFooter.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
||||
self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: synchronous, transition: transition, animateGridItems: false)
|
||||
}
|
||||
@ -3555,7 +3732,19 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
transition.updateFrame(layer: barBackgroundLayer, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: gridTopInset)))
|
||||
}
|
||||
|
||||
let defaultBottomInset = bottomInset
|
||||
var bottomInset = bottomInset
|
||||
|
||||
if case .botPreview = self.scope, self.canManageStories {
|
||||
updateBotPreviewLanguageTab(size: size, topInset: topInset, transition: transition)
|
||||
gridTopInset += 50.0
|
||||
|
||||
updateBotPreviewFooter(size: size, bottomInset: defaultBottomInset, transition: transition)
|
||||
if let botPreviewFooterView = self.botPreviewFooter?.view {
|
||||
bottomInset += 18.0 + botPreviewFooterView.bounds.height
|
||||
}
|
||||
}
|
||||
|
||||
if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories, case let .peer(peerId, _, isArchived) = self.scope {
|
||||
let selectionPanel: ComponentView<Empty>
|
||||
var selectionPanelTransition = ComponentTransition(transition)
|
||||
@ -3816,6 +4005,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
self.emptyStateView = emptyStateView
|
||||
}
|
||||
//TODO:localize
|
||||
|
||||
var isMainLanguage = true
|
||||
if let listSource = self.listSource as? BotPreviewStoryListContext, let _ = listSource.language {
|
||||
isMainLanguage = false
|
||||
}
|
||||
|
||||
let emptyStateSize = emptyStateView.update(
|
||||
transition: emptyStateTransition,
|
||||
component: AnyComponent(EmptyStateIndicatorComponent(
|
||||
@ -3824,7 +4019,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
fitToHeight: self.isProfileEmbedded,
|
||||
animationName: nil,
|
||||
title: "No Preview",
|
||||
text: "Upload screenshots and video demos for your Mini App that will be visible for your users here.",
|
||||
text: "Upload up to \(self.maxBotPreviewCount) screenshots and video demos for your mini app.",
|
||||
actionTitle: self.canManageStories ? "Add Preview" : nil,
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
@ -3832,8 +4027,15 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
}
|
||||
self.emptyAction?()
|
||||
},
|
||||
additionalActionTitle: nil,
|
||||
additionalAction: {}
|
||||
additionalActionTitle: self.canManageStories ? (isMainLanguage ? "Create a Translation" : "Delete this Translation") : nil,
|
||||
additionalAction: {
|
||||
if isMainLanguage {
|
||||
self.presentAddBotPreviewLanguage()
|
||||
} else {
|
||||
self.presentDeleteBotPreviewLanguage()
|
||||
}
|
||||
},
|
||||
additionalActionSeparator: self.canManageStories ? "or" : nil
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width, height: size.height - gridTopInset - bottomInset)
|
||||
@ -3860,7 +4062,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
if self.isProfileEmbedded, case .botPreview = self.scope {
|
||||
backgroundColor = presentationData.theme.list.blocksBackgroundColor
|
||||
} else if self.isProfileEmbedded {
|
||||
backgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
backgroundColor = presentationData.theme.list.blocksBackgroundColor
|
||||
} else {
|
||||
backgroundColor = presentationData.theme.list.blocksBackgroundColor
|
||||
}
|
||||
@ -3876,18 +4078,28 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
self.emptyStateView = nil
|
||||
|
||||
if let emptyStateComponentView = emptyStateView.view {
|
||||
subTransition.setAlpha(view: emptyStateComponentView, alpha: 0.0, completion: { [weak emptyStateComponentView] _ in
|
||||
emptyStateComponentView?.removeFromSuperview()
|
||||
})
|
||||
if self.didUpdateItemsOnce {
|
||||
subTransition.setAlpha(view: emptyStateComponentView, alpha: 0.0, completion: { [weak emptyStateComponentView] _ in
|
||||
emptyStateComponentView?.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
emptyStateComponentView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
if self.isProfileEmbedded {
|
||||
if self.isProfileEmbedded, case .botPreview = self.scope {
|
||||
subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor)
|
||||
} else if self.isProfileEmbedded {
|
||||
subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.plainBackgroundColor)
|
||||
} else {
|
||||
subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor)
|
||||
}
|
||||
} else {
|
||||
self.view.backgroundColor = .clear
|
||||
if self.isProfileEmbedded, case .botPreview = self.scope {
|
||||
self.view.backgroundColor = presentationData.theme.list.blocksBackgroundColor
|
||||
} else {
|
||||
self.view.backgroundColor = .clear
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3904,6 +4116,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
self.itemGrid.pinchEnabled = items.count > 2
|
||||
self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none, transition: animateGridItems ? .spring(duration: 0.35) : .immediate)
|
||||
}
|
||||
|
||||
if case .botPreview = self.scope, self.canManageStories {
|
||||
updateBotPreviewFooter(size: size, bottomInset: defaultBottomInset, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public func currentTopTimestamp() -> Int32? {
|
||||
@ -3992,12 +4208,109 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
return false
|
||||
}
|
||||
|
||||
var maxCount = 10
|
||||
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["bot_preview_medias_max"] as? Double {
|
||||
maxCount = Int(value)
|
||||
return items.count < self.maxBotPreviewCount
|
||||
}
|
||||
|
||||
private func presentAddBotPreviewLanguage() {
|
||||
self.parentController?.push(LanguageSelectionScreen(context: self.context, selectLocalization: { [weak self] info in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.addBotPreviewLanguage(language: StoryListContext.State.Language(id: info.languageCode, name: info.title))
|
||||
}))
|
||||
}
|
||||
|
||||
public func presentDeleteBotPreviewLanguage() {
|
||||
//TODO:localize
|
||||
self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: "Delete Translation", text: "Are you sure you want to delete this translation?", actions: [
|
||||
TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {
|
||||
}),
|
||||
TextAlertAction(type: .destructiveAction, title: "OK", action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let listSource = self.listSource as? BotPreviewStoryListContext, let language = listSource.language {
|
||||
self.deleteBotPreviewLanguage(id: language)
|
||||
}
|
||||
})
|
||||
], parseMarkdown: true), in: .window(.root))
|
||||
}
|
||||
|
||||
private func addBotPreviewLanguage(language: StoryListContext.State.Language) {
|
||||
var botPreviewLanguages = self.currentBotPreviewLanguages
|
||||
|
||||
var assumeEmpty = false
|
||||
if !botPreviewLanguages.contains(where: { $0.id == language.id}) {
|
||||
botPreviewLanguages.append(language)
|
||||
assumeEmpty = true
|
||||
}
|
||||
botPreviewLanguages.sort(by: { $0.name < $1.name })
|
||||
self.removedBotPreviewLanguages.remove(language.id)
|
||||
|
||||
if self.currentBotPreviewLanguages != botPreviewLanguages {
|
||||
self.currentBotPreviewLanguages = botPreviewLanguages
|
||||
if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams {
|
||||
self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
return items.count < maxCount
|
||||
self.setBotPreviewLanguage(id: language.id, assumeEmpty: assumeEmpty)
|
||||
}
|
||||
|
||||
private func deleteBotPreviewLanguage(id: String) {
|
||||
var botPreviewLanguages = self.currentBotPreviewLanguages
|
||||
|
||||
if let index = botPreviewLanguages.firstIndex(where: { $0.id == id}) {
|
||||
botPreviewLanguages.remove(at: index)
|
||||
}
|
||||
self.removedBotPreviewLanguages.insert(id)
|
||||
|
||||
guard case let .botPreview(peerId) = self.scope else {
|
||||
return
|
||||
}
|
||||
|
||||
var mappedMedia: [Media] = []
|
||||
if let items = self.items {
|
||||
mappedMedia = items.items.compactMap { item -> Media? in
|
||||
guard let item = item as? VisualMediaItem else {
|
||||
return nil
|
||||
}
|
||||
return item.story.media._asMedia()
|
||||
}
|
||||
}
|
||||
let _ = self.context.engine.messages.deleteBotPreviewsLanguage(peerId: peerId, language: id, media: mappedMedia).startStandalone()
|
||||
|
||||
if self.currentBotPreviewLanguages != botPreviewLanguages {
|
||||
self.currentBotPreviewLanguages = botPreviewLanguages
|
||||
if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams {
|
||||
self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
self.setBotPreviewLanguage(id: nil, assumeEmpty: false)
|
||||
}
|
||||
|
||||
private func setBotPreviewLanguage(id: String?, assumeEmpty: Bool) {
|
||||
guard case let .botPreview(peerId) = self.scope else {
|
||||
return
|
||||
}
|
||||
if let listSource = self.listSource as? BotPreviewStoryListContext, listSource.language == id {
|
||||
return
|
||||
}
|
||||
|
||||
if let id {
|
||||
if let cachedListSource = self.cachedListSources[id] {
|
||||
self.listSource = cachedListSource
|
||||
} else {
|
||||
let listSource = BotPreviewStoryListContext(account: self.context.account, engine: self.context.engine, peerId: peerId, language: id, assumeEmpty: assumeEmpty)
|
||||
self.listSource = listSource
|
||||
self.cachedListSources[id] = listSource
|
||||
}
|
||||
} else {
|
||||
self.listSource = self.defaultListSource
|
||||
}
|
||||
|
||||
self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: true)
|
||||
}
|
||||
|
||||
public func beginReordering() {
|
||||
@ -4027,7 +4340,17 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
if !isReordering, let reorderedIds = self.reorderedIds {
|
||||
self.reorderedIds = nil
|
||||
if case .botPreview = self.scope, let listSource = self.listSource as? BotPreviewStoryListContext {
|
||||
listSource.reorderItems(ids: reorderedIds)
|
||||
if let items = self.items {
|
||||
var reorderedMedia: [Media] = []
|
||||
|
||||
for id in reorderedIds {
|
||||
if let item = items.items.first(where: { ($0 as? VisualMediaItem)?.storyId == id }) as? VisualMediaItem {
|
||||
reorderedMedia.append(item.story.media._asMedia())
|
||||
}
|
||||
}
|
||||
|
||||
listSource.reorderItems(media: reorderedMedia)
|
||||
}
|
||||
} else if case let .peer(id, _, _) = self.scope, id == self.context.account.peerId, let items = self.items {
|
||||
var updatedPinnedIds: [Int32] = []
|
||||
for id in reorderedIds {
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 })
|
||||
}
|
||||
}
|
@ -1093,7 +1093,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate
|
||||
state.displayPatternPanel = false
|
||||
return state
|
||||
}, animated: true)
|
||||
}, clickThroughMessage: {
|
||||
}, clickThroughMessage: { _, _ in
|
||||
}, backgroundNode: self.backgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)
|
||||
return item
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ swift_library(
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/ComponentFlow",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -2,7 +2,429 @@ import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
|
||||
/*open class SpaceWarpView: UIView {
|
||||
private final class WarpPartView: UIView {
|
||||
let cloneView: PortalView
|
||||
|
||||
init?(contentView: PortalSourceView) {
|
||||
guard let cloneView = PortalView(matchPosition: false) else {
|
||||
return nil
|
||||
}
|
||||
self.cloneView = cloneView
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0)
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.addSubview(cloneView.view)
|
||||
contentView.addPortal(view: cloneView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(containerSize: CGSize, rect: CGRect, transition: ComponentTransition) {
|
||||
transition.setFrame(view: self.cloneView.view, frame: CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: CGSize(width: containerSize.width, height: containerSize.height)))
|
||||
}
|
||||
}
|
||||
|
||||
public var contentView: UIView {
|
||||
return self.contentViewImpl
|
||||
}
|
||||
|
||||
let contentViewImpl: PortalSourceView
|
||||
|
||||
private var warpViews: [WarpPartView] = []
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
self.contentViewImpl = PortalSourceView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.contentView)
|
||||
self.contentView.alpha = 0.1
|
||||
|
||||
for _ in 0 ..< 8 {
|
||||
if let warpView = WarpPartView(contentView: self.contentViewImpl) {
|
||||
self.warpViews.append(warpView)
|
||||
self.addSubview(warpView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func update(size: CGSize, warpHeight: CGFloat, transition: ComponentTransition) {
|
||||
transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let allItemsHeight = warpHeight * 0.5
|
||||
for i in 0 ..< self.warpViews.count {
|
||||
let itemHeight = warpHeight / CGFloat(self.warpViews.count)
|
||||
let itemFraction = CGFloat(i + 1) / CGFloat(self.warpViews.count)
|
||||
let _ = itemHeight
|
||||
|
||||
let da = CGFloat.pi * 0.5 / CGFloat(self.warpViews.count)
|
||||
let alpha = CGFloat.pi * 0.5 - itemFraction * CGFloat.pi * 0.5
|
||||
let endPoint = CGPoint(x: cos(alpha), y: sin(alpha))
|
||||
let prevAngle = alpha + da
|
||||
let prevPt = CGPoint(x: cos(prevAngle), y: sin(prevAngle))
|
||||
var angle: CGFloat
|
||||
angle = -atan2(endPoint.y - prevPt.y, endPoint.x - prevPt.x)
|
||||
|
||||
let itemLengthVector = CGPoint(x: endPoint.x - prevPt.x, y: endPoint.y - prevPt.y)
|
||||
let itemLength = sqrt(itemLengthVector.x * itemLengthVector.x + itemLengthVector.y * itemLengthVector.y) * warpHeight * 0.5
|
||||
let _ = itemLength
|
||||
|
||||
var transform: CATransform3D
|
||||
transform = CATransform3DIdentity
|
||||
transform.m34 = 1.0 / 240.0
|
||||
|
||||
transform = CATransform3DTranslate(transform, 0.0, prevPt.x * allItemsHeight, (1.0 - prevPt.y) * allItemsHeight)
|
||||
transform = CATransform3DRotate(transform, angle, 1.0, 0.0, 0.0)
|
||||
|
||||
let positionY = size.height - allItemsHeight + 4.0 + CGFloat(i) * itemLength
|
||||
let rect = CGRect(origin: CGPoint(x: 0.0, y: positionY), size: CGSize(width: size.width, height: itemLength))
|
||||
transition.setPosition(view: self.warpViews[i], position: CGPoint(x: rect.midX, y: 4.0))
|
||||
transition.setBounds(view: self.warpViews[i], bounds: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: itemLength)))
|
||||
transition.setTransform(view: self.warpViews[i], transform: transform)
|
||||
self.warpViews[i].update(containerSize: size, rect: rect, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return self.contentView.hitTest(point, with: event)
|
||||
}
|
||||
}*/
|
||||
|
||||
private extension CGPoint {
|
||||
static func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
|
||||
}
|
||||
|
||||
static func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
|
||||
}
|
||||
|
||||
static func *(lhs: CGPoint, rhs: CGFloat) -> CGPoint {
|
||||
return CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
private func length(_ v: CGPoint) -> CGFloat {
|
||||
return sqrt(v.x * v.x + v.y * v.y)
|
||||
}
|
||||
|
||||
private func normalize(_ v: CGPoint) -> CGPoint {
|
||||
let len = length(v)
|
||||
return CGPoint(x: v.x / len, y: v.y / len)
|
||||
}
|
||||
|
||||
private struct RippleParams {
|
||||
var amplitude: CGFloat
|
||||
var frequency: CGFloat
|
||||
var decay: CGFloat
|
||||
var speed: CGFloat
|
||||
|
||||
init(amplitude: CGFloat, frequency: CGFloat, decay: CGFloat, speed: CGFloat) {
|
||||
self.amplitude = amplitude
|
||||
self.frequency = frequency
|
||||
self.decay = decay
|
||||
self.speed = speed
|
||||
}
|
||||
}
|
||||
|
||||
private func transformCoordinate(
|
||||
position: CGPoint,
|
||||
origin: CGPoint,
|
||||
time: CGFloat,
|
||||
params: RippleParams
|
||||
) -> CGPoint {
|
||||
// The distance of the current pixel position from `origin`.
|
||||
let distance = length(position - origin)
|
||||
|
||||
if distance < 10.0 {
|
||||
return position
|
||||
}
|
||||
|
||||
// The amount of time it takes for the ripple to arrive at the current pixel position.
|
||||
let delay = distance / params.speed
|
||||
|
||||
// Adjust for delay, clamp to 0.
|
||||
var time = time
|
||||
time -= delay
|
||||
time = max(0.0, time)
|
||||
|
||||
// The ripple is a sine wave that Metal scales by an exponential decay
|
||||
// function.
|
||||
let rippleAmount = params.amplitude * sin(params.frequency * time) * exp(-params.decay * time)
|
||||
|
||||
// A vector of length `amplitude` that points away from position.
|
||||
let n = normalize(position - origin)
|
||||
|
||||
// Scale `n` by the ripple amount at the current pixel position and add it
|
||||
// to the current pixel position.
|
||||
//
|
||||
// This new position moves toward or away from `origin` based on the
|
||||
// sign and magnitude of `rippleAmount`.
|
||||
let newPosition = position + n * rippleAmount
|
||||
return newPosition
|
||||
}
|
||||
|
||||
private func rectToQuad(
|
||||
rect: CGRect,
|
||||
quadTL: CGPoint,
|
||||
quadTR: CGPoint,
|
||||
quadBL: CGPoint,
|
||||
quadBR: CGPoint
|
||||
) -> CATransform3D {
|
||||
let x1a = quadTL.x
|
||||
let y1a = quadTL.y
|
||||
let x2a = quadTR.x
|
||||
let y2a = quadTR.y
|
||||
let x3a = quadBL.x
|
||||
let y3a = quadBL.y
|
||||
let x4a = quadBR.x
|
||||
let y4a = quadBR.y
|
||||
|
||||
let X = rect.origin.x
|
||||
let Y = rect.origin.y
|
||||
let W = rect.size.width
|
||||
let H = rect.size.height
|
||||
|
||||
let y21 = y2a - y1a
|
||||
let y32 = y3a - y2a
|
||||
let y43 = y4a - y3a
|
||||
let y14 = y1a - y4a
|
||||
let y31 = y3a - y1a
|
||||
let y42 = y4a - y2a
|
||||
|
||||
let a = -H*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42)
|
||||
let b = W*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43)
|
||||
let c = H*X*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42) - H*W*x1a*(x4a*y32 - x3a*y42 + x2a*y43) - W*Y*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43)
|
||||
|
||||
let d = H*(-x4a*y21*y3a + x2a*y1a*y43 - x1a*y2a*y43 - x3a*y1a*y4a + x3a*y2a*y4a)
|
||||
let e = W*(x4a*y2a*y31 - x3a*y1a*y42 - x2a*y31*y4a + x1a*y3a*y42)
|
||||
let f = -(W*(x4a*(Y*y2a*y31 + H*y1a*y32) - x3a*(H + Y)*y1a*y42 + H*x2a*y1a*y43 + x2a*Y*(y1a - y3a)*y4a + x1a*Y*y3a*(-y2a + y4a)) - H*X*(x4a*y21*y3a - x2a*y1a*y43 + x3a*(y1a - y2a)*y4a + x1a*y2a*(-y3a + y4a)))
|
||||
|
||||
let g = H*(x3a*y21 - x4a*y21 + (-x1a + x2a)*y43)
|
||||
let h = W*(-x2a*y31 + x4a*y31 + (x1a - x3a)*y42)
|
||||
var i = W*Y*(x2a*y31 - x4a*y31 - x1a*y42 + x3a*y42) + H*(X*(-(x3a*y21) + x4a*y21 + x1a*y43 - x2a*y43) + W*(-(x3a*y2a) + x4a*y2a + x2a*y3a - x4a*y3a - x2a*y4a + x3a*y4a))
|
||||
|
||||
let kEpsilon = 0.0001
|
||||
|
||||
if fabs(i) < kEpsilon {
|
||||
i = kEpsilon * (i > 0 ? 1.0 : -1.0)
|
||||
}
|
||||
|
||||
//CATransform3D transform = {a/i, d/i, 0, g/i, b/i, e/i, 0, h/i, 0, 0, 1, 0, c/i, f/i, 0, 1.0}
|
||||
let transform = CATransform3D(m11: a/i, m12: d/i, m13: 0, m14: g/i, m21: b/i, m22: e/i, m23: 0, m24: h/i, m31: 0, m32: 0, m33: 1, m34: 0, m41: c/i, m42: f/i, m43: 0, m44: 1.0)
|
||||
return transform
|
||||
}
|
||||
|
||||
open class SpaceWarpView: UIView {
|
||||
private final class GridView: UIView {
|
||||
let cloneView: PortalView
|
||||
let gridPosition: CGPoint
|
||||
|
||||
init?(contentView: PortalSourceView, gridPosition: CGPoint) {
|
||||
self.gridPosition = gridPosition
|
||||
|
||||
guard let cloneView = PortalView(matchPosition: false) else {
|
||||
return nil
|
||||
}
|
||||
self.cloneView = cloneView
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.layer.anchorPoint = CGPoint(x: 0.0, y: 0.0)
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.isUserInteractionEnabled = false
|
||||
self.addSubview(cloneView.view)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func updateIsActive(contentView: PortalSourceView, isActive: Bool) {
|
||||
if isActive {
|
||||
contentView.addPortal(view: self.cloneView)
|
||||
} else {
|
||||
contentView.removePortal(view: self.cloneView)
|
||||
}
|
||||
}
|
||||
|
||||
func update(containerSize: CGSize, rect: CGRect, transition: ComponentTransition) {
|
||||
transition.setFrame(view: self.cloneView.view, frame: CGRect(origin: CGPoint(x: -rect.minX - containerSize.width * 0.5, y: -rect.minY - containerSize.height * 0.5), size: CGSize(width: containerSize.width, height: containerSize.height)))
|
||||
}
|
||||
}
|
||||
|
||||
private var gridViews: [GridView] = []
|
||||
|
||||
public var contentView: UIView {
|
||||
return self.contentViewImpl
|
||||
}
|
||||
|
||||
let contentViewImpl: PortalSourceView
|
||||
|
||||
private var link: SharedDisplayLinkDriver.Link?
|
||||
private var startPoint: CGPoint?
|
||||
|
||||
private var timeValue: CGFloat = 0.0
|
||||
private var currentActiveViews: Int = 0
|
||||
|
||||
private var resolution: (x: Int, y: Int)?
|
||||
private var size: CGSize?
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
self.contentViewImpl = PortalSourceView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.contentView)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func trigger(at point: CGPoint) {
|
||||
self.startPoint = point
|
||||
self.timeValue = 0.0
|
||||
|
||||
if self.link == nil {
|
||||
self.link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.timeValue += deltaTime * (1.0 / CGFloat(UIView.animationDurationFactor()))
|
||||
|
||||
if let size = self.size {
|
||||
self.update(size: size, transition: .immediate)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func updateGrid(resolutionX: Int, resolutionY: Int) {
|
||||
if let resolution = self.resolution, resolution.x == resolutionX, resolution.y == resolutionY {
|
||||
return
|
||||
}
|
||||
self.resolution = (resolutionX, resolutionY)
|
||||
|
||||
for gridView in self.gridViews {
|
||||
gridView.removeFromSuperview()
|
||||
}
|
||||
|
||||
var gridViews: [GridView] = []
|
||||
for y in 0 ..< resolutionY {
|
||||
for x in 0 ..< resolutionX {
|
||||
if let gridView = GridView(contentView: self.contentViewImpl, gridPosition: CGPoint(x: CGFloat(x) / CGFloat(resolutionX), y: CGFloat(y) / CGFloat(resolutionY))) {
|
||||
gridView.isUserInteractionEnabled = false
|
||||
gridViews.append(gridView)
|
||||
self.addSubview(gridView)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.gridViews = gridViews
|
||||
}
|
||||
|
||||
public func update(size: CGSize, transition: ComponentTransition) {
|
||||
self.size = size
|
||||
if size.width <= 0.0 || size.height <= 0.0 {
|
||||
return
|
||||
}
|
||||
|
||||
self.updateGrid(resolutionX: max(2, Int(size.width / 100.0)), resolutionY: max(2, Int(size.height / 100.0)))
|
||||
guard let resolution = self.resolution else {
|
||||
return
|
||||
}
|
||||
|
||||
//let pixelStep = CGPoint(x: CGFloat(resolution.x) * 0.33, y: CGFloat(resolution.y) * 0.33)
|
||||
let pixelStep = CGPoint()
|
||||
let itemSize = CGSize(width: size.width / CGFloat(resolution.x), height: size.height / CGFloat(resolution.y))
|
||||
|
||||
let params = RippleParams(amplitude: 22.0, frequency: 15.0, decay: 8.0, speed: 1400.0)
|
||||
|
||||
var activeViews = 0
|
||||
for gridView in self.gridViews {
|
||||
let sourceRect = CGRect(origin: CGPoint(x: gridView.gridPosition.x * (size.width + pixelStep.x), y: gridView.gridPosition.y * (size.height + pixelStep.y)), size: itemSize)
|
||||
|
||||
gridView.bounds = CGRect(origin: CGPoint(), size: sourceRect.size)
|
||||
gridView.update(containerSize: size, rect: sourceRect, transition: transition)
|
||||
|
||||
let initialTopLeft = CGPoint(x: sourceRect.minX, y: sourceRect.minY)
|
||||
let initialTopRight = CGPoint(x: sourceRect.maxX, y: sourceRect.minY)
|
||||
let initialBottomLeft = CGPoint(x: sourceRect.minX, y: sourceRect.maxY)
|
||||
let initialBottomRight = CGPoint(x: sourceRect.maxX, y: sourceRect.maxY)
|
||||
|
||||
var topLeft = initialTopLeft
|
||||
var topRight = initialTopRight
|
||||
var bottomLeft = initialBottomLeft
|
||||
var bottomRight = initialBottomRight
|
||||
|
||||
if let startPoint = self.startPoint {
|
||||
topLeft = transformCoordinate(position: topLeft, origin: startPoint, time: self.timeValue, params: params)
|
||||
topRight = transformCoordinate(position: topRight, origin: startPoint, time: self.timeValue, params: params)
|
||||
bottomLeft = transformCoordinate(position: bottomLeft, origin: startPoint, time: self.timeValue, params: params)
|
||||
bottomRight = transformCoordinate(position: bottomRight, origin: startPoint, time: self.timeValue, params: params)
|
||||
}
|
||||
|
||||
let distanceTopLeft = length(topLeft - initialTopLeft)
|
||||
let distanceTopRight = length(topRight - initialTopRight)
|
||||
let distanceBottomLeft = length(bottomLeft - initialBottomLeft)
|
||||
let distanceBottomRight = length(bottomRight - initialBottomRight)
|
||||
var maxDistance = max(distanceTopLeft, distanceTopRight)
|
||||
maxDistance = max(maxDistance, distanceBottomLeft)
|
||||
maxDistance = max(maxDistance, distanceBottomRight)
|
||||
|
||||
let isActive: Bool
|
||||
if maxDistance <= 0.5 {
|
||||
gridView.layer.transform = CATransform3DIdentity
|
||||
isActive = false
|
||||
} else {
|
||||
let transform = rectToQuad(rect: CGRect(origin: CGPoint(), size: itemSize), quadTL: topLeft, quadTR: topRight, quadBL: bottomLeft, quadBR: bottomRight)
|
||||
gridView.layer.transform = transform
|
||||
isActive = true
|
||||
activeViews += 1
|
||||
}
|
||||
if gridView.isHidden != !isActive {
|
||||
gridView.isHidden = !isActive
|
||||
gridView.updateIsActive(contentView: self.contentViewImpl, isActive: isActive)
|
||||
}
|
||||
}
|
||||
|
||||
if self.currentActiveViews != activeViews {
|
||||
self.currentActiveViews = activeViews
|
||||
#if DEBUG
|
||||
print("SpaceWarpView: activeViews = \(activeViews)")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.alpha.isZero || self.isHidden || !self.isUserInteractionEnabled {
|
||||
return nil
|
||||
}
|
||||
for view in self.contentView.subviews.reversed() {
|
||||
if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
let result = super.hitTest(point, with: event)
|
||||
if result != self {
|
||||
return result
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1655,9 +1655,8 @@ private final class StoryContainerScreenComponent: Component {
|
||||
}
|
||||
|
||||
if case let .user(user) = slice.peer, user.botInfo != nil {
|
||||
if let id = slice.item.storyItem.media.id {
|
||||
let _ = component.context.engine.messages.deleteBotPreviews(peerId: slice.peer.id, ids: [id]).startStandalone()
|
||||
}
|
||||
//TODO:localize
|
||||
let _ = component.context.engine.messages.deleteBotPreviews(peerId: slice.peer.id, language: nil, media: [slice.item.storyItem.media._asMedia()]).startStandalone()
|
||||
} else {
|
||||
let _ = component.context.engine.messages.deleteStories(peerId: slice.peer.id, ids: [slice.item.storyItem.id]).startStandalone()
|
||||
}
|
||||
|
@ -22,11 +22,13 @@ public final class TabSelectorComponent: Component {
|
||||
public var font: UIFont
|
||||
public var spacing: CGFloat
|
||||
public var lineSelection: Bool
|
||||
public var verticalInset: CGFloat
|
||||
|
||||
public init(font: UIFont, spacing: CGFloat, lineSelection: Bool = false) {
|
||||
public init(font: UIFont, spacing: CGFloat, lineSelection: Bool = false, verticalInset: CGFloat = 0.0) {
|
||||
self.font = font
|
||||
self.spacing = spacing
|
||||
self.lineSelection = lineSelection
|
||||
self.verticalInset = verticalInset
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,7 +94,7 @@ public final class TabSelectorComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
public final class View: UIScrollView {
|
||||
private var component: TabSelectorComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
@ -104,6 +106,14 @@ public final class TabSelectorComponent: Component {
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.showsVerticalScrollIndicator = false
|
||||
self.showsHorizontalScrollIndicator = false
|
||||
self.scrollsToTop = false
|
||||
self.delaysContentTouches = false
|
||||
self.canCancelContentTouches = true
|
||||
self.contentInsetAdjustmentBehavior = .never
|
||||
self.alwaysBounceVertical = false
|
||||
|
||||
self.addSubview(self.selectionView)
|
||||
}
|
||||
|
||||
@ -114,6 +124,10 @@ public final class TabSelectorComponent: Component {
|
||||
deinit {
|
||||
}
|
||||
|
||||
override public func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func update(component: TabSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let selectionColorUpdated = component.colors.selection != self.component?.colors.selection
|
||||
|
||||
@ -121,6 +135,12 @@ public final class TabSelectorComponent: Component {
|
||||
self.state = state
|
||||
|
||||
let baseHeight: CGFloat = 28.0
|
||||
|
||||
var verticalInset: CGFloat = 0.0
|
||||
if let customLayout = component.customLayout {
|
||||
verticalInset = customLayout.verticalInset * 2.0
|
||||
}
|
||||
|
||||
let innerInset: CGFloat = 12.0
|
||||
let spacing: CGFloat = component.customLayout?.spacing ?? 2.0
|
||||
|
||||
@ -148,7 +168,7 @@ public final class TabSelectorComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
var contentWidth: CGFloat = 0.0
|
||||
var contentWidth: CGFloat = spacing
|
||||
var previousBackgroundRect: CGRect?
|
||||
var selectedBackgroundRect: CGRect?
|
||||
var nextBackgroundRect: CGRect?
|
||||
@ -213,8 +233,8 @@ public final class TabSelectorComponent: Component {
|
||||
if !contentWidth.isZero {
|
||||
contentWidth += spacing
|
||||
}
|
||||
let itemTitleFrame = CGRect(origin: CGPoint(x: contentWidth + innerInset, y: floor((baseHeight - itemSize.height) * 0.5)), size: itemSize)
|
||||
let itemBackgroundRect = CGRect(origin: CGPoint(x: contentWidth, y: 0.0), size: CGSize(width: innerInset + itemSize.width + innerInset, height: baseHeight))
|
||||
let itemTitleFrame = CGRect(origin: CGPoint(x: contentWidth + innerInset, y: verticalInset + floor((baseHeight - itemSize.height) * 0.5)), size: itemSize)
|
||||
let itemBackgroundRect = CGRect(origin: CGPoint(x: contentWidth, y: verticalInset), size: CGSize(width: innerInset + itemSize.width + innerInset, height: baseHeight))
|
||||
contentWidth = itemBackgroundRect.maxX
|
||||
|
||||
if item.id == component.selectedId {
|
||||
@ -237,6 +257,7 @@ public final class TabSelectorComponent: Component {
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
contentWidth += spacing
|
||||
|
||||
var removeIds: [AnyHashable] = []
|
||||
for (id, itemView) in self.visibleItems {
|
||||
@ -277,7 +298,14 @@ public final class TabSelectorComponent: Component {
|
||||
self.selectionView.alpha = 0.0
|
||||
}
|
||||
|
||||
return CGSize(width: contentWidth, height: baseHeight)
|
||||
self.contentSize = CGSize(width: contentWidth, height: baseHeight + verticalInset * 2.0)
|
||||
self.disablesInteractiveTransitionGestureRecognizer = contentWidth > availableSize.width
|
||||
|
||||
if let selectedBackgroundRect {
|
||||
self.scrollRectToVisible(selectedBackgroundRect.insetBy(dx: -spacing, dy: 0.0), animated: false)
|
||||
}
|
||||
|
||||
return CGSize(width: min(contentWidth, availableSize.width), height: baseHeight + verticalInset * 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1816,8 +1816,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
if let context = self?.context, let navigationController = self?.effectiveNavigationController {
|
||||
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: threadId, messageId: messageId, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: false, keepStack: .always).startStandalone()
|
||||
}
|
||||
}, tapMessage: nil, clickThroughMessage: { [weak self] in
|
||||
self?.chatDisplayNode.dismissInput()
|
||||
}, tapMessage: nil, clickThroughMessage: { [weak self] view, location in
|
||||
self?.chatDisplayNode.dismissInput(view: view, location: location)
|
||||
}, toggleMessagesSelection: { [weak self] ids, value in
|
||||
guard let strongSelf = self, strongSelf.isNodeLoaded else {
|
||||
return
|
||||
|
@ -43,6 +43,7 @@ import ChatInlineSearchResultsListComponent
|
||||
import ComponentDisplayAdapters
|
||||
import ComponentFlow
|
||||
import ChatEmptyNode
|
||||
import SpaceWarpView
|
||||
|
||||
final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem {
|
||||
let itemNode: OverlayMediaItemNode
|
||||
@ -86,6 +87,41 @@ private struct ChatControllerNodeDerivedLayoutState {
|
||||
var upperInputPositionBound: CGFloat?
|
||||
}
|
||||
|
||||
class ChatNodeContainer: ASDisplayNode {
|
||||
private let contentNodeImpl: ASDisplayNode
|
||||
|
||||
var contentNode: ASDisplayNode {
|
||||
if self.view is SpaceWarpView {
|
||||
return self.contentNodeImpl
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
self.contentNodeImpl = ASDisplayNode()
|
||||
|
||||
super.init()
|
||||
|
||||
#if DEBUG && false
|
||||
self.setViewBlock({
|
||||
return SpaceWarpView(frame: CGRect())
|
||||
})
|
||||
#endif
|
||||
|
||||
(self.view as? SpaceWarpView)?.contentView.addSubnode(self.contentNodeImpl)
|
||||
}
|
||||
|
||||
func triggerRipple(at point: CGPoint) {
|
||||
(self.view as? SpaceWarpView)?.trigger(at: point)
|
||||
}
|
||||
|
||||
func update(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
transition.updateFrame(node: self.contentNodeImpl, frame: CGRect(origin: CGPoint(), size: size))
|
||||
(self.view as? SpaceWarpView)?.update(size: size, transition: ComponentTransition(transition))
|
||||
}
|
||||
}
|
||||
|
||||
class HistoryNodeContainer: ASDisplayNode {
|
||||
var isSecret: Bool {
|
||||
didSet {
|
||||
@ -95,7 +131,19 @@ class HistoryNodeContainer: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
private let contentNodeImpl: ASDisplayNode
|
||||
|
||||
var contentNode: ASDisplayNode {
|
||||
if self.view is SpaceWarpView {
|
||||
return self.contentNodeImpl
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
init(isSecret: Bool) {
|
||||
self.contentNodeImpl = ASDisplayNode()
|
||||
|
||||
self.isSecret = isSecret
|
||||
|
||||
super.init()
|
||||
@ -103,6 +151,23 @@ class HistoryNodeContainer: ASDisplayNode {
|
||||
if self.isSecret {
|
||||
setLayerDisableScreenshots(self.layer, self.isSecret)
|
||||
}
|
||||
|
||||
#if DEBUG && false
|
||||
self.setViewBlock({
|
||||
return SpaceWarpView(frame: CGRect())
|
||||
})
|
||||
#endif
|
||||
|
||||
(self.view as? SpaceWarpView)?.contentView.addSubnode(self.contentNodeImpl)
|
||||
}
|
||||
|
||||
func triggerRipple(at point: CGPoint) {
|
||||
(self.view as? SpaceWarpView)?.trigger(at: point)
|
||||
}
|
||||
|
||||
func update(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
transition.updateFrame(node: self.contentNodeImpl, frame: CGRect(origin: CGPoint(), size: size))
|
||||
(self.view as? SpaceWarpView)?.update(size: size, transition: ComponentTransition(transition))
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,12 +192,12 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
let contentContainerNode: ASDisplayNode
|
||||
let contentContainerNode: ChatNodeContainer
|
||||
let contentDimNode: ASDisplayNode
|
||||
let backgroundNode: WallpaperBackgroundNode
|
||||
let historyNode: ChatHistoryListNodeImpl
|
||||
var blurredHistoryNode: ASImageNode?
|
||||
let historyNodeContainer: ASDisplayNode
|
||||
let historyNodeContainer: HistoryNodeContainer
|
||||
let loadingNode: ChatLoadingNode
|
||||
private(set) var loadingPlaceholderNode: ChatLoadingPlaceholderNode?
|
||||
|
||||
@ -379,7 +444,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
|
||||
self.backgroundNode = backgroundNode
|
||||
|
||||
self.contentContainerNode = ASDisplayNode()
|
||||
self.contentContainerNode = ChatNodeContainer()
|
||||
self.contentDimNode = ASDisplayNode()
|
||||
self.contentDimNode.isUserInteractionEnabled = false
|
||||
self.contentDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.2)
|
||||
@ -633,7 +698,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
|
||||
self.historyNodeContainer = HistoryNodeContainer(isSecret: chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat)
|
||||
|
||||
self.historyNodeContainer.addSubnode(self.historyNode)
|
||||
self.historyNodeContainer.contentNode.addSubnode(self.historyNode)
|
||||
|
||||
var getContentAreaInScreenSpaceImpl: (() -> CGRect)?
|
||||
var onTransitionEventImpl: ((ContainedViewLayoutTransition) -> Void)?
|
||||
@ -787,11 +852,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
self.historyNode.enableExtractedBackgrounds = true
|
||||
|
||||
self.addSubnode(self.contentContainerNode)
|
||||
self.contentContainerNode.addSubnode(self.backgroundNode)
|
||||
self.contentContainerNode.addSubnode(self.historyNodeContainer)
|
||||
self.contentContainerNode.contentNode.addSubnode(self.backgroundNode)
|
||||
self.contentContainerNode.contentNode.addSubnode(self.historyNodeContainer)
|
||||
|
||||
if let navigationBar = self.navigationBar {
|
||||
self.contentContainerNode.addSubnode(navigationBar)
|
||||
self.contentContainerNode.contentNode.addSubnode(navigationBar)
|
||||
}
|
||||
|
||||
self.inputPanelContainerNode.expansionUpdated = { [weak self] transition in
|
||||
@ -817,9 +882,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
self.inputPanelBackgroundNode.addSubnode(self.inputPanelBottomBackgroundSeparatorNode)
|
||||
|
||||
self.addSubnode(self.messageTransitionNode)
|
||||
self.contentContainerNode.addSubnode(self.navigateButtons)
|
||||
self.contentContainerNode.contentNode.addSubnode(self.navigateButtons)
|
||||
self.addSubnode(self.presentationContextMarker)
|
||||
self.contentContainerNode.addSubnode(self.contentDimNode)
|
||||
self.contentContainerNode.contentNode.addSubnode(self.contentDimNode)
|
||||
|
||||
self.navigationBar?.additionalContentNode.addSubnode(self.titleAccessoryPanelContainer)
|
||||
|
||||
@ -1004,9 +1069,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
self.emptyNode = emptyNode
|
||||
|
||||
if let inlineSearchResultsView = self.inlineSearchResults?.view {
|
||||
self.contentContainerNode.view.insertSubview(emptyNode.view, belowSubview: inlineSearchResultsView)
|
||||
self.contentContainerNode.contentNode.view.insertSubview(emptyNode.view, belowSubview: inlineSearchResultsView)
|
||||
} else {
|
||||
self.contentContainerNode.insertSubnode(emptyNode, aboveSubnode: self.historyNodeContainer)
|
||||
self.contentContainerNode.contentNode.insertSubnode(emptyNode, aboveSubnode: self.historyNodeContainer)
|
||||
}
|
||||
|
||||
if let (size, insets) = self.validEmptyNodeLayout {
|
||||
@ -1081,12 +1146,10 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
if let historyNodeContainer = self.historyNodeContainer as? HistoryNodeContainer {
|
||||
let isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat
|
||||
if historyNodeContainer.isSecret != isSecret {
|
||||
historyNodeContainer.isSecret = isSecret
|
||||
setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, isSecret)
|
||||
}
|
||||
let isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat
|
||||
if self.historyNodeContainer.isSecret != isSecret {
|
||||
self.historyNodeContainer.isSecret = isSecret
|
||||
setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, isSecret)
|
||||
}
|
||||
|
||||
var previousListBottomInset: CGFloat?
|
||||
@ -1097,6 +1160,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
self.messageTransitionNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
|
||||
self.contentContainerNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
self.contentContainerNode.update(size: layout.size, transition: transition)
|
||||
|
||||
let isOverlay: Bool
|
||||
switch self.chatPresentationInterfaceState.mode {
|
||||
@ -1239,10 +1303,10 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
if let containerNode = self.containerNode {
|
||||
self.containerNode = nil
|
||||
containerNode.removeFromSupernode()
|
||||
self.contentContainerNode.insertSubnode(self.backgroundNode, at: 0)
|
||||
self.contentContainerNode.insertSubnode(self.historyNodeContainer, aboveSubnode: self.backgroundNode)
|
||||
self.contentContainerNode.contentNode.insertSubnode(self.backgroundNode, at: 0)
|
||||
self.contentContainerNode.contentNode.insertSubnode(self.historyNodeContainer, aboveSubnode: self.backgroundNode)
|
||||
if let restrictedNode = self.restrictedNode {
|
||||
self.contentContainerNode.insertSubnode(restrictedNode, aboveSubnode: self.historyNodeContainer)
|
||||
self.contentContainerNode.contentNode.insertSubnode(restrictedNode, aboveSubnode: self.historyNodeContainer)
|
||||
}
|
||||
self.navigationBar?.isHidden = false
|
||||
}
|
||||
@ -1392,7 +1456,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
if self.chatImportStatusPanel != importStatusPanelNode {
|
||||
dismissedImportStatusPanelNode = self.chatImportStatusPanel
|
||||
self.chatImportStatusPanel = importStatusPanelNode
|
||||
self.contentContainerNode.addSubnode(importStatusPanelNode)
|
||||
self.contentContainerNode.contentNode.addSubnode(importStatusPanelNode)
|
||||
}
|
||||
|
||||
importStatusPanelHeight = importStatusPanelNode.update(context: self.context, progress: CGFloat(importState.progress), presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: self.chatPresentationInterfaceState.theme, wallpaper: self.chatPresentationInterfaceState.chatWallpaper), fontSize: self.chatPresentationInterfaceState.fontSize, strings: self.chatPresentationInterfaceState.strings, dateTimeFormat: self.chatPresentationInterfaceState.dateTimeFormat, nameDisplayOrder: self.chatPresentationInterfaceState.nameDisplayOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: false)), width: layout.size.width)
|
||||
@ -1732,6 +1796,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
|
||||
transition.updateBounds(node: self.historyNodeContainer, bounds: contentBounds)
|
||||
transition.updatePosition(node: self.historyNodeContainer, position: contentBounds.center)
|
||||
self.historyNodeContainer.update(size: contentBounds.size, transition: transition)
|
||||
|
||||
transition.updateBounds(node: self.historyNode, bounds: CGRect(origin: CGPoint(), size: contentBounds.size))
|
||||
transition.updatePosition(node: self.historyNode, position: CGPoint(x: contentBounds.size.width / 2.0, y: contentBounds.size.height / 2.0))
|
||||
@ -1779,7 +1844,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
dismissedOverlayContextPanelNode = self.overlayContextPanelNode
|
||||
self.overlayContextPanelNode = overlayContextPanelNode
|
||||
|
||||
self.contentContainerNode.addSubnode(overlayContextPanelNode)
|
||||
self.contentContainerNode.contentNode.addSubnode(overlayContextPanelNode)
|
||||
immediatelyLayoutOverlayContextPanelAndAnimateAppearance = true
|
||||
}
|
||||
} else if let overlayContextPanelNode = self.overlayContextPanelNode {
|
||||
@ -1999,7 +2064,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
expandedInputDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
expandedInputDimNode.alpha = 0.0
|
||||
self.expandedInputDimNode = expandedInputDimNode
|
||||
self.contentContainerNode.insertSubnode(expandedInputDimNode, aboveSubnode: self.historyNodeContainer)
|
||||
self.contentContainerNode.contentNode.insertSubnode(expandedInputDimNode, aboveSubnode: self.historyNodeContainer)
|
||||
transition.updateAlpha(node: expandedInputDimNode, alpha: 1.0)
|
||||
expandedInputDimNode.frame = exandedFrame
|
||||
transition.animatePositionAdditive(node: expandedInputDimNode, offset: CGPoint(x: 0.0, y: previousInputPanelOrigin.y - inputPanelOrigin))
|
||||
@ -2831,9 +2896,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
self.skippedShowSearchResultsAsListAnimationOnce = true
|
||||
inlineSearchResultsView.layer.allowsGroupOpacity = true
|
||||
if let emptyNode = self.emptyNode {
|
||||
self.contentContainerNode.view.insertSubview(inlineSearchResultsView, aboveSubview: emptyNode.view)
|
||||
self.contentContainerNode.contentNode.view.insertSubview(inlineSearchResultsView, aboveSubview: emptyNode.view)
|
||||
} else {
|
||||
self.contentContainerNode.view.insertSubview(inlineSearchResultsView, aboveSubview: self.historyNodeContainer.view)
|
||||
self.contentContainerNode.contentNode.view.insertSubview(inlineSearchResultsView, aboveSubview: self.historyNodeContainer.view)
|
||||
}
|
||||
}
|
||||
inlineSearchResultsTransition.setFrame(view: inlineSearchResultsView, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
@ -3269,15 +3334,20 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
|
||||
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if recognizer.state == .ended {
|
||||
self.dismissInput()
|
||||
self.dismissInput(view: self.view, location: recognizer.location(in: self.contentContainerNode.view))
|
||||
}
|
||||
}
|
||||
|
||||
func dismissInput() {
|
||||
func dismissInput(view: UIView? = nil, location: CGPoint? = nil) {
|
||||
if let _ = self.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState {
|
||||
return
|
||||
}
|
||||
|
||||
if let view, let location {
|
||||
self.contentContainerNode.triggerRipple(at: self.contentContainerNode.view.convert(location, from: view))
|
||||
self.historyNodeContainer.triggerRipple(at: self.historyNodeContainer.view.convert(location, from: view))
|
||||
}
|
||||
|
||||
switch self.chatPresentationInterfaceState.inputMode {
|
||||
case .none:
|
||||
break
|
||||
@ -3739,7 +3809,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
let dropDimNode = ASDisplayNode()
|
||||
dropDimNode.backgroundColor = self.chatPresentationInterfaceState.theme.chatList.backgroundColor.withAlphaComponent(0.35)
|
||||
self.dropDimNode = dropDimNode
|
||||
self.contentContainerNode.addSubnode(dropDimNode)
|
||||
self.contentContainerNode.contentNode.addSubnode(dropDimNode)
|
||||
if let (layout, _) = self.validLayout {
|
||||
dropDimNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
dropDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
|
@ -84,7 +84,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
|
||||
}, navigateToMessage: { _, _, _ in
|
||||
}, navigateToMessageStandalone: { _ in
|
||||
}, navigateToThreadMessage: { _, _, _ in
|
||||
}, tapMessage: nil, clickThroughMessage: {
|
||||
}, tapMessage: nil, clickThroughMessage: { _, _ in
|
||||
}, toggleMessagesSelection: { _, _ in
|
||||
}, sendCurrentMessage: { _, _ in
|
||||
}, sendMessage: { _ in
|
||||
|
@ -69,6 +69,7 @@ import StarsPurchaseScreen
|
||||
import StarsTransferScreen
|
||||
import StarsTransactionScreen
|
||||
import StarsWithdrawalScreen
|
||||
import MiniAppListScreen
|
||||
|
||||
private final class AccountUserInterfaceInUseContext {
|
||||
let subscribers = Bag<(Bool) -> Void>()
|
||||
@ -1701,7 +1702,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
return presentAddMembersImpl(context: context, updatedPresentationData: updatedPresentationData, parentController: parentController, groupPeer: groupPeer, selectAddMemberDisposable: selectAddMemberDisposable, addMemberDisposable: addMemberDisposable)
|
||||
}
|
||||
|
||||
public func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)? = nil, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool, isStandalone: Bool) -> ListViewItem {
|
||||
public func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: ((UIView?, CGPoint?) -> Void)? = nil, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool, isStandalone: Bool) -> ListViewItem {
|
||||
let controllerInteraction: ChatControllerInteraction
|
||||
|
||||
controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in
|
||||
@ -1711,8 +1712,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}, navigateToThreadMessage: { _, _, _ in
|
||||
}, tapMessage: { message in
|
||||
tapMessage?(message)
|
||||
}, clickThroughMessage: {
|
||||
clickThroughMessage?()
|
||||
}, clickThroughMessage: { view, location in
|
||||
clickThroughMessage?(view, location)
|
||||
}, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in
|
||||
return false
|
||||
}, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
|
||||
@ -2727,6 +2728,14 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
return StarsTransactionScreen(context: context, subject: .gift(message), action: {})
|
||||
}
|
||||
|
||||
public func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError> {
|
||||
return MiniAppListScreen.initialData(context: context)
|
||||
}
|
||||
|
||||
public func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController {
|
||||
return MiniAppListScreen(context: context, initialData: initialData as! MiniAppListScreen.InitialData)
|
||||
}
|
||||
|
||||
public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) {
|
||||
openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, peer: peer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService)
|
||||
}
|
||||
|
@ -393,7 +393,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
return nil
|
||||
case let .peer(id):
|
||||
return id
|
||||
case let .botPreview(id):
|
||||
case let .botPreview(id, _):
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
@ -37,10 +37,10 @@ public class LocalizationListItem: ListViewItem, ItemListItem {
|
||||
public let sectionId: ItemListSectionId
|
||||
let alwaysPlain: Bool
|
||||
let action: () -> Void
|
||||
let setItemWithRevealedOptions: (String?, String?) -> Void
|
||||
let removeItem: (String) -> Void
|
||||
let setItemWithRevealedOptions: ((String?, String?) -> Void)?
|
||||
let removeItem: ((String) -> Void)?
|
||||
|
||||
public init(presentationData: ItemListPresentationData, id: String, title: String, subtitle: String, checked: Bool, activity: Bool, loading: Bool, editing: LocalizationListItemEditing, enabled: Bool = true, sectionId: ItemListSectionId, alwaysPlain: Bool, action: @escaping () -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) {
|
||||
public init(presentationData: ItemListPresentationData, id: String, title: String, subtitle: String, checked: Bool, activity: Bool, loading: Bool, editing: LocalizationListItemEditing, enabled: Bool = true, sectionId: ItemListSectionId, alwaysPlain: Bool, action: @escaping () -> Void, setItemWithRevealedOptions: ((String?, String?) -> Void)?, removeItem: ((String) -> Void)?) {
|
||||
self.presentationData = presentationData
|
||||
self.id = id
|
||||
self.title = title
|
||||
@ -368,7 +368,7 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
|
||||
|
||||
if item.editing.editable {
|
||||
if item.editing.editable, item.removeItem != nil {
|
||||
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
|
||||
} else {
|
||||
strongSelf.setRevealOptions((left: [], right: []))
|
||||
@ -491,13 +491,13 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
override func revealOptionsInteractivelyOpened() {
|
||||
if let item = self.item {
|
||||
item.setItemWithRevealedOptions(item.id, nil)
|
||||
item.setItemWithRevealedOptions?(item.id, nil)
|
||||
}
|
||||
}
|
||||
|
||||
override func revealOptionsInteractivelyClosed() {
|
||||
if let item = self.item {
|
||||
item.setItemWithRevealedOptions(nil, item.id)
|
||||
item.setItemWithRevealedOptions?(nil, item.id)
|
||||
}
|
||||
}
|
||||
|
||||
@ -506,7 +506,7 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode {
|
||||
self.revealOptionsInteractivelyClosed()
|
||||
|
||||
if let item = self.item {
|
||||
item.removeItem(item.id)
|
||||
item.removeItem?(item.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user