From 8d44e4d9c6b60ebdd77566bd24315e92323465f7 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 5 Nov 2024 21:38:27 +0100 Subject: [PATCH 1/5] Fix camera layout --- .../LegacyComponents/Sources/TGAttachmentCarouselItemView.m | 1 + .../LegacyComponents/Sources/TGMediaAssetsPickerController.m | 1 + submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m | 1 + .../Components/LegacyCamera/Sources/LegacyCamera.swift | 1 + 4 files changed, 4 insertions(+) diff --git a/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m b/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m index 22d9c03d91..d64d69ffa1 100644 --- a/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m +++ b/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m @@ -920,6 +920,7 @@ const NSUInteger TGAttachmentDisplayedAssetLimit = 500; } TGPhotoEditorController *controller = [[TGPhotoEditorController alloc] initWithContext:[windowManager context] item:editableItem intent:intent adjustments:nil caption:nil screenImage:thumbnailImage availableTabs:[TGPhotoEditorController defaultTabsForAvatarIntent:!_disableStickers] selectedTab:TGPhotoEditorCropTab]; + controller.modalPresentationStyle = UIModalPresentationFullScreen; controller.editingContext = _editingContext; controller.stickersContext = _stickersContext; controller.dontHideStatusBar = true; diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsPickerController.m b/submodules/LegacyComponents/Sources/TGMediaAssetsPickerController.m index 493ff73f5a..a71c324917 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsPickerController.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsPickerController.m @@ -449,6 +449,7 @@ } TGPhotoEditorController *controller = [[TGPhotoEditorController alloc] initWithContext:_context item:editableItem intent:intent adjustments:nil caption:nil screenImage:thumbnailImage availableTabs:[TGPhotoEditorController defaultTabsForAvatarIntent:_intent != TGMediaAssetsControllerSetSignupProfilePhotoIntent] selectedTab:TGPhotoEditorCropTab]; + controller.modalPresentationStyle = UIModalPresentationFullScreen; controller.stickersContext = self.stickersContext; controller.editingContext = self.editingContext; controller.didFinishRenderingFullSizeImage = ^(UIImage *resultImage) diff --git a/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m b/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m index ebd4719cfb..b415e1b0cb 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m +++ b/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m @@ -340,6 +340,7 @@ controller = [[TGCameraController alloc] initWithContext:[windowManager context] saveEditedPhotos:_saveEditedPhotos saveCapturedMedia:_saveCapturedMedia camera:cameraView.previewView.camera previewView:cameraView.previewView intent:_signup ? TGCameraControllerSignupAvatarIntent : TGCameraControllerAvatarIntent]; else controller = [[TGCameraController alloc] initWithContext:[windowManager context] saveEditedPhotos:_saveEditedPhotos saveCapturedMedia:_saveCapturedMedia intent:_signup ? TGCameraControllerSignupAvatarIntent : TGCameraControllerAvatarIntent]; + controller.modalPresentationStyle = UIModalPresentationFullScreen; controller.stickersContext = _stickersContext; controller.shouldStoreCapturedAssets = true; diff --git a/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift b/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift index 17eccfc3ea..0cac4f3e96 100644 --- a/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift +++ b/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift @@ -35,6 +35,7 @@ public func presentedLegacyCamera(context: AccountContext, peer: Peer?, chatLoca } else { controller = TGCameraController(context: legacyController.context, saveEditedPhotos: saveCapturedPhotos && !isSecretChat, saveCapturedMedia: saveCapturedPhotos && !isSecretChat) } + controller.modalPresentationStyle = .fullScreen controller.inhibitMultipleCapture = editingMedia if !initialCaption.string.isEmpty { From 8ec6964dfe378e10d8707029de21f5b28581f1ff Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 7 Nov 2024 14:01:54 +0100 Subject: [PATCH 2/5] Update API [skip ci] --- submodules/TelegramApi/Sources/Api0.swift | 5 +-- submodules/TelegramApi/Sources/Api13.swift | 18 +++++++---- submodules/TelegramApi/Sources/Api23.swift | 28 ++++++++++++---- submodules/TelegramApi/Sources/Api25.swift | 32 +++++++++++++++++++ .../Sources/State/Serialization.swift | 2 +- .../SyncCore_TelegramMediaInvoice.swift | 4 +-- .../TelegramEngine/Messages/BotWebView.swift | 13 ++++++++ .../Payments/BotPaymentForm.swift | 8 +++-- .../TelegramEngine/Payments/Stars.swift | 26 ++++++++++++--- 9 files changed, 110 insertions(+), 26 deletions(-) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index ab271c55da..0c6f11c5b9 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -487,7 +487,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-193992412] = { return Api.InputWebFileLocation.parse_inputWebFileAudioAlbumThumbLocation($0) } dict[-1625153079] = { return Api.InputWebFileLocation.parse_inputWebFileGeoPointLocation($0) } dict[-1036396922] = { return Api.InputWebFileLocation.parse_inputWebFileLocation($0) } - dict[1572428309] = { return Api.Invoice.parse_invoice($0) } + dict[77522308] = { return Api.Invoice.parse_invoice($0) } dict[-1059185703] = { return Api.JSONObjectValue.parse_jsonObjectValue($0) } dict[-146520221] = { return Api.JSONValue.parse_jsonArray($0) } dict[-952869270] = { return Api.JSONValue.parse_jsonBool($0) } @@ -899,7 +899,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1798404822] = { return Api.StarsGiveawayOption.parse_starsGiveawayOption($0) } dict[1411605001] = { return Api.StarsGiveawayWinnersOption.parse_starsGiveawayWinnersOption($0) } dict[2033461574] = { return Api.StarsRevenueStatus.parse_starsRevenueStatus($0) } - dict[1401868056] = { return Api.StarsSubscription.parse_starsSubscription($0) } + dict[779004698] = { return Api.StarsSubscription.parse_starsSubscription($0) } dict[88173912] = { return Api.StarsSubscriptionPricing.parse_starsSubscriptionPricing($0) } dict[198776256] = { return Api.StarsTopupOption.parse_starsTopupOption($0) } dict[903148150] = { return Api.StarsTransaction.parse_starsTransaction($0) } @@ -974,6 +974,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[675009298] = { return Api.Update.parse_updateBotPurchasedPaidMedia($0) } dict[-1246823043] = { return Api.Update.parse_updateBotShippingQuery($0) } dict[-997782967] = { return Api.Update.parse_updateBotStopped($0) } + dict[-1464975695] = { return Api.Update.parse_updateBotSubscriptionExpire($0) } dict[-2095595325] = { return Api.Update.parse_updateBotWebhookJSON($0) } dict[-1684914010] = { return Api.Update.parse_updateBotWebhookJSONQuery($0) } dict[-539401739] = { return Api.Update.parse_updateBroadcastRevenueTransactions($0) } diff --git a/submodules/TelegramApi/Sources/Api13.swift b/submodules/TelegramApi/Sources/Api13.swift index c5c11feab8..f9f84e3481 100644 --- a/submodules/TelegramApi/Sources/Api13.swift +++ b/submodules/TelegramApi/Sources/Api13.swift @@ -416,13 +416,13 @@ public extension Api { } public extension Api { enum Invoice: TypeConstructorDescription { - case invoice(flags: Int32, currency: String, prices: [Api.LabeledPrice], maxTipAmount: Int64?, suggestedTipAmounts: [Int64]?, termsUrl: String?) + case invoice(flags: Int32, currency: String, prices: [Api.LabeledPrice], maxTipAmount: Int64?, suggestedTipAmounts: [Int64]?, termsUrl: String?, subscriptionPeriod: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .invoice(let flags, let currency, let prices, let maxTipAmount, let suggestedTipAmounts, let termsUrl): + case .invoice(let flags, let currency, let prices, let maxTipAmount, let suggestedTipAmounts, let termsUrl, let subscriptionPeriod): if boxed { - buffer.appendInt32(1572428309) + buffer.appendInt32(77522308) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(currency, buffer: buffer, boxed: false) @@ -438,14 +438,15 @@ public extension Api { serializeInt64(item, buffer: buffer, boxed: false) }} if Int(flags) & Int(1 << 10) != 0 {serializeString(termsUrl!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 11) != 0 {serializeInt32(subscriptionPeriod!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .invoice(let flags, let currency, let prices, let maxTipAmount, let suggestedTipAmounts, let termsUrl): - return ("invoice", [("flags", flags as Any), ("currency", currency as Any), ("prices", prices as Any), ("maxTipAmount", maxTipAmount as Any), ("suggestedTipAmounts", suggestedTipAmounts as Any), ("termsUrl", termsUrl as Any)]) + case .invoice(let flags, let currency, let prices, let maxTipAmount, let suggestedTipAmounts, let termsUrl, let subscriptionPeriod): + return ("invoice", [("flags", flags as Any), ("currency", currency as Any), ("prices", prices as Any), ("maxTipAmount", maxTipAmount as Any), ("suggestedTipAmounts", suggestedTipAmounts as Any), ("termsUrl", termsUrl as Any), ("subscriptionPeriod", subscriptionPeriod as Any)]) } } @@ -466,14 +467,17 @@ public extension Api { } } var _6: String? if Int(_1!) & Int(1 << 10) != 0 {_6 = parseString(reader) } + var _7: Int32? + if Int(_1!) & Int(1 << 11) != 0 {_7 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = (Int(_1!) & Int(1 << 8) == 0) || _4 != nil let _c5 = (Int(_1!) & Int(1 << 8) == 0) || _5 != nil let _c6 = (Int(_1!) & Int(1 << 10) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.Invoice.invoice(flags: _1!, currency: _2!, prices: _3!, maxTipAmount: _4, suggestedTipAmounts: _5, termsUrl: _6) + let _c7 = (Int(_1!) & Int(1 << 11) == 0) || _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.Invoice.invoice(flags: _1!, currency: _2!, prices: _3!, maxTipAmount: _4, suggestedTipAmounts: _5, termsUrl: _6, subscriptionPeriod: _7) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index 83c674e9d3..a04310d35e 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -858,13 +858,13 @@ public extension Api { } public extension Api { enum StarsSubscription: TypeConstructorDescription { - case starsSubscription(flags: Int32, id: String, peer: Api.Peer, untilDate: Int32, pricing: Api.StarsSubscriptionPricing, chatInviteHash: String?) + case starsSubscription(flags: Int32, id: String, peer: Api.Peer, untilDate: Int32, pricing: Api.StarsSubscriptionPricing, chatInviteHash: String?, title: String?, photo: Api.WebDocument?, invoiceSlug: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .starsSubscription(let flags, let id, let peer, let untilDate, let pricing, let chatInviteHash): + case .starsSubscription(let flags, let id, let peer, let untilDate, let pricing, let chatInviteHash, let title, let photo, let invoiceSlug): if boxed { - buffer.appendInt32(1401868056) + buffer.appendInt32(779004698) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(id, buffer: buffer, boxed: false) @@ -872,14 +872,17 @@ public extension Api { serializeInt32(untilDate, buffer: buffer, boxed: false) pricing.serialize(buffer, true) if Int(flags) & Int(1 << 3) != 0 {serializeString(chatInviteHash!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {serializeString(title!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 5) != 0 {photo!.serialize(buffer, true)} + if Int(flags) & Int(1 << 6) != 0 {serializeString(invoiceSlug!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .starsSubscription(let flags, let id, let peer, let untilDate, let pricing, let chatInviteHash): - return ("starsSubscription", [("flags", flags as Any), ("id", id as Any), ("peer", peer as Any), ("untilDate", untilDate as Any), ("pricing", pricing as Any), ("chatInviteHash", chatInviteHash as Any)]) + case .starsSubscription(let flags, let id, let peer, let untilDate, let pricing, let chatInviteHash, let title, let photo, let invoiceSlug): + return ("starsSubscription", [("flags", flags as Any), ("id", id as Any), ("peer", peer as Any), ("untilDate", untilDate as Any), ("pricing", pricing as Any), ("chatInviteHash", chatInviteHash as Any), ("title", title as Any), ("photo", photo as Any), ("invoiceSlug", invoiceSlug as Any)]) } } @@ -900,14 +903,25 @@ public extension Api { } var _6: String? if Int(_1!) & Int(1 << 3) != 0 {_6 = parseString(reader) } + var _7: String? + if Int(_1!) & Int(1 << 4) != 0 {_7 = parseString(reader) } + var _8: Api.WebDocument? + if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() { + _8 = Api.parse(reader, signature: signature) as? Api.WebDocument + } } + var _9: String? + if Int(_1!) & Int(1 << 6) != 0 {_9 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.StarsSubscription.starsSubscription(flags: _1!, id: _2!, peer: _3!, untilDate: _4!, pricing: _5!, chatInviteHash: _6) + let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil + let _c8 = (Int(_1!) & Int(1 << 5) == 0) || _8 != nil + let _c9 = (Int(_1!) & Int(1 << 6) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.StarsSubscription.starsSubscription(flags: _1!, id: _2!, peer: _3!, untilDate: _4!, pricing: _5!, chatInviteHash: _6, title: _7, photo: _8, invoiceSlug: _9) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index 2c19d44fd9..1d41cbd8c1 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -645,6 +645,7 @@ public extension Api { case updateBotPurchasedPaidMedia(userId: Int64, payload: String, qts: Int32) case updateBotShippingQuery(queryId: Int64, userId: Int64, payload: Buffer, shippingAddress: Api.PostAddress) case updateBotStopped(userId: Int64, date: Int32, stopped: Api.Bool, qts: Int32) + case updateBotSubscriptionExpire(userId: Int64, payload: String, untilDate: Int32, qts: Int32) case updateBotWebhookJSON(data: Api.DataJSON) case updateBotWebhookJSONQuery(queryId: Int64, data: Api.DataJSON, timeout: Int32) case updateBroadcastRevenueTransactions(peer: Api.Peer, balances: Api.BroadcastRevenueBalances) @@ -969,6 +970,15 @@ public extension Api { stopped.serialize(buffer, true) serializeInt32(qts, buffer: buffer, boxed: false) break + case .updateBotSubscriptionExpire(let userId, let payload, let untilDate, let qts): + if boxed { + buffer.appendInt32(-1464975695) + } + serializeInt64(userId, buffer: buffer, boxed: false) + serializeString(payload, buffer: buffer, boxed: false) + serializeInt32(untilDate, buffer: buffer, boxed: false) + serializeInt32(qts, buffer: buffer, boxed: false) + break case .updateBotWebhookJSON(let data): if boxed { buffer.appendInt32(-2095595325) @@ -2041,6 +2051,8 @@ public extension Api { return ("updateBotShippingQuery", [("queryId", queryId as Any), ("userId", userId as Any), ("payload", payload as Any), ("shippingAddress", shippingAddress as Any)]) case .updateBotStopped(let userId, let date, let stopped, let qts): return ("updateBotStopped", [("userId", userId as Any), ("date", date as Any), ("stopped", stopped as Any), ("qts", qts as Any)]) + case .updateBotSubscriptionExpire(let userId, let payload, let untilDate, let qts): + return ("updateBotSubscriptionExpire", [("userId", userId as Any), ("payload", payload as Any), ("untilDate", untilDate as Any), ("qts", qts as Any)]) case .updateBotWebhookJSON(let data): return ("updateBotWebhookJSON", [("data", data as Any)]) case .updateBotWebhookJSONQuery(let queryId, let data, let timeout): @@ -2732,6 +2744,26 @@ public extension Api { return nil } } + public static func parse_updateBotSubscriptionExpire(_ reader: BufferReader) -> Update? { + var _1: Int64? + _1 = reader.readInt64() + var _2: String? + _2 = parseString(reader) + var _3: Int32? + _3 = reader.readInt32() + 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.Update.updateBotSubscriptionExpire(userId: _1!, payload: _2!, untilDate: _3!, qts: _4!) + } + else { + return nil + } + } public static func parse_updateBotWebhookJSON(_ reader: BufferReader) -> Update? { var _1: Api.DataJSON? if let signature = reader.readInt32() { diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 3450b1b4a7..bc0dc07962 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 192 + return 194 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaInvoice.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaInvoice.swift index 3cb14d3ea0..0fccecc92e 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaInvoice.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaInvoice.swift @@ -114,8 +114,8 @@ public final class TelegramMediaInvoice: Media, Equatable { self.currency = currency self.totalAmount = totalAmount self.startParam = startParam - self.flags = flags self.extendedMedia = extendedMedia + self.flags = flags self.version = version } @@ -126,8 +126,8 @@ public final class TelegramMediaInvoice: Media, Equatable { self.totalAmount = decoder.decodeInt64ForKey("ta", orElse: 0) self.startParam = decoder.decodeStringForKey("sp", orElse: "") self.photo = decoder.decodeObjectForKey("p") as? TelegramMediaWebFile - self.flags = TelegramMediaInvoiceFlags(rawValue: decoder.decodeInt32ForKey("f", orElse: 0)) self.extendedMedia = decoder.decodeObjectForKey("m", decoder: { TelegramExtendedMedia(decoder: $0) }) as? TelegramExtendedMedia + self.flags = TelegramMediaInvoiceFlags(rawValue: decoder.decodeInt32ForKey("f", orElse: 0)) if let receiptMessageIdPeerId = decoder.decodeOptionalInt64ForKey("r.p") as Int64?, let receiptMessageIdNamespace = decoder.decodeOptionalInt32ForKey("r.n") as Int32?, let receiptMessageIdId = decoder.decodeOptionalInt32ForKey("r.i") as Int32? { self.receiptMessageId = MessageId(peerId: PeerId(receiptMessageIdPeerId), namespace: receiptMessageIdNamespace, id: receiptMessageIdId) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index dac0e5b6db..220ef1bdaa 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -56,6 +56,9 @@ func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: P if (flags & (1 << 1)) != 0 { resultFlags.insert(.fullSize) } + if (flags & (1 << 2)) != 0 { + resultFlags.insert(.fullScreen) + } return .single(RequestWebViewResult(flags: resultFlags, queryId: queryId, url: url, keepAliveSignal: nil)) } } @@ -103,6 +106,9 @@ func _internal_requestMainWebView(postbox: Postbox, network: Network, botId: Pee if (flags & (1 << 1)) != 0 { resultFlags.insert(.fullSize) } + if (flags & (1 << 2)) != 0 { + resultFlags.insert(.fullScreen) + } return .single(RequestWebViewResult(flags: resultFlags, queryId: queryId, url: url, keepAliveSignal: nil)) } } @@ -128,6 +134,7 @@ public struct RequestWebViewResult { } public static let fullSize = Flags(rawValue: 1 << 0) + public static let fullScreen = Flags(rawValue: 1 << 1) } public let flags: Flags @@ -237,6 +244,9 @@ func _internal_requestWebView(postbox: Postbox, network: Network, stateManager: if (webViewFlags & (1 << 1)) != 0 { resultFlags.insert(.fullSize) } + if (flags & (1 << 2)) != 0 { + resultFlags.insert(.fullScreen) + } let keepAlive: Signal? if let queryId { keepAlive = keepWebViewSignal(network: network, stateManager: stateManager, flags: flags, peer: inputPeer, bot: inputBot, queryId: queryId, replyToMessageId: replyToMessageId, threadId: threadId, sendAs: nil) @@ -323,6 +333,9 @@ func _internal_requestAppWebView(postbox: Postbox, network: Network, stateManage if (flags & (1 << 1)) != 0 { resultFlags.insert(.fullSize) } + if (flags & (1 << 2)) != 0 { + resultFlags.insert(.fullScreen) + } return .single(RequestWebViewResult(flags: resultFlags, queryId: queryId, url: url, keepAliveSignal: nil)) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 3fed134536..416d00acb9 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -63,14 +63,16 @@ public struct BotPaymentInvoice : Equatable { public let prices: [BotPaymentPrice] public let tip: Tip? public let termsInfo: RecurrentInfo? + public let subscriptionPeriod: Int32? - public init(isTest: Bool, requestedFields: BotPaymentInvoiceFields, currency: String, prices: [BotPaymentPrice], tip: Tip?, termsInfo: RecurrentInfo?) { + public init(isTest: Bool, requestedFields: BotPaymentInvoiceFields, currency: String, prices: [BotPaymentPrice], tip: Tip?, termsInfo: RecurrentInfo?, subscriptionPeriod: Int32?) { self.isTest = isTest self.requestedFields = requestedFields self.currency = currency self.prices = prices self.tip = tip self.termsInfo = termsInfo + self.subscriptionPeriod = subscriptionPeriod } } @@ -175,7 +177,7 @@ public enum BotPaymentFormRequestError { extension BotPaymentInvoice { init(apiInvoice: Api.Invoice) { switch apiInvoice { - case let .invoice(flags, currency, prices, maxTipAmount, suggestedTipAmounts, termsUrl): + case let .invoice(flags, currency, prices, maxTipAmount, suggestedTipAmounts, termsUrl, subscriptionPeriod): var fields = BotPaymentInvoiceFields() if (flags & (1 << 1)) != 0 { fields.insert(.name) @@ -212,7 +214,7 @@ extension BotPaymentInvoice { case let .labeledPrice(label, amount): return BotPaymentPrice(label: label, amount: amount) } - }, tip: parsedTip, termsInfo: termsInfo) + }, tip: parsedTip, termsInfo: termsInfo, subscriptionPeriod: subscriptionPeriod) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 48ea7ace5f..27c51e0721 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -554,7 +554,7 @@ private extension StarsContext.State.Transaction { private extension StarsContext.State.Subscription { init?(apiSubscription: Api.StarsSubscription, transaction: Transaction) { switch apiSubscription { - case let .starsSubscription(apiFlags, id, apiPeer, untilDate, pricing, inviteHash): + case let .starsSubscription(apiFlags, id, apiPeer, untilDate, pricing, inviteHash, title, photo, invoiceSlug): guard let peer = transaction.getPeer(apiPeer.peerId) else { return nil } @@ -568,7 +568,7 @@ private extension StarsContext.State.Subscription { if (apiFlags & (1 << 2)) != 0 { flags.insert(.missingBalance) } - self.init(flags: flags, id: id, peer: EnginePeer(peer), untilDate: untilDate, pricing: StarsSubscriptionPricing(apiStarsSubscriptionPricing: pricing), inviteHash: inviteHash) + self.init(flags: flags, id: id, peer: EnginePeer(peer), untilDate: untilDate, pricing: StarsSubscriptionPricing(apiStarsSubscriptionPricing: pricing), inviteHash: inviteHash, title: title, photo: photo.flatMap(TelegramMediaWebFile.init), invoiceSlug: invoiceSlug) } } } @@ -727,6 +727,9 @@ public final class StarsContext { public let untilDate: Int32 public let pricing: StarsSubscriptionPricing public let inviteHash: String? + public let title: String? + public let photo: TelegramMediaWebFile? + public let invoiceSlug: String? public init( flags: Flags, @@ -734,7 +737,10 @@ public final class StarsContext { peer: EnginePeer, untilDate: Int32, pricing: StarsSubscriptionPricing, - inviteHash: String? + inviteHash: String?, + title: String?, + photo: TelegramMediaWebFile?, + invoiceSlug: String? ) { self.flags = flags self.id = id @@ -742,6 +748,9 @@ public final class StarsContext { self.untilDate = untilDate self.pricing = pricing self.inviteHash = inviteHash + self.title = title + self.photo = photo + self.invoiceSlug = invoiceSlug } public static func == (lhs: Subscription, rhs: Subscription) -> Bool { @@ -763,6 +772,15 @@ public final class StarsContext { if lhs.inviteHash != rhs.inviteHash { return false } + if lhs.title != rhs.title { + return false + } + if lhs.photo != rhs.photo { + return false + } + if lhs.invoiceSlug != rhs.invoiceSlug { + return false + } return true } } @@ -1192,7 +1210,7 @@ private final class StarsSubscriptionsContextImpl { } else { updatedFlags.remove(.isCancelled) } - let updatedSubscription = StarsContext.State.Subscription(flags: updatedFlags, id: subscription.id, peer: subscription.peer, untilDate: subscription.untilDate, pricing: subscription.pricing, inviteHash: subscription.inviteHash) + let updatedSubscription = StarsContext.State.Subscription(flags: updatedFlags, id: subscription.id, peer: subscription.peer, untilDate: subscription.untilDate, pricing: subscription.pricing, inviteHash: subscription.inviteHash, title: subscription.title, photo: subscription.photo, invoiceSlug: subscription.invoiceSlug) updatedState.subscriptions[index] = updatedSubscription } self.updateState(updatedState) From 37c91f89c5197bc82f4b602dfcad0d988205cb18 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 7 Nov 2024 16:32:33 +0100 Subject: [PATCH 3/5] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 3 + .../Sources/AttachmentContainer.swift | 2 +- .../Sources/AttachmentController.swift | 18 +- .../BrowserUI/Sources/BrowserWebContent.swift | 7 +- .../Navigation/MinimizedContainer.swift | 5 + .../Sources/MoreButtonNode.swift | 25 +- .../TelegramCore/Sources/Utils/JSON.swift | 32 +- .../Sources/TonFormat.swift | 6 +- .../ChatRecentActionsControllerNode.swift | 2 +- .../Components/Gifts/GiftSetupScreen/BUILD | 1 + .../Sources/GiftSetupScreen.swift | 39 +- .../Sources/MinimizedContainer.swift | 14 +- .../PeerInfoScreen/Sources/PeerInfoData.swift | 37 +- .../Sources/PeerInfoScreen.swift | 19 +- .../Sources/PeerInfoGiftsPaneNode.swift | 10 +- .../PremiumPeerShortcutComponent.swift | 9 +- .../Sources/StarsImageComponent.swift | 90 ++- .../Sources/StarsTransactionScreen.swift | 87 ++- .../Sources/StarsTransactionsScreen.swift | 27 +- .../Sources/StarsTransferScreen.swift | 53 +- .../Stories/StoryContainerScreen/BUILD | 1 - ...StoryItemSetContainerViewSendMessage.swift | 64 +- .../Chat/Info/Location.imageset/Contents.json | 12 + .../Chat/Info/Location.imageset/location.pdf | 137 ++++ .../Chat/Info/Status.imageset/Contents.json | 12 + .../Chat/Info/Status.imageset/status (3).pdf | Bin 0 -> 41429 bytes .../MinimizeArrow.imageset/Contents.json | 12 + .../miniappminimize_30.pdf | 92 +++ .../Verified.imageset/Contents.json | 12 + .../Verified.imageset/miniappverify_14.pdf | Bin 0 -> 1604 bytes .../Animations/anim_baremoredots.json | 1 + .../Animations/web_backToCancel.json | 1 + .../Chat/ChatControllerOpenWebApp.swift | 16 +- .../ChatControllerOpenAttachmentMenu.swift | 2 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 2 +- .../Sources/PostboxKeys.swift | 2 + submodules/WebUI/BUILD | 7 + .../Sources/FullscreenControlsComponent.swift | 352 ++++++++++ .../WebUI/Sources/WebAppController.swift | 641 ++++++++++++++++-- .../WebAppEmojiStatusAlertController.swift | 365 ++++++++++ .../WebAppLocationAlertController.swift | 293 ++++++++ .../WebAppMessageChatPreviewItem.swift | 454 +++++++++++++ .../Sources/WebAppMessagePreviewScreen.swift | 337 +++++++++ .../WebUI/Sources/WebAppPermissions.swift | 98 +++ .../Sources/WebAppSetEmojiStatusScreen.swift | 390 +++++++++++ submodules/WebUI/Sources/WebAppWebView.swift | 23 +- 46 files changed, 3649 insertions(+), 163 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/location.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Info/Status.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Info/Status.imageset/status (3).pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/miniappminimize_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/Verified.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/Verified.imageset/miniappverify_14.pdf create mode 100644 submodules/TelegramUI/Resources/Animations/anim_baremoredots.json create mode 100644 submodules/TelegramUI/Resources/Animations/web_backToCancel.json create mode 100644 submodules/WebUI/Sources/FullscreenControlsComponent.swift create mode 100644 submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift create mode 100644 submodules/WebUI/Sources/WebAppLocationAlertController.swift create mode 100644 submodules/WebUI/Sources/WebAppMessageChatPreviewItem.swift create mode 100644 submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift create mode 100644 submodules/WebUI/Sources/WebAppPermissions.swift create mode 100644 submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index c8c19facf4..0b0d604f0e 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13218,3 +13218,6 @@ Sorry for the inconvenience."; "Gift.Convert.Period.Unavailable.Text" = "Sorry, you can't convert this gift.\n\nStars can only be claimed within %@ after receiving a gift."; "Gift.Convert.Period.Unavailable.Days_1" = "%@ day"; "Gift.Convert.Period.Unavailable.Days_any" = "%@ days"; + +"Gift.Send.TitleTo" = "Gift to %@"; +"Gift.Send.SendShort" = "Send"; diff --git a/submodules/AttachmentUI/Sources/AttachmentContainer.swift b/submodules/AttachmentUI/Sources/AttachmentContainer.swift index 1ddaf947db..259199b5f5 100644 --- a/submodules/AttachmentUI/Sources/AttachmentContainer.swift +++ b/submodules/AttachmentUI/Sources/AttachmentContainer.swift @@ -524,7 +524,7 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { } var containerTopInset: CGFloat - if isLandscape { + if isLandscape || controllers.last?.isFullscreen == true { containerTopInset = 0.0 containerLayout = layout diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 4653f67ba9..10730e317f 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -124,7 +124,7 @@ public protocol AttachmentContainable: ViewController, MinimizableController { var isInnerPanGestureEnabled: (() -> Bool)? { get } var mediaPickerContext: AttachmentMediaPickerContext? { get } var getCurrentSendMessageContextMediaPreview: (() -> ChatSendMessageContextScreenMediaPreview?)? { get } - + func isContainerPanningUpdated(_ panning: Bool) func resetForReuse() @@ -165,6 +165,10 @@ public extension AttachmentContainable { return nil } + var isFullscreen: Bool { + return false + } + var minimizedTopEdgeOffset: CGFloat? { return nil } @@ -363,6 +367,10 @@ public class AttachmentController: ViewController, MinimizableController { public var minimizedIcon: UIImage? { return self.mainController.minimizedIcon } + + public var isFullscreen: Bool { + return self.mainController.isFullscreen + } private final class Node: ASDisplayNode { private weak var controller: AttachmentController? @@ -1268,6 +1276,10 @@ public class AttachmentController: ViewController, MinimizableController { public var ensureUnfocused = true + public func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?) { + self.node.minimize() + } + public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { if self.ensureUnfocused { self.view.endEditing(true) @@ -1397,7 +1409,9 @@ public class AttachmentController: ViewController, MinimizableController { public func makeContentSnapshotView() -> UIView? { let snapshotView = self.view.snapshotView(afterScreenUpdates: false) if let contentSnapshotView = self.mainController.makeContentSnapshotView() { - contentSnapshotView.frame = contentSnapshotView.frame.offsetBy(dx: 0.0, dy: 64.0 + 56.0) + if !self.mainController.isFullscreen { + contentSnapshotView.frame = contentSnapshotView.frame.offsetBy(dx: 0.0, dy: 64.0 + 56.0) + } snapshotView?.addSubview(contentSnapshotView) } return snapshotView diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 9fa6fab203..7e87fd08a2 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -638,8 +638,11 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.webView.reloadInputViews() } - self.webView.customBottomInset = safeInsets.bottom * (1.0 - insets.bottom / fullInsets.bottom) - + if fullInsets.bottom.isZero { + self.webView.customBottomInset = safeInsets.bottom + } else { + self.webView.customBottomInset = safeInsets.bottom * (1.0 - insets.bottom / fullInsets.bottom) + } // self.webView.scrollView.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right) // self.webView.scrollView.horizontalScrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right) diff --git a/submodules/Display/Source/Navigation/MinimizedContainer.swift b/submodules/Display/Source/Navigation/MinimizedContainer.swift index 129a9e7b1e..d6ea34f500 100644 --- a/submodules/Display/Source/Navigation/MinimizedContainer.swift +++ b/submodules/Display/Source/Navigation/MinimizedContainer.swift @@ -30,6 +30,7 @@ public protocol MinimizableController: ViewController { var isMinimizable: Bool { get } var minimizedIcon: UIImage? { get } var minimizedProgress: Float? { get } + var isFullscreen: Bool { get } func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?) func makeContentSnapshotView() -> UIView? @@ -41,6 +42,10 @@ public protocol MinimizableController: ViewController { } public extension MinimizableController { + var isFullscreen: Bool { + return false + } + var minimizedTopEdgeOffset: CGFloat? { return nil } diff --git a/submodules/MoreButtonNode/Sources/MoreButtonNode.swift b/submodules/MoreButtonNode/Sources/MoreButtonNode.swift index 8122965095..af3ffd7bc6 100644 --- a/submodules/MoreButtonNode/Sources/MoreButtonNode.swift +++ b/submodules/MoreButtonNode/Sources/MoreButtonNode.swift @@ -13,18 +13,27 @@ public final class MoreButtonNode: ASDisplayNode { case search } + private let encircled: Bool private let duration: Double = 0.21 public var iconState: State = .search - init() { - super.init(size: CGSize(width: 30.0, height: 30.0)) + init(size: CGSize = CGSize(width: 30.0, height: 30.0), encircled: Bool) { + self.encircled = encircled - self.trackTo(item: ManagedAnimationItem(source: .local("anim_moretosearch"), frames: .range(startFrame: 90, endFrame: 90), duration: 0.0)) + super.init(size: size) + + if self.encircled { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_moretosearch"), frames: .range(startFrame: 90, endFrame: 90), duration: 0.0)) + } else { + self.iconState = .more + self.trackTo(item: ManagedAnimationItem(source: .local("anim_baremoredots"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.0)) + } } func play() { if case .more = self.iconState { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_moredots"), frames: .range(startFrame: 0, endFrame: 46), duration: 0.76)) + let animationName = self.encircled ? "anim_moredots" : "anim_baremoredots" + self.trackTo(item: ManagedAnimationItem(source: .local(animationName), frames: .range(startFrame: 0, endFrame: 46), duration: 0.76)) } } @@ -81,6 +90,7 @@ public final class MoreButtonNode: ASDisplayNode { self.update() } } + private let size: CGSize public func updateColor(_ color: UIColor?, transition: ContainedViewLayoutTransition) { self.color = color @@ -104,15 +114,16 @@ public final class MoreButtonNode: ASDisplayNode { self.iconNode.customColor = color } - public init(theme: PresentationTheme) { + public init(theme: PresentationTheme, size: CGSize = CGSize(width: 30.0, height: 30.0), encircled: Bool = true) { self.theme = theme + self.size = size self.contextSourceNode = ContextReferenceContentNode() self.containerNode = ContextControllerSourceNode() self.containerNode.animateScale = false self.buttonNode = HighlightableButtonNode() - self.iconNode = MoreIconNode() + self.iconNode = MoreIconNode(size: size, encircled: encircled) self.iconNode.customColor = self.theme.rootController.navigationBar.buttonColor super.init() @@ -143,7 +154,7 @@ public final class MoreButtonNode: ASDisplayNode { } override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - let animationSize = CGSize(width: 30.0, height: 30.0) + let animationSize = self.size let inset: CGFloat = 0.0 let iconFrame = CGRect(origin: CGPoint(x: inset + 6.0, y: floor((constrainedSize.height - animationSize.height) / 2.0) + 1.0), size: animationSize) diff --git a/submodules/TelegramCore/Sources/Utils/JSON.swift b/submodules/TelegramCore/Sources/Utils/JSON.swift index eea17f436c..3251cd2204 100644 --- a/submodules/TelegramCore/Sources/Utils/JSON.swift +++ b/submodules/TelegramCore/Sources/Utils/JSON.swift @@ -50,6 +50,18 @@ extension JSON { return nil } } + + public init?(dictionary: [String: Any]) { + var values: [String: JSON] = [:] + for (key, value) in dictionary { + if let v = JSON(value) { + values[key] = v + } else { + return nil + } + } + self = .dictionary(values) + } } extension JSON: Collection { @@ -125,7 +137,7 @@ extension JSON { get { switch self { case .null: - return 0 + return NSNull() case let .number(value): return value case let .string(value): @@ -172,6 +184,18 @@ extension JSON { } } +extension JSON { + public var string: String? { + guard let jsonData = try? JSONSerialization.data(withJSONObject: self.value) else { + return nil + } + guard let jsonDataString = String(data: jsonData, encoding: .utf8) else { + return nil + } + return jsonDataString + } +} + extension JSON: ExpressibleByDictionaryLiteral { public init(dictionaryLiteral elements: (String, Any)...) { self = .dictionary(elements.reduce([String: JSON]()) { (dictionary, element) in @@ -195,6 +219,12 @@ private protocol JSONValue { var jsonValue: JSON { get } } +extension NSNull: JSONElement, JSONValue { + var jsonValue: JSON { + return .null + } +} + extension Int: JSONElement, JSONValue { var jsonValue: JSON { return .number(Double(self)) diff --git a/submodules/TelegramStringFormatting/Sources/TonFormat.swift b/submodules/TelegramStringFormatting/Sources/TonFormat.swift index f5e49e1fd6..ede7945cad 100644 --- a/submodules/TelegramStringFormatting/Sources/TonFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/TonFormat.swift @@ -48,7 +48,11 @@ public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDate } if let dotIndex = balanceText.range(of: dateTimeFormat.decimalSeparator) { - balanceText = String(balanceText[balanceText.startIndex ..< min(balanceText.endIndex, balanceText.index(dotIndex.upperBound, offsetBy: 2))]) + if let endIndex = balanceText.index(dotIndex.upperBound, offsetBy: 2, limitedBy: balanceText.endIndex) { + balanceText = String(balanceText[balanceText.startIndex..? switch component.subject { case .premium: @@ -960,6 +952,20 @@ final class GiftSetupScreenComponent: Component { buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - bottomPanelHeight + bottomPanelPadding), size: buttonSize) } + let controller = environment.controller() + if inputHeight > 10.0 { + if self.inProgress { + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: environment.theme.rootController.navigationBar.accentTextColor)) + controller?.navigationItem.rightBarButtonItem = item + } else { + let rightBarButtonItem = UIBarButtonItem(title: environment.strings.Gift_Send_SendShort, style: .done, target: self, action: #selector(self.proceed)) + rightBarButtonItem.isEnabled = buttonIsEnabled + controller?.navigationItem.setRightBarButton(rightBarButtonItem, animated: controller?.navigationItem.rightBarButtonItem == nil) + } + } else { + controller?.navigationItem.setRightBarButton(nil, animated: true) + } + if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil { emojiSuggestion.disposable = (EmojiSuggestionsComponent.suggestionData(context: component.context, isSavedMessages: false, query: emojiSuggestion.position.value) |> deliverOnMainQueue).start(next: { [weak self, weak emojiSuggestion] result in @@ -1090,7 +1096,6 @@ final class GiftSetupScreenComponent: Component { } } - let previousBounds = self.scrollView.bounds self.recenterOnTag = nil diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift index b316bf2f11..321dd2f7fc 100644 --- a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift @@ -1077,13 +1077,23 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll transition.updateTransform(node: itemNode, transform: CATransform3DIdentity) if let _ = itemNode.snapshotView { - if itemNode.item.controller.minimizedTopEdgeOffset == nil, let snapshotView = itemNode.snapshotView, snapshotView.frame.origin.y == -12.0 { + if itemNode.item.controller.isFullscreen { + if layout.size.width < layout.size.height { + let snapshotFrame = itemNode.snapshotContainerView.frame.offsetBy(dx: 0.0, dy: 64.0) + transition.updateFrame(view: itemNode.snapshotContainerView, frame: snapshotFrame) + } + } else if itemNode.item.controller.minimizedTopEdgeOffset == nil, let snapshotView = itemNode.snapshotView, snapshotView.frame.origin.y == -12.0 { let snapshotFrame = snapshotView.frame.offsetBy(dx: 0.0, dy: 12.0) transition.updateFrame(view: snapshotView, frame: snapshotFrame) } } - transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + topInset + self.scrollView.contentOffset.y), completion: { _ in + var maximizeTopInset = 0.0 + if !itemNode.item.controller.isFullscreen { + maximizeTopInset = topInset + } + + transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + maximizeTopInset + self.scrollView.contentOffset.y), completion: { _ in self.isApplyingTransition = false if self.currentTransition == currentTransition { self.currentTransition = nil diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 647c1d93a2..f0f10d9518 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -15,6 +15,7 @@ import DeviceAccess import PeerInfoVisualMediaPaneNode import PhotoResources import PeerInfoPaneNode +import WebUI enum PeerInfoUpdatingAvatar { case none @@ -386,6 +387,7 @@ final class PeerInfoScreenData { let revenueStatsContext: RevenueStatsContext? let profileGiftsContext: ProfileGiftsContext? let premiumGiftOptions: [PremiumGiftCodeOption] + let webAppPermissions: WebAppPermissionsState? let _isContact: Bool var forceIsContact: Bool = false @@ -434,7 +436,8 @@ final class PeerInfoScreenData { revenueStatsState: RevenueStats?, revenueStatsContext: RevenueStatsContext?, profileGiftsContext: ProfileGiftsContext?, - premiumGiftOptions: [PremiumGiftCodeOption] + premiumGiftOptions: [PremiumGiftCodeOption], + webAppPermissions: WebAppPermissionsState? ) { self.peer = peer self.chatPeer = chatPeer @@ -472,6 +475,7 @@ final class PeerInfoScreenData { self.revenueStatsContext = revenueStatsContext self.profileGiftsContext = profileGiftsContext self.premiumGiftOptions = premiumGiftOptions + self.webAppPermissions = webAppPermissions } } @@ -967,7 +971,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, revenueStatsState: nil, revenueStatsContext: nil, profileGiftsContext: nil, - premiumGiftOptions: [] + premiumGiftOptions: [], + webAppPermissions: nil ) } } @@ -1015,7 +1020,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen revenueStatsState: nil, revenueStatsContext: nil, profileGiftsContext: nil, - premiumGiftOptions: [] + premiumGiftOptions: [], + webAppPermissions: nil )) case let .user(userPeerId, secretChatId, kind): let groupsInCommon: GroupsInCommonContext? @@ -1310,7 +1316,16 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen return (revenueStatsContext, state.stats) } } - + + let webAppPermissions: Signal = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer, case let .user(user) = peer, let _ = user.botInfo { + return webAppPermissionsState(context: context, peerId: peerId) + } else { + return .single(nil) + } + } + return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, isMyProfile: isMyProfile, chatLocationContextHolder: chatLocationContextHolder), @@ -1329,9 +1344,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen privacySettings, starsRevenueContextAndState, revenueContextAndState, - premiumGiftOptions + premiumGiftOptions, + webAppPermissions ) - |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions, webAppPermissions -> PeerInfoScreenData in var availablePanes = availablePanes if isMyProfile { availablePanes?.insert(.stories, at: 0) @@ -1450,7 +1466,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen revenueStatsState: revenueContextAndState.1, revenueStatsContext: revenueContextAndState.0, profileGiftsContext: profileGiftsContext, - premiumGiftOptions: premiumGiftOptions + premiumGiftOptions: premiumGiftOptions, + webAppPermissions: webAppPermissions ) } case .channel: @@ -1662,7 +1679,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen revenueStatsState: revenueContextAndState.1, revenueStatsContext: revenueContextAndState.0, profileGiftsContext: nil, - premiumGiftOptions: [] + premiumGiftOptions: [], + webAppPermissions: nil ) } case let .group(groupId): @@ -1965,7 +1983,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen revenueStatsState: nil, revenueStatsContext: nil, profileGiftsContext: nil, - premiumGiftOptions: [] + premiumGiftOptions: [], + webAppPermissions: nil )) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 6354ffbb83..7040216a1e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1209,6 +1209,7 @@ private enum InfoSection: Int, CaseIterable { case personalChannel case peerInfo case balances + case permissions case peerInfoTrailing case peerMembers } @@ -1540,7 +1541,23 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } else { print() } - + + if let _ = user.botInfo { + //TODO:localize + items[.permissions]!.append(PeerInfoScreenHeaderItem(id: 30, text: "ALLOW ACCESS TO")) +// items[.permissions]!.append(PeerInfoScreenSwitchItem(id: 31, text: "Emoji Status", value: false, icon: UIImage(bundleImageName: "Chat/Info/Status"), isLocked: false, toggled: { value in +// +// })) + + if data.webAppPermissions?.location?.isRequested == true { + items[.permissions]!.append(PeerInfoScreenSwitchItem(id: 32, text: "Geolocation", value: data.webAppPermissions?.location?.isAllowed ?? false, icon: UIImage(bundleImageName: "Chat/Info/Location"), isLocked: false, toggled: { value in + let _ = updateWebAppPermissionsStateInteractively(context: context, peerId: user.id) { current in + return WebAppPermissionsState(location: WebAppPermissionsState.Location(isRequested: true, isAllowed: value)) + }.start() + })) + } + } + if let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { items[currentPeerInfoSection]!.append(PeerInfoScreenDisclosureItem(id: 10, label: .none, text: presentationData.strings.Bot_Settings, icon: UIImage(bundleImageName: "Chat/Info/SettingsIcon"), action: { interaction.openEditing() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index feed0fde86..6cdd4a7c86 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -317,14 +317,18 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr return nil }) - let scrollOffset: CGFloat = max(0.0, size.height - params.visibleHeight) - + var scrollOffset: CGFloat = max(0.0, size.height - params.visibleHeight) + let buttonSideInset = sideInset + 16.0 let buttonSize = CGSize(width: size.width - buttonSideInset * 2.0, height: 50.0) + let bottomPanelHeight = bottomInset + buttonSize.height + 8.0 + if params.visibleHeight < 110.0 { + scrollOffset -= bottomPanelHeight + } + transition.setFrame(view: unlockButton.view, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - buttonSize.height - scrollOffset), size: buttonSize)) let _ = unlockButton.updateLayout(width: buttonSize.width, transition: .immediate) - let bottomPanelHeight = bottomInset + buttonSize.height + 8.0 transition.setFrame(view: unlockBackground.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: bottomPanelHeight)) unlockBackground.update(size: CGSize(width: size.width, height: bottomPanelHeight), transition: transition.containedViewLayoutTransition) transition.setFrame(view: unlockSeparator.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: UIScreenPixel)) diff --git a/submodules/TelegramUI/Components/PremiumPeerShortcutComponent/Sources/PremiumPeerShortcutComponent.swift b/submodules/TelegramUI/Components/PremiumPeerShortcutComponent/Sources/PremiumPeerShortcutComponent.swift index 65694c3c57..d75f042acb 100644 --- a/submodules/TelegramUI/Components/PremiumPeerShortcutComponent/Sources/PremiumPeerShortcutComponent.swift +++ b/submodules/TelegramUI/Components/PremiumPeerShortcutComponent/Sources/PremiumPeerShortcutComponent.swift @@ -12,11 +12,18 @@ public final class PremiumPeerShortcutComponent: Component { let context: AccountContext let theme: PresentationTheme let peer: EnginePeer + let icon: TelegramMediaFile? - public init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) { + public init( + context: AccountContext, + theme: PresentationTheme, + peer: EnginePeer, + icon: TelegramMediaFile? = nil + ) { self.context = context self.theme = theme self.peer = peer + self.icon = icon } public static func ==(lhs: PremiumPeerShortcutComponent, rhs: PremiumPeerShortcutComponent) -> Bool { diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift index fc2a18c3aa..8e4fb473f6 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift @@ -254,6 +254,7 @@ public final class StarsImageComponent: Component { case extendedMedia([TelegramExtendedMedia]) case transactionPeer(StarsContext.State.Transaction.Peer) case gift(Int64) + case color(UIColor) public static func == (lhs: StarsImageComponent.Subject, rhs: StarsImageComponent.Subject) -> Bool { switch lhs { @@ -293,6 +294,12 @@ public final class StarsImageComponent: Component { } else { return false } + case let .color(lhsColor): + if case let .color(rhsColor) = rhs, lhsColor == rhsColor { + return true + } else { + return false + } } } } @@ -307,6 +314,7 @@ public final class StarsImageComponent: Component { public let diameter: CGFloat public let backgroundColor: UIColor public let icon: Icon? + public let value: Int64? public let action: ((@escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? public init( @@ -316,6 +324,7 @@ public final class StarsImageComponent: Component { diameter: CGFloat, backgroundColor: UIColor, icon: Icon? = nil, + value: Int64? = nil, action: ((@escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? = nil ) { self.context = context @@ -324,6 +333,7 @@ public final class StarsImageComponent: Component { self.diameter = diameter self.backgroundColor = backgroundColor self.icon = icon + self.value = value self.action = action } @@ -346,6 +356,9 @@ public final class StarsImageComponent: Component { if lhs.icon != rhs.icon { return false } + if lhs.value != rhs.value { + return false + } return true } @@ -368,10 +381,14 @@ public final class StarsImageComponent: Component { private var dustNode: MediaDustNode? private var button: UIControl? + private var amountIconView: UIImageView? + private var amountBackgroundView = ComponentView() + private let amountView = ComponentView() + private var animationNode: AnimatedStickerNode? private var lockView: UIImageView? - private var countView = ComponentView() + private let countView = ComponentView() private let fetchDisposable = MetaDisposable() private var hiddenMediaDisposable: Disposable? @@ -471,6 +488,21 @@ public final class StarsImageComponent: Component { switch component.subject { case .none: break + case let .color(color): + let imageNode: TransformImageNode + if let current = self.imageNode { + imageNode = current + } else { + imageNode = TransformImageNode() + imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] + containerNode.view.addSubview(imageNode.view) + self.imageNode = imageNode + + imageNode.setSignal(solidColorImage(color)) + } + + imageNode.frame = imageFrame + imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() case let .photo(photo): let imageNode: TransformImageNode if let current = self.imageNode { @@ -873,6 +905,62 @@ public final class StarsImageComponent: Component { smallIconOutlineView.removeFromSuperview() } + if let amount = component.value { + let smallIconView: UIImageView + if let current = self.amountIconView { + smallIconView = current + } else { + smallIconView = UIImageView() + self.amountIconView = smallIconView + + smallIconView.image = UIImage(bundleImageName: "Premium/SendStarsPeerBadgeStarIcon")?.withRenderingMode(.alwaysTemplate) + smallIconView.tintColor = .white + } + + let countSize = self.amountView.update( + transition: .immediate, + component: AnyComponent( + Text(text: "\(amount)", font: Font.with(size: 12.0, design: .round, weight: .bold), color: .white) + ), + environment: {}, + containerSize: imageFrame.size + ) + + let iconSize = CGSize(width: 11.0, height: 11.0) + let iconSpacing: CGFloat = 1.0 + + let totalLabelWidth = iconSize.width + iconSpacing + countSize.width + let iconFrame = CGRect(origin: CGPoint(x: imageFrame.minX + floorToScreenPixels((imageFrame.width - totalLabelWidth) / 2.0), y: imageFrame.maxY - countSize.height + 4.0), size: iconSize) + smallIconView.frame = iconFrame + + let countFrame = CGRect(origin: CGPoint(x: imageFrame.minX + floorToScreenPixels((imageFrame.width - totalLabelWidth) / 2.0) + iconSize.width + iconSpacing, y: imageFrame.maxY - countSize.height + 2.0), size: countSize) + + let amountBackgroundFrame = CGRect(origin: CGPoint(x: imageFrame.minX + floorToScreenPixels((imageFrame.width - totalLabelWidth) / 2.0) - 7.0, y: imageFrame.maxY - countSize.height - 3.0), size: CGSize(width: totalLabelWidth + 14.0, height: countFrame.height + 10.0)) + + let _ = self.amountBackgroundView.update( + transition: .immediate, + component: AnyComponent( + RoundedRectangle(colors: [UIColor(rgb: 0xffaa01)], cornerRadius: amountBackgroundFrame.height / 2.0, gradientDirection: .horizontal, stroke: 2.0 - UIScreenPixel, strokeColor: component.backgroundColor, size: amountBackgroundFrame.size) + ), + environment: {}, + containerSize: amountBackgroundFrame.size + ) + if let backgroundView = self.amountBackgroundView.view { + if backgroundView.superview == nil { + containerNode.view.addSubview(backgroundView) + } + backgroundView.frame = amountBackgroundFrame + } + + if let countView = self.amountView.view { + if countView.superview == nil { + containerNode.view.addSubview(countView) + containerNode.view.addSubview(smallIconView) + } + countView.frame = countFrame + } + } + if let _ = component.action { if self.button == nil { let button = UIControl(frame: imageFrame) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index dee42dd278..bead20a23f 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -154,6 +154,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { let table = Child(TableComponent.self) let additional = Child(BalancedTextComponent.self) let status = Child(BalancedTextComponent.self) + let cancelButton = Child(SolidRoundedButtonComponent.self) let button = Child(SolidRoundedButtonComponent.self) let transactionStatusBackgound = Child(RoundedRectangle.self) @@ -201,7 +202,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { var boostsText: String? let additionalText = strings.Stars_Transaction_Terms var buttonText: String? = strings.Common_OK - var buttonIsDestructive = false + + var cancelButtonText: String? var statusText: String? var statusIsDestructive = false @@ -223,6 +225,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { var isSubscription = false var isSubscriber = false var isSubscriptionFee = false + var isBotSubscription = false var isCancelled = false var isReaction = false var giveawayMessageId: MessageId? @@ -256,7 +259,16 @@ private final class StarsTransactionSheetContent: CombinedComponent { transactionPeer = .peer(peer) isSubscriber = true case let .subscription(subscription): - titleText = strings.Stars_Transaction_Subscription_Title + if case let .user(user) = subscription.peer, user.botInfo != nil { + isBotSubscription = true + } + if let title = subscription.title { + titleText = title + } else { + titleText = strings.Stars_Transaction_Subscription_Title + } + photo = subscription.photo + descriptionText = "" count = subscription.pricing.amount date = subscription.untilDate @@ -320,8 +332,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { } } else { statusText = strings.Stars_Transaction_Subscription_Active(stringForMediumDate(timestamp: subscription.untilDate, strings: strings, dateTimeFormat: dateTimeFormat, withTime: false)).string - buttonText = strings.Stars_Transaction_Subscription_Cancel - buttonIsDestructive = true + cancelButtonText = strings.Stars_Transaction_Subscription_Cancel + buttonText = strings.Common_OK } } case let .transaction(transaction, parentPeer): @@ -571,7 +583,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) let imageSubject: StarsImageComponent.Subject - let imageIcon: StarsImageComponent.Icon? + var imageIcon: StarsImageComponent.Icon? if isGift { imageSubject = .gift(count) } else if !media.isEmpty { @@ -590,6 +602,11 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else { imageIcon = nil } + + if isSubscription && "".isEmpty { + imageIcon = nil + } + var starChild: _UpdatedChildComponent if let giftAnimation { starChild = gift.update( @@ -690,7 +707,12 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else if let toPeer { let title: String if isSubscription { - title = strings.Stars_Transaction_Subscription_Subscription + if isBotSubscription { + //TODO:localize + title = "Bot" + } else { + title = strings.Stars_Transaction_Subscription_Subscription + } } else if isSubscriber { title = strings.Stars_Transaction_Subscription_Subscriber } else { @@ -724,6 +746,16 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) ) )) + if case let .subscription(subscription) = component.subject, let title = subscription.title { + //TODO:localize + tableItems.append(.init( + id: "subscription", + title: "Subscription", + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: title, font: tableFont, textColor: tableTextColor))) + ) + )) + } } else if let via { tableItems.append(.init( id: "via", @@ -1069,12 +1101,12 @@ private final class StarsTransactionSheetContent: CombinedComponent { originY += status.size.height + (statusIsDestructive ? 23.0 : 13.0) } - if let buttonText { - let button = button.update( + if let cancelButtonText { + let cancelButton = cancelButton.update( component: SolidRoundedButtonComponent( - title: buttonText, - theme: buttonIsDestructive ? SolidRoundedButtonComponent.Theme(backgroundColor: .clear, foregroundColor: destructiveColor) : SolidRoundedButtonComponent.Theme(theme: theme), - font: buttonIsDestructive ? .regular : .bold, + title: cancelButtonText, + theme: SolidRoundedButtonComponent.Theme(backgroundColor: .clear, foregroundColor: linkColor), + font: .regular, fontSize: 17.0, height: 50.0, cornerRadius: 10.0, @@ -1094,6 +1126,39 @@ private final class StarsTransactionSheetContent: CombinedComponent { transition: context.transition ) + let cancelButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: cancelButton.size) + context.add(cancelButton + .position(CGPoint(x: cancelButtonFrame.midX, y: cancelButtonFrame.midY)) + ) + originY += cancelButton.size.height + originY += 8.0 + } + + if let buttonText { + let button = button.update( + component: SolidRoundedButtonComponent( + title: buttonText, + theme: SolidRoundedButtonComponent.Theme(theme: theme), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: false, + iconName: nil, + animationName: nil, + iconPosition: .left, + isLoading: state.inProgress, + action: { + component.cancel(true) + if isSubscription && cancelButtonText == nil { + component.updateSubscription() + } + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: button.size) context.add(button .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 22e2aa4e50..1c5efbbcf8 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -698,8 +698,33 @@ final class StarsTransactionsScreenComponent: Component { isExpired = true } } + + if let title = subscription.title { + let nameComponent = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: title, + font: Font.regular(fontBaseDisplaySize * 16.0 / 17.0), + textColor: environment.theme.list.itemPrimaryTextColor + )) + )) + var nameGroupComponent: AnyComponent + if let _ = subscription.photo { + nameGroupComponent = AnyComponent( + HStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(RoundedRectangle(color: .lightGray, cornerRadius: 3.0, size: CGSize(width: 19.0, height: 19.0)))), + AnyComponentWithIdentity(id: AnyHashable(1), component: nameComponent) + ], spacing: 6.0) + ) + } else { + nameGroupComponent = nameComponent + } + titleComponents.append( + AnyComponentWithIdentity(id: AnyHashable(1), component: nameGroupComponent) + ) + } + titleComponents.append( - AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + AnyComponentWithIdentity(id: AnyHashable(2), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: dateText, font: Font.regular(floor(fontBaseDisplaySize * 15.0 / 17.0)), diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index ed5fd15396..0b7065258c 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -19,6 +19,7 @@ import AccountContext import PresentationDataUtils import StarsImageComponent import ConfettiEffect +import PremiumPeerShortcutComponent private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -263,6 +264,8 @@ private final class SheetContent: CombinedComponent { let star = Child(StarsImageComponent.self) let closeButton = Child(Button.self) let title = Child(Text.self) + let peerShortcut = Child(PremiumPeerShortcutComponent.self) + let text = Child(BalancedTextComponent.self) let button = Child(ButtonComponent.self) let balanceTitle = Child(MultilineTextComponent.self) @@ -297,14 +300,25 @@ private final class SheetContent: CombinedComponent { if let photo = component.invoice.photo { subject = .photo(photo) } else { - subject = .transactionPeer(.peer(peer)) + if "".isEmpty { + subject = .color(.lightGray) + } else { + subject = .transactionPeer(.peer(peer)) + } } } else { subject = .none } + var isBot = false + if case let .user(user) = state.botPeer, user.botInfo != nil { + isBot = true + } + var isSubscription = false - if case .starsChatSubscription = context.component.source { + if case .starsChatSubscription = component.source { + isSubscription = true + } else if "".isEmpty { isSubscription = true } let star = star.update( @@ -314,7 +328,8 @@ private final class SheetContent: CombinedComponent { theme: theme, diameter: 90.0, backgroundColor: theme.actionSheet.opaqueItemBackgroundColor, - icon: isSubscription ? .star : nil + icon: isSubscription && !isBot ? .star : nil, + value: isBot ? component.invoice.totalAmount : nil ), availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), transition: context.transition @@ -350,7 +365,11 @@ private final class SheetContent: CombinedComponent { let titleString: String if isSubscription { - titleString = strings.Stars_Transfer_Subscribe_Channel_Title + if isBot { + titleString = "Subscription Name" + } else { + titleString = strings.Stars_Transfer_Subscribe_Channel_Title + } } else { titleString = strings.Stars_Transfer_Title } @@ -365,6 +384,24 @@ private final class SheetContent: CombinedComponent { ) contentSize.height += title.size.height contentSize.height += 13.0 + + if isBot, let peer = state.botPeer { + contentSize.height -= 3.0 + let peerShortcut = peerShortcut.update( + component: PremiumPeerShortcutComponent( + context: component.context, + theme: theme, + peer: peer + ), + availableSize: CGSize(width: context.availableSize.width - 32.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(peerShortcut + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + peerShortcut.size.height / 2.0)) + ) + contentSize.height += peerShortcut.size.height + contentSize.height += 13.0 + } let textFont = Font.regular(15.0) let boldTextFont = Font.semibold(15.0) @@ -378,6 +415,8 @@ private final class SheetContent: CombinedComponent { let infoText: String if case .starsChatSubscription = context.component.source { infoText = strings.Stars_Transfer_SubscribeInfo(state.botPeer?.compactDisplayTitle ?? "", strings.Stars_Transfer_Info_Stars(Int32(amount))).string + } else if "".isEmpty { + infoText = "Do you want to subscribe to **Subscription Name** in **\(state.botPeer?.compactDisplayTitle ?? "")** for **\(strings.Stars_Transfer_Info_Stars(Int32(amount)))** per month?" } else if !component.extendedMedia.isEmpty { var description: String = "" var photoCount: Int32 = 0 @@ -499,7 +538,11 @@ private final class SheetContent: CombinedComponent { let amountString = presentationStringsFormattedNumber(Int32(amount), presentationData.dateTimeFormat.groupingSeparator) let buttonAttributedString: NSMutableAttributedString if case .starsChatSubscription = component.source { - buttonAttributedString = NSMutableAttributedString(string: strings.Stars_Transfer_Subscribe, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + //TODO:localize + buttonAttributedString = NSMutableAttributedString(string: "Subscribe for # \(amountString) / month", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + //buttonAttributedString = NSMutableAttributedString(string: strings.Stars_Transfer_Subscribe, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + } else if "".isEmpty { + buttonAttributedString = NSMutableAttributedString(string: "Subscribe for # \(amountString) / month", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) } else { buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_Pay) # \(amountString)", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 4d244dd6fc..c923c845a9 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -30,7 +30,6 @@ swift_library( "//submodules/MediaPickerUI", "//submodules/LegacyMediaPickerUI", "//submodules/LocationUI", - "//submodules/WebUI", "//submodules/TelegramUI/Components/ChatScheduleTimeController", "//submodules/TelegramUI/Components/ChatTimerScreen", "//submodules/TextFormat", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index c28cb72788..932be07c77 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -12,7 +12,6 @@ import MediaPickerUI import LegacyMediaPickerUI import LocationUI import ChatEntityKeyboardInputNode -import WebUI import ChatScheduleTimeController import TextFormat import PhoneNumberFormat @@ -1808,37 +1807,38 @@ final class StoryItemSetContainerSendMessage { //TODO:gift controller break case let .app(bot): - let params = WebAppParameters(source: .attachMenu, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: true) - let theme = component.theme - let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) - let controller = WebAppController(context: component.context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil, threadId: nil) - controller.openUrl = { [weak self] url, _, _, _ in - guard let self else { - return - } - let _ = self - //self?.openUrl(url, concealed: true, forceExternal: true) - } - controller.getNavigationController = { [weak view] in - guard let view, let controller = view.component?.controller() else { - return nil - } - return controller.navigationController as? NavigationController - } - controller.completion = { [weak self] in - guard let self else { - return - } - let _ = self - /*if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } - }) - strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() - }*/ - } - completion(controller, controller.mediaPickerContext) - self.controllerNavigationDisposable.set(nil) + let _ = bot +// let params = WebAppParameters(source: .attachMenu, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: true) +// let theme = component.theme +// let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) +// let controller = WebAppController(context: component.context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil, threadId: nil) +// controller.openUrl = { [weak self] url, _, _, _ in +// guard let self else { +// return +// } +// let _ = self +// //self?.openUrl(url, concealed: true, forceExternal: true) +// } +// controller.getNavigationController = { [weak view] in +// guard let view, let controller = view.component?.controller() else { +// return nil +// } +// return controller.navigationController as? NavigationController +// } +// controller.completion = { [weak self] in +// guard let self else { +// return +// } +// let _ = self +// /*if let strongSelf = self { +// strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { +// $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } +// }) +// strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() +// }*/ +// } +// completion(controller, controller.mediaPickerContext) +// self.controllerNavigationDisposable.set(nil) default: break } diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/Contents.json new file mode 100644 index 0000000000..8baec95c43 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "location.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/location.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/location.pdf new file mode 100644 index 0000000000..c9fcd9d6cd --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/location.pdf @@ -0,0 +1,137 @@ +%PDF-1.7 + +1 0 obj + << /Filter /FlateDecode + /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 30.000000 30.000000 ] + >> +stream +xmTKŠc1 Üû^Œ°åìcÌ4³H74}è’ã'-’Jé©TÒ³ó2¥1Ý௡לë3$ʳ' »ŒZ2ÀLàrü¹¾Æsˆ˜ÉÔˆZ™I8•.(ÂoDäJ}¤Ö”tž­Fî”Ef)ñ‚ÑZjê#²Pm!j©Î‘µÞ&,Ì©÷È“¸Hª ¥JžµŽX¬™Ëìv*gb|9‡¶æÞÙGpûκÿƒ³N?Çfü@žµ׹¡þ®ãg>ÃY}Ï›1³ocës?{ÍÁø½›`¿´y °_îÚƒù·^nß(wo^Ë6ÉG0ïÆ™uc¶óÆ}ÜÆÍîùNíÇë- £›èk›W+·ã|úlð?|„¯ð/|ã¨#‡ðâ7wRàLÍñ‰Ð;iÙW wÒj¶ãJN &¹èBÀ(—MQ(ã‚÷vPY7¢1¶S¯¬*s#Þ‚*³KœÂA¤Ò Kn¤—sêèk¤ùk8˜¬b;ÅHíǹ°áÍ–8Ÿ ­äÖGçrz"¬"‘þ³UÌåÜáä`ëS¦AfÑ¥­¤ú\ ”þ+-i6#ëà9Ssêhë¤ù35›Â𣸸³Ù/ɬ#= +endstream +endobj + +2 0 obj + 470 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 30.000000 30.000000 ] + >> +stream +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 18.799999 m +0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c +1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c +5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c +18.799999 30.000000 l +22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c +27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c +30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c +30.000000 11.200001 l +30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c +28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c +24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c +11.200000 0.000000 l +7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c +2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c +0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c +0.000000 18.799999 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 916 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q + +endstream +endobj + +7 0 obj + 46 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000000754 00000 n +0000000776 00000 n +0000001940 00000 n +0000001962 00000 n +0000002260 00000 n +0000002362 00000 n +0000002383 00000 n +0000002556 00000 n +0000002630 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +2690 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Info/Status.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Info/Status.imageset/Contents.json new file mode 100644 index 0000000000..7f32403ef0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Info/Status.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "status (3).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Info/Status.imageset/status (3).pdf b/submodules/TelegramUI/Images.xcassets/Chat/Info/Status.imageset/status (3).pdf new file mode 100644 index 0000000000000000000000000000000000000000..b6dd968ea1254bbb4091715b7dd0ca625226fc11 GIT binary patch literal 41429 zcmeFYcT`hdw>}!W1x1RWAVm>D5D;lff9W7q4815l^p2EJY)BW8-ix%*30;T^0@7PR zYCxp--jlm`)c1SOxaa%bd+s=Y+%eAC3}9uIIiES_Gv`iL)@IX?mFMQ;5k7a0kL50l zqp8g~78Wrv7T(8RP8KY@y6UDj7G`chA8_$%xtqG-yOr#pT3P(=l!m#vYFN0)INCco zI#@WkvE2VXpkm=*FFF&z6VTxmmcd7_jil+dXx&khL&#G`C*1zUxm0dqJ7HwIXCY;ZuvMVL3;CaRNnv*w z@0(AO(CL{jTId+=irN;{V=072OqW;vK5myb?1@iJHART+y^K~DT~6NSJ=*=CV|-lC zAdiLZFnVJ4r&uE;Psc+`8aLCbG4S!@rZg&YA`%lc$ zrMNCR2Qik;tH}r-Vm)IMP?7S)>6Y6i6%)omsRAeEh?1I}ACxGu_2ddQQA?bWCsS1q zfu#6SOAzD=~!WPah*|)d)0n={Sgu&uMtuU`_=hu1rjC766@(AF6AedH0^S zk4*f{tw9xLJ@o;2h5P}5{Y}skbl?(`N){~ zIc*V2sm-0V!dYe|-=`~OR0G#C_z7rwEcH+Mf(>^JJu*ucRUluZ4@M*zLYX?%kjMZv z5{>(j!e2)o@0bTCI1HWVNLSK7iIU~?z9)*Fapw%k)YG$-Y-G=E5Qlv!L+vA;910I`bLS2a(6oo^A5v1k zN$pXoMwY3OX>eo#Q*T`oPfJ;w6k(Lg7%zIoTo$;nx#HH-zX&-u>!^P~$?%4$M{Qpu z9F})~}f~v$bO2 zv;-Vpx1Cl{uDkpuW$hSL6^8y2`CW>pGYEX~G4~boZ8vN!A;-CS6a9lI>3RQTa1AeP zTH#y@F47&4=Ouk!^3lSHe*tKR337w=RktJ^M0(n=5Wg1H3qrcjeWIA(rVgP75sUyU za@r2(cO}nR^^)Tq)U%J%5L1WEw$dz}{iL>MlyV_m_qFY8>-!KS$&=i6UdUgYg(WeL zGz(`xb?;aJqfdzhV@bhth(!@oJ|i<8``4KH%o=s5AM$&6^-TN)TUsjd?m;C;NWuEkG!Bw!<>pF$oo`aZAPZU3E3Rm23cM)?iVTKJsM(-fI_;kb$KHsrmJ>y_L z;A(Q(W30+g`u46oxy&y?NYHxh+uHhU%H0?JUw6ANP!{ z``w+(QkP7+yP1^FZ~34$9QSO3c%iL3jleT>&KrUdmLYj60` zK$|3Xx_a19wDG3~-Bssf&efs>!aOYu!3)w0*X}?06o5TF-d!H6R!^I3@L66P-Dy-v zKqj~4%m?EVIM1=1L22LO7Ou#zHHzVZszUy9Dp6dp6cWruJX)L_HOYWjhpAd|#=MRm z5$WvHe%`m!G7c;v=2Fp6E+U^P`N-~D6&6Bx=JUxKO2+L*QYr)PDnmX;z@;6yYyxt; zGiPpb338d_(o5URN*&Q7NHL>u4M533!VQ?$y`7jJB$7eb0NWM#PIwh zO~`fK8C#3~HUX@fNgCWhj^O2!D2k+aAm$eT7K+C-QfElT5Gwj=fV*cfNED#|3wKEN z>^D38)ZzoUztr9C$2YmvEeE#Tz79!V3VW(1EuNs-wHi|7&vIp16?!YYrN#fG8VbmX zJ<|OJQzAyiYMJ@W*S9(`N*%#=-VP^8ewmpN>E2@=x&AG4A#QaYyHldiS27g!Q#*5J z7ndBWuz?!a^6)R`Ja*N=mv;TV%kWyGvRw*dvqRqZN*!HxHfvhnb}oRb=6tk%`Mo6# z!~tj4{*BQ+12h2r$AC3c5C-VbAl6^rx7~sg~X!hID@;RHHM~RJrpyR(V zb415dkL-*e2(RV>fbULSmFst5O-f|Pq8@8P{<@O?2Qd$t_|nS(d>jVHi;WpT^GcJV zds|v?S|IvDQD}m|y>Qe3odHhjZtRKTZ@4I!38?sb3CFuwhLJ(BP0&#-jDO>O3o5M6rvad@QT{D& z35xe~k)nQhI4&0hlHF)-{78uGBxXoLNn&5q`T*rvZK7=|`P zhk}(O3JA~qwennYc03Ma@nsc(ECK!g zSBQCiY$@oYRfJm(*kj2b^Ub5n*}l=@E70`<>{2%2jOJ@cV0%K?v5{c0>BLptyQ<^D zYCx7-($4_!E^uMr)1p7os6Q#}zEO;X{v#;C%c=eL?3md&oVm$|{`b3J5m9^0QDknu z9JTT=Z9N}!;>@)V{Tj0T(F+JKvuQ(+^p&@`Vqf1#ygH`idX(e<1*za$=mNW)l@Pp- zeuvG815M*wkdy1Txd_uXg;(aF7R^AfjTb2D7x8t;59&NPRn-D!Njo>*fW^2b23IwX ze&Gc$W_(uIFwr=4w@LDd4pTsTx)yBU5yv= z^t(sZ4cN0ANr}1mrGVvniV|)b)umVa2kOFHD&jNa$jA!Bp zxA8;{sd%s0K@HRbc*?+esVjRXi$A*#(U2ODsk(yssut+(37H@g? zA_)um{O#W=@D7x~k9j-b4g2m?cLOGHA1sO)2Yb){$h_xn1RBDFn$!sfVsOdGzoEn= zk2B!XiTrOz;A_D}d>H>k#*+WCQr729ob_37?PYd+CEXdHrvmxVi8J{{4`IBnh2uB) z%GHCf>Kw(;-+aKw!Yl@i;tswLNSCeJ$@N=1oq${|JGv>?-zJ1rvjXwz9>C@LITvFu z{}zcu-he#_E_%TL7Q-jwdA~hr`yNcr{6ALnUVH?F5G-t&?0yF0E{`jq$mItSx*e7r zajX^>KnU-Gi33s@FZlD7qW>Vt2_O95L0JLDE<{Lz86gy3tVlo7f?33uIwLGX0#Bq_ z)Y1!(#trL#Y!p-m)86!-Vy+ebOH4}sKg0lUXl`<|m$^I5^nhp&Cjn;t>t!Pa*YC*E ze<*-RJB3rhynr`5zX~;wV@0H`;7y%oOyEEVem@1z#+cxc?CD!o7aj0EjK|skF;&$H z!5IMA*|a-dj=ijYqL2@S+prjK`tKZ(d9R50j3?OF79iHFV+W$YWG3`nR%%kUV;t`W zP39Y326E>npaKlqbLU(tSZ%Os4>e8`EXwnE$iU47kA|16-v*4W!T~MDDQ!kg)U$aO z)iB+nEgTbmr@j`=ZQ*aTJ711^A8sR9w=(2WU0E;gqEIh7HUag(VVM1YC}XdtDatkU^b};X zLf#&8E=i(j*{ALrr#Cs8FBs?YVkno3{J>;n)!4@5?j}4e>x>vt4WE*JQL zfnwFLZ2-o!%F?^U9=YcCKVN&F6|qFe0M&!V7Z6U5Odj$)D{e0LNU_w4@rzuv=34sT zS9ZPf=`!=v(Yu&UVVJ1Nc7BVX$WVy;BVn+lv8o{8`LhX*Wgi(cuR=XuMj%a%2Jr>= zC9F&gK7eA?^tLK0dH^jyyu%2SWiOU7+}2|J5C# z@hXa2yx9rv)`5|3Zzy+xTRAwPpie(dK1XkXepSW+mp(8&mgN@EH+kp0gUJktAek4J zV3~nHqKG+Aw%$5g%Wae1#twyg`Yxn)guDP{6 zRbX-*Kg*ux-7+$tyx&1dXX0`Q9X8w_!poMTgP6Q_ z%GgtVj}olXeD+4HQoW8Ab5p&yg4vn;Ps&=c*nI_N_|K>yX77d1WXv@)EeIa5(>JTb zR|@r|47YNp#%Gf#W}jA5_`|!p9NSvGzgyczzuF@}F8eO~PyI#lasFg-lQdy_s7rx2 zH&IFt(}UzO1=dj86Y-*K>!hN$MRg+S?N!Trg~H;yv7F@#UfqsNFr@J7lAJG66-AXd zOvO5cdD~@x+1?t_+xjjcOyRb+Q6%anDR!ql;$jbCi#Hb{8la2|?kTY{VfDV-Sz>!z z^*sU)N*H#ZB;)T?OV5^3I6fn6C2B&{z!vfep-)cPbJP{N^}S;~F}8n2YzXG_QFUuu zj9%aD9MT8{}qajP`ubSr=RX)z#Ybppx{(4N*e`3sqaW3PS8;tKL9B zyp>REM^WYQJNdy6e>L7nsDn$fSg*bO9y&n7aL;Etl?_lm9fuK5Uwoj3CgN-C&6!zV zw7INugwk}~QM}88eyd>^2fH9EW_w?K^fl%P%baNZD6QG?zP0#0BDS8$jc^atx5mkd z1ZZd58vW&S%lh1biq@eO^7c)Ra9nIo95P`oaueRCmV1g}mStKidEP>S&8a8a?&74s z60+nC%vP$tX3OBWd^{}Jkj>@hud*C?RYTz$dq68G!g!o^ZMH>b!)Dg@Lq=do{cJEUwCHUq@@WM@ZrCs!{|43|CYXzAk*DCyM+wSLKwQ(pHtQ?<2^3+<*TnT6b2;@ae0mSeQhcQqA%j|!%_O45g{Xx-s z33{hc)y+|5`RQ&*ZvJl5KCMm>?fK;G4-)ITp4levpjJFZWtnf(hJiI@JX`oRyn{Q1 z(e92n6P~-N@NzS&>#BW7ZlAs@^huBpy&;omo#AL2`c0JpK>cw|sV8Kopc7ye4Vb}Y1GoW}RDpc@Cpi`o%nwH)zWVVL4VF5Ov@DWwgpUpGc zw?wr|Aq#(Nhoh%TZ2Su-8`B~BI$`hrx4Qr@U7>G#JlFs6Bk0-B}sP-gozqsX^a-C+_&*Onx!_gA^js0B~)!Wl56U=3; z!wP|p%WBM(@GBys-NJx*Pl7u8I2Yu1XK(c#kg9Sl(y2NJle%lIB`s!sEH#7cTAh&- zLT=llg_DgUE0stcUAx#}HEWSK>1p1I6AdquXY|JU%PglKf8W(jk{xqy5KPJ5-j%x9 zml?2f^}fEo~wn^1iFm)4LB`3bkcCOP9-Teanpy6`&mM*-^}{M`1=*S04Ci zvY%N%BQRLpkoSTMC}PBoRkf)yjtQp2>5ZkHU!fEEODeRB=Tz!prY+l$8TUJNMqcZ4 zOa==X9^ZssMaSdz-JS;s*{*m_Pw;*eb{_5IITA_Gji_|W{214=zR4&)$YX*j=rJ}u znsN7olt{q&J%akJx~e(=I601ceevS7XtPx+oPMbjRRv0Ao@{}N4)$3uxI|vid2}t4 zN}|DP;j<7)kN;M7QPDP$+bg2V$-0y;QBp7S1uWqR6YA zZWx}=UCk+&-e(XWl|S^ARY|?CD3w@2f zx^~pXi7cY_M25Pe^Pdy;ev-I31hmxacXYXEn_U>M;`Q8UKxrQ@zA2B*;dTsZN!jrd z5tN!?%tBk%BFelS#`J9S6v#Izs zxo1=t%Bg#Al)y=}lqU8lFOu|8U}Ap43+B_m;;!U>wLGLVcNeo)e-srGfey4tWrM@Dp$^;$oK7osiE)Z)U` zTEoy7&Z2`@dZdde=evB#!4U|(eH)j)y|(ygS1p548EApfI^5>b+#d7_e|29ignmt} zNC=Vo-D?ypEoakv{QQBK=KJ0zS;G9oMz8>OYBk$xuxPewK zfpv{t$^PtNQd1~^S01i@BF}AbdGqR2M#28WKyQH~ZxdB!?hMY#B;5SWU`0cegRQrPA)y;B@a~V16kb7)>tr$xHCZ##&Mc7-yC2qaV}faOUqC@0TLd zu448|2zP0wXcu4-V)mM<*A@sb02>)X!^KISBkn z@l~#_1aaGz9hlZF|Lxu8lE&nX!}&CSzNcT}OjMHv7MC4wsVCfXAKkgiZ`qwF-pW5* z2S15x(iwIOEOxG>O@a25?;%e^Lj+_oY_TTriSVlNr{C2tifja&`j@Xu_^em9t~Gf~ zSj?yReQes39Q8=bFmSB>I+x^7zDeO7#A{b@_;B(ygJTuCN_6GpSn4s8Q*VX>lh4Vw+!XijLpmeh#h76% zHj)+RyPUE=j_buR*W$W0SxdxV-@VTx6w*7cWxdo-J)_fOe=H4rTLCf3m>xc+udXS8 zoY5E>K6as3KLqwo&S*51>_bOYp%CIcVBALJAr$fwY#mjEK+g1CRnD6YcG?}U!d=PZ zgb)sHEW^VsfGzvSza&ieL#5K`{TfR8iwg)@q`-&`Tm@SV>)O)X>Ms#y30K{~xx=669pD}gy zrXq+~QY1>%SD&GJ)0OBkiBv$M{}RS>$WOZ(w`sc^AhlYE9Gj5XR_aO~iXMJ^8A71% zQ+~tdo^2Yr&8z^=YBs12*zFs9J>>g{t9mmZ1pdP0k+i+d4D)cOy)ObpZm(=z!BOqr zaO64G*b3|v!m7)Lc)%=VGn@zGW@jtw+Sf`TI>YVJS?)4zp;qfqUT zGrhCQ)`m(uJlwhVuoFU1wgN}ykRAIML8sB!Tg70ckn#=BAQn#-<>U% zyN5kOG-JW}=Nl1k^@E9rn}Mhtf*q%6j}^J)geqmn=F}DP&jnnXNPn;2m7v;Dg?@<3 z$(cWdGE1fV6~}{p+(X*aj@T6$Y#+s?yt!q#9(RT+qM@i9bF`T)2s&H8c;Di^S&~|9 z8PNBa*1$>zsG?~2_}=LABzf1R%kTz!PmsvH>2xs~T&WZKF_vR19Up*Mveld!+V@bE z8_0(0YH)@Zp227_dy^D^+Qil-Y=c{}HNZV5GWHP=w-BRs#GRP~bv*@eFrkM=vm+ef z`+UR$QNRkw$TGariOP^j7^})&0K8@^-bZ&*PZt1%6!;$HC5Kl7uOpu2P9~tMoBq%o z$6o>{2lK4I>&R}z81fw1t^*KTzQ%jawH5dh53rKnw}E#A#FU4l{sXc{`!eKuo$=X9 zG0Nh^+|@2lz#?|8j9B(B&}}5#CIGZZ&6P&837yZpz3 zgd_xj*aF_bTfO<$Z`b@1)SxK$E6MAOfW~i^*rZnRUIf$=jF$ERg`y(j8IPGAena@9+{a z5Az8L0TSlhA2;FBZ$PAxW;w&hTgX!%Nl>9+`?bb+sFxZ(D<4^m|dX&=Dk0)<=E;nPz1ocAMAKV3RGax^2^v-dyI)J zNS3v*lKl@!g3f~AsKoy5%-H$MzUc9)iYkCu*Xthta@N3}1}I4e&kTS!#)TmCuXp4I zH)Lx7pL_0t4$*%wunhP5f!n$S3L}}+#A9?Fi!hM>JHo2QK!JR@KoOD$)j3J2a`n)l zgj^R<`lIGPsOGiF`Z7J(_=Wrlo|Ru)IPhVrwZ{qmibj(Ln8 zfoz-S{5|)fPu`h;ORGhHBJdiFz^K+`kb8g~li$xv#nYZh;njChmCPhm{sKM};!<*u_q%JtPd3K4c}h1j zANnqjbrjWX*v&tX%WWO&_bw`TRY+jg1)#$#UGA3g=%c()ekjh`)|hjF`e zW9xA6n1w{r26|X4r_0UQ-pb;~i`MLeO8O*`VE47WRCa820co!cw^wu9KC+#NZ#i(n z*7tJnv7jJ7kU@aE{K+3trn4`dX8Bm!hLg==ngmLh81rtuW^aPJCSe^l(Gc(STD9Xe z?Pt%7C-h$+`lliXS$M_{)b(Wq15$I>HD@Y6HMuECUMS_XT3V>7`>~EpX$V)odFS3r zO=xlL+!CVr;g`3(?oP>IRRTWPK4J@drRM7pvDWi_u$t+k`T8tcyjO59t=OHe zREKtmg-i5NP)jXP;n=Zp`O@P@!h&0c;kZ+cV&2z0&Q(JO-zW3tObr?^Ls5LGo#ytwuhI@E5!#`>=&C!KrWJL6HcDz^>7*d0C zMYT8Q3OX669TuNM>aG-)9erF@nersh2&_uXl8YrX*AFIVLFugD$VsB9I)b(Z$Gbmfo0%qG(&+$hk4j68Iwf8aN7WK*-gE^*y=N$F z;1;>>8Fynh(Bt%ut=fav`5KuDe!#w$?{B2oHueQg{4k6mEB8#-j(fQIP)@bt$MP!i zg&MQmg*R8Kz1+pznHPrUIUtGICIrK(!?@h#;`{9V$ zDj(Oz4?XL1D7LPH##?91?P`W%V?=~6OodsfUZ`ayS`kD=s{+#)aMATv%SQY3)pFd9yu62mH*5yU-svtBl!xtQjR@S!T?7D`io>;%AO4&AOtgq|by|nHSyy><_ z7clKD_Ri&2u;mfgn}()`WEP5y-m3)<2=$Z=IZ!iShwZ6Y_5XUY_G!TT{yS?OHXlNf zvu4=^FKjX{$}!*cy_a_Dthv>ip1uxWDs*^)N@i3mYweS{_%$UYLQTx0{mDv2#AA1j zbB%t7w;|@XK1BsrdJ>mCg85FBv-S*(BU`>(qJz;}vpt&VzhJd+bz|KD;(M;a+R(6y zAJR|AcH`w&tD~91YZmjR4U(2zj3zYQo$7FvIcLr1ytoVAR7^g9q9CZ0wI?bQDQ|~Z zLRM)O8b~g@zgX}iPlS!jmtS_9xcr_{DCavvho_2RHgXrnY~M45-PDI>3Dh5|Ld%NR z@{ZULE4f~TSM`)1a^9*PC43rGS=+L@8^RR!T^_BGjj$VpE;<%x3-eLfrb@{R4ZjPy zIi!DgrRFtk+?ZMD>xev5QND=Phz@HRY+5njFB@pLQp|R_>>>9x55pkaabuI^&u{D> zzgnVqvp-_fbU3d{)7ElbUo)!t(fQYm!&m96)tSX<%^0BCU7JQseoBwU9`civJ7;P? zzF9W-{AsIue#>gbTc*&ma887;FR+zr*Ec4h#lnzlBPX|_d_IVASTTIeibR~Y%$^hR z%vbn>$t(BJkC_RCBW-3_=H42(cApvLSWbC&AQ17w)3&T z9o#v8_DCVaOb>t6%Wchz`WjKq${;x1@Uh%a6)9%f=s5;@-nQ&*>Y2^By5BY=%_jS*6n$qdtr<&21BHC}0!2OkN($vdcUY>K#+*F4|H%DYkz_$bJ|re#&r5u0@TFq1;sv~}RyE=3@H zcyCC^S%=l?@+|sdXCLz@+$A=w3hmJ2K)XW3<;!upc7dfY>a=I}7RnBXxnHN@{(3#x z_b%l8*4a-Hi?=R}WnDQ;D@Y*JE5i}$O8XL)%GBvK?Uf#;6&dtAVaEkqo-_ZjKBoUz z`&kdfLkfj16%UER=?H4E@cYGq%%#jM^B4ZVT;m5`00 ztEaDo{k(Hg{(C6Vz@MLWEk;_H?IkZ?{;qGaQwvLYC zodwRD`tMVRof>Epvx3I*p!dWMXF8uCbIuBUO3*hpVyVsKQWoN1EctxYkRCK9Anf`h znt$!UKV6S7ytGx8yHwsa&W$aY^aCsuR_Hgsv?)~7<^A?!)MB)OH6@{j> z&x}=m$Nup8z%w|jpSBh;eJ(DF8I~d#=BVk}{=jdKLa_ZJH{~&xN@gr!3^RQRh5xZa z)`f#UD6@6Gpls0jdZM+>4_N}uYR&#>fdlu~{c_I|UZ`$s2?{${<@OXmS&_t?{zL@d zRz(k&-6wfetzXj`=YU=ianFt5bWNHc8= z=0h5fUq#r>F5W1yTr0>YsO`lx?Oc<5B(C!`VW9+)sBHeDI>2g1h%^PKj(IrPX4xA&6!JcSPk4ogF$ zYZJ8=u9wn?< zRuq zoZcTc-a~})T)~D`s7cOdQ~P=BXb$x`w_a(bm*kJw7M)xtE}8MG8@|ZEJ7Hi(x;4k% zUWiV{BqEth7OI8(vU7l4mPBW6W4|mB~oZ#3T;4MtD==_7mRB%IQ_S^^bR&%XZ>dZ3c zh^+US0%uVydv}U!Bygy*?L|@a@NxSMEDqSNX$yp!=}hvt&Wu0UKYR`xE}@_Y4(>>oC@&TGl@;DJ4{12e66fP}4Uz`>QbWVsx_<@wi?fPhj`HY_iJ=XAt$>#_bj zGeS>5J&vlHhTQC3m+_~V+FpF7rqSt8awP!{y+&yCK&0mnTTl%1YF-g|P~X?bc($8s zUDtpUD@0ySh18SeKX91CLH>YG_HRE+9y!zdegjk!b@=B0ZTor9_U(_;FNbThsHzEo zV>=gjYB6U^Tou?Hh(I(ool1^d+acqmc$qcSnC9G=+6%y`maQiUy>AVo$9jUvv?j%Q zsRYChF(qP~MNtEQ|5zw!eRZ5G4=dB4pY1<^N>9@?^z8$>*wIc5hWV)kFV{#vpl{tT zqti(65gP`iP0cGKnr|fkb<$eCOY!rQn*FrBPudNv+Ka1z;s>RB=3gu24Bku#=u8Q5 zT+r%)5LO@PeyxNWgp5t%-wDnxU6q<5ZOB8$#<-3jRe8@^I`bA-K{D#0F#BODb9ErH zOgKK!zT{k_d}0>H7ZL1pl1&172RPK&5+l2AGc6R~SPll`LM~Tviyh$i`#S?;sN zf7$+&o96Xz1O3K5d|cj6S*-tXF6Jb5zX))=OFCvoRjKTXj#b1vM+V@~(9h4i$_R^S zLy~`Bh#)Vih~RDeED zr+cs6@%P*-MM(YBlDR*OpH@adeNIyFG$@Vix2XlB;3G@6w%a%kgaw8Vb4QqtKDTgJ z%62pE97ru!R0dMt0mw53u|WkRqB90*$AFS;^v6Fe&F{BCGx_I&%wxJ%@Oy44XcTP) zjZPB)Cl%EBpw#8{@Noz@@QH98$epCQb9hcZc>5L5KLhOPZB0@91EbTse`1tO&8ndX zU~CA0X%*OFxDFmQB%@`>`iDj-v*LZ=xZPW_ED)k;qw6oH-PeItf(HJ|$c7lak5Wm4 z@&Ou*rC%QgPhqwNI!59Rzgz?6hRc(+;a~IcBCai*>HyW`WBhvZ7(OaInxK<@&E5JV z;>90S+So_`(D*mlBk>#{B6Qm+t_*nptpC>VLN5tqq^RbvMZe+@UONdMNW55h7~XI5 ztluyYub7P96z8iKO_IW|64KOUa?=6WJJbjH$~5Nd(q>yiYp_g z!3u=u)BW_@-~J|y?!x)6Xr}?gz+>;HgkU(wv++K6!o&SC7lN41sU=i@F;|51LREaz zHlUdBsAslEHART7e(Wf2^1zt&H0+IdqS99Ur*NVRQ!*FDS2kE9#E#<4{5^m(N2zXG zJ?i4CJN(^Z$8___q9-Tk{k*2-SpD3)d;H3qZ=c{D9&vd}^5H+41NR&5r8lnZrY>g-`vq(`J zXd@Wxz;>WUrN>hbi%&)|rrkpHBEX(O-rcW*G&5Dbo?b_@{x1ZdLEOtEPjn348c?i> zoq8kHmYU}YFEB|N@#wt2!T72RaX1yRJ(BsQ398B5sAgH1-A9xcbP7i`rn}3Q(ZGAl z&Qa)e8O#%eGf9cJa4Au$cZvwK(zV}rv>DjiseJyLf~A2ZChLl@IpY>F@EFMH4-e0( z7N9dcChJX7Sp@VxGPbzj{mPboIJ1;@&~1e%*-s+q#j*M38}tbYuJX zRUuL~SLab>i#ovf(8?#?f2A`e%%S=gbx0;mX@|%m*)bxx?y__KTg9ZMY}x~02NYX> z^TZTkmw6SU$OXsFkalvoX5;!u>bW^)dgtj9}R z7%+EK@peer<~eoK3TKioh?KtTnAAwmm*^{)=fKjzNlm~5B_*HBnixtpi4@mqXE|C( z@7(uz7HNF~=^vAM5Rza|O&j0b5sd*ml6>l((5>1o;wUn?{3{xTJ@qzgvhL*xBu<;%`+{p#%# z{-)c$`^!9qy{F?RQ9d>G3s-0zC!)v5OZl z$3*1=Sf5YZQ8+F^bBNvpnS7hrP&$ZzZvcV${KCk&*q3QIJ*&MTAhoQ=l-gsrn2)GG z9Wz*$Ldq*!f>gV{_z}jR)_Vy&hUghg6~v;5h);+$>fA4fQ%kWHI{Uls$-oKYCA=BK zDW$RseZLNOsY3`Ph(7ub64EmV$S>_rG&w$7>*BR~wN10pRj+PRd2C8gAHZ|Z`h#lF zYM;%1`BRJfnAK66xbl<^56-c&l~xPU_x2`xS>>zkRor7ab_+uW_O-L59T)nZ{7-wD z83Gaif+LGo$PPGX;b8vzsd1nMKKp(Sd|Lh2L-2fp5BLPZ8Tfwwzx)4NAC4FJKRz6< zrTWy>mIZt+|IY*T|1Tbwf21J&e|uV9R_5`4`m8+eJ8lYcNm*V=9zs9>fe-*c5Zn)f z?@BTFf!I5^xd0kiboKOE$fka+0LPx1xjKRW|IG!+-5Ua> zLm&g(|7Onrsr#(Cg{v7bVFUQR=>jMRz|sJ18mnJz4zO+dt1Sk$J>8t#fH@jq+vV{i z8K7+rv^lK)(Kh`@+sw%YoF4|v5wo{*2X%oLh?v~M0jdT3KLh+;gSbK-K@=d;p#8vW z;Bw4_K<@8CAVhEf)n}RjfmFYOK(37btM6ty1VZr=0;%fxSKq&U;`G$zDP9~g@R!ii z5(3%Eg+MOoK_Jw<5XdKA<6~kI5)ry9a_27JT~G+YnX_lf&ymxSlhfU0VPLuYU%qgkAe3i_A;b_O zf*TM*N&+HE0$ekM9&k4i0Z7EZFH*8IB*a9(n9?PHoJ54ggd}H4h|iD`0h0&_iHJ!k zNy(_LQ}dm<@t5`h4K3Z3dz#SbY-#71vi{4g!hugM-CusFK+fD`;}>}3BCpl-4iH82 zA2i@qfxVRf)cdP<6haOtBBUgugh)bqrv66tzm5O(!2f#Se~<@C?`uV0vu*ZcGZ{gR z2w6{xsj3&WLN%+cvvO=x(T*r}by)a*cpUT1TBR3FqsBtD%~z_nDhm@iP!1EvMx|zz zkDIj~JJOkYBp~b;<41)?EfG#vaS+uVYaC=uCx25C2RYkT5gBw`m!CNc#U6y@MDFYy zQ&Auy$*^q2e(9@VKn2ku za*>V=&PJ5t`)L)-p8kRxL@dt!x1P1W+E};5G?%5lh_r2Khpc2{JIMofQ`2-?n5OXX$^Ayup}*2ehF7fK`0+ncmDApgZdE^2MJ2_Ki0;a zBz1oJH`4xV-9(OCGbduxC41WJCyBa&$V0h&J+zlaQ|*?kIdPDtF3D4bBUW|p2@D5OMr~}X;~+L;I7qZt{J&fGU+Xtc z4Ks9ok)spyCGo@9v+a~9x{YEegIq_2iWao40ArmHV!aq%-}Bh<;{df>0A@zar6Ub< zy{V=D*(mxX#D3Kw<c(pR!Mk(YAj;D|OqI89bl z&i!jr8C3g6-ehW>??S?;50p|oca04E6!4#1TG$x>>A5s4x*7+$wSAYj=b@^0V!6Nt zrXkhtI*x`IeJO>oUdf^Yg=VhsK;#Ro^5FC znn3}qASBZ>B-8ApiaH!~ms*u8$T1{x6);CP_AwUJt7=h>b-+Jj z50wOP5Jd;U=B8@X(;_aBS%!5#thlC;PiN1w%sSZW>}dSf@o|9@9odj*nB;|C9OTCS zSBbLmrw0x}hjfjcX#`Givzn%j35qRVU#(@v7UtIr{!u@lJVc*Surn<16QJkYzz^9&jTM)0n0m>MGc&YS6_mUavLJ z$FH`d!1Ijm@l2P?U4ic#Wvlku+D2~c^5WF1X^r`#-mtw@2~BFgvI9@~rs-9R^NIu3 z3tzufsul>p2}U@I6pY@uRgmkQkPkBomuf_q;-d1GMuaw-1aIVx3a4aML$w&S=2yb!ZU;CWAWXV{q_T7lRq7X^ zt(;GGg`Dt~xgHy+P9Qyc2dRoeSxNZzda3kW@CyTkqsvQyv3YHQ^Z@-Dy<`=KB zZB*UDPy`}Hz35hUmusu?lRMC>{I>8MyFO&6oIp=<^U(mqp&Xy0#{<_-btVK0&Vf0t zp;4YT<>kw{Hd|bgcBg|a+XLzqUfT%?34ezH(vuox*we5TI)1&W*5gOB=0Lu=UV&vk z0w4#b{RXQ;QI01j$73|_ZSZzeqKO1QM?W&eTi^~?BU{~amd%!ftg6J#^~pq!0@Fm_ z<4*LX>21WP|GJO(rKc5v``4}d_^FxG3x23}90lkONwyI7+@MIF&$Mj7~PW)+! zWJR^E(kZjoQ=iwHOFwv6o2%vXqh;mUlHT_O2RWE}=XkEFx;MAlSLd+W&lD7x{`_0( zB?Ba;*iUi~=21nJLlS13^xn8@L*0E3E_Z%nN+v=lYJZ6vg5lL}8h6OeSbvd4i zIaVJ#dw>GqWo5OstB6b%4WbNoKC$szFw6N${%C6zp;n_0uC!M_0HtXVRb0ZLmv#?`7txv z`nA&GYp8%+y~pYR&43&qgB%~_=ndAY9*OV|PZqq5B@(h{48AtCWM!fY#PTEO`S}4G zy%G{q=8Ql6F^@^y3i`Qs=VQ3K_P6TLpl}7FvC2a&=59+XYmnZQ44p6HT2J(XJ{$7J z6j^D`JTuU=Y}TQH`ps%p3dr^xcCJ$MsS2tMRCPLkJy9=k_NevI8CpN6rGx9kaw}=A zBRM%DE%-iSPvb@7jx zH*P%ngYUoXKha1QOmB29KQae)fB0U-J=+D~3s{ld!~d(jF9B=n+V+pN>aD#(k*Srj zaEl{D41`IbtpschP)q{KAT^N$!VsAuOj@rc0fG9Ql0e`I*8oA_QijM7u*zUHK+_PB z1cHFfgn5$jzvBRQy6yey|Gn>f@13tThm~{oUVH7m*IIk6-`=scCBa}o6B2yRMw=b} z+QiYI?ZD8kk5W~2#tSf*@T?|<0nd<8mT78NwXH3$&hg=s6jhmatmDs&gahrUNR}Fh z0_~Vx*yq^~w4?rjk4U%laKZxv&)%t}!xcnQOb)7Pun0a2vr;r37a$aDN}MpB{ie)* zLSSp%ll2B_9ZH)E3L1Or?a9H`GxYi%Uf-AKn?+0R&!Yp4+7D&Zf7##07(?}_kp5qBHQRvz8B8hD$| zg=vfmpjLUwb46m9B2ubgg*IES{mtF9E+_TjZMF|RUR*8qt8WXIB0^EY)SRcr7%i+0 zR?h(GC`2{}&NK$}=hkND9EfXF4bqcegY25gcO}g8x)^X^a``ZuSohdO#s*hgY=$=T z#?#256KfE8LL^z@*tMsymE@vNr>+#NxL+lyMe$J8izUXNf7Vok|8ipMbtPvN*=kiU zC`d0FrFrtBrmA`c%eiSb&PPmSrJuVTH|K)rxR}o|?BaTKR0Q{-Fc=}MS_rf1$48IJ zzab}aqX7_ze)A_&cjtEwUyDp|uj9C!Px5F|85h7l;OItDD06%V&wfHAJ%yR`Xef2+ z^0larc(y}W6VFq1*X@h9Zl}yTWhRi~1Rf2Y%Xw5ugoKvwIdt1>5zkCMcb;^&zOD?B zWE|$;X^c{EfL4#gN^PUggBa_}D|K=WqH=1&_1!+d6RnXFmDUr*%Js@#6= zNgy2IABa3Y$0f4!xe@u979*JfsM0to&}KS1mg_CFjxrtX>FvD#ph~8@9Ufjcz&zL8 zO(>v+bZf@550O8ecGBs`du{VkjX4nQ)k*xoWrUh0fJ&NRiXvJUais)m+{{;ORj`f< zIS35KfhR-I=LRFM41PK%S9bYIf z^r|J_<^*^h0>k4tr z>^ycjU^9AAQ1;=Tr}6r$IG?bm!{z>%!hr~`r)KG6j@S?CGObwJFzWyYXdRjFaPZr8 z?%$SAi{X9uWQW0?EPXJjjgdV)$T`-b>Z559i8>L|0dxbuT541SG+%{RxB6(c-~lJOx__@E_5|Z2nieu`{@ZU2fcCA3TN&<`X&;$Q1uDJ|+THI#&B$Xe2@#44;s7>X95uwI3rHCzNoKd_ ziYEiyi?K}#ft^*M^~QXp9Uo~MPN7Ik24A&cbK~z#_iut1)*wkn3;QnsRXTi#^e*wh zObHI4^Arye|% zfvW{Y?+9TwdK`_m)REr5Q{ox=C5{(bpXNR%q0v(+C{%U?bAWTj))x7S#W(jEZT!14 zUT)w`t)_yW{add7@92cl8!ZNSRHNI?bPYWN=d7VwA)L)7vV!w7Bp;z=4vbP=BN~B0 zN`-=*Ek{oJKK0-|d?I~o{6ebVQo-SUok1`%-m;yAjYg?Sy~^==bde4@_spLVt%jrW z53t)g6r&l>eQ9CdeQn149*n>N3Zg{lmQJO*mVg0{4}Dagu;t9js)ows9GR`FY)i*}>M*EM(M>SQfirm=janIp_=E?yvTB=p$LjhOH9dokl4j&6R z;>0dHDr;zdI4eLHqN>u#X3=SM7M?~GM^eQr?+yQ~`P0MizfaE4;8`8r*?`w?svPa& zsP4)4r6v#2XiSz){P%;K+$ZDz7HLV*KUtA*VI_$f!Zn@Xy6IpuXwzw)BO5FE5EY`^@U9M)XZ|?FsmVemH}!PF?!~iYmS>qHwVzM{^WLL2ZZ=3Z@0&2L=w>kQBj|K#$%!eOYikf_ zsl`;7JA7YTNR^~8)y>~sT1EqFN)e9C0+ShQ<-?-euBbmFIBHrO?i!vB>5Ir^pMlP? zQ@1B(`KDbZanM3!3oUcJG>x=n_MVuR~^=z^+2*B{w_=u{~d3H@F z*DrjCD$#u;uI9lGdo6yl2Kl?i_)_f(qWt4cho0taMm4JA5En;W_x*S#cXC>f&d$i4 z%IZSf`?8DhlQ0JpDb(m(S(or2(%BN5Qbrac;27hnYpCiXuep zd{Ou}$Jg2W%Nj&$60f!8OpQw!(>}R70FkEP;fz_j>e$Lxv>AYvBI}KXmX2ZzMsxo8 z8$(gMG72t}RONT2RB`;KST^arX1;<(KVHX_j&sBXJ<4ljJRKNDb>l>mbdS&XNiLFi zw`;|sO2aTx(V;9IZS!TXy2Tt-zw2eOfxuK;iV(6G(aw0vR2F3prPeYokuCcODT6*1 znt62=$?xyd+p;{$p36uwCJ7XwP8d|5gQZYSXlYB5xW;cf^7Gt`9(Ip`dRAB+O=eND<9gDfnKMVn@`*|bF5151u>*4>EIhZD71v}Lk+SNS{mBnAHJXClJA9)9U-s}z zXHH}X=1~&K>`V|Dqe=241c38@t9CtKl<>oKLo3NjpnctO$o@6Rdcr97o#5%t@kdW% z5NH33o~G1r2fOb*iwzrOl!vDUSCx1<4Ex?MLnzkfA<1Ro($XfB$Z{N3Ik+j=Z2oGs z!<`xDcvK_jY63kVWia}EsUYqVE`c+bkUXTVe$ABsi3g2UsaJ`eORBkP z5&9jheXL46oPpmB)KLIEBtt#t88bZDE8&(`#SIT>bm0KijH8z)+Fgotr!jn4cp|e0 zONh!jlXquhiu{4QO+_nOHg`4NUFluT>eV*!%GSIYZF$W(H9ReppVKdCC^N1b$H0_2 zC55Z8uEZ2^IAvu@vR2T=YXB z?OKJ(^_|1xeF9>xv^FZW>jNVVIdy?e`9cEn#~9M)zGvaZF()$$Q!0Bh3OLDgU{A%@ zBMrp_RZUs^FZ{rXELr@I#$xQH$l`xss+6CW`}BCp=Cm=Rsmy>^5EQ`$)N=3`hV>6|;_EB`O{5V-hx@ZR5 zb}_%9xK7o;>&HpIq}C_m)iDFlp~4LI2z8i7VOwD7Ilf(NqD1GL7Ioh`57*{G?Q{dR zAE(BA&BDfUyO>!F0kEq=RQJvvVS8iFr&mRa{!SJJ3-0EE3YA{V+~!h4s=xnV;KP+IZ+&sW`QNi1T~d1QPsw`B* zghfR;n0PSsETzUyV~ISKoNUcBf+fB9Qh5HAww&&h-5ffK$HzrxlF($@HyiZ#e2X})$1l6cl@^}5Zf{^~rSeDRyF{Q4Jf z-G34oHtDqeRPT_mDcRMbn2%5i&LRV|%N)#l8UU?!AxETMB)Eyo7 zI4(D{iMOAjYCWqI0%w)xndODVEJC?p4mUR5Kj^#qO^5PzdSLyMTj7q%dCAH$8J`HJ zW=tZ&FkKJJU<2T+>a)Oj3~{b!Ospp9c4sw*K@us5Hw5X_Y`N40kKbR3eY>A9VT$*TyY^_}k=o_fE$-2Kl5ReS5x>r=} z@12%eruD)r(_y^AojuHBOw*5B$6gqu9Jv*!19Sj`!Pp|8eQLbMD9SVqm#p@AbDIe= z7WP>~#Z~%cFGM~%N@xRC>0p`1SAbdC!fg&js%<+LNocKoOAK+1G9^fUa7_b zSgFfkF-~W1t!JnZ?M%GQFu;^q^h_ z!nr{7OEjfT?gyFGY~aMR^H|I|Y0b1~6T|9nkx5H_G|n0S&XM{b^-)Pk=jcLeg>~73 zEZs5_651IZwFZe$dlsM^&Ces8CX!PeL zu{ypW>yjTeTN$!>C^>G<0CDT0JLiB`iH8R#xD(}o#wb=}6g7IV(PKO2U<5<1_H^m3 z=w0!uSdZ38sk0I)4PUhE$X5<%Q+90IJ=(A&070NxTipc48}7A|s13eyU`f8Z0XXY=2!&JM=c`L98or^nB)jDR#TrG523k8Df*Tv*yk z(7ctb`$b*r0hEWB-LF3!C{VVwZGsu|8kUfbT|)`~d@0zVS-AtW&6{<*Tr@FU zurO@m1Xe~+Xc9VG&y-39YCrt#_<^o%OKmsSAPOZb_ANE?^F@bObqR~-v=!DMnK4@9 zCli+KT4uGZ7i=V4`Gw@n;kVD)uf|TqbIDqhYBsH7IIohcseZaD(yBQ_XI1?U4jSVS z$|EV;Hu_Rimd?M7*!;!oy%5}2#)sln=HJ8OT`|hSz(-qxE@MKFv6-ky79o-{%)rL2 z3jT>aNNTTdlx4X(lT>`3XSw#W60xQr%LTlQ*mhWk`u%6qh0e)0Pu4nKI4^2aUM!9q zh#d!UwuVVdlrjmb02^+?e9{z1BBqD*4Bak`Isa)A7kwgi#eGsOv}s6!4W|t!;!2SX zD<~wo;MFjEDYQTOv4#4*qlct7MLS#fi-w9rHVIy<1_K`a9%1>6h=m1?#t@@iOpGmt zId*H$#P(gt_}l$_%I(yhedLoSLGND+L}_Yz-=`1LySQj)s7ARckIBhknIR7aeRSENlv|23D=VM ziFC6|v$r!)W(PP|+d8;M30?Rg0AwkIaiZv%eyI;Lw*0stfB51K_#uy^D$LdEo|dkc zJfaC>EYl1|Y+6e?kN4o!Nco?V`+`{09vOO{b_1E&;FTp|y(}|pg7HLIg3PQ0PbhDS z_`b%ydjg(7(pkyiM!(8gF+xodlaO}eMhv?C{wpc&1+W=SLX23DlduqNdGFCw_%lR! zHo4fJ=OiT!cjQNko3YFRAwBDuTyTi446;C*GYkWe1@=Wt;{X6z=rMGyMgqvf6(m`p z7KK1L+J0XPpbukq7_kic5a>}6oF{`mNQm?d0DYL>_WvO&4%tKwe)me-w^K$7Rg}RB z+^(k3r@oKHO(rMVyU8SsG*Ks$~niEg4{?5Jjch$^a`E3=-tfpjt6PsK#8%O22NE zW{6r!nd70h-8RQE-_?9H)6?VZGK1q=-(MPLX0wX|`F#osn;!NQ#9Jmto`<;!i5!=c zoPbyR5rUo7rXqgfxnIKz$5?`vh_?;{v zoX)uTA|cGsgUb@a0gjkKI!Flng-X?;Y=zZ}e5E70G2(uFgx2sSghGfaqM?wWx|6^~ z_4Rm&OWUBeqh0N5V5tVAs2!mEvgy|ke++l?CoQ|Fj_Gz6I%?lq#hD(U=Y}->2k@`?7 z*O$kgVmg^-Rf}7BGe|+LgOcrPlNBa{CW)Xkt5Axs8lr4tWuB9Iw=&%BTz2PVSVt8K z5$0a#`)r4@y_im=CS6wKLKVejD6vnjZXCcGl}ts;utpJj%WcoGMmn?{Uw}0NP!NDM zf{R%ZlH7|6uUhsdKlM`(Qzg2IW$Ll<$IH5Xj$g-C?ya!JObriZ7AruZ@E#nIg{vjw zYNn5TwxVHoe|D5^}VosUaoQZ2l=-^3P3m4gZ6#Wh)> z=RmHVl3XSHK=Fhyq^t?0ixPet=#4+!))p>}3v-&HP(aqU)YjIHFHcaNw3f{ish4}L zY@YbCGGy~4H_Pfu!}QWT0C&;py{|gw6`%k9Cx1EV>HM7Q_X}xB(AsIaHlu$A0u+7u zT>ZmKYmm{Uyp>e**_t~T^$#C7LVNOEg3rvf{KuD)amSMZCzTa7tenv(No5|5CSv>2 zn9EC!kG5tG^64RRoo?3^A2&W=E=nKY1B(+$xTvBKAmE(ZQ>$vz4xepVgSdFjR;_}l zPUzydYmkDjiLRvr$Yz_B6Y&7=Ls}6GKV0R+--{nBh5U65GV8qtaj)&Jo%c+(;a7bv z?H1=8dlcqzF}JI*kIKO(RyB*AjoYANpC#1%+>|E-n63?sf2>EO&7^+F^yuY}y?&CfDP^)pF(p>otYI1BLd;({2*7 z0ZM}NrA$e}nLW{Ux4(qEvkh`4gRpDT&LHW$*ZH1t&yi`{Hix9phFMprR-Zz3odX7? z*05Bb@q4H*j8<0HU!Bk2eHLSX3Flw%`DlA_a3OuQhbkh6&*n)6Jdj9$HDgxUHoclg zzmu4JGCg8ZD3@^aDUn?6Q0;MuXff7V^nkV+0^%7U=R~7Oypzq78{L3Ic{wxB2{@GG zprAeZGKcbX>@0}>xN>~J45HebnGL|`>CS}DyOU0|yW!!J$w|Zxp$q1IA}e2_ZtvPI z(b*l9h$RSz*%~SD&4jmw6t>sae{jvX`owr}JtJ$*o>)F4vHCnBCn_ha;-P-hpF>m-;#;!wp0biTs~A7g?PU|`TPMYU&qrDt|ayg@z79(MKMQMxIdN5{Wrn)!fW|gP8%a z(p4!7=zHwulz(3%Jbyz!)lMSU*ki5F%_l5AUevk}mlWh7F!5rr0(er5p_D{cbYo;r zv+374n7E7NfKkm8b_h{{(C$WBv|cL;#x<@(`4Ow4X5HA)BoS^Nk^3_~w%hzzln{ry zF_eB7`)J;%u-U+tPl!qjn`f~{Quq0?nOVEb=VLEhO`j)BUq`OC^~Jx}8!3@bpqhJn zTDdR4RLS9zLyPdP_L`Mt+8s!Q+1C2>LW}Ovn0%M<$K1SL_lm$iD8@u!3epnTXe2WH zv6{^VTe?`jS!gxc%Ks%fy)x za(IdA*q8N>tAgwEg>I}>K2)jAn1=-ECKo<*#2L8+4~K-zXviP!mRelgq4-vVoW7b9 zf&mAC^&EB{SPSA2#|_SIh#+ZN?ObThp&te|A4`2#=g};-kMB6!+h+ZEX$UxT znZTKYD(*b{KJ3JgC8bs*^XN4RVG?dcGxp8Ns@_*y*^;}BN9dz8qT`LyXH+Awy z*d1Q{sePvr;49bdyuT`;(LO`pd@f|;_Vw-&Gt&`LPmslc(sxt z<|blw0gN<&7F7EfvgO?1vsFVMuW_xJdF91BGcf@AQEE7!x)^=7?a9x|eEuiM=zSIo z>sv0Wz1$la-(C?OTuF|YG^LjbYF27zBf4ta#o}u3t(=+U=Ek}{yPE`^0R&H6e&Bct zJ)^QmYN}@$MJ^x70KvR2qwjyMfiWFkVR75;H{N|v*dHNZ9~Mai+hp8^Y4k!NN}{*B)^P>K zJ&CgVV%KQU0xq%CVE?`JLe17uf_t{KUJ>fa?Z;XSGTAwCba7!hvxeN6LtS(wG0)Pk3@#;MXY^8g>;a5 z$%$mLba`qWAqx>F?)7NMzw0XS8>w$9a}b%*=sEEgL=j$xO(5eODXf_ z*EjKP+XqFS=#I2!=4Ge(Xxq?wxU|W{ws~osI~*<@%`^=3KdwI0Ysg#qyX|th7R{s(6_W6OX_oLei;}y8F zK%UWjk;kfT^x~z7oyb7rUVj!4>Wh561oMo)fxxC9m){O$tEK0o7p`q{rD8Z8u*=-0Gf~OGwKf#>`;5-PqD-q^*#1)p#7L zh(VP>)#mAxIWGNQ;tNNq&5oz>LCqB*n)PGJ?{1emC*YHN45ZbF;du(3ojXNMb=gfa zXA|YLpk^(y-Ln%qcrcB~nkxj<40bCRp1%nL$GjGRN0I^ax_0<0ByqE_w z1t&AO@50Ba!IgU?)qa;1oTzFMY-S#XooUTCo~I_3X(qFu0LY7V&Oe@tWssLbq3FdUj5sn+abD*Ka?S>xA#}>FFer6@U1Xzo|zT&dDu;ha%tl>k*3F+ zcFF%E{yl7{bHR|A#nnoEgoX<-UsD>^AmO4$P+c{XN{vy{QoHZtGKX~UMHoqic|M{- zYxHmQr#zx9-t zW~#@n`xSI*gT+eGyP9qDm2vtHbnr90a4s4d&N7vx`tuOZrM6+V&(t1TM2?`j%zTFj z#-7%dSgCP6x)~GFb&pj9z`(3L>M(^(ORlWUcKz2;<;^dH^uSM2Wq7rRf4g96n-lYJ zuG;F;mC*1qkTRGUqyn4@F?o23k|8M_KZ3B*uE-=%5AN-js6VVYc|F;q*>fb!K7k&r zXMu%}W0b&-xj8l=HiyMvrTtb*{}f$O4n1LDuNj26rBNgjfK*8o^D2>+xjh-|@QMz@ zKn0p{?IJ8aXyJ|Ri}=`L7yK%K9YrqUFDE~T9VNNszJMKpfMP!$A3=|P(WJM%XdPiU zCo~hx7q5WrBoR5PI}^)`bZ*btcOpp|m}t{fa~c(Zoer5_npQiUt@b$=TM%Jc#@Ij0 zs;^lB`<{R4g9=Oc0nuVCYb7>*{qTRg@hI25!wZ`B!Wyy4AJsC3bXy~H>W9?`lGIw}i~ z^o&%5XUW1Nf~j5mQbBk`QB`j@2#;{cxbo>Qj+i4JR>yhyMCdz=Um1?#D)&oRge*LB z(I;#7obPacb;EQuPb#9Nzu-nb!(qwn_I&WcLNP!JD+c#Hyo!(^g{RS=Zp!#=^JPrY zlL?M|zn@!y>)yJ$tbIlIa@cGZNEpykYlyUzCHuH(h0YXnYC}{m{VP8kciO#dJ~q|l?WI6C{&yH)5@?b(q@RU`#6w?~i2 zK9q$MQo$Z_M*hTaNb66LHw6aEUsJt(9bHmqA^E7XRNbxLhZ9GqHC3q9+1i&U631bd zb-YliHRs5YKNx1(e31M}h;_@()2MsZ=GVcIqPr^6s~!!f${&1+46O^0u=mh1mZa$B zB_{_HWYHf~43201R5O3;*dFy5kPUl%J;C!Scy zsNw~vA%TO!jAuRR9b$yTx9+HYS9PZl1ZjmPp?r`&0Ambb2>S;n{s>z41-IL~WEsii z8+?$FEOx14$}*AyY?mw}iF6dGp_Bs@VXzT|$)}(Ef%tr{0|6t$TFADvJ;z$ewzNIR zT7bYYUqM05c5}<`p&Wioh<}D|#eiRB77}SP)fciKrdI=0-!wb^iY@mOk|*J1CG>sxB#`!u#CoD?!#?9LX-)OC21d&0bA&TGSuR z{P2lC*KN!uAsYQKBZ(Wma4n$G!HOe-7n1C+AXla5UQJ0k8^wucX1!U^`GWeT1lyXC)nT9bd$+)vd0O%O zN-KacB%D~ia`x@`{@vS_@&MdGu4Ki~`o_bQ?X5;**sku_eMu2;i9Zg7y)D8w=dt8dp*l*5bSbh^sg`%Ibb_f#W0;F72T$Yc9X0I9WDFT zAn%>ACR`}M&fESX3z?;@O)vukVci(hy!3{5-Y;z-?bT6C&OGC0Jik9 zJwl8DKXTNpw;Nyx&8@@CU7J8~ZA!<(-@G|*^lWcQ(6FOc-`71WIU;vS(!mL&qivtNWyPh%Up z`WhFa?s<*ga(ZU`)yF>{gxWE4KkV(1J3%a+5vMK2YKc^ch3vdUy$)oXRds*B!j(Fw zfZ1%MtQ~xYj@Q(AfJJhQV^``wQI-q8u#=brbNktP$=NJxq5FJ>^%h!z1eIZy>a!*e6txTiAEu#PEPwwZvk z1~J)Pr}-b`fcIApncwK^s>LWbo{U9{Se@?V%K}@WNyE~(nxJo>s|?zZ9#t1ZxMVU? znf&mYPEA$hwUwhPRl@RCdtX{2%OZzdE`@0iheXim>fg2LcP82B35+TS@0OtLJrM3; zt{#n2jKIlvY+fxOTq&q29sZ)BjYc_lwhJU!FsYjymJ5ZFt-XOZ3KtzN8_&0|}u%+VI!TwzUr? zg`LsfI9~Vp-!C55UcU@c+J@-hQ-LAiz*O%*T+~m#x3+#+Iw}ks`n*y*QgFaa+A;4C zQdls~JLGv4FS&Vf57m|(?|V$!+8X@#^6^hUB}HnxYa1E7INWsO2DVPxxbp-rfj3{C zB^oP2_x@~=LXITFi`2(M%yWQV%^TIK8^N7sT#daNb z2wWNVZw-o{TOHUxGhFC6Da6F}eg=7n+xeH<&KCPkM1&yLQfH#+X~uUhnNhk^xzQ!i zZ(|?8I2AW;Y`Wh2&D+yIeEp7*{QTo>-Pg;Mybjz{eYf#o_k59j?fXYQ9PN3h((H2e zUFQ@mu9A(SVZC{!;Qa`r&nrhJVOnv-TQ6|53MQ1{DQEPr%=h&|d zPuUoHF+5?$X6pwwo32wgM%1qkL9_p+A^0XnTF`drFUF_-`uP0ick5r_0=BQ;|MDgT z7r5~?)Xc!tTy|-{_RhhB2Btt4z=4!sF>uh>0B&YvVggRkHZX-BG&eEUJ_t8}nwpx! zVA?pqs4>jg2yUi*(A2=#+~oP4fr*i^IqabJK{EprxQRIo%tHe+pkv^0?Stk92Vtg0 z#wOrZ69X93+}PMmdt<_x8XPn=hl7GQNKmN3!RK@JH5#aPo%@X^{kLzhzd_yL|25ir z177FnwHB;1zR`r&zFlv_>whOK2(N9G6(szJ9KN$ZtyK89c9qxMOrwHCJ2@c+;eNX+n-h-^94G%>& zG+rI~f0z^)>KzDbd@hAo)GuW2PpCHZx#I#VeqCBW{jGwdzNZ1V+Rs~YO#2nrhe=_w z*MHH5ke`*c;rq$jE^8%Vt7MlpFgdad3Wpkl@@3a+B$=Ukodh#82BrL*WCG;&lc&s~M!-Ax8R;OHj<1u9 zjSiZFF8(PA1_dquDG4qU;ZI4X(4Q4=Y-9@iMe*jpEX&+{quHUsr+odrgSX2B{FyE2 zsy!j30PnrpdwfstebHZr$4Dg5S?fO*VTkf2d&}esdOalcRB-5XNgG3rOrYC8`Uv4< G_x}K!8XI*0 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/Contents.json new file mode 100644 index 0000000000..595c2fda0f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "miniappminimize_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/miniappminimize_30.pdf b/submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/miniappminimize_30.pdf new file mode 100644 index 0000000000..33da1d357a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/miniappminimize_30.pdf @@ -0,0 +1,92 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 8.500000 8.804749 cm +0.000000 0.000000 0.000000 scn +0.707107 9.402358 m +0.316583 9.792883 -0.316583 9.792883 -0.707107 9.402358 c +-1.097631 9.011834 -1.097631 8.378669 -0.707107 7.988145 c +0.707107 9.402358 l +h +6.500000 2.195251 m +5.792893 1.488145 l +6.183417 1.097620 6.816583 1.097620 7.207107 1.488145 c +6.500000 2.195251 l +h +13.707107 7.988145 m +14.097631 8.378669 14.097631 9.011834 13.707107 9.402358 c +13.316583 9.792883 12.683417 9.792883 12.292893 9.402358 c +13.707107 7.988145 l +h +-0.707107 7.988145 m +5.792893 1.488145 l +7.207107 2.902358 l +0.707107 9.402358 l +-0.707107 7.988145 l +h +7.207107 1.488145 m +13.707107 7.988145 l +12.292893 9.402358 l +5.792893 2.902358 l +7.207107 1.488145 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 772 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000862 00000 n +0000000884 00000 n +0000001057 00000 n +0000001131 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1190 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/Verified.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Instant View/Verified.imageset/Contents.json new file mode 100644 index 0000000000..4245696764 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Instant View/Verified.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "miniappverify_14.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/Verified.imageset/miniappverify_14.pdf b/submodules/TelegramUI/Images.xcassets/Instant View/Verified.imageset/miniappverify_14.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a8edc9d6d7cea1fc09303f8ec89190cf7f9a4704 GIT binary patch literal 1604 zcmY!laBvK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~f-!aK>2r6@cv`;KajoYLx!5bR zGnc#M?l(ccpe+Y78K z+U5VxSAF{U{=D^v&%fTXZ)!*zk9OgURaW1w*Wcdt?eW9UuZ8Dt<w9hE|&siyd@3-vAl~BLHUBfg+Shb; zM0I4m-r4N&ypmmVQtAP@LaDv1&s?(%^LW>$CF@^of487u+=|t%{$dR;k(+(h|B<{8#O5wjtaLr z#9yWd>|*ZxqISjIx>rw$lWXgoSiTF{&Ostt_c(Sa`o?I!J+aHlH1xe(?HZAeC1DeU zGQ^s^W_+046fbSGaL2?)7iTSfXm{Zcr`50BQ`I>&uI^I2AS|`@P?$q^m4Dj9HU|Ap zi>o>@ds}59#kr3hVHWygy+QKMh2zXx0SCgkUM!h+QShwXI;J^7 zhDmV}t{nSYe6*Z*w1ouc&$;f#_b4#ymeSW1&O2Nm9kv!Pit3RPGurz?Id1+9>1_YY zn{qUIW^(3KSua0ka^Qp%!>^2-=7`jqPRoUz2k+nT)hOu`j#zWRc|q|rxf3$J6Rh+6 zK2+8A{#?aSm?Z6TCosK2JlAXLuNNQBig8s|ev^I^0>G17AYV36xDHvSy*!AL6eQzc`Ai+-0P)3KVN!8guS%H?t8b8(f@%+%F^zk9mx z3!iSD?|UX8<&#uMtjFgwY&#ws@a#T4b%$T4?Dr3W#ilzf>%5mFs2H3O>YW;JQ@rG_ zLyIi8xwhVpnKioen*Z*vzh#$OpA*j*G<91oD91yyEG+MXGBc!1urxM9_Rd0pk6}^_ZLHoF<4-jp_l~nU@_c6ki#GzcTOxx%*jtj)ml-Mn#N_IV9o{c zK8RE>Gc`3fRR9V@fuVr{n5B>h7cvIA69nKwh6bjXLgqk+p{g=6u|yX#HUh>aR8>h) zVrEWi5f>=-JY9gnp;4Tlo2sdxk(r_i2^9UH{QMFHkcYtmtRI|Nl?rq{xJ*whDgk@P O(AdI=OI6j?-wgmK_g}pL literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/Animations/anim_baremoredots.json b/submodules/TelegramUI/Resources/Animations/anim_baremoredots.json new file mode 100644 index 0000000000..e9c79a9f1a --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_baremoredots.json @@ -0,0 +1 @@ +{"nm":"Main Scene","ddd":0,"h":300,"w":300,"meta":{"g":"@lottiefiles/creator 1.31.1"},"layers":[{"ty":4,"nm":"more","sr":1,"st":-38,"op":47,"ip":-35,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[150,150],"ix":1},"s":{"a":0,"k":[100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[150,150],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"3","ix":2,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[-8.284,0],[0,8.284],[8.284,0],[0,-8.284]],"o":[[8.284,0],[0,-8.284],[-8.284,0],[0,8.284]],"v":[[50,15],[65,0],[50,-15],[35,0]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,1,1],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[50,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[100,100],"t":15},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[125,125],"t":25},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[95,95],"t":35},{"s":[100,100],"t":45}],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":1,"k":[{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[200,150],"t":10},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[200,120],"t":20},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[200,162],"t":30},{"s":[200,150],"t":40}],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"2","ix":3,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 2","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[-8.284,0],[0,8.284],[8.284,0],[0,-8.284]],"o":[[8.284,0],[0,-8.284],[-8.284,0],[0,8.284]],"v":[[0.5,15],[15.5,0],[0.5,-15],[-14.5,0]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,1,1],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[100,100],"t":10},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[125,125],"t":20},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[95,95],"t":30},{"s":[100,100],"t":40}],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":1,"k":[{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[150,150],"t":5},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[150,120],"t":15},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[150,162],"t":25},{"s":[150,150],"t":35}],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"1","ix":4,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 3","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[-8.284,0],[0,8.284],[8.284,0],[0,-8.284]],"o":[[8.284,0],[0,-8.284],[-8.284,0],[0,8.284]],"v":[[-49,15],[-34,0],[-49,-15],[-64,0]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,1,1],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[-50,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[100,100],"t":5},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[125,125],"t":15},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[95,95],"t":25},{"s":[100,100],"t":35}],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":1,"k":[{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[100,150],"t":0},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[100,120],"t":10},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[100,162],"t":20},{"s":[100,150],"t":30}],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1}],"v":"5.7.0","fr":60,"op":60,"ip":0,"assets":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/web_backToCancel.json b/submodules/TelegramUI/Resources/Animations/web_backToCancel.json new file mode 100644 index 0000000000..1ae3321c25 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/web_backToCancel.json @@ -0,0 +1 @@ +{"nm":"Main Scene","ddd":0,"h":99,"w":99,"meta":{"g":"@lottiefiles/creator 1.31.1"},"layers":[{"ty":4,"nm":"Artboard Copy 3 Outlines","sr":1,"st":0,"op":60,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[49.5,49.5,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[49.5,49.5],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":1,"k":[{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-18.672,-16.5],[-2.172,0],[-18.672,16.5]]}],"t":0},{"o":{"x":0.8,"y":0},"i":{"x":0.8,"y":1},"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-8.25,-16.5],[8.25,0],[-8.25,16.5]]}],"t":15},{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-8.25,-16.5],[8.25,0],[-8.25,16.5]]}],"t":30},{"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-18.672,-16.5],[-2.172,0],[-18.672,16.5]]}],"t":45}],"ix":2}},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[50],"t":0},{"o":{"x":0.8,"y":0},"i":{"x":0.8,"y":1},"s":[100],"t":15},{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[100],"t":30},{"s":[50],"t":45}],"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":1,"k":[{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[50],"t":0},{"o":{"x":0.8,"y":0},"i":{"x":0.8,"y":1},"s":[0],"t":15},{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[0],"t":30},{"s":[50],"t":45}],"ix":1},"m":1},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":2,"ml":1,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[41.25,49.5],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 2","ix":2,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":1,"k":[{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-1.5,-17.25],[-18.75,0],[-1.5,17.25]]}],"t":0},{"o":{"x":0.8,"y":0},"i":{"x":0.8,"y":1},"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[8.25,-16.5],[-8.25,0],[8.25,16.5]]}],"t":15},{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[8.25,-16.5],[-8.25,0],[8.25,16.5]]}],"t":30},{"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-1.5,-17.25],[-18.75,0],[-1.5,17.25]]}],"t":45}],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":2,"ml":1,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[57.75,49.5],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1}],"v":"5.7.0","fr":60,"op":60,"ip":0,"assets":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index 1fec78f193..97da9a68e3 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -103,12 +103,18 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u } var fullSize = false + var isFullscreen = false if isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, url: url), case .peer(_, .appStart) = internalUrl { - fullSize = !url.contains("?mode=compact") + if url.contains("&mode=fullscreen") { + isFullscreen = true + fullSize = true + } else { + fullSize = !url.contains("?mode=compact") + } } var presentImpl: ((ViewController, Any?) -> Void)? - let params = WebAppParameters(source: .menu, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: fullSize) + let params = WebAppParameters(source: .menu, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: fullSize, isFullscreen: isFullscreen) let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) @@ -196,7 +202,7 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u } else { source = url.isEmpty ? .generic : .simple } - let params = WebAppParameters(source: source, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: payload, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) + let params = WebAppParameters(source: source, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: payload, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen)) let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) @@ -242,7 +248,7 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u return } var presentImpl: ((ViewController, Any?) -> Void)? - let params = WebAppParameters(source: .button, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) + let params = WebAppParameters(source: .button, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false, fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen)) let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) @@ -472,7 +478,7 @@ public extension ChatControllerImpl { return } let context = strongSelf.context - let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, botVerified: botPeer.isVerified, url: result.url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings), fullSize: result.flags.contains(.fullSize)) + let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, botVerified: botPeer.isVerified, url: result.url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings), fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen)) var presentImpl: ((ViewController, Any?) -> Void)? let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, forceUpdate, commit in ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index a248e12346..ab8901e8b6 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -613,7 +613,7 @@ extension ChatControllerImpl { payload = botPayload fromAttachMenu = false } - let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: false) + let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: false, isFullscreen: false) let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageSubject?.messageId, threadId: strongSelf.chatLocation.threadId) controller.openUrl = { [weak self] url, concealed, forceUpdate, commit in diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index acb60a6399..32ed35f82d 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -308,7 +308,7 @@ func openResolvedUrlImpl( id: subscriptionFormId, canSaveCredentials: false, passwordMissing: false, - invoice: BotPaymentInvoice(isTest: false, requestedFields: [], currency: "XTR", prices: [BotPaymentPrice(label: "", amount: subscriptionPricing.amount)], tip: nil, termsInfo: nil), + invoice: BotPaymentInvoice(isTest: false, requestedFields: [], currency: "XTR", prices: [BotPaymentPrice(label: "", amount: subscriptionPricing.amount)], tip: nil, termsInfo: nil, subscriptionPeriod: subscriptionPricing.period), paymentBotId: channel.id, providerId: nil, url: nil, diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index ebefd6a4f9..c894a82f00 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -79,6 +79,7 @@ private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 { case storySource = 11 case mediaEditorState = 12 case shareWithPeersState = 13 + case webAppPermissionsState = 14 } public struct ApplicationSpecificItemCacheCollectionId { @@ -94,6 +95,7 @@ public struct ApplicationSpecificItemCacheCollectionId { public static let storySource = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.storySource.rawValue) public static let mediaEditorState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.mediaEditorState.rawValue) public static let shareWithPeersState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.shareWithPeersState.rawValue) + public static let webAppPermissionsState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.webAppPermissionsState.rawValue) } private enum ApplicationSpecificOrderedItemListCollectionIdValues: Int32 { diff --git a/submodules/WebUI/BUILD b/submodules/WebUI/BUILD index 19b80d186e..49d110863f 100644 --- a/submodules/WebUI/BUILD +++ b/submodules/WebUI/BUILD @@ -41,6 +41,13 @@ swift_library( "//submodules/UndoUI", "//submodules/OverlayStatusController", "//submodules/TelegramUIPreferences", + "//submodules/Components/LottieAnimationComponent", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView", + "//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent", + "//submodules/TelegramUI/Components/ListItemComponentAdaptor", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemImpl", + "//submodules/DeviceLocationManager", ], visibility = [ "//visibility:public", diff --git a/submodules/WebUI/Sources/FullscreenControlsComponent.swift b/submodules/WebUI/Sources/FullscreenControlsComponent.swift new file mode 100644 index 0000000000..a2764744b3 --- /dev/null +++ b/submodules/WebUI/Sources/FullscreenControlsComponent.swift @@ -0,0 +1,352 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import BundleIconComponent +import MultilineTextComponent +import MoreButtonNode +import AccountContext +import TelegramPresentationData +import LottieAnimationComponent + +final class FullscreenControlsComponent: Component { + let context: AccountContext + let title: String + let isVerified: Bool + let insets: UIEdgeInsets + var hasBack: Bool + let backPressed: () -> Void + let minimizePressed: () -> Void + let morePressed: (ASDisplayNode, ContextGesture?) -> Void + + init( + context: AccountContext, + title: String, + isVerified: Bool, + insets: UIEdgeInsets, + hasBack: Bool, + backPressed: @escaping () -> Void, + minimizePressed: @escaping () -> Void, + morePressed: @escaping (ASDisplayNode, ContextGesture?) -> Void + ) { + self.context = context + self.title = title + self.isVerified = isVerified + self.insets = insets + self.hasBack = hasBack + self.backPressed = backPressed + self.minimizePressed = minimizePressed + self.morePressed = morePressed + } + + static func ==(lhs: FullscreenControlsComponent, rhs: FullscreenControlsComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.isVerified != rhs.isVerified { + return false + } + if lhs.insets != rhs.insets { + return false + } + if lhs.hasBack != rhs.hasBack { + return false + } + return true + } + + final class View: UIView { + private let leftBackgroundView: BlurredBackgroundView + private let rightBackgroundView: BlurredBackgroundView + + private let closeIcon = ComponentView() + private let leftButton = HighlightTrackingButton() + + private let titleClippingView = UIView() + private let title = ComponentView() + private let credibility = ComponentView() + private let buttonTitle = ComponentView() + private let minimizeButton = ComponentView() + private let moreNode = MoreButtonNode(theme: defaultPresentationTheme, size: CGSize(width: 36.0, height: 36.0), encircled: false) + + private var displayTitle = true + private var timer: Timer? + + private var component: FullscreenControlsComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.leftBackgroundView = BlurredBackgroundView(color: nil) + self.rightBackgroundView = BlurredBackgroundView(color: nil) + + super.init(frame: frame) + + self.titleClippingView.clipsToBounds = true + self.titleClippingView.isUserInteractionEnabled = false + + self.leftBackgroundView.clipsToBounds = true + self.addSubview(self.leftBackgroundView) + self.addSubview(self.leftButton) + + self.addSubview(self.titleClippingView) + + self.rightBackgroundView.clipsToBounds = true + self.addSubview(self.rightBackgroundView) + + self.addSubview(self.moreNode.view) + + self.moreNode.updateColor(.white, transition: .immediate) + + self.leftButton.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if highlighted { + if let view = self.closeIcon.view { + view.layer.removeAnimation(forKey: "opacity") + view.alpha = 0.6 + } + if let view = self.buttonTitle.view { + view.layer.removeAnimation(forKey: "opacity") + view.alpha = 0.6 + } + } else { + if let view = self.closeIcon.view { + view.alpha = 1.0 + view.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) + } + if let view = self.buttonTitle.view { + view.alpha = 1.0 + view.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) + } + } + } + self.leftButton.addTarget(self, action: #selector(self.closePressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.timer?.invalidate() + } + + @objc private func closePressed() { + guard let component = self.component else { + return + } + component.backPressed() + } + + @objc private func timerEvent() { + self.timer?.invalidate() + self.timer = nil + + self.displayTitle = false + self.state?.updated(transition: .spring(duration: 0.3)) + } + + func update(component: FullscreenControlsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let isFirstTime = self.component == nil + let previousComponent = self.component + self.component = component + self.state = state + + let sideInset: CGFloat = 16.0 + let leftBackgroundSize = CGSize(width: 30.0, height: 30.0) + let rightBackgroundSize = CGSize(width: 72.0, height: 30.0) + + self.leftBackgroundView.updateColor(color: UIColor(white: 0.67, alpha: 0.35), transition: transition.containedViewLayoutTransition) + self.rightBackgroundView.updateColor(color: UIColor(white: 0.67, alpha: 0.35), transition: transition.containedViewLayoutTransition) + + let rightBackgroundFrame = CGRect(origin: CGPoint(x: availableSize.width - component.insets.right - sideInset - rightBackgroundSize.width, y: 0.0), size: rightBackgroundSize) + self.rightBackgroundView.update(size: rightBackgroundSize, cornerRadius: rightBackgroundFrame.height / 2.0, transition: transition.containedViewLayoutTransition) + transition.setFrame(view: self.rightBackgroundView, frame: rightBackgroundFrame) + + var isAnimatingTextTransition = false + + var additionalLeftWidth: CGFloat = 0.0 + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.title, font: Font.with(size: 13.0, design: .round, weight: .semibold), textColor: .white)))), + environment: {}, + containerSize: availableSize + ) + let titleFrame = CGRect(origin: CGPoint(x: self.displayTitle ? 3.0 : -titleSize.width - 15.0, y: floorToScreenPixels((leftBackgroundSize.height - titleSize.height) / 2.0)), size: titleSize) + if let view = self.title.view { + if view.superview == nil { + self.titleClippingView.addSubview(view) + } + + if !view.alpha.isZero && !self.displayTitle { + isAnimatingTextTransition = true + } + + transition.setFrame(view: view, frame: titleFrame) + transition.setAlpha(view: view, alpha: self.displayTitle ? 1.0 : 0.0) + } + + let buttonTitleUpdated = (previousComponent?.hasBack ?? false) != component.hasBack + let animationMultiplier = !component.hasBack ? -1.0 : 1.0 + if buttonTitleUpdated { + isAnimatingTextTransition = true + + if let view = self.buttonTitle.view, let snapshotView = view.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = view.frame + self.titleClippingView.addSubview(snapshotView) + snapshotView.layer.animatePosition(from: .zero, to: CGPoint(x: -(snapshotView.frame.width * 1.5) * animationMultiplier, y: 0.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in + snapshotView.removeFromSuperview() + }) + } + } + + let buttonTitleSize = self.buttonTitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.hasBack ? "Back" : "Close", font: Font.with(size: 13.0, design: .round, weight: .semibold), textColor: .white)))), + environment: {}, + containerSize: availableSize + ) + + if self.displayTitle { + additionalLeftWidth += titleSize.width + 10.0 + } else { + additionalLeftWidth += buttonTitleSize.width + 10.0 + } + + let buttonTitleFrame = CGRect(origin: CGPoint(x: self.displayTitle ? leftBackgroundSize.width + additionalLeftWidth + 3.0 : 3.0, y: floorToScreenPixels((leftBackgroundSize.height - buttonTitleSize.height) / 2.0)), size: buttonTitleSize) + if let view = self.buttonTitle.view { + if view.superview == nil { + self.titleClippingView.addSubview(view) + } + transition.setFrame(view: view, frame: buttonTitleFrame) + + if buttonTitleUpdated { + view.layer.animatePosition(from: CGPoint(x: (view.frame.width * 1.5) * animationMultiplier, y: 0.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + } + + if component.isVerified { + let credibilitySize = self.credibility.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent(name: "Instant View/Verified", tintColor: .white)), + environment: {}, + containerSize: availableSize + ) + if let view = self.credibility.view { + if view.superview == nil { + view.alpha = 0.6 + self.titleClippingView.addSubview(view) + } + let credibilityFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 2.0, y: floorToScreenPixels((leftBackgroundSize.height - credibilitySize.height) / 2.0)), size: credibilitySize) + transition.setFrame(view: view, frame: credibilityFrame) + } + if self.displayTitle { + additionalLeftWidth += credibilitySize.width + 2.0 + } + } + + var leftBackgroundTransition = transition + if buttonTitleUpdated { + leftBackgroundTransition = .spring(duration: 0.3) + } + + let leftBackgroundFrame = CGRect(origin: CGPoint(x: sideInset + component.insets.left, y: 0.0), size: CGSize(width: leftBackgroundSize.width + additionalLeftWidth, height: leftBackgroundSize.height)) + self.leftBackgroundView.update(size: leftBackgroundFrame.size, cornerRadius: leftBackgroundSize.height / 2.0, transition: leftBackgroundTransition.containedViewLayoutTransition) + leftBackgroundTransition.setFrame(view: self.leftBackgroundView, frame: leftBackgroundFrame) + self.leftButton.frame = leftBackgroundFrame + + if isAnimatingTextTransition, self.titleClippingView.mask == nil { + if let maskImage = generateGradientImage(size: CGSize(width: 42.0, height: 10.0), colors: [UIColor.clear, UIColor.black, UIColor.black, UIColor.clear], locations: [0.0, 0.1, 0.9, 1.0], direction: .horizontal) { + let maskView = UIImageView(image: maskImage.stretchableImage(withLeftCapWidth: 4, topCapHeight: 0)) + self.titleClippingView.mask = maskView + maskView.frame = CGRect(origin: .zero, size: CGSize(width: self.titleClippingView.bounds.width, height: self.titleClippingView.bounds.height)) + } + } + + transition.setFrame(view: self.titleClippingView, frame: CGRect(origin: CGPoint(x: sideInset + component.insets.left + leftBackgroundSize.height - 3.0, y: 0.0), size: CGSize(width: leftBackgroundFrame.width - leftBackgroundSize.height, height: leftBackgroundSize.height))) + if let maskView = self.titleClippingView.mask { + leftBackgroundTransition.setFrame(view: maskView, frame: CGRect(origin: .zero, size: CGSize(width: self.titleClippingView.bounds.width, height: self.titleClippingView.bounds.height)), completion: { _ in + self.titleClippingView.mask = nil + }) + } + + let backButtonSize = self.closeIcon.update( + transition: .immediate, + component: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: "web_backToCancel", + mode: .animating(loop: false), + range: component.hasBack ? (0.5, 1.0) : (0.0, 0.5) + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 30.0, height: 30.0) + ) + ), + environment: {}, + containerSize: CGSize(width: 30.0, height: 30.0) + ) + if let view = self.closeIcon.view { + if view.superview == nil { + view.isUserInteractionEnabled = false + self.addSubview(view) + } + let buttonFrame = CGRect(origin: CGPoint(x: leftBackgroundFrame.minX, y: 0.0), size: backButtonSize) + transition.setFrame(view: view, frame: buttonFrame) + } + + let minimizeButtonSize = self.minimizeButton.update( + transition: .immediate, + component: AnyComponent(Button( + content: AnyComponent( + BundleIconComponent(name: "Instant View/MinimizeArrow", tintColor: .white) + ), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.minimizePressed() + } + ).minSize(CGSize(width: 30.0, height: 30.0))), + environment: {}, + containerSize: CGSize(width: 30.0, height: 30.0) + ) + if let view = self.minimizeButton.view { + if view.superview == nil { + self.addSubview(view) + } + let buttonFrame = CGRect(origin: CGPoint(x: rightBackgroundFrame.minX + 2.0, y: 0.0), size: minimizeButtonSize) + transition.setFrame(view: view, frame: buttonFrame) + } + + transition.setFrame(view: self.moreNode.view, frame: CGRect(origin: CGPoint(x: rightBackgroundFrame.maxX - 42.0, y: -4.0), size: CGSize(width: 36.0, height: 36.0))) + self.moreNode.action = { [weak self] node, gesture in + guard let self, let component = self.component else { + return + } + component.morePressed(node, gesture) + } + + if isFirstTime { + let timer = Timer(timeInterval: 2.5, target: self, selector: #selector(self.timerEvent), userInfo: nil, repeats: false) + self.timer = timer + RunLoop.main.add(timer, forMode: .common) + } + + return CGSize(width: availableSize.width, height: leftBackgroundSize.height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index b1ac4f8d1f..48c4c656e8 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -6,6 +6,7 @@ import AsyncDisplayKit import Postbox import TelegramCore import SwiftSignalKit +import ComponentFlow import TelegramPresentationData import AccountContext import AttachmentUI @@ -30,6 +31,8 @@ import UndoUI import AvatarNode import OverlayStatusController import TelegramUIPreferences +import CoreMotion +import DeviceLocationManager private let durgerKingBotIds: [Int64] = [5104055776, 2200339955] @@ -64,6 +67,7 @@ public struct WebAppParameters { let keepAliveSignal: Signal? let forceHasSettings: Bool let fullSize: Bool + let isFullscreen: Bool public init( source: Source, @@ -77,7 +81,8 @@ public struct WebAppParameters { buttonText: String?, keepAliveSignal: Signal?, forceHasSettings: Bool, - fullSize: Bool + fullSize: Bool, + isFullscreen: Bool = false ) { self.source = source self.peerId = peerId @@ -91,6 +96,11 @@ public struct WebAppParameters { self.keepAliveSignal = keepAliveSignal self.forceHasSettings = forceHasSettings self.fullSize = fullSize +// #if DEBUG +// self.isFullscreen = true +// #else + self.isFullscreen = isFullscreen +// #endif } } @@ -136,6 +146,7 @@ public final class WebAppController: ViewController, AttachmentContainable { fileprivate var webView: WebAppWebView? private var placeholderIcon: (UIImage, Bool)? private var placeholderNode: ShimmerEffectNode? + private var fullscreenControls: ComponentView? fileprivate let loadingProgressPromise = Promise(nil) @@ -158,6 +169,8 @@ public final class WebAppController: ViewController, AttachmentContainable { private var queryId: Int64? fileprivate let canMinimize = true + private var hasBackButton = false + private var placeholderDisposable = MetaDisposable() private var keepAliveDisposable: Disposable? private var paymentDisposable: Disposable? @@ -180,7 +193,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.backgroundNode = ASDisplayNode() self.headerBackgroundNode = ASDisplayNode() self.topOverscrollNode = ASDisplayNode() - + super.init() if self.presentationData.theme.list.plainBackgroundColor.rgb == 0x000000 { @@ -321,6 +334,13 @@ public final class WebAppController: ViewController, AttachmentContainable { self.paymentDisposable?.dispose() self.webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress)) + + if self.motionManager.isAccelerometerActive { + self.motionManager.stopAccelerometerUpdates() + } + if self.motionManager.isGyroActive { + self.motionManager.stopGyroUpdates() + } } override func didLoad() { @@ -593,7 +613,15 @@ public final class WebAppController: ViewController, AttachmentContainable { func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { let previousLayout = self.validLayout?.0 self.validLayout = (layout, navigationBarHeight) - + + guard let controller = self.controller else { + return + } + + self.controller?.navigationBar?.alpha = controller.isFullscreen ? 0.0 : 1.0 + transition.updateAlpha(node: self.topOverscrollNode, alpha: controller.isFullscreen ? 0.0 : 1.0) + transition.updateAlpha(node: self.headerBackgroundNode, alpha: controller.isFullscreen ? 0.0 : 1.0) + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: .zero, size: layout.size)) transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: navigationBarHeight))) transition.updateFrame(node: self.topOverscrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -1000.0), size: CGSize(width: layout.size.width, height: 1000.0))) @@ -606,8 +634,10 @@ public final class WebAppController: ViewController, AttachmentContainable { scrollInset.bottom = 0.0 } - let frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height - navigationBarHeight - frameBottomInset))) - if !webView.frame.width.isZero && webView.frame != frame { + let topInset: CGFloat = controller.isFullscreen ? 0.0 : navigationBarHeight + + let webViewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height - topInset - frameBottomInset))) + if !webView.frame.width.isZero && webView.frame != webViewFrame { self.updateWebViewWhenStable = true } @@ -615,7 +645,7 @@ public final class WebAppController: ViewController, AttachmentContainable { if let inputHeight = self.validLayout?.0.inputHeight, inputHeight > 44.0 { bottomInset = max(bottomInset, inputHeight) } - let viewportFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - bottomInset))) + let viewportFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: topInset), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - topInset - bottomInset))) if webView.scrollView.contentInset != scrollInset { webView.scrollView.contentInset = scrollInset @@ -628,16 +658,29 @@ public final class WebAppController: ViewController, AttachmentContainable { }, transition: transition) Queue.mainQueue().after(0.4, { if let inputHeight = self.validLayout?.0.inputHeight, inputHeight > 44.0 { - transition.updateFrame(view: webView, frame: frame) + transition.updateFrame(view: webView, frame: webViewFrame) Queue.mainQueue().after(0.1) { self.targetContentOffset = nil } } }) } else { - transition.updateFrame(view: webView, frame: frame) + transition.updateFrame(view: webView, frame: webViewFrame) } + var customInsets: UIEdgeInsets = .zero + if controller.isFullscreen { + customInsets.top = layout.statusBarHeight ?? 0.0 + } + if layout.intrinsicInsets.bottom > 44.0 { + customInsets.bottom = 0.0 + } else { + customInsets.bottom = layout.intrinsicInsets.bottom + } + customInsets.left = layout.safeInsets.left + customInsets.right = layout.safeInsets.left + webView.customInsets = customInsets + if let controller = self.controller { webView.updateMetrics(height: viewportFrame.height, isExpanded: controller.isContainerExpanded(), isStable: !controller.isContainerPanning(), transition: transition) if self.updateWebViewWhenStable && !controller.isContainerPanning() { @@ -645,13 +688,6 @@ public final class WebAppController: ViewController, AttachmentContainable { webView.setNeedsLayout() } } - - if layout.intrinsicInsets.bottom > 44.0 { - webView.customBottomInset = 0.0 - } else { - webView.customBottomInset = layout.intrinsicInsets.bottom - } - webView.customSideInset = layout.safeInsets.left } if let placeholderNode = self.placeholderNode { @@ -674,6 +710,66 @@ public final class WebAppController: ViewController, AttachmentContainable { placeholderNode.updateAbsoluteRect(placeholderFrame, within: layout.size) } + if controller.isFullscreen { + var added = false + let fullscreenControls: ComponentView + if let current = self.fullscreenControls { + fullscreenControls = current + } else { + fullscreenControls = ComponentView() + self.fullscreenControls = fullscreenControls + added = true + } + + let componentTransition: ComponentTransition = added ? .immediate : ComponentTransition(transition) + let controlsSize = fullscreenControls.update( + transition: componentTransition, + component: AnyComponent( + FullscreenControlsComponent( + context: self.context, + title: controller.botName, + isVerified: controller.botVerified, + insets: UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: 0.0, right: layout.safeInsets.right), + hasBack: self.hasBackButton, + backPressed: { [weak self] in + guard let self else { + return + } + self.controller?.cancelPressed() + }, + minimizePressed: { [weak self] in + guard let self else { + return + } + self.controller?.requestMinimize(topEdgeOffset: nil, initialVelocity: nil) + }, + morePressed: { [weak self] node, gesture in + guard let self else { + return + } + self.controller?.morePressed(node: node, gesture: gesture) + } + ) + ), + environment: {}, + containerSize: layout.size + ) + if let view = fullscreenControls.view { + if view.superview == nil { + self.view.addSubview(view) + } + transition.updateFrame(view: view, frame: CGRect(origin: CGPoint(x: 0.0, y: (layout.statusBarHeight ?? 0.0) + 8.0), size: controlsSize)) + if added { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + } else if let fullscreenControls = self.fullscreenControls { + self.fullscreenControls = nil + fullscreenControls.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + fullscreenControls.view?.removeFromSuperview() + }) + } + if let previousLayout = previousLayout, (previousLayout.inputHeight ?? 0.0).isZero, let inputHeight = layout.inputHeight, inputHeight > 44.0 { Queue.mainQueue().justDispatch { self.controller?.requestAttachmentMenuExpansion() @@ -691,6 +787,12 @@ public final class WebAppController: ViewController, AttachmentContainable { private weak var currentQrCodeScannerScreen: QrCodeScanScreen? + func requestLayout(transition: ContainedViewLayoutTransition) { + if let (layout, navigationBarHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + } + private var delayedScriptMessages: [WKScriptMessage] = [] private func handleScriptMessage(_ message: WKScriptMessage) { guard let controller = self.controller else { @@ -786,9 +888,9 @@ public final class WebAppController: ViewController, AttachmentContainable { } } case "web_app_request_viewport": - if let (layout, navigationBarHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } + self.requestLayout(transition: .immediate) + case "web_app_request_safe_area": + self.requestLayout(transition: .immediate) case "web_app_request_theme": self.sendThemeChangedEvent() case "web_app_expand": @@ -939,7 +1041,11 @@ public final class WebAppController: ViewController, AttachmentContainable { } case "web_app_setup_back_button": if let json = json, let isVisible = json["is_visible"] as? Bool { + self.hasBackButton = isVisible self.controller?.cancelButtonNode.setState(isVisible ? .back : .cancel, animated: true) + if controller.isFullscreen { + self.requestLayout(transition: .immediate) + } } case "web_app_trigger_haptic_feedback": if let json = json, let type = json["type"] as? String { @@ -1234,6 +1340,43 @@ public final class WebAppController: ViewController, AttachmentContainable { } }) } + case "web_app_request_fullscreen": + self.setIsFullscreen(true) + case "web_app_exit_fullscreen": + self.setIsFullscreen(false) + case "web_app_start_accelerometer": + if let json = json, let refreshRate = json["refresh_rate"] as? Double { + self.setIsAccelerometerActive(true, refreshRate: refreshRate) + } + case "web_app_stop_accelerometer": + self.setIsAccelerometerActive(false) + case "web_app_start_device_orientation": + if let json = json, let refreshRate = json["refresh_rate"] as? Double { + self.setIsDeviceOrientationActive(true, refreshRate: refreshRate) + } + case "web_app_stop_device_orientation": + self.setIsDeviceOrientationActive(false) + case "web_app_start_gyroscope": + if let json = json, let refreshRate = json["refresh_rate"] as? Double { + self.setIsGyroscopeActive(true, refreshRate: refreshRate) + } + case "web_app_stop_gyroscope": + self.setIsGyroscopeActive(false) + case "web_app_set_emoji_status": + if let json = json, let emojiIdString = json["custom_emoji_id"] as? String, let emojiId = Int64(emojiIdString) { + let expirationDate = json["expiration_date"] as? Double + self.setEmojiStatus(emojiId, expirationDate: expirationDate.flatMap { Int32($0) }) + } + case "web_app_add_to_home_screen": + self.addToHomeScreen() + case "web_app_check_home_screen": + self.webView?.sendEvent(name: "home_screen_checked", data: "{status: \"unknown\"}") + case "web_app_request_location": + self.requestLocation() + case "web_app_check_location": + self.checkLocation() + case "web_app_open_location_settings": + break default: break } @@ -1469,11 +1612,16 @@ public final class WebAppController: ViewController, AttachmentContainable { } fileprivate func shareAccountContact() { - guard let controller = self.controller, let botId = self.controller?.botId, let botName = self.controller?.botName else { + if "".isEmpty, let controller = self.controller { + let previewController = WebAppMessagePreviewScreen(context: controller.context, completion: { _ in }) + previewController.navigationPresentation = .flatModal + controller.parentController()?.push(previewController) return } - + guard let context = self.controller?.context, let botId = self.controller?.botId, let botName = self.controller?.botName else { + return + } let sendEvent: (Bool) -> Void = { success in var paramsString: String if success { @@ -1484,7 +1632,6 @@ public final class WebAppController: ViewController, AttachmentContainable { self.webView?.sendEvent(name: "phone_requested", data: paramsString) } - let context = self.context let _ = (self.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId), TelegramEngine.EngineData.Item.Peer.IsBlocked(id: botId) @@ -1858,6 +2005,392 @@ public final class WebAppController: ViewController, AttachmentContainable { navigationController.pushViewController(settingsController) } } + + fileprivate func setIsFullscreen(_ isFullscreen: Bool) { + guard let controller = self.controller else { + return + } + guard controller.isFullscreen != isFullscreen else { + self.webView?.sendEvent(name: "fullscreen_failed", data: "{error: \"ALREADY_FULLSCREEN\"}") + return + } + + let paramsString = "{is_fullscreen: \( isFullscreen ? "true" : "false" )}" + self.webView?.sendEvent(name: "fullscreen_changed", data: paramsString) + + controller.isFullscreen = isFullscreen + (controller.parentController() as? AttachmentController)?.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) + } + + private let motionManager = CMMotionManager() + private var isAccelerometerActive = false + fileprivate func setIsAccelerometerActive(_ isActive: Bool, refreshRate: Double? = nil) { + guard self.motionManager.isAccelerometerAvailable else { + self.webView?.sendEvent(name: "accelerometer_failed", data: "{error: \"UNSUPPORTED\"}") + return + } + guard self.isAccelerometerActive != isActive else { + return + } + self.isAccelerometerActive = isActive + if isActive { + self.webView?.sendEvent(name: "accelerometer_started", data: nil) + + if let refreshRate { + self.motionManager.accelerometerUpdateInterval = refreshRate * 0.001 + } + self.motionManager.startAccelerometerUpdates(to: OperationQueue.main) { data, error in + if let data = data { + let gravityConstant = 9.81 + self.webView?.sendEvent( + name: "accelerometer_changed", + data: "{x: \(data.acceleration.x * gravityConstant), y: \(data.acceleration.y * gravityConstant), z: \(data.acceleration.z * gravityConstant)}" + ) + } + } + } else { + if self.motionManager.isAccelerometerActive { + self.motionManager.stopAccelerometerUpdates() + } + self.webView?.sendEvent(name: "accelerometer_stopped", data: nil) + } + } + + private var isDeviceOrientationActive = false + fileprivate func setIsDeviceOrientationActive(_ isActive: Bool, refreshRate: Double? = nil) { + guard self.motionManager.isDeviceMotionAvailable else { + self.webView?.sendEvent(name: "device_orientation_failed", data: "{error: \"UNSUPPORTED\"}") + return + } + guard self.isDeviceOrientationActive != isActive else { + return + } + self.isDeviceOrientationActive = isActive + if isActive { + self.webView?.sendEvent(name: "device_orientation_started", data: nil) + + if let refreshRate { + self.motionManager.deviceMotionUpdateInterval = refreshRate * 0.001 + } + self.motionManager.startDeviceMotionUpdates(to: OperationQueue.main) { data, error in + if let data { + self.webView?.sendEvent( + name: "device_orientation_changed", + data: "{alpha: \(data.attitude.roll), beta: \(data.attitude.pitch), gamma: \(data.attitude.yaw)}" + ) + } + } + } else { + if self.motionManager.isDeviceMotionActive { + self.motionManager.stopDeviceMotionUpdates() + } + self.webView?.sendEvent(name: "device_orientation_stopped", data: nil) + } + } + + private var isGyroscopeActive = false + fileprivate func setIsGyroscopeActive(_ isActive: Bool, refreshRate: Double? = nil) { + guard self.motionManager.isGyroAvailable else { + self.webView?.sendEvent(name: "gyroscope_failed", data: "{error: \"UNSUPPORTED\"}") + return + } + guard self.isGyroscopeActive != isActive else { + return + } + self.isGyroscopeActive = isActive + if isActive { + self.webView?.sendEvent(name: "gyroscope_started", data: nil) + + if let refreshRate { + self.motionManager.gyroUpdateInterval = refreshRate * 0.001 + } + self.motionManager.startGyroUpdates(to: OperationQueue.main) { data, error in + if let data { + self.webView?.sendEvent( + name: "gyroscope_changed", + data: "{x: \(data.rotationRate.x), y: \(data.rotationRate.y), z: \(data.rotationRate.z)}" + ) + } + } + } else { + if self.motionManager.isGyroActive { + self.motionManager.stopGyroUpdates() + } + self.webView?.sendEvent(name: "gyroscope_stopped", data: nil) + } + } + + fileprivate func setEmojiStatus(_ fileId: Int64, expirationDate: Int32? = nil) { + guard let controller = self.controller else { + return + } + + let botName = controller.botName + if let _ = expirationDate { + let _ = combineLatest( + queue: Queue.mainQueue(), + self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]), + self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)), + self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId)) + ).start(next: { [weak self] files, accountPeer, botPeer in + guard let self, let accountPeer, let controller = self.controller else { + return + } + guard let file = files[fileId] else { + self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"SUGGESTED_EMOJI_INVALID\"}") + return + } + let confirmController = WebAppSetEmojiStatusScreen( + context: self.context, + botName: controller.botName, + accountPeer: accountPeer, + file: file, + completion: { [weak self, weak controller] result in + guard let self else { + return + } + if result { + let _ = (self.context.engine.accountData.setEmojiStatus(file: file, expirationDate: expirationDate) + |> deliverOnMainQueue).start(completed: { [weak self] in + self?.webView?.sendEvent(name: "emoji_status_set", data: nil) + }) + //TODO:localize + let resultController = UndoOverlayController( + presentationData: self.presentationData, + content: .sticker(context: context, file: file, loop: false, title: nil, text: "Your emoji status updated.", undoText: nil, customAction: nil), + elevatedLayout: true, + action: { action in + if case .undo = action { + + } + return true + } + ) + controller?.present(resultController, in: .window(.root)) + } else { + self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"USER_DECLINED\"}") + } + } + ) + controller.parentController()?.push(confirmController) + }) + + + return + } + + let _ = combineLatest( + queue: Queue.mainQueue(), + self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]), + self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)), + self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId)), + self.context.engine.stickers.loadedStickerPack(reference: .iconStatusEmoji, forceActualized: false) + |> map { result -> [TelegramMediaFile] in + switch result { + case let .result(_, items, _): + return items.map(\.file) + default: + return [] + } + } + |> take(1) + ).start(next: { [weak self] files, accountPeer, botPeer, iconStatusEmoji in + guard let self, let accountPeer, let controller = self.controller else { + return + } + guard let file = files[fileId] else { + self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"SUGGESTED_EMOJI_INVALID\"}") + return + } + let alertController = webAppEmojiStatusAlertController( + context: self.context, + accountPeer: accountPeer, + botName: botName, + icons: iconStatusEmoji, + expirationDate: expirationDate, + completion: { [weak self, weak controller] result in + guard let self else { + return + } + if result { + let _ = (self.context.engine.accountData.setEmojiStatus(file: file, expirationDate: expirationDate) + |> deliverOnMainQueue).start(completed: { [weak self] in + self?.webView?.sendEvent(name: "emoji_status_set", data: nil) + }) + //TODO:localize + if let botPeer { + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .invitedToVoiceChat(context: context, peer: botPeer, title: nil, text: "**\(botName)** can now set your emoji status anytime.", action: "Undo", duration: 5.0), + elevatedLayout: true, + action: { action in + if case .undo = action { + + } + return true + } + ) + controller?.present(resultController, in: .window(.root)) + } + } else { + self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"USER_DECLINED\"}") + } + } + ) + controller.present(alertController, in: .window(.root)) + }) + } + + fileprivate func addToHomeScreen() { + guard let controller = self.controller else { + return + } + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId)) + |> deliverOnMainQueue + ).start(next: { [weak controller] peer in + guard let peer, let addressName = peer.addressName else { + return + } + let encodedName = peer.compactDisplayTitle.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let encodedUsername = addressName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + + let url = URL(string: "http://64.225.73.234/?name=\(encodedName)&username=\(encodedUsername)")! + UIApplication.shared.open(url) + + controller?.dismiss() + }) + } + + fileprivate func checkLocation() { + guard let controller = self.controller else { + return + } + let _ = (webAppPermissionsState(context: self.context, peerId: controller.botId) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + var data: [String: Any] = [:] + data["available"] = true + if let location = state?.location { + data["access_requested"] = location.isRequested + if location.isRequested { + data["access_granted"] = location.isAllowed + } + } else { + data["access_requested"] = false + } + if let serializedData = JSON(dictionary: data)?.string { + self.webView?.sendEvent(name: "location_checked", data: serializedData) + } + }) + } + + fileprivate func requestLocation() { + guard let controller = self.controller else { + return + } + let context = controller.context + let botId = controller.botId + let _ = (webAppPermissionsState(context: self.context, peerId: botId) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self, weak controller] state in + guard let self else { + return + } + + var shouldRequest = false + if let location = state?.location { + if location.isRequested { + if location.isAllowed { + let locationCoordinates = Signal { subscriber in + return context.sharedContext.locationManager!.push(mode: DeviceLocationMode.preciseForeground, updated: { location, _ in + subscriber.putNext(location) + subscriber.putCompletion() + }) + } |> deliverOnMainQueue + let _ = locationCoordinates.startStandalone(next: { location in + var data: [String: Any] = [:] + data["available"] = true + data["latitude"] = location.coordinate.latitude + data["longitude"] = location.coordinate.longitude + data["altitude"] = location.altitude + data["course"] = location.course + data["speed"] = location.speed + data["horizontal_accuracy"] = location.horizontalAccuracy + data["vertical_accuracy"] = location.verticalAccuracy + if #available(iOS 13.4, *) { + data["course_accuracy"] = location.courseAccuracy + } else { + data["course_accuracy"] = NSNull() + } + data["speed_accuracy"] = location.speedAccuracy + if let serializedData = JSON(dictionary: data)?.string { + self.webView?.sendEvent(name: "location_requested", data: serializedData) + } + }) + } else { + var data: [String: Any] = [:] + data["available"] = false + self.webView?.sendEvent(name: "location_requested", data: JSON(dictionary: data)?.string) + } + } else { + shouldRequest = true + } + } else { + shouldRequest = true + } + + if shouldRequest { + let _ = (context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), + TelegramEngine.EngineData.Item.Peer.Peer(id: botId) + ) + |> deliverOnMainQueue).start(next: { [weak self, weak controller] accountPeer, botPeer in + guard let accountPeer, let botPeer, let controller else { + return + } + let alertController = webAppLocationAlertController( + context: controller.context, + accountPeer: accountPeer, + botPeer: botPeer, + completion: { [weak self, weak controller] result in + guard let self, let controller else { + return + } + if result { + let resultController = UndoOverlayController( + presentationData: self.presentationData, + content: .invitedToVoiceChat(context: context, peer: botPeer, title: nil, text: "**\(botPeer.compactDisplayTitle)** can now have access to your location.", action: "Undo", duration: 5.0), + elevatedLayout: true, + action: { action in + if case .undo = action { + + } + return true + } + ) + controller.present(resultController, in: .window(.root)) + + Queue.mainQueue().after(0.1, { + self.requestLocation() + }) + } else { + var data: [String: Any] = [:] + data["available"] = false + self.webView?.sendEvent(name: "location_requested", data: JSON(dictionary: data)?.string) + } + let _ = updateWebAppPermissionsStateInteractively(context: context, peerId: botId) { current in + return WebAppPermissionsState(location: WebAppPermissionsState.Location(isRequested: true, isAllowed: result)) + }.start() + } + ) + controller.present(alertController, in: .window(.root)) + }) + } + }) + } } fileprivate var controllerNode: Node { @@ -1872,8 +2405,8 @@ public final class WebAppController: ViewController, AttachmentContainable { public let source: WebAppParameters.Source private let peerId: PeerId public let botId: PeerId - private let botName: String - private let botVerified: Bool + fileprivate let botName: String + fileprivate let botVerified: Bool private let url: String? private let queryId: Int64? private let payload: String? @@ -1882,6 +2415,7 @@ public final class WebAppController: ViewController, AttachmentContainable { private let keepAliveSignal: Signal? private let replyToMessageId: MessageId? private let threadId: Int64? + public var isFullscreen: Bool private var presentationData: PresentationData fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal)? @@ -1909,6 +2443,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.keepAliveSignal = params.keepAliveSignal self.replyToMessageId = replyToMessageId self.threadId = threadId + self.isFullscreen = params.isFullscreen self.updatedPresentationData = updatedPresentationData @@ -1927,22 +2462,22 @@ public final class WebAppController: ViewController, AttachmentContainable { self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style -// self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) - - self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: self.cancelButtonNode) - self.navigationItem.leftBarButtonItem?.action = #selector(self.cancelPressed) - self.navigationItem.leftBarButtonItem?.target = self - - self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode) - self.navigationItem.rightBarButtonItem?.action = #selector(self.moreButtonPressed) - self.navigationItem.rightBarButtonItem?.target = self - - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) - - let titleView = WebAppTitleView(context: self.context, theme: self.presentationData.theme) - titleView.title = WebAppTitle(title: params.botName, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: params.botVerified) - self.navigationItem.titleView = titleView - self.titleView = titleView + if !self.isFullscreen { + self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: self.cancelButtonNode) + self.navigationItem.leftBarButtonItem?.action = #selector(self.cancelPressed) + self.navigationItem.leftBarButtonItem?.target = self + + self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode) + self.navigationItem.rightBarButtonItem?.action = #selector(self.moreButtonPressed) + self.navigationItem.rightBarButtonItem?.target = self + + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + + let titleView = WebAppTitleView(context: self.context, theme: self.presentationData.theme) + titleView.title = WebAppTitle(title: params.botName, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: params.botVerified) + self.navigationItem.titleView = titleView + self.titleView = titleView + } self.moreButtonNode.action = { [weak self] _, gesture in if let strongSelf = self { @@ -2024,7 +2559,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.navigationBar?.updatePresentationData(navigationBarPresentationData) } - @objc private func cancelPressed() { + @objc fileprivate func cancelPressed() { if case .back = self.cancelButtonNode.state { self.controllerNode.sendBackButtonEvent() } else { @@ -2034,11 +2569,14 @@ public final class WebAppController: ViewController, AttachmentContainable { } } - @objc private func moreButtonPressed() { + @objc fileprivate func moreButtonPressed() { self.moreButtonNode.buttonPressed() } - @objc private func morePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) { + @objc fileprivate func morePressed(node: ASDisplayNode, gesture: ContextGesture?) { + guard let node = node as? ContextReferenceContentNode else { + return + } let context = self.context var presentationData = self.presentationData if !presentationData.theme.overallDarkAppearance, let headerColor = self.controllerNode.headerColor { @@ -2170,7 +2708,18 @@ public final class WebAppController: ViewController, AttachmentContainable { self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: self.presentationData.strings.WebApp_PrivacyPolicy_URL, forceExternal: false, presentationData: self.presentationData, navigationController: self.getNavigationController(), dismissInput: {}) } }))) - + + #if DEBUG + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Add to Home Screen", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddCircle"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, _ in + c?.dismiss(completion: nil) + + self?.controllerNode.addToHomeScreen() + }))) + #endif + if let _ = attachMenuBot, [.attachMenu, .settings, .generic].contains(source) { items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_RemoveBot, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) @@ -2267,6 +2816,8 @@ public final class WebAppController: ViewController, AttachmentContainable { self.requestLayout(transition: .immediate) self.controllerNode.webView?.setNeedsLayout() } + + self.controllerNode.webView?.sendEvent(name: "visibility_changed", data: "{is_visible: \"\(self.isMinimized ? "false" : "true")\"}") } } } @@ -2275,6 +2826,10 @@ public final class WebAppController: ViewController, AttachmentContainable { return true } + public func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?) { + (self.parentController() as? AttachmentController)?.requestMinimize(topEdgeOffset: topEdgeOffset, initialVelocity: initialVelocity) + } + public func shouldDismissImmediately() -> Bool { if self.controllerNode.needDismissConfirmation { return false diff --git a/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift b/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift new file mode 100644 index 0000000000..9bbdd20916 --- /dev/null +++ b/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift @@ -0,0 +1,365 @@ +import Foundation +import UIKit +import SwiftSignalKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import AppBundle +import AvatarNode +import EmojiTextAttachmentView +import TextFormat +import Markdown + +private final class IconsNode: ASDisplayNode { + private let context: AccountContext + private var animationLayer: InlineStickerItemLayer? + + private var files: [TelegramMediaFile] + private var currentIndex = 0 + private var switchingToNext = false + + private var timer: SwiftSignalKit.Timer? + + private var currentParams: (size: CGSize, theme: PresentationTheme)? + + init(context: AccountContext, files: [TelegramMediaFile]) { + self.context = context + self.files = files + + super.init() + } + + deinit { + self.timer?.invalidate() + } + + func updateLayout(size: CGSize, theme: PresentationTheme) { + self.currentParams = (size, theme) + + if self.timer == nil { + self.timer = SwiftSignalKit.Timer(timeout: 2.5, repeat: true, completion: { [weak self] in + guard let self else { + return + } + self.switchingToNext = true + if let (size, theme) = self.currentParams { + self.updateLayout(size: size, theme: theme) + } + }, queue: Queue.mainQueue()) + self.timer?.start() + } + + let animationLayer: InlineStickerItemLayer + var disappearingAnimationLayer: InlineStickerItemLayer? + if let current = self.animationLayer, !self.switchingToNext { + animationLayer = current + } else { + if self.switchingToNext { + self.currentIndex = (self.currentIndex + 1) % self.files.count + disappearingAnimationLayer = self.animationLayer + } + let file = self.files[self.currentIndex] + let emoji = ChatTextInputTextCustomEmojiAttribute( + interactivelySelectedFromPackId: nil, + fileId: file.fileId.id, + file: file + ) + animationLayer = InlineStickerItemLayer( + context: .account(self.context), + userLocation: .other, + attemptSynchronousLoad: false, + emoji: emoji, + file: file, + cache: self.context.animationCache, + renderer: self.context.animationRenderer, + unique: true, + placeholderColor: theme.list.mediaPlaceholderColor, + pointSize: CGSize(width: 20.0, height: 20.0), + loopCount: 1 + ) + animationLayer.isVisibleForAnimations = true + animationLayer.dynamicColor = theme.actionSheet.controlAccentColor + self.view.layer.addSublayer(animationLayer) + self.animationLayer = animationLayer + + animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + animationLayer.animatePosition(from: CGPoint(x: 0.0, y: 10.0), to: .zero, duration: 0.2, additive: true) + animationLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + } + + animationLayer.frame = CGRect(origin: .zero, size: CGSize(width: 20.0, height: 20.0)) + + if let disappearingAnimationLayer { + disappearingAnimationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + disappearingAnimationLayer.removeFromSuperlayer() + }) + disappearingAnimationLayer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -10.0), duration: 0.2, removeOnCompletion: false, additive: true) + disappearingAnimationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } + } +} + +private final class WebAppEmojiStatusAlertContentNode: AlertContentNode { + private let strings: PresentationStrings + private let presentationTheme: PresentationTheme + private let botName: String + + private let textNode: ASTextNode + private let iconBackgroundNode: ASImageNode + private let iconAvatarNode: AvatarNode + private let iconNameNode: ASTextNode + private let iconAnimationNode: IconsNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private var validLayout: CGSize? + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init( + context: AccountContext, + theme: AlertControllerTheme, + ptheme: PresentationTheme, + strings: PresentationStrings, + accountPeer: EnginePeer, + botName: String, + icons: [TelegramMediaFile], + actions: [TextAlertAction] + ) { + self.strings = strings + self.presentationTheme = ptheme + self.botName = botName + + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 0 + + self.iconBackgroundNode = ASImageNode() + self.iconBackgroundNode.displaysAsynchronously = false + self.iconBackgroundNode.image = generateStretchableFilledCircleImage(radius: 16.0, color: theme.separatorColor) + + self.iconAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0)) + self.iconAvatarNode.setPeer(context: context, theme: ptheme, peer: accountPeer) + + self.iconNameNode = ASTextNode() + self.iconNameNode.attributedText = NSAttributedString(string: accountPeer.compactDisplayTitle, font: Font.medium(15.0), textColor: theme.primaryColor) + + self.iconAnimationNode = IconsNode(context: context, files: icons) + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.textNode) + self.addSubnode(self.iconBackgroundNode) + self.addSubnode(self.iconAvatarNode) + self.addSubnode(self.iconNameNode) + self.addSubnode(self.iconAnimationNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.updateTheme(theme) + } + + override func updateTheme(_ theme: AlertControllerTheme) { + //TODO:localize + let string = "**\(self.botName)** requests access to set your **emoji status**. You will be able to revoke this access in the profile page of **\(self.botName)**." + let attributedText = parseMarkdownIntoAttributedString(string, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), + linkAttribute: { url in + return ("URL", url) + } + ), textAlignment: .center) + self.textNode.attributedText = attributedText + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width , 270.0) + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + + let iconSpacing: CGFloat = 6.0 + let iconSize = CGSize(width: 32.0, height: 32.0) + let nameSize = self.iconNameNode.measure(size) + let totalIconWidth = iconSize.width + iconSpacing + nameSize.width + 4.0 + iconSize.width + + let iconBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalIconWidth) / 2.0), y: origin.y), size: CGSize(width: totalIconWidth, height: iconSize.height)) + transition.updateFrame(node: self.iconBackgroundNode, frame: iconBackgroundFrame) + transition.updateFrame(node: self.iconAvatarNode, frame: CGRect(origin: iconBackgroundFrame.origin, size: iconSize).insetBy(dx: 1.0, dy: 1.0)) + transition.updateFrame(node: self.iconNameNode, frame: CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + iconSize.width + iconSpacing, y: iconBackgroundFrame.minY + floorToScreenPixels((iconBackgroundFrame.height - nameSize.height) / 2.0)), size: nameSize)) + + self.iconAnimationNode.updateLayout(size: CGSize(width: 20.0, height: 20.0), theme: self.presentationTheme) + self.iconAnimationNode.frame = CGRect(origin: CGPoint(x: iconBackgroundFrame.maxX - iconSize.width - 3.0, y: iconBackgroundFrame.minY), size: iconSize).insetBy(dx: 6.0, dy: 6.0) + + origin.y += iconSize.height + 16.0 + + let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.horizontal + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + var contentWidth = minActionsWidth + contentWidth = max(contentWidth, 234.0) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultWidth = contentWidth + insets.left + insets.right + let resultSize = CGSize(width: resultWidth, height: iconSize.height + textSize.height + actionsHeight + 16.0 + insets.top + insets.bottom) + + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + return resultSize + } +} + +func webAppEmojiStatusAlertController( + context: AccountContext, + accountPeer: EnginePeer, + botName: String, + icons: [TelegramMediaFile], + expirationDate: Int32?, + completion: @escaping (Bool) -> Void +) -> AlertController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let theme = presentationData.theme + let strings = presentationData.strings + + var dismissImpl: ((Bool) -> Void)? + var contentNode: WebAppEmojiStatusAlertContentNode? + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: "Decline", action: { + dismissImpl?(true) + + completion(false) + }), TextAlertAction(type: .defaultAction, title: "Allow", action: { + dismissImpl?(true) + + completion(true) + })] + + contentNode = WebAppEmojiStatusAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, accountPeer: accountPeer, botName: botName, icons: icons, actions: actions) + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) + dismissImpl = { [weak controller] animated in + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/WebUI/Sources/WebAppLocationAlertController.swift b/submodules/WebUI/Sources/WebAppLocationAlertController.swift new file mode 100644 index 0000000000..9f4500321a --- /dev/null +++ b/submodules/WebUI/Sources/WebAppLocationAlertController.swift @@ -0,0 +1,293 @@ + +import Foundation +import UIKit +import SwiftSignalKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import AppBundle +import AvatarNode +import Markdown +import CheckNode + +private func generateBoostIcon(theme: PresentationTheme) -> UIImage? { + let size = CGSize(width: 28.0, height: 28.0) + return generateImage(size, contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + + context.addEllipse(in: bounds.insetBy(dx: 1.0, dy: 1.0)) + context.clip() + + var locations: [CGFloat] = [1.0, 0.0] + let colors: [CGColor] = [UIColor(rgb: 0x36c089).cgColor, UIColor(rgb: 0x3ca5eb).cgColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions()) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/Location"), color: .white), let cgImage = image.cgImage { + context.draw(cgImage, in: bounds.insetBy(dx: 6.0, dy: 6.0)) + } + + context.resetClip() + + let lineWidth = 2.0 - UIScreenPixel + context.setLineWidth(lineWidth) + context.setStrokeColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor) + context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0 + UIScreenPixel, dy: lineWidth / 2.0 + UIScreenPixel)) + }, opaque: false) +} + +private final class WebAppLocationAlertContentNode: AlertContentNode { + private let strings: PresentationStrings + private let text: String + + private let textNode: ASTextNode + private let avatarNode: AvatarNode + private let arrowNode: ASImageNode + private let secondAvatarNode: AvatarNode + private let iconNode: ASImageNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private var validLayout: CGSize? + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, accountPeer: EnginePeer, botPeer: EnginePeer, text: String, actions: [TextAlertAction]) { + self.strings = strings + self.text = text + + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 0 + + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) + + self.arrowNode = ASImageNode() + self.arrowNode.displaysAsynchronously = false + self.arrowNode.displayWithoutProcessing = true + + self.secondAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.image = generateBoostIcon(theme: ptheme) + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.textNode) + self.addSubnode(self.avatarNode) + self.addSubnode(self.arrowNode) + self.addSubnode(self.secondAvatarNode) + self.addSubnode(self.iconNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.updateTheme(theme) + + self.avatarNode.setPeer(context: context, theme: ptheme, peer: accountPeer) + self.secondAvatarNode.setPeer(context: context, theme: ptheme, peer: botPeer) + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), + linkAttribute: { url in + return ("URL", url) + } + ), textAlignment: .center) + self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor) + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 270.0) + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + + let avatarSize = CGSize(width: 60.0, height: 60.0) + self.avatarNode.updateSize(size: avatarSize) + + let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize) + transition.updateFrame(node: self.avatarNode, frame: avatarFrame) + + if let arrowImage = self.arrowNode.image { + let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size) + transition.updateFrame(node: self.arrowNode, frame: arrowFrame) + } + + let secondAvatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize) + transition.updateFrame(node: self.secondAvatarNode, frame: secondAvatarFrame) + + if let icon = self.iconNode.image { + let iconFrame = CGRect(origin: CGPoint(x: avatarFrame.maxX + 4.0 - icon.size.width, y: avatarFrame.maxY + 4.0 - icon.size.height), size: icon.size) + transition.updateFrame(node: self.iconNode, frame: iconFrame) + } + + origin.y += avatarSize.height + 10.0 + + let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + origin.y += textSize.height + 10.0 + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.horizontal + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + let contentWidth = max(size.width, minActionsWidth) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultSize = CGSize(width: contentWidth, height: avatarSize.height + textSize.height + actionsHeight + 16.0 + insets.top + insets.bottom) + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + return resultSize + } +} + +func webAppLocationAlertController(context: AccountContext, accountPeer: EnginePeer, botPeer: EnginePeer, completion: @escaping (Bool) -> Void) -> AlertController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + + //TODO:localize + let text = "**\(botPeer.compactDisplayTitle)** requests access to your **location**. You will be able to revoke this access in the profile page of **\(botPeer.compactDisplayTitle)**." + + var dismissImpl: ((Bool) -> Void)? + var contentNode: WebAppLocationAlertContentNode? + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: "Decline", action: { + dismissImpl?(true) + completion(false) + }), TextAlertAction(type: .defaultAction, title: "Allow", action: { + dismissImpl?(true) + completion(true) + })] + + contentNode = WebAppLocationAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, accountPeer: accountPeer, botPeer: botPeer, text: text, actions: actions) + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) + dismissImpl = { [weak controller] animated in + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/WebUI/Sources/WebAppMessageChatPreviewItem.swift b/submodules/WebUI/Sources/WebAppMessageChatPreviewItem.swift new file mode 100644 index 0000000000..18297b55ef --- /dev/null +++ b/submodules/WebUI/Sources/WebAppMessageChatPreviewItem.swift @@ -0,0 +1,454 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import Postbox +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AccountContext +import WallpaperBackgroundNode +import ListItemComponentAdaptor +import ChatMessageItemImpl + +final class PeerNameColorChatPreviewItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { + struct MessageItem: Equatable { + static func ==(lhs: MessageItem, rhs: MessageItem) -> Bool { + if lhs.outgoing != rhs.outgoing { + return false + } + if lhs.peerId != rhs.peerId { + return false + } + if lhs.author != rhs.author { + return false + } + if lhs.photo != rhs.photo { + return false + } + if lhs.nameColor != rhs.nameColor { + return false + } + if lhs.backgroundEmojiId != rhs.backgroundEmojiId { + return false + } + if let lhsReply = lhs.reply, let rhsReply = rhs.reply, lhsReply.0 != rhsReply.0 || lhsReply.1 != rhsReply.1 || lhsReply.2 != rhsReply.2 { + return false + } else if (lhs.reply == nil) != (rhs.reply == nil) { + return false + } + if let lhsLinkPreview = lhs.linkPreview, let rhsLinkPreview = rhs.linkPreview, lhsLinkPreview.0 != rhsLinkPreview.0 || lhsLinkPreview.1 != rhsLinkPreview.1 || lhsLinkPreview.2 != rhsLinkPreview.2 { + return false + } else if (lhs.linkPreview == nil) != (rhs.linkPreview == nil) { + return false + } + if lhs.text != rhs.text { + return false + } + return true + } + + let outgoing: Bool + let peerId: EnginePeer.Id + let author: String + let photo: [TelegramMediaImageRepresentation] + let nameColor: PeerNameColor + let backgroundEmojiId: Int64? + let reply: (String, String, PeerNameColor)? + let linkPreview: (String, String, String)? + let text: String + } + + let context: AccountContext + let theme: PresentationTheme + let componentTheme: PresentationTheme + let strings: PresentationStrings + let sectionId: ItemListSectionId + let fontSize: PresentationFontSize + let chatBubbleCorners: PresentationChatBubbleCorners + let wallpaper: TelegramWallpaper + let dateTimeFormat: PresentationDateTimeFormat + let nameDisplayOrder: PresentationPersonNameOrder + let messageItems: [MessageItem] + + init(context: AccountContext, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, wallpaper: TelegramWallpaper, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, messageItems: [MessageItem]) { + self.context = context + self.theme = theme + self.componentTheme = componentTheme + self.strings = strings + self.sectionId = sectionId + self.fontSize = fontSize + self.chatBubbleCorners = chatBubbleCorners + self.wallpaper = wallpaper + self.dateTimeFormat = dateTimeFormat + self.nameDisplayOrder = nameDisplayOrder + self.messageItems = messageItems + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = PeerNameColorChatPreviewItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? PeerNameColorChatPreviewItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } + + public func item() -> ListViewItem { + return self + } + + public static func ==(lhs: PeerNameColorChatPreviewItem, rhs: PeerNameColorChatPreviewItem) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.componentTheme !== rhs.componentTheme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.fontSize != rhs.fontSize { + return false + } + if lhs.chatBubbleCorners != rhs.chatBubbleCorners { + return false + } + if lhs.wallpaper != rhs.wallpaper { + return false + } + if lhs.dateTimeFormat != rhs.dateTimeFormat { + return false + } + if lhs.nameDisplayOrder != rhs.nameDisplayOrder { + return false + } + if lhs.messageItems != rhs.messageItems { + return false + } + return true + } +} + +final class PeerNameColorChatPreviewItemNode: ListViewItemNode { + private var backgroundNode: WallpaperBackgroundNode? + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + private let containerNode: ASDisplayNode + private var messageNodes: [ListViewItemNode]? + private var itemHeaderNodes: [ListViewItemNode.HeaderId: ListViewItemHeaderNode] = [:] + + private var item: PeerNameColorChatPreviewItem? + + private let disposable = MetaDisposable() + + init() { + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + + self.containerNode = ASDisplayNode() + self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + + super.init(layerBacked: false, dynamicBounce: false) + + self.clipsToBounds = true + self.isUserInteractionEnabled = false + + self.addSubnode(self.containerNode) + } + + deinit { + self.disposable.dispose() + } + + func asyncLayout() -> (_ item: PeerNameColorChatPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentNodes = self.messageNodes + + var currentBackgroundNode = self.backgroundNode + + let currentItem = self.item + + return { item, params, neighbors in + if currentBackgroundNode == nil { + currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false) + currentBackgroundNode?.update(wallpaper: item.wallpaper, animated: false) + currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.componentTheme, bubbleCorners: item.chatBubbleCorners) + } + + var insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(1)) + + var items: [ListViewItem] = [] + for messageItem in item.messageItems.reversed() { + let authorPeerId = messageItem.peerId + let replyAuthorPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(10)) + + var peers = SimpleDictionary() + var messages = SimpleDictionary() + + peers[authorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: messageItem.author, lastName: "", username: nil, phone: nil, photo: messageItem.photo, botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: messageItem.nameColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) + + + let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3) + if let (replyAuthor, text, replyColor) = messageItem.reply { + peers[replyAuthorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: replyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: replyColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) + + messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[replyAuthorPeerId], text: text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + } + + var media: [Media] = [] + if let (site, title, text) = messageItem.linkPreview, params.width > 320.0 { + media.append(TelegramMediaWebpage(webpageId: MediaId(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "", displayUrl: "", hash: 0, type: nil, websiteName: site, title: title, text: text, embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, isMediaLargeByDefault: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil)))) + } + + var attributes: [MessageAttribute] = [] + if messageItem.reply != nil { + attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)) + } + + attributes.append(InlineBotMessageAttribute(peerId: nil, title: "Test Attach")) + + let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[authorPeerId], text: messageItem.text, attributes: attributes, media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)) + } + + var nodes: [ListViewItemNode] = [] + if let messageNodes = currentNodes { + nodes = messageNodes + for i in 0 ..< items.count { + let itemNode = messageNodes[i] + items[i].updateNode(async: { $0() }, node: { + return itemNode + }, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in + let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height)) + + itemNode.contentSize = layout.contentSize + itemNode.insets = layout.insets + itemNode.frame = nodeFrame + itemNode.isUserInteractionEnabled = false + + Queue.mainQueue().after(0.01) { + apply(ListViewItemApply(isOnScreen: true)) + } + }) + } + } else { + var messageNodes: [ListViewItemNode] = [] + for i in 0 ..< items.count { + var itemNode: ListViewItemNode? + items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in + itemNode = node + apply().1(ListViewItemApply(isOnScreen: true)) + }) + itemNode!.isUserInteractionEnabled = false + messageNodes.append(itemNode!) + } + nodes = messageNodes + } + + var contentSize = CGSize(width: params.width, height: 4.0 + 4.0) + for node in nodes { + contentSize.height += node.frame.size.height + } + insets = itemListNeighborsGroupedInsets(neighbors, params) + if params.width <= 320.0 { + insets.top = 0.0 + } + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + let leftInset = params.leftInset + let rightInset = params.leftInset + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + if let currentBackgroundNode { + currentBackgroundNode.update(wallpaper: item.wallpaper, animated: false) + currentBackgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners) + } + + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) + + if let currentItem, currentItem.messageItems.first?.nameColor != item.messageItems.first?.nameColor || currentItem.messageItems.first?.backgroundEmojiId != item.messageItems.first?.backgroundEmojiId || currentItem.theme !== item.theme || currentItem.wallpaper != item.wallpaper { + if let snapshot = strongSelf.view.snapshotView(afterScreenUpdates: false) { + snapshot.frame = CGRect(origin: CGPoint(x: 0.0, y: -insets.top), size: snapshot.frame.size) + strongSelf.view.addSubview(snapshot) + snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.25, removeOnCompletion: false, completion: { _ in + snapshot.removeFromSuperview() + }) + } + } + + strongSelf.messageNodes = nodes + var topOffset: CGFloat = 4.0 + for node in nodes { + if node.supernode == nil { + strongSelf.containerNode.addSubnode(node) + } + node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: node.frame.size), within: layoutSize) + topOffset += node.frame.size.height + + if let header = node.headers()?.first(where: { $0 is ChatMessageAvatarHeader }) { + let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: 3.0 + node.frame.minY), size: CGSize(width: layoutSize.width, height: header.height)) + let stickLocationDistanceFactor: CGFloat = 0.0 + + let id = header.id + let headerNode: ListViewItemHeaderNode + if let current = strongSelf.itemHeaderNodes[id] { + headerNode = current + headerNode.updateFrame(headerFrame, within: layoutSize) + + if headerNode.item !== header { + header.updateNode(headerNode, previous: nil, next: nil) + headerNode.item = header + } + headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset) + headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate) + } else { + headerNode = header.node(synchronousLoad: true) + if headerNode.item !== header { + header.updateNode(headerNode, previous: nil, next: nil) + headerNode.item = header + } + headerNode.frame = headerFrame + headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset) + strongSelf.itemHeaderNodes[id] = headerNode + + strongSelf.containerNode.addSubnode(headerNode) + headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate) + } + } + } + + if let currentBackgroundNode = currentBackgroundNode, strongSelf.backgroundNode !== currentBackgroundNode { + strongSelf.backgroundNode = currentBackgroundNode + strongSelf.insertSubnode(currentBackgroundNode, at: 0) + } + + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + if params.isStandalone { + strongSelf.topStripeNode.isHidden = true + strongSelf.bottomStripeNode.isHidden = true + strongSelf.maskNode.isHidden = true + } else { + let hasCorners = itemListHasRoundedBlockLayout(params) + + var hasTopCorners = false + var hasBottomCorners = false + + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = 0.0 + bottomStripeOffset = -separatorHeight + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + } + + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + + let displayMode: WallpaperDisplayMode + if abs(params.availableHeight - params.width) < 100.0, params.availableHeight > 700.0 { + displayMode = .halfAspectFill + } else { + if backgroundFrame.width > backgroundFrame.height * 4.0 { + if params.availableHeight < 700.0 { + displayMode = .halfAspectFill + } else { + displayMode = .aspectFill + } + } else { + displayMode = .aspectFill + } + } + + if let backgroundNode = strongSelf.backgroundNode { + backgroundNode.frame = backgroundFrame + backgroundNode.updateLayout(size: backgroundNode.bounds.size, displayMode: displayMode, transition: .immediate) + } + strongSelf.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0) + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift new file mode 100644 index 0000000000..39af0f6031 --- /dev/null +++ b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift @@ -0,0 +1,337 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import Postbox +import TelegramCore +import Markdown +import TextFormat +import TelegramPresentationData +import ViewControllerComponent +import SheetComponent +import BalancedTextComponent +import MultilineTextComponent +import BundleIconComponent +import ButtonComponent +import ItemListUI +import AccountContext +import PresentationDataUtils +import ListSectionComponent +import ListItemComponentAdaptor +import TelegramStringFormatting + +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let dismiss: () -> Void + + init( + context: AccountContext, + dismiss: @escaping () -> Void + ) { + self.context = context + self.dismiss = dismiss + } + + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + static var body: Body { + let closeButton = Child(Button.self) + let title = Child(Text.self) + let amountSection = Child(ListSectionComponent.self) + let button = Child(ButtonComponent.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let component = context.component + + let controller = environment.controller + + let theme = environment.theme.withModalBlocksBackground() + let strings = environment.strings + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let sideInset: CGFloat = 16.0 + var contentSize = CGSize(width: context.availableSize.width, height: 18.0) + + let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 + + let closeButton = closeButton.update( + component: Button( + content: AnyComponent(Text(text: "Cancel", font: Font.regular(17.0), color: theme.actionSheet.controlAccentColor)), + action: { + component.dismiss() + } + ), + availableSize: CGSize(width: 120.0, height: 30.0), + transition: .immediate + ) + context.add(closeButton + .position(CGPoint(x: closeButton.size.width / 2.0 + sideInset, y: 28.0)) + ) + + let title = title.update( + component: Text(text: "Share Message", font: Font.bold(17.0), color: theme.list.itemPrimaryTextColor), + availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) + ) + contentSize.height += title.size.height + contentSize.height += 40.0 + + let amountFont = Font.regular(13.0) + let amountTextColor = theme.list.freeTextColor + let amountMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), bold: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), link: MarkdownAttributeSet(font: amountFont, textColor: theme.list.itemAccentColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + + let amountInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Test Attach mini app suggests you to send this message to a chat you select.", attributes: amountMarkdownAttributes, textAlignment: .natural)) + let amountFooter = AnyComponent(MultilineTextComponent( + text: .plain(amountInfoString), + maximumNumberOfLines: 0, + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { attributes, _ in + if let controller = controller() as? WebAppMessagePreviewScreen, let navigationController = controller.navigationController as? NavigationController { + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_PaidContent_AmountInfo_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + } + } + )) + + let messageItem = PeerNameColorChatPreviewItem.MessageItem( + outgoing: true, + peerId: EnginePeer.Id(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(0)), + author: "", + photo: [], + nameColor: .blue, + backgroundEmojiId: nil, + reply: nil, + linkPreview: nil, + text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua" + ) + + let listItemParams = ListViewItemLayoutParams(width: context.availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) + + let amountSection = amountSection.update( + component: ListSectionComponent( + theme: theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Message Preview".uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: amountFooter, + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: PeerNameColorChatPreviewItem( + context: component.context, + theme: environment.theme, + componentTheme: environment.theme, + strings: environment.strings, + sectionId: 0, + fontSize: presentationData.chatFontSize, + chatBubbleCorners: presentationData.chatBubbleCorners, + wallpaper: presentationData.chatWallpaper, + dateTimeFormat: environment.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + messageItems: [messageItem] + ), + params: listItemParams + ))) + ] + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(amountSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + amountSection.size.height / 2.0)) + .clipsToBounds(true) + .cornerRadius(10.0) + ) + contentSize.height += amountSection.size.height + contentSize.height += 32.0 + + let buttonString: String = "Share With..." + let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + + let button = button.update( + component: ButtonComponent( + background: ButtonComponent.Background( + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 10.0 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) + ), + isEnabled: true, + displaysProgress: false, + action: { + if let controller = controller() as? WebAppMessagePreviewScreen { + let _ = controller + } + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50), + transition: .immediate + ) + context.add(button + .clipsToBounds(true) + .cornerRadius(10.0) + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) + ) + contentSize.height += button.size.height + contentSize.height += 15.0 + + contentSize.height += max(environment.inputHeight, environment.safeInsets.bottom) + + return contentSize + } + } + + final class State: ComponentState { + var cachedCloseImage: (UIImage, PresentationTheme)? + } + + func makeState() -> State { + return State() + } +} + +private final class WebAppMessagePreviewSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + private let context: AccountContext + + init( + context: AccountContext + ) { + self.context = context + } + + static func ==(lhs: WebAppMessagePreviewSheetComponent, rhs: WebAppMessagePreviewSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(SheetContent( + context: context.component.context, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } + )), + backgroundColor: .color(environment.theme.list.blocksBackgroundColor), + followContentSizeChanges: false, + clipsContent: true, + isScrollEnabled: false, + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +public final class WebAppMessagePreviewScreen: ViewControllerComponentContainer { + private let context: AccountContext + fileprivate let completion: (Bool) -> Void + + public init( + context: AccountContext, + completion: @escaping (Bool) -> Void + ) { + self.context = context + self.completion = completion + + super.init( + context: context, + component: WebAppMessagePreviewSheetComponent( + context: context + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} diff --git a/submodules/WebUI/Sources/WebAppPermissions.swift b/submodules/WebUI/Sources/WebAppPermissions.swift new file mode 100644 index 0000000000..2a53edab6e --- /dev/null +++ b/submodules/WebUI/Sources/WebAppPermissions.swift @@ -0,0 +1,98 @@ +import Foundation +import NaturalLanguage +import SwiftSignalKit +import TelegramCore +import AccountContext +import TelegramUIPreferences + +public struct WebAppPermissionsState: Codable { + enum CodingKeys: String, CodingKey { + case location + } + + public struct Location: Codable { + enum CodingKeys: String, CodingKey { + case isRequested + case isAllowed + } + + public let isRequested: Bool + public let isAllowed: Bool + + public init( + isRequested: Bool, + isAllowed: Bool + ) { + self.isRequested = isRequested + self.isAllowed = isAllowed + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.isRequested = try container.decode(Bool.self, forKey: .isRequested) + self.isAllowed = try container.decode(Bool.self, forKey: .isAllowed) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.isRequested, forKey: .isRequested) + try container.encode(self.isAllowed, forKey: .isAllowed) + } + } + + public let location: Location? + + public init( + location: Location? + ) { + self.location = location + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.location = try container.decode(WebAppPermissionsState.Location.self, forKey: .location) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(self.location, forKey: .location) + } +} + +public func webAppPermissionsState(context: AccountContext, peerId: EnginePeer.Id) -> Signal { + let key = EngineDataBuffer(length: 8) + key.setInt64(0, value: peerId.id._internalGetInt64Value()) + + return context.engine.data.subscribe(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.webAppPermissionsState, id: key)) + |> map { entry -> WebAppPermissionsState? in + return entry?.get(WebAppPermissionsState.self) + } +} + +private func updateWebAppPermissionsState(context: AccountContext, peerId: EnginePeer.Id, state: WebAppPermissionsState?) -> Signal { + let key = EngineDataBuffer(length: 8) + key.setInt64(0, value: peerId.id._internalGetInt64Value()) + + if let state { + return context.engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.webAppPermissionsState, id: key, item: state) + } else { + return context.engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.webAppPermissionsState, id: key) + } +} + +public func updateWebAppPermissionsStateInteractively(context: AccountContext, peerId: EnginePeer.Id, _ f: @escaping (WebAppPermissionsState?) -> WebAppPermissionsState?) -> Signal { + let key = EngineDataBuffer(length: 8) + key.setInt64(0, value: peerId.id._internalGetInt64Value()) + + return context.engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.webAppPermissionsState, id: key)) + |> map { entry -> WebAppPermissionsState? in + return entry?.get(WebAppPermissionsState.self) + } + |> mapToSignal { current -> Signal in + return updateWebAppPermissionsState(context: context, peerId: peerId, state: f(current)) + } +} diff --git a/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift b/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift new file mode 100644 index 0000000000..db35fbc387 --- /dev/null +++ b/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift @@ -0,0 +1,390 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import Markdown +import TextFormat +import TelegramPresentationData +import ViewControllerComponent +import SheetComponent +import BalancedTextComponent +import MultilineTextComponent +import BundleIconComponent +import ButtonComponent +import AccountContext +import PresentationDataUtils +import PremiumPeerShortcutComponent +import GiftAnimationComponent + +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let botName: String + let accountPeer: EnginePeer + let file: TelegramMediaFile + let dismiss: () -> Void + + init( + context: AccountContext, + botName: String, + accountPeer: EnginePeer, + file: TelegramMediaFile, + dismiss: @escaping () -> Void + ) { + self.context = context + self.botName = botName + self.accountPeer = accountPeer + self.file = file + self.dismiss = dismiss + } + + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.botName != rhs.botName { + return false + } + if lhs.file != rhs.file { + return false + } + return true + } + + final class State: ComponentState { + var cachedCloseImage: (UIImage, PresentationTheme)? + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let background = Child(RoundedRectangle.self) + let animation = Child(GiftAnimationComponent.self) + let closeButton = Child(Button.self) + let title = Child(Text.self) + let text = Child(BalancedTextComponent.self) + + let peerShortcut = Child(PremiumPeerShortcutComponent.self) + let button = Child(ButtonComponent.self) + + + return { context in + let environment = context.environment[EnvironmentType.self] + let component = context.component + let state = context.state + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let theme = presentationData.theme + + var contentSize = CGSize(width: context.availableSize.width, height: 18.0) + + let background = background.update( + component: RoundedRectangle(color: theme.actionSheet.opaqueItemBackgroundColor, cornerRadius: 8.0), + availableSize: CGSize(width: context.availableSize.width, height: 1000.0), + transition: .immediate + ) + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) + ) + + let animation = animation.update( + component: GiftAnimationComponent( + context: component.context, + theme: environment.theme, + file: component.file + ), + availableSize: CGSize(width: 128.0, height: 128.0), + transition: .immediate + ) + context.add(animation + .position(CGPoint(x: context.availableSize.width / 2.0, y: animation.size.height / 2.0 + 12.0)) + ) + + let closeImage: UIImage + if let (image, cacheTheme) = state.cachedCloseImage, theme === cacheTheme { + closeImage = image + } else { + closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)! + state.cachedCloseImage = (closeImage, theme) + } + let closeButton = closeButton.update( + component: Button( + content: AnyComponent(Image(image: closeImage)), + action: { + component.dismiss() + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + context.add(closeButton + .position(CGPoint(x: context.availableSize.width - closeButton.size.width, y: 28.0)) + ) + + let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 + + contentSize.height += 128.0 + + let title = title.update( + component: Text(text: "Set Emoji Status", font: Font.bold(24.0), color: theme.list.itemPrimaryTextColor), + availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) + ) + contentSize.height += title.size.height + contentSize.height += 13.0 + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = theme.actionSheet.primaryTextColor + let linkColor = theme.actionSheet.controlAccentColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + + let text = text.update( + component: BalancedTextComponent( + text: .markdown( + text: "Do you want to set this emoji status suggested by **\(component.botName)**?", + attributes: markdownAttributes + ), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), + transition: .immediate + ) + context.add(text + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0)) + ) + contentSize.height += text.size.height + contentSize.height += 15.0 + + let peerShortcut = peerShortcut.update( + component: PremiumPeerShortcutComponent( + context: component.context, + theme: theme, + peer: component.accountPeer + ), + availableSize: CGSize(width: context.availableSize.width - 32.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(peerShortcut + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + peerShortcut.size.height / 2.0)) + ) + contentSize.height += peerShortcut.size.height + contentSize.height += 32.0 + + let controller = environment.controller() as? WebAppSetEmojiStatusScreen + + let button = button.update( + component: ButtonComponent( + background: ButtonComponent.Background( + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 10.0 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: "Confirm", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak controller] in + controller?.complete(result: true) + controller?.dismissAnimated() + } + ), + availableSize: CGSize(width: context.availableSize.width - 16.0 * 2.0, height: 50), + transition: .immediate + ) + context.add(button + .clipsToBounds(true) + .cornerRadius(10.0) + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) + ) + contentSize.height += button.size.height + + contentSize.height += 48.0 + + return contentSize + } + } +} + +private final class WebAppSetEmojiStatusSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + private let context: AccountContext + private let botName: String + private let accountPeer: EnginePeer + private let file: TelegramMediaFile + + init( + context: AccountContext, + botName: String, + accountPeer: EnginePeer, + file: TelegramMediaFile + ) { + self.context = context + self.botName = botName + self.accountPeer = accountPeer + self.file = file + } + + static func ==(lhs: WebAppSetEmojiStatusSheetComponent, rhs: WebAppSetEmojiStatusSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.botName != rhs.botName { + return false + } + if lhs.accountPeer != rhs.accountPeer { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(SheetContent( + context: context.component.context, + botName: context.component.botName, + accountPeer: context.component.accountPeer, + file: context.component.file, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } + )), + backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor), + followContentSizeChanges: true, + clipsContent: true, + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +public final class WebAppSetEmojiStatusScreen: ViewControllerComponentContainer { + private let context: AccountContext + private let completion: (Bool) -> Void + + public init( + context: AccountContext, + botName: String, + accountPeer: EnginePeer, + file: TelegramMediaFile, + completion: @escaping (Bool) -> Void + ) { + self.context = context + self.completion = completion + + super.init( + context: context, + component: WebAppSetEmojiStatusSheetComponent( + context: context, + botName: botName, + accountPeer: accountPeer, + file: file + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var didComplete = false + fileprivate func complete(result: Bool) { + guard !self.didComplete else { + return + } + self.didComplete = true + self.completion(result) + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} + +func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(foregroundColor.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + }) +} diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index 20956490af..f1be891cc9 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -92,24 +92,16 @@ function tgBrowserDisconnectObserver() { final class WebAppWebView: WKWebView { var handleScriptMessage: (WKScriptMessage) -> Void = { _ in } - var customSideInset: CGFloat = 0.0 { + var customInsets: UIEdgeInsets = .zero { didSet { - if self.customSideInset != oldValue { + if self.customInsets != oldValue { self.setNeedsLayout() } } } - - var customBottomInset: CGFloat = 0.0 { - didSet { - if self.customBottomInset != oldValue { - self.setNeedsLayout() - } - } - } - + override var safeAreaInsets: UIEdgeInsets { - return UIEdgeInsets(top: 0.0, left: self.customSideInset, bottom: self.customBottomInset, right: self.customSideInset) + return UIEdgeInsets(top: self.customInsets.top, left: self.customInsets.left, bottom: self.customInsets.bottom, right: self.customInsets.right) } init(account: Account) { @@ -241,8 +233,11 @@ final class WebAppWebView: WKWebView { } func updateMetrics(height: CGFloat, isExpanded: Bool, isStable: Bool, transition: ContainedViewLayoutTransition) { - let data = "{height:\(height), is_expanded:\(isExpanded ? "true" : "false"), is_state_stable:\(isStable ? "true" : "false")}" - self.sendEvent(name: "viewport_changed", data: data) + let viewportData = "{height:\(height), is_expanded:\(isExpanded ? "true" : "false"), is_state_stable:\(isStable ? "true" : "false")}" + self.sendEvent(name: "viewport_changed", data: viewportData) + + let safeInsetsData = "{top:\(self.customInsets.top), bottom:\(self.customInsets.bottom), left:\(self.customInsets.left), right:\(self.customInsets.right)}" + self.sendEvent(name: "safe_area_changed", data: safeInsetsData) } var lastTouchTimestamp: Double? From 83a7019d0f1e5a3a2807e9d9486b3dfa7930947c Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 7 Nov 2024 18:05:26 +0100 Subject: [PATCH 4/5] Update API --- .../Sources/ApiUtils/ChatContextResult.swift | 2 +- .../Sources/ApiUtils/StoreMessage_Telegram.swift | 2 +- .../SyncCore/SyncCore_TelegramMediaInvoice.swift | 16 +++++++++++++++- .../TelegramEngine/Payments/BotPaymentForm.swift | 8 +++++--- .../ChatRecentActionsControllerNode.swift | 2 +- .../Sources/StarsTransferScreen.swift | 14 +++++--------- .../TelegramUI/Sources/ChatController.swift | 2 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 2 +- 8 files changed, 30 insertions(+), 18 deletions(-) diff --git a/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift b/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift index 40db07ab3f..6e133a7aa5 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift @@ -549,7 +549,7 @@ extension ChatContextResultMessage { if let replyMarkup = replyMarkup { parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup) } - self = .invoice(media: TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: currency, totalAmount: totalAmount, startParam: "", extendedMedia: nil, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), replyMarkup: parsedReplyMarkup) + self = .invoice(media: TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: currency, totalAmount: totalAmount, startParam: "", extendedMedia: nil, subscriptionPeriod: nil, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), replyMarkup: parsedReplyMarkup) case let .botInlineMessageMediaWebPage(flags, message, entities, url, replyMarkup): var parsedReplyMarkup: ReplyMarkupMessageAttribute? if let replyMarkup = replyMarkup { diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 1b650fa415..d37ba11013 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -395,7 +395,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI if (flags & (1 << 1)) != 0 { parsedFlags.insert(.shippingAddressRequested) } - return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: apiExtendedMedia.flatMap({ TelegramExtendedMedia(apiExtendedMedia: $0, peerId: peerId) }), flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil, nil, nil) + return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: apiExtendedMedia.flatMap({ TelegramExtendedMedia(apiExtendedMedia: $0, peerId: peerId) }), subscriptionPeriod: nil, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil, nil, nil) case let .messageMediaPoll(poll, results): switch poll { case let .poll(id, flags, question, answers, closePeriod, _): diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaInvoice.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaInvoice.swift index 0fccecc92e..78842cbec6 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaInvoice.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaInvoice.swift @@ -103,10 +103,11 @@ public final class TelegramMediaInvoice: Media, Equatable { public let photo: TelegramMediaWebFile? public let flags: TelegramMediaInvoiceFlags public let extendedMedia: TelegramExtendedMedia? + public let subscriptionPeriod: Int32? public let version: Int32 - public init(title: String, description: String, photo: TelegramMediaWebFile?, receiptMessageId: MessageId?, currency: String, totalAmount: Int64, startParam: String, extendedMedia: TelegramExtendedMedia?, flags: TelegramMediaInvoiceFlags, version: Int32) { + public init(title: String, description: String, photo: TelegramMediaWebFile?, receiptMessageId: MessageId?, currency: String, totalAmount: Int64, startParam: String, extendedMedia: TelegramExtendedMedia?, subscriptionPeriod: Int32?, flags: TelegramMediaInvoiceFlags, version: Int32) { self.title = title self.description = description self.photo = photo @@ -115,6 +116,7 @@ public final class TelegramMediaInvoice: Media, Equatable { self.totalAmount = totalAmount self.startParam = startParam self.extendedMedia = extendedMedia + self.subscriptionPeriod = subscriptionPeriod self.flags = flags self.version = version } @@ -127,6 +129,7 @@ public final class TelegramMediaInvoice: Media, Equatable { self.startParam = decoder.decodeStringForKey("sp", orElse: "") self.photo = decoder.decodeObjectForKey("p") as? TelegramMediaWebFile self.extendedMedia = decoder.decodeObjectForKey("m", decoder: { TelegramExtendedMedia(decoder: $0) }) as? TelegramExtendedMedia + self.subscriptionPeriod = decoder.decodeOptionalInt32ForKey("sp") self.flags = TelegramMediaInvoiceFlags(rawValue: decoder.decodeInt32ForKey("f", orElse: 0)) if let receiptMessageIdPeerId = decoder.decodeOptionalInt64ForKey("r.p") as Int64?, let receiptMessageIdNamespace = decoder.decodeOptionalInt32ForKey("r.n") as Int32?, let receiptMessageIdId = decoder.decodeOptionalInt32ForKey("r.i") as Int32? { @@ -158,6 +161,12 @@ public final class TelegramMediaInvoice: Media, Equatable { encoder.encodeNil(forKey: "m") } + if let subscriptionPeriod = self.subscriptionPeriod { + encoder.encodeInt32(subscriptionPeriod, forKey: "sp") + } else { + encoder.encodeNil(forKey: "sp") + } + if let receiptMessageId = self.receiptMessageId { encoder.encodeInt64(receiptMessageId.peerId.toInt64(), forKey: "r.p") encoder.encodeInt32(receiptMessageId.namespace, forKey: "r.n") @@ -212,6 +221,10 @@ public final class TelegramMediaInvoice: Media, Equatable { return false } + if self.subscriptionPeriod != other.subscriptionPeriod { + return false + } + if self.version != other.version { return false } @@ -233,6 +246,7 @@ public final class TelegramMediaInvoice: Media, Equatable { totalAmount: self.totalAmount, startParam: self.startParam, extendedMedia: extendedMedia, + subscriptionPeriod: self.subscriptionPeriod, flags: self.flags, version: self.version ) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 416d00acb9..2225e01142 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -402,13 +402,13 @@ func _internal_fetchBotPaymentInvoice(postbox: Postbox, network: Network, source parsedFlags.insert(.shippingAddressRequested) } - return TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: parsedInvoice.currency, totalAmount: 0, startParam: "", extendedMedia: nil, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion) + return TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: parsedInvoice.currency, totalAmount: 0, startParam: "", extendedMedia: nil, subscriptionPeriod: parsedInvoice.subscriptionPeriod, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion) case let .paymentFormStars(_, _, _, title, description, photo, invoice, _): let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice) - return TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: parsedInvoice.currency, totalAmount: parsedInvoice.prices.reduce(0, { $0 + $1.amount }), startParam: "", extendedMedia: nil, flags: [], version: TelegramMediaInvoice.lastVersion) + return TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: parsedInvoice.currency, totalAmount: parsedInvoice.prices.reduce(0, { $0 + $1.amount }), startParam: "", extendedMedia: nil, subscriptionPeriod: parsedInvoice.subscriptionPeriod, flags: [], version: TelegramMediaInvoice.lastVersion) case let .paymentFormStarGift(_, invoice): let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice) - return TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: parsedInvoice.currency, totalAmount: parsedInvoice.prices.reduce(0, { $0 + $1.amount }), startParam: "", extendedMedia: nil, flags: [], version: TelegramMediaInvoice.lastVersion) + return TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: parsedInvoice.currency, totalAmount: parsedInvoice.prices.reduce(0, { $0 + $1.amount }), startParam: "", extendedMedia: nil, subscriptionPeriod: parsedInvoice.subscriptionPeriod, flags: [], version: TelegramMediaInvoice.lastVersion) } } |> mapError { _ -> BotPaymentFormRequestError in } @@ -800,6 +800,7 @@ func _internal_requestBotPaymentReceipt(account: Account, messageId: MessageId) totalAmount: totalAmount, startParam: "", extendedMedia: nil, + subscriptionPeriod: parsedInvoice.subscriptionPeriod, flags: [], version: TelegramMediaInvoice.lastVersion ) @@ -822,6 +823,7 @@ func _internal_requestBotPaymentReceipt(account: Account, messageId: MessageId) totalAmount: totalAmount, startParam: "", extendedMedia: nil, + subscriptionPeriod: parsedInvoice.subscriptionPeriod, flags: [], version: TelegramMediaInvoice.lastVersion ) diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index d0041d3b6f..352d74bb44 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -1283,7 +1283,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { photo.append(photoRepresentation) } let channel = TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(0)), accessHash: .genericPublic(0), title: invite.title, username: nil, photo: photo, creationDate: 0, version: 0, participationStatus: .left, info: .broadcast(TelegramChannelBroadcastInfo(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: invite.nameColor, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil) - let invoice = TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: "XTR", totalAmount: subscriptionPricing.amount, startParam: "", extendedMedia: nil, flags: [], version: 0) + let invoice = TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: "XTR", totalAmount: subscriptionPricing.amount, startParam: "", extendedMedia: nil, subscriptionPeriod: nil, flags: [], version: 0) inputData.set(.single(BotCheckoutController.InputData( form: BotPaymentForm( diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 0b7065258c..f4ba1e0a17 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -300,11 +300,7 @@ private final class SheetContent: CombinedComponent { if let photo = component.invoice.photo { subject = .photo(photo) } else { - if "".isEmpty { - subject = .color(.lightGray) - } else { - subject = .transactionPeer(.peer(peer)) - } + subject = .transactionPeer(.peer(peer)) } } else { subject = .none @@ -318,7 +314,7 @@ private final class SheetContent: CombinedComponent { var isSubscription = false if case .starsChatSubscription = component.source { isSubscription = true - } else if "".isEmpty { + } else if let _ = component.invoice.subscriptionPeriod { isSubscription = true } let star = star.update( @@ -415,8 +411,8 @@ private final class SheetContent: CombinedComponent { let infoText: String if case .starsChatSubscription = context.component.source { infoText = strings.Stars_Transfer_SubscribeInfo(state.botPeer?.compactDisplayTitle ?? "", strings.Stars_Transfer_Info_Stars(Int32(amount))).string - } else if "".isEmpty { - infoText = "Do you want to subscribe to **Subscription Name** in **\(state.botPeer?.compactDisplayTitle ?? "")** for **\(strings.Stars_Transfer_Info_Stars(Int32(amount)))** per month?" + } else if let _ = component.invoice.subscriptionPeriod { + infoText = "Do you want to subscribe to **\(component.invoice.title)** in **\(state.botPeer?.compactDisplayTitle ?? "")** for **\(strings.Stars_Transfer_Info_Stars(Int32(amount)))** per month?" } else if !component.extendedMedia.isEmpty { var description: String = "" var photoCount: Int32 = 0 @@ -541,7 +537,7 @@ private final class SheetContent: CombinedComponent { //TODO:localize buttonAttributedString = NSMutableAttributedString(string: "Subscribe for # \(amountString) / month", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) //buttonAttributedString = NSMutableAttributedString(string: strings.Stars_Transfer_Subscribe, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) - } else if "".isEmpty { + } else if let _ = component.invoice.subscriptionPeriod { buttonAttributedString = NSMutableAttributedString(string: "Subscribe for # \(amountString) / month", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) } else { buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_Pay) # \(amountString)", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 9c4a1ce845..6ea48775d8 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2880,7 +2880,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let sourceMessageId = message.forwardInfo?.sourceMessageId { messageId = sourceMessageId } - let invoice = TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: "XTR", totalAmount: paidContent.amount, startParam: "", extendedMedia: .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: nil), flags: [], version: 0) + let invoice = TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: "XTR", totalAmount: paidContent.amount, startParam: "", extendedMedia: .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: nil), subscriptionPeriod: nil, flags: [], version: 0) let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(messageId), extendedMedia: paidContent.extendedMedia, inputData: starsInputData, completion: { _ in }) strongSelf.push(controller) diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 32ed35f82d..7c0d5fca95 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -301,7 +301,7 @@ func openResolvedUrlImpl( photo.append(photoRepresentation) } let channel = TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(0)), accessHash: .genericPublic(0), title: invite.title, username: nil, photo: photo, creationDate: 0, version: 0, participationStatus: .left, info: .broadcast(TelegramChannelBroadcastInfo(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: invite.nameColor, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil) - let invoice = TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: "XTR", totalAmount: subscriptionPricing.amount, startParam: "", extendedMedia: nil, flags: [], version: 0) + let invoice = TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: "XTR", totalAmount: subscriptionPricing.amount, startParam: "", extendedMedia: nil, subscriptionPeriod: nil, flags: [], version: 0) inputData.set(.single(BotCheckoutController.InputData( form: BotPaymentForm( From 85b6a8ffe96bf8a5b1c7c5b5180024bc07770ba2 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 8 Nov 2024 15:45:48 +0100 Subject: [PATCH 5/5] Update API [skip ci] --- .../Sources/AccountContext.swift | 8 +- .../Sources/ChatController.swift | 6 +- .../BrowserUI/Sources/BrowserWebContent.swift | 3 +- .../Sources/ChatListSearchListPaneNode.swift | 6 +- submodules/TelegramApi/Sources/Api0.swift | 13 +- submodules/TelegramApi/Sources/Api1.swift | 98 ++-- submodules/TelegramApi/Sources/Api2.swift | 106 ++--- submodules/TelegramApi/Sources/Api25.swift | 22 +- submodules/TelegramApi/Sources/Api3.swift | 46 ++ submodules/TelegramApi/Sources/Api31.swift | 98 ++-- submodules/TelegramApi/Sources/Api32.swift | 320 +++++-------- submodules/TelegramApi/Sources/Api33.swift | 440 ++++++++---------- submodules/TelegramApi/Sources/Api34.swift | 240 ++++++++++ submodules/TelegramApi/Sources/Api36.swift | 88 ++++ .../Sources/ApiUtils/BotInfo.swift | 19 +- .../ReplyMarkupMessageAttribute.swift | 16 + .../State/AccountStateManagementUtils.swift | 8 +- .../Sources/SyncCore/SyncCore_BotInfo.swift | 72 ++- .../SyncCore/SyncCore_CachedUserData.swift | 1 + .../TelegramEngine/Data/PeersData.swift | 28 ++ .../TelegramEngine/Messages/BotWebView.swift | 9 +- .../Messages/PreparedInlineMessages.swift | 40 ++ .../Messages/TelegramEngineMessages.swift | 12 +- .../Peers/TelegramEnginePeers.swift | 4 + .../TelegramEngine/Peers/UpdateBotInfo.swift | 42 +- .../Peers/UpdateCachedPeerData.swift | 10 +- .../Sources/PeerInfoScreen.swift | 16 +- .../Sources/ApplicationContext.swift | 2 +- .../Chat/ChatControllerOpenWebApp.swift | 68 ++- .../TelegramUI/Sources/ChatController.swift | 19 +- .../Sources/ChatHistoryEntriesForView.swift | 6 +- .../Sources/NavigateToChatController.swift | 6 +- .../Sources/SharedAccountContext.swift | 4 +- .../Sources/UndoOverlayController.swift | 1 + .../Sources/UndoOverlayControllerNode.swift | 77 ++- .../UrlHandling/Sources/UrlHandling.swift | 55 ++- submodules/WebUI/Sources/FileDownload.swift | 75 +++ .../WebUI/Sources/WebAppController.swift | 420 +++++++++++------ .../WebAppEmojiStatusAlertController.swift | 1 - .../Sources/WebAppMessagePreviewScreen.swift | 22 +- 40 files changed, 1686 insertions(+), 841 deletions(-) create mode 100644 submodules/TelegramCore/Sources/TelegramEngine/Messages/PreparedInlineMessages.swift create mode 100644 submodules/WebUI/Sources/FileDownload.swift diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 5a701c043b..8ee52a8839 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -278,6 +278,12 @@ public enum StickerPackUrlType { case emoji } +public enum ResolvedStartAppMode { + case generic + case compact + case fullscreen +} + public enum ResolvedUrl { case externalUrl(String) case urlAuth(String) @@ -1044,7 +1050,7 @@ public protocol SharedAccountContext: AnyObject { func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController - func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) + func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) func makeDebugSettingsController(context: AccountContext?) -> ViewController? diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 05ab49c8b2..5369f4d437 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -301,13 +301,13 @@ public struct ChatControllerInitialBotAppStart { public let botApp: BotApp? public let payload: String? public let justInstalled: Bool - public let compact: Bool + public let mode: ResolvedStartAppMode - public init(botApp: BotApp?, payload: String?, justInstalled: Bool, compact: Bool) { + public init(botApp: BotApp?, payload: String?, justInstalled: Bool, mode: ResolvedStartAppMode) { self.botApp = botApp self.payload = payload self.justInstalled = justInstalled - self.compact = compact + self.mode = mode } } diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 7e87fd08a2..102cede554 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -1460,8 +1460,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } } - if result.isEmpty, let webViewUrl = self.webView.url { - let schemeAndHostUrl = webViewUrl.deletingPathExtension() + if result.isEmpty, let webViewUrl = self.webView.url, let schemeAndHostUrl = URL(string: "/", relativeTo: webViewUrl) { let url = schemeAndHostUrl.appendingPathComponent("favicon.ico") result.insert(Favicon(url: url.absoluteString, dimensions: nil)) } diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index d5c691cc9a..ebf73ea3ab 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -3816,7 +3816,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { context: self.context, parentController: parentController, updatedPresentationData: nil, - peer: peer, + botPeer: peer, + chatPeer: nil, threadId: nil, buttonText: "", url: "", @@ -3840,7 +3841,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { context: self.context, parentController: parentController, updatedPresentationData: nil, - peer: peer, + botPeer: peer, + chatPeer: nil, threadId: nil, buttonText: "", url: "", diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 0c6f11c5b9..6316a8399f 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -83,6 +83,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1262359766] = { return Api.Boost.parse_boost($0) } dict[-1778593322] = { return Api.BotApp.parse_botApp($0) } dict[1571189943] = { return Api.BotApp.parse_botAppNotModified($0) } + dict[-2103898979] = { return Api.BotAppSettings.parse_botAppSettings($0) } dict[-1989921868] = { return Api.BotBusinessConnection.parse_botBusinessConnection($0) } dict[-1032140601] = { return Api.BotCommand.parse_botCommand($0) } dict[-1180016534] = { return Api.BotCommandScope.parse_botCommandScopeChatAdmins($0) } @@ -92,7 +93,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1071145937] = { return Api.BotCommandScope.parse_botCommandScopePeerAdmins($0) } dict[169026035] = { return Api.BotCommandScope.parse_botCommandScopePeerUser($0) } dict[1011811544] = { return Api.BotCommandScope.parse_botCommandScopeUsers($0) } - dict[-2109505932] = { return Api.BotInfo.parse_botInfo($0) } + dict[912290611] = { return Api.BotInfo.parse_botInfo($0) } dict[1984755728] = { return Api.BotInlineMessage.parse_botInlineMessageMediaAuto($0) } dict[416402882] = { return Api.BotInlineMessage.parse_botInlineMessageMediaContact($0) } dict[85477117] = { return Api.BotInlineMessage.parse_botInlineMessageMediaGeo($0) } @@ -974,7 +975,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[675009298] = { return Api.Update.parse_updateBotPurchasedPaidMedia($0) } dict[-1246823043] = { return Api.Update.parse_updateBotShippingQuery($0) } dict[-997782967] = { return Api.Update.parse_updateBotStopped($0) } - dict[-1464975695] = { return Api.Update.parse_updateBotSubscriptionExpire($0) } + dict[756270830] = { return Api.Update.parse_updateBotSubscriptionExpire($0) } dict[-2095595325] = { return Api.Update.parse_updateBotWebhookJSON($0) } dict[-1684914010] = { return Api.Update.parse_updateBotWebhookJSONQuery($0) } dict[-539401739] = { return Api.Update.parse_updateBroadcastRevenueTransactions($0) } @@ -1266,6 +1267,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1626924713] = { return Api.messages.AvailableReactions.parse_availableReactionsNotModified($0) } dict[-347034123] = { return Api.messages.BotApp.parse_botApp($0) } dict[911761060] = { return Api.messages.BotCallbackAnswer.parse_botCallbackAnswer($0) } + dict[-1899035375] = { return Api.messages.BotPreparedInlineMessage.parse_botPreparedInlineMessage($0) } dict[-534646026] = { return Api.messages.BotResults.parse_botResults($0) } dict[-1231326505] = { return Api.messages.ChatAdminsWithInvites.parse_chatAdminsWithInvites($0) } dict[-438840932] = { return Api.messages.ChatFull.parse_chatFull($0) } @@ -1307,6 +1309,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-83926371] = { return Api.messages.MyStickers.parse_myStickers($0) } dict[863093588] = { return Api.messages.PeerDialogs.parse_peerDialogs($0) } dict[1753266509] = { return Api.messages.PeerSettings.parse_peerSettings($0) } + dict[1636301421] = { return Api.messages.PreparedInlineMessage.parse_preparedInlineMessage($0) } dict[-963811691] = { return Api.messages.QuickReplies.parse_quickReplies($0) } dict[1603398491] = { return Api.messages.QuickReplies.parse_quickRepliesNotModified($0) } dict[-352454890] = { return Api.messages.Reactions.parse_reactions($0) } @@ -1511,6 +1514,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.BotApp: _1.serialize(buffer, boxed) + case let _1 as Api.BotAppSettings: + _1.serialize(buffer, boxed) case let _1 as Api.BotBusinessConnection: _1.serialize(buffer, boxed) case let _1 as Api.BotCommand: @@ -2297,6 +2302,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.messages.BotCallbackAnswer: _1.serialize(buffer, boxed) + case let _1 as Api.messages.BotPreparedInlineMessage: + _1.serialize(buffer, boxed) case let _1 as Api.messages.BotResults: _1.serialize(buffer, boxed) case let _1 as Api.messages.ChatAdminsWithInvites: @@ -2355,6 +2362,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.messages.PeerSettings: _1.serialize(buffer, boxed) + case let _1 as Api.messages.PreparedInlineMessage: + _1.serialize(buffer, boxed) case let _1 as Api.messages.QuickReplies: _1.serialize(buffer, boxed) case let _1 as Api.messages.Reactions: diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index bfb77ee42c..3c674d8c8b 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -1112,6 +1112,64 @@ public extension Api { } } +public extension Api { + enum BotAppSettings: TypeConstructorDescription { + case botAppSettings(flags: Int32, placeholderDocument: Api.Document?, backgroundColor: Int32?, backgroundDarkColor: Int32?, headerColor: Int32?, headerDarkColor: Int32?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .botAppSettings(let flags, let placeholderDocument, let backgroundColor, let backgroundDarkColor, let headerColor, let headerDarkColor): + if boxed { + buffer.appendInt32(-2103898979) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {placeholderDocument!.serialize(buffer, true)} + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(backgroundColor!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {serializeInt32(backgroundDarkColor!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {serializeInt32(headerColor!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {serializeInt32(headerDarkColor!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .botAppSettings(let flags, let placeholderDocument, let backgroundColor, let backgroundDarkColor, let headerColor, let headerDarkColor): + return ("botAppSettings", [("flags", flags as Any), ("placeholderDocument", placeholderDocument as Any), ("backgroundColor", backgroundColor as Any), ("backgroundDarkColor", backgroundDarkColor as Any), ("headerColor", headerColor as Any), ("headerDarkColor", headerDarkColor as Any)]) + } + } + + public static func parse_botAppSettings(_ reader: BufferReader) -> BotAppSettings? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.Document? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Document + } } + var _3: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_3 = reader.readInt32() } + var _4: Int32? + if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt32() } + var _5: Int32? + if Int(_1!) & Int(1 << 3) != 0 {_5 = reader.readInt32() } + var _6: Int32? + if Int(_1!) & Int(1 << 4) != 0 {_6 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.BotAppSettings.botAppSettings(flags: _1!, placeholderDocument: _2, backgroundColor: _3, backgroundDarkColor: _4, headerColor: _5, headerDarkColor: _6) + } + else { + return nil + } + } + + } +} public extension Api { enum BotBusinessConnection: TypeConstructorDescription { case botBusinessConnection(flags: Int32, connectionId: String, userId: Int64, dcId: Int32, date: Int32) @@ -1164,43 +1222,3 @@ public extension Api { } } -public extension Api { - enum BotCommand: TypeConstructorDescription { - case botCommand(command: String, description: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .botCommand(let command, let description): - if boxed { - buffer.appendInt32(-1032140601) - } - serializeString(command, buffer: buffer, boxed: false) - serializeString(description, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .botCommand(let command, let description): - return ("botCommand", [("command", command as Any), ("description", description as Any)]) - } - } - - public static func parse_botCommand(_ reader: BufferReader) -> BotCommand? { - var _1: String? - _1 = parseString(reader) - var _2: String? - _2 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.BotCommand.botCommand(command: _1!, description: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index 5407c5f76c..04d777013b 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -1,3 +1,43 @@ +public extension Api { + enum BotCommand: TypeConstructorDescription { + case botCommand(command: String, description: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .botCommand(let command, let description): + if boxed { + buffer.appendInt32(-1032140601) + } + serializeString(command, buffer: buffer, boxed: false) + serializeString(description, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .botCommand(let command, let description): + return ("botCommand", [("command", command as Any), ("description", description as Any)]) + } + } + + public static func parse_botCommand(_ reader: BufferReader) -> BotCommand? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.BotCommand.botCommand(command: _1!, description: _2!) + } + else { + return nil + } + } + + } +} public extension Api { indirect enum BotCommandScope: TypeConstructorDescription { case botCommandScopeChatAdmins @@ -136,13 +176,13 @@ public extension Api { } public extension Api { enum BotInfo: TypeConstructorDescription { - case botInfo(flags: Int32, userId: Int64?, description: String?, descriptionPhoto: Api.Photo?, descriptionDocument: Api.Document?, commands: [Api.BotCommand]?, menuButton: Api.BotMenuButton?, privacyPolicyUrl: String?) + case botInfo(flags: Int32, userId: Int64?, description: String?, descriptionPhoto: Api.Photo?, descriptionDocument: Api.Document?, commands: [Api.BotCommand]?, menuButton: Api.BotMenuButton?, privacyPolicyUrl: String?, appSettings: Api.BotAppSettings?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .botInfo(let flags, let userId, let description, let descriptionPhoto, let descriptionDocument, let commands, let menuButton, let privacyPolicyUrl): + case .botInfo(let flags, let userId, let description, let descriptionPhoto, let descriptionDocument, let commands, let menuButton, let privacyPolicyUrl, let appSettings): if boxed { - buffer.appendInt32(-2109505932) + buffer.appendInt32(912290611) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeInt64(userId!, buffer: buffer, boxed: false)} @@ -156,14 +196,15 @@ public extension Api { }} if Int(flags) & Int(1 << 3) != 0 {menuButton!.serialize(buffer, true)} if Int(flags) & Int(1 << 7) != 0 {serializeString(privacyPolicyUrl!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 8) != 0 {appSettings!.serialize(buffer, true)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .botInfo(let flags, let userId, let description, let descriptionPhoto, let descriptionDocument, let commands, let menuButton, let privacyPolicyUrl): - return ("botInfo", [("flags", flags as Any), ("userId", userId as Any), ("description", description as Any), ("descriptionPhoto", descriptionPhoto as Any), ("descriptionDocument", descriptionDocument as Any), ("commands", commands as Any), ("menuButton", menuButton as Any), ("privacyPolicyUrl", privacyPolicyUrl as Any)]) + case .botInfo(let flags, let userId, let description, let descriptionPhoto, let descriptionDocument, let commands, let menuButton, let privacyPolicyUrl, let appSettings): + return ("botInfo", [("flags", flags as Any), ("userId", userId as Any), ("description", description as Any), ("descriptionPhoto", descriptionPhoto as Any), ("descriptionDocument", descriptionDocument as Any), ("commands", commands as Any), ("menuButton", menuButton as Any), ("privacyPolicyUrl", privacyPolicyUrl as Any), ("appSettings", appSettings as Any)]) } } @@ -192,6 +233,10 @@ public extension Api { } } var _8: String? if Int(_1!) & Int(1 << 7) != 0 {_8 = parseString(reader) } + var _9: Api.BotAppSettings? + if Int(_1!) & Int(1 << 8) != 0 {if let signature = reader.readInt32() { + _9 = Api.parse(reader, signature: signature) as? Api.BotAppSettings + } } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil @@ -200,8 +245,9 @@ public extension Api { let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil let _c8 = (Int(_1!) & Int(1 << 7) == 0) || _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return Api.BotInfo.botInfo(flags: _1!, userId: _2, description: _3, descriptionPhoto: _4, descriptionDocument: _5, commands: _6, menuButton: _7, privacyPolicyUrl: _8) + let _c9 = (Int(_1!) & Int(1 << 8) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.BotInfo.botInfo(flags: _1!, userId: _2, description: _3, descriptionPhoto: _4, descriptionDocument: _5, commands: _6, menuButton: _7, privacyPolicyUrl: _8, appSettings: _9) } else { return nil @@ -1164,49 +1210,3 @@ public extension Api { } } -public extension Api { - enum BusinessGreetingMessage: TypeConstructorDescription { - case businessGreetingMessage(shortcutId: Int32, recipients: Api.BusinessRecipients, noActivityDays: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .businessGreetingMessage(let shortcutId, let recipients, let noActivityDays): - if boxed { - buffer.appendInt32(-451302485) - } - serializeInt32(shortcutId, buffer: buffer, boxed: false) - recipients.serialize(buffer, true) - serializeInt32(noActivityDays, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .businessGreetingMessage(let shortcutId, let recipients, let noActivityDays): - return ("businessGreetingMessage", [("shortcutId", shortcutId as Any), ("recipients", recipients as Any), ("noActivityDays", noActivityDays as Any)]) - } - } - - public static func parse_businessGreetingMessage(_ reader: BufferReader) -> BusinessGreetingMessage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.BusinessRecipients? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.BusinessRecipients - } - var _3: Int32? - _3 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.BusinessGreetingMessage.businessGreetingMessage(shortcutId: _1!, recipients: _2!, noActivityDays: _3!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index 1d41cbd8c1..99dd0d3621 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -645,7 +645,7 @@ public extension Api { case updateBotPurchasedPaidMedia(userId: Int64, payload: String, qts: Int32) case updateBotShippingQuery(queryId: Int64, userId: Int64, payload: Buffer, shippingAddress: Api.PostAddress) case updateBotStopped(userId: Int64, date: Int32, stopped: Api.Bool, qts: Int32) - case updateBotSubscriptionExpire(userId: Int64, payload: String, untilDate: Int32, qts: Int32) + case updateBotSubscriptionExpire(userId: Int64, payload: String, invoiceSlug: String, untilDate: Int32, qts: Int32) case updateBotWebhookJSON(data: Api.DataJSON) case updateBotWebhookJSONQuery(queryId: Int64, data: Api.DataJSON, timeout: Int32) case updateBroadcastRevenueTransactions(peer: Api.Peer, balances: Api.BroadcastRevenueBalances) @@ -970,12 +970,13 @@ public extension Api { stopped.serialize(buffer, true) serializeInt32(qts, buffer: buffer, boxed: false) break - case .updateBotSubscriptionExpire(let userId, let payload, let untilDate, let qts): + case .updateBotSubscriptionExpire(let userId, let payload, let invoiceSlug, let untilDate, let qts): if boxed { - buffer.appendInt32(-1464975695) + buffer.appendInt32(756270830) } serializeInt64(userId, buffer: buffer, boxed: false) serializeString(payload, buffer: buffer, boxed: false) + serializeString(invoiceSlug, buffer: buffer, boxed: false) serializeInt32(untilDate, buffer: buffer, boxed: false) serializeInt32(qts, buffer: buffer, boxed: false) break @@ -2051,8 +2052,8 @@ public extension Api { return ("updateBotShippingQuery", [("queryId", queryId as Any), ("userId", userId as Any), ("payload", payload as Any), ("shippingAddress", shippingAddress as Any)]) case .updateBotStopped(let userId, let date, let stopped, let qts): return ("updateBotStopped", [("userId", userId as Any), ("date", date as Any), ("stopped", stopped as Any), ("qts", qts as Any)]) - case .updateBotSubscriptionExpire(let userId, let payload, let untilDate, let qts): - return ("updateBotSubscriptionExpire", [("userId", userId as Any), ("payload", payload as Any), ("untilDate", untilDate as Any), ("qts", qts as Any)]) + case .updateBotSubscriptionExpire(let userId, let payload, let invoiceSlug, let untilDate, let qts): + return ("updateBotSubscriptionExpire", [("userId", userId as Any), ("payload", payload as Any), ("invoiceSlug", invoiceSlug as Any), ("untilDate", untilDate as Any), ("qts", qts as Any)]) case .updateBotWebhookJSON(let data): return ("updateBotWebhookJSON", [("data", data as Any)]) case .updateBotWebhookJSONQuery(let queryId, let data, let timeout): @@ -2749,16 +2750,19 @@ public extension Api { _1 = reader.readInt64() var _2: String? _2 = parseString(reader) - var _3: Int32? - _3 = reader.readInt32() + var _3: String? + _3 = parseString(reader) var _4: Int32? _4 = reader.readInt32() + var _5: Int32? + _5 = 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.Update.updateBotSubscriptionExpire(userId: _1!, payload: _2!, untilDate: _3!, qts: _4!) + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.Update.updateBotSubscriptionExpire(userId: _1!, payload: _2!, invoiceSlug: _3!, untilDate: _4!, qts: _5!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index 2a1c1fb212..600a076078 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -1,3 +1,49 @@ +public extension Api { + enum BusinessGreetingMessage: TypeConstructorDescription { + case businessGreetingMessage(shortcutId: Int32, recipients: Api.BusinessRecipients, noActivityDays: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessGreetingMessage(let shortcutId, let recipients, let noActivityDays): + if boxed { + buffer.appendInt32(-451302485) + } + serializeInt32(shortcutId, buffer: buffer, boxed: false) + recipients.serialize(buffer, true) + serializeInt32(noActivityDays, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .businessGreetingMessage(let shortcutId, let recipients, let noActivityDays): + return ("businessGreetingMessage", [("shortcutId", shortcutId as Any), ("recipients", recipients as Any), ("noActivityDays", noActivityDays as Any)]) + } + } + + public static func parse_businessGreetingMessage(_ reader: BufferReader) -> BusinessGreetingMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.BusinessRecipients? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.BusinessRecipients + } + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.BusinessGreetingMessage.businessGreetingMessage(shortcutId: _1!, recipients: _2!, noActivityDays: _3!) + } + else { + return nil + } + } + + } +} public extension Api { enum BusinessIntro: TypeConstructorDescription { case businessIntro(flags: Int32, title: String, description: String, sticker: Api.Document?) diff --git a/submodules/TelegramApi/Sources/Api31.swift b/submodules/TelegramApi/Sources/Api31.swift index 98d0728567..0c987587ba 100644 --- a/submodules/TelegramApi/Sources/Api31.swift +++ b/submodules/TelegramApi/Sources/Api31.swift @@ -260,6 +260,46 @@ public extension Api.messages { } } +public extension Api.messages { + enum BotPreparedInlineMessage: TypeConstructorDescription { + case botPreparedInlineMessage(id: String, expireDate: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .botPreparedInlineMessage(let id, let expireDate): + if boxed { + buffer.appendInt32(-1899035375) + } + serializeString(id, buffer: buffer, boxed: false) + serializeInt32(expireDate, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .botPreparedInlineMessage(let id, let expireDate): + return ("botPreparedInlineMessage", [("id", id as Any), ("expireDate", expireDate as Any)]) + } + } + + public static func parse_botPreparedInlineMessage(_ reader: BufferReader) -> BotPreparedInlineMessage? { + var _1: String? + _1 = parseString(reader) + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.BotPreparedInlineMessage.botPreparedInlineMessage(id: _1!, expireDate: _2!) + } + else { + return nil + } + } + + } +} public extension Api.messages { enum BotResults: TypeConstructorDescription { case botResults(flags: Int32, queryId: Int64, nextOffset: String?, switchPm: Api.InlineBotSwitchPM?, switchWebview: Api.InlineBotWebView?, results: [Api.BotInlineResult], cacheTime: Int32, users: [Api.User]) @@ -1398,61 +1438,3 @@ public extension Api.messages { } } -public extension Api.messages { - enum FoundStickerSets: TypeConstructorDescription { - case foundStickerSets(hash: Int64, sets: [Api.StickerSetCovered]) - case foundStickerSetsNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .foundStickerSets(let hash, let sets): - if boxed { - buffer.appendInt32(-1963942446) - } - serializeInt64(hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(sets.count)) - for item in sets { - item.serialize(buffer, true) - } - break - case .foundStickerSetsNotModified: - if boxed { - buffer.appendInt32(223655517) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .foundStickerSets(let hash, let sets): - return ("foundStickerSets", [("hash", hash as Any), ("sets", sets as Any)]) - case .foundStickerSetsNotModified: - return ("foundStickerSetsNotModified", []) - } - } - - public static func parse_foundStickerSets(_ reader: BufferReader) -> FoundStickerSets? { - var _1: Int64? - _1 = reader.readInt64() - var _2: [Api.StickerSetCovered]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.FoundStickerSets.foundStickerSets(hash: _1!, sets: _2!) - } - else { - return nil - } - } - public static func parse_foundStickerSetsNotModified(_ reader: BufferReader) -> FoundStickerSets? { - return Api.messages.FoundStickerSets.foundStickerSetsNotModified - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api32.swift b/submodules/TelegramApi/Sources/Api32.swift index ae3204c4a7..cf310b87b3 100644 --- a/submodules/TelegramApi/Sources/Api32.swift +++ b/submodules/TelegramApi/Sources/Api32.swift @@ -1,3 +1,61 @@ +public extension Api.messages { + enum FoundStickerSets: TypeConstructorDescription { + case foundStickerSets(hash: Int64, sets: [Api.StickerSetCovered]) + case foundStickerSetsNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .foundStickerSets(let hash, let sets): + if boxed { + buffer.appendInt32(-1963942446) + } + serializeInt64(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sets.count)) + for item in sets { + item.serialize(buffer, true) + } + break + case .foundStickerSetsNotModified: + if boxed { + buffer.appendInt32(223655517) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .foundStickerSets(let hash, let sets): + return ("foundStickerSets", [("hash", hash as Any), ("sets", sets as Any)]) + case .foundStickerSetsNotModified: + return ("foundStickerSetsNotModified", []) + } + } + + public static func parse_foundStickerSets(_ reader: BufferReader) -> FoundStickerSets? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.StickerSetCovered]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.FoundStickerSets.foundStickerSets(hash: _1!, sets: _2!) + } + else { + return nil + } + } + public static func parse_foundStickerSetsNotModified(_ reader: BufferReader) -> FoundStickerSets? { + return Api.messages.FoundStickerSets.foundStickerSetsNotModified + } + + } +} public extension Api.messages { enum HighScores: TypeConstructorDescription { case highScores(scores: [Api.HighScore], users: [Api.User]) @@ -806,6 +864,68 @@ public extension Api.messages { } } +public extension Api.messages { + enum PreparedInlineMessage: TypeConstructorDescription { + case preparedInlineMessage(queryId: Int64, result: Api.BotInlineResult, peerTypes: [Api.InlineQueryPeerType], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .preparedInlineMessage(let queryId, let result, let peerTypes, let users): + if boxed { + buffer.appendInt32(1636301421) + } + serializeInt64(queryId, buffer: buffer, boxed: false) + result.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(peerTypes.count)) + for item in peerTypes { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .preparedInlineMessage(let queryId, let result, let peerTypes, let users): + return ("preparedInlineMessage", [("queryId", queryId as Any), ("result", result as Any), ("peerTypes", peerTypes as Any), ("users", users as Any)]) + } + } + + public static func parse_preparedInlineMessage(_ reader: BufferReader) -> PreparedInlineMessage? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Api.BotInlineResult? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.BotInlineResult + } + var _3: [Api.InlineQueryPeerType]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InlineQueryPeerType.self) + } + var _4: [Api.User]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.messages.PreparedInlineMessage.preparedInlineMessage(queryId: _1!, result: _2!, peerTypes: _3!, users: _4!) + } + else { + return nil + } + } + + } +} public extension Api.messages { enum QuickReplies: TypeConstructorDescription { case quickReplies(quickReplies: [Api.QuickReply], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) @@ -1340,203 +1460,3 @@ public extension Api.messages { } } -public extension Api.messages { - enum SearchResultsCalendar: TypeConstructorDescription { - case searchResultsCalendar(flags: Int32, count: Int32, minDate: Int32, minMsgId: Int32, offsetIdOffset: Int32?, periods: [Api.SearchResultsCalendarPeriod], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .searchResultsCalendar(let flags, let count, let minDate, let minMsgId, let offsetIdOffset, let periods, let messages, let chats, let users): - if boxed { - buffer.appendInt32(343859772) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - serializeInt32(minDate, buffer: buffer, boxed: false) - serializeInt32(minMsgId, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {serializeInt32(offsetIdOffset!, buffer: buffer, boxed: false)} - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(periods.count)) - for item in periods { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(messages.count)) - for item in messages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .searchResultsCalendar(let flags, let count, let minDate, let minMsgId, let offsetIdOffset, let periods, let messages, let chats, let users): - return ("searchResultsCalendar", [("flags", flags as Any), ("count", count as Any), ("minDate", minDate as Any), ("minMsgId", minMsgId as Any), ("offsetIdOffset", offsetIdOffset as Any), ("periods", periods as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_searchResultsCalendar(_ reader: BufferReader) -> SearchResultsCalendar? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Int32? - if Int(_1!) & Int(1 << 1) != 0 {_5 = reader.readInt32() } - var _6: [Api.SearchResultsCalendarPeriod]? - if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SearchResultsCalendarPeriod.self) - } - var _7: [Api.Message]? - if let _ = reader.readInt32() { - _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _8: [Api.Chat]? - if let _ = reader.readInt32() { - _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _9: [Api.User]? - if let _ = reader.readInt32() { - _9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - let _c8 = _8 != nil - let _c9 = _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.messages.SearchResultsCalendar.searchResultsCalendar(flags: _1!, count: _2!, minDate: _3!, minMsgId: _4!, offsetIdOffset: _5, periods: _6!, messages: _7!, chats: _8!, users: _9!) - } - else { - return nil - } - } - - } -} -public extension Api.messages { - enum SearchResultsPositions: TypeConstructorDescription { - case searchResultsPositions(count: Int32, positions: [Api.SearchResultsPosition]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .searchResultsPositions(let count, let positions): - if boxed { - buffer.appendInt32(1404185519) - } - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(positions.count)) - for item in positions { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .searchResultsPositions(let count, let positions): - return ("searchResultsPositions", [("count", count as Any), ("positions", positions as Any)]) - } - } - - public static func parse_searchResultsPositions(_ reader: BufferReader) -> SearchResultsPositions? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.SearchResultsPosition]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SearchResultsPosition.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.SearchResultsPositions.searchResultsPositions(count: _1!, positions: _2!) - } - else { - return nil - } - } - - } -} -public extension Api.messages { - enum SentEncryptedMessage: TypeConstructorDescription { - case sentEncryptedFile(date: Int32, file: Api.EncryptedFile) - case sentEncryptedMessage(date: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .sentEncryptedFile(let date, let file): - if boxed { - buffer.appendInt32(-1802240206) - } - serializeInt32(date, buffer: buffer, boxed: false) - file.serialize(buffer, true) - break - case .sentEncryptedMessage(let date): - if boxed { - buffer.appendInt32(1443858741) - } - serializeInt32(date, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .sentEncryptedFile(let date, let file): - return ("sentEncryptedFile", [("date", date as Any), ("file", file as Any)]) - case .sentEncryptedMessage(let date): - return ("sentEncryptedMessage", [("date", date as Any)]) - } - } - - public static func parse_sentEncryptedFile(_ reader: BufferReader) -> SentEncryptedMessage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.EncryptedFile? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.EncryptedFile - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.SentEncryptedMessage.sentEncryptedFile(date: _1!, file: _2!) - } - else { - return nil - } - } - public static func parse_sentEncryptedMessage(_ reader: BufferReader) -> SentEncryptedMessage? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.messages.SentEncryptedMessage.sentEncryptedMessage(date: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api33.swift b/submodules/TelegramApi/Sources/Api33.swift index 289b3d5fb5..faaa885d0b 100644 --- a/submodules/TelegramApi/Sources/Api33.swift +++ b/submodules/TelegramApi/Sources/Api33.swift @@ -1,3 +1,203 @@ +public extension Api.messages { + enum SearchResultsCalendar: TypeConstructorDescription { + case searchResultsCalendar(flags: Int32, count: Int32, minDate: Int32, minMsgId: Int32, offsetIdOffset: Int32?, periods: [Api.SearchResultsCalendarPeriod], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .searchResultsCalendar(let flags, let count, let minDate, let minMsgId, let offsetIdOffset, let periods, let messages, let chats, let users): + if boxed { + buffer.appendInt32(343859772) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + serializeInt32(minDate, buffer: buffer, boxed: false) + serializeInt32(minMsgId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(offsetIdOffset!, buffer: buffer, boxed: false)} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(periods.count)) + for item in periods { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .searchResultsCalendar(let flags, let count, let minDate, let minMsgId, let offsetIdOffset, let periods, let messages, let chats, let users): + return ("searchResultsCalendar", [("flags", flags as Any), ("count", count as Any), ("minDate", minDate as Any), ("minMsgId", minMsgId as Any), ("offsetIdOffset", offsetIdOffset as Any), ("periods", periods as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_searchResultsCalendar(_ reader: BufferReader) -> SearchResultsCalendar? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_5 = reader.readInt32() } + var _6: [Api.SearchResultsCalendarPeriod]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SearchResultsCalendarPeriod.self) + } + var _7: [Api.Message]? + if let _ = reader.readInt32() { + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _8: [Api.Chat]? + if let _ = reader.readInt32() { + _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _9: [Api.User]? + if let _ = reader.readInt32() { + _9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.messages.SearchResultsCalendar.searchResultsCalendar(flags: _1!, count: _2!, minDate: _3!, minMsgId: _4!, offsetIdOffset: _5, periods: _6!, messages: _7!, chats: _8!, users: _9!) + } + else { + return nil + } + } + + } +} +public extension Api.messages { + enum SearchResultsPositions: TypeConstructorDescription { + case searchResultsPositions(count: Int32, positions: [Api.SearchResultsPosition]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .searchResultsPositions(let count, let positions): + if boxed { + buffer.appendInt32(1404185519) + } + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(positions.count)) + for item in positions { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .searchResultsPositions(let count, let positions): + return ("searchResultsPositions", [("count", count as Any), ("positions", positions as Any)]) + } + } + + public static func parse_searchResultsPositions(_ reader: BufferReader) -> SearchResultsPositions? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.SearchResultsPosition]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SearchResultsPosition.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.SearchResultsPositions.searchResultsPositions(count: _1!, positions: _2!) + } + else { + return nil + } + } + + } +} +public extension Api.messages { + enum SentEncryptedMessage: TypeConstructorDescription { + case sentEncryptedFile(date: Int32, file: Api.EncryptedFile) + case sentEncryptedMessage(date: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .sentEncryptedFile(let date, let file): + if boxed { + buffer.appendInt32(-1802240206) + } + serializeInt32(date, buffer: buffer, boxed: false) + file.serialize(buffer, true) + break + case .sentEncryptedMessage(let date): + if boxed { + buffer.appendInt32(1443858741) + } + serializeInt32(date, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .sentEncryptedFile(let date, let file): + return ("sentEncryptedFile", [("date", date as Any), ("file", file as Any)]) + case .sentEncryptedMessage(let date): + return ("sentEncryptedMessage", [("date", date as Any)]) + } + } + + public static func parse_sentEncryptedFile(_ reader: BufferReader) -> SentEncryptedMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.EncryptedFile? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.EncryptedFile + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.SentEncryptedMessage.sentEncryptedFile(date: _1!, file: _2!) + } + else { + return nil + } + } + public static func parse_sentEncryptedMessage(_ reader: BufferReader) -> SentEncryptedMessage? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.messages.SentEncryptedMessage.sentEncryptedMessage(date: _1!) + } + else { + return nil + } + } + + } +} public extension Api.messages { enum SponsoredMessages: TypeConstructorDescription { case sponsoredMessages(flags: Int32, postsBetween: Int32?, messages: [Api.SponsoredMessage], chats: [Api.Chat], users: [Api.User]) @@ -1330,243 +1530,3 @@ public extension Api.payments { } } -public extension Api.payments { - enum StarsRevenueStats: TypeConstructorDescription { - case starsRevenueStats(revenueGraph: Api.StatsGraph, status: Api.StarsRevenueStatus, usdRate: Double) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .starsRevenueStats(let revenueGraph, let status, let usdRate): - if boxed { - buffer.appendInt32(-919881925) - } - revenueGraph.serialize(buffer, true) - status.serialize(buffer, true) - serializeDouble(usdRate, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .starsRevenueStats(let revenueGraph, let status, let usdRate): - return ("starsRevenueStats", [("revenueGraph", revenueGraph as Any), ("status", status as Any), ("usdRate", usdRate as Any)]) - } - } - - public static func parse_starsRevenueStats(_ reader: BufferReader) -> StarsRevenueStats? { - var _1: Api.StatsGraph? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _2: Api.StarsRevenueStatus? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.StarsRevenueStatus - } - var _3: Double? - _3 = reader.readDouble() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.payments.StarsRevenueStats.starsRevenueStats(revenueGraph: _1!, status: _2!, usdRate: _3!) - } - else { - return nil - } - } - - } -} -public extension Api.payments { - enum StarsRevenueWithdrawalUrl: TypeConstructorDescription { - case starsRevenueWithdrawalUrl(url: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .starsRevenueWithdrawalUrl(let url): - if boxed { - buffer.appendInt32(497778871) - } - serializeString(url, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .starsRevenueWithdrawalUrl(let url): - return ("starsRevenueWithdrawalUrl", [("url", url as Any)]) - } - } - - public static func parse_starsRevenueWithdrawalUrl(_ reader: BufferReader) -> StarsRevenueWithdrawalUrl? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.payments.StarsRevenueWithdrawalUrl.starsRevenueWithdrawalUrl(url: _1!) - } - else { - return nil - } - } - - } -} -public extension Api.payments { - enum StarsStatus: TypeConstructorDescription { - case starsStatus(flags: Int32, balance: Int64, subscriptions: [Api.StarsSubscription]?, subscriptionsNextOffset: String?, subscriptionsMissingBalance: Int64?, history: [Api.StarsTransaction]?, nextOffset: String?, chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .starsStatus(let flags, let balance, let subscriptions, let subscriptionsNextOffset, let subscriptionsMissingBalance, let history, let nextOffset, let chats, let users): - if boxed { - buffer.appendInt32(-1141231252) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt64(balance, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(subscriptions!.count)) - for item in subscriptions! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 2) != 0 {serializeString(subscriptionsNextOffset!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 4) != 0 {serializeInt64(subscriptionsMissingBalance!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(history!.count)) - for item in history! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .starsStatus(let flags, let balance, let subscriptions, let subscriptionsNextOffset, let subscriptionsMissingBalance, let history, let nextOffset, let chats, let users): - return ("starsStatus", [("flags", flags as Any), ("balance", balance as Any), ("subscriptions", subscriptions as Any), ("subscriptionsNextOffset", subscriptionsNextOffset as Any), ("subscriptionsMissingBalance", subscriptionsMissingBalance as Any), ("history", history as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_starsStatus(_ reader: BufferReader) -> StarsStatus? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: [Api.StarsSubscription]? - if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarsSubscription.self) - } } - var _4: String? - if Int(_1!) & Int(1 << 2) != 0 {_4 = parseString(reader) } - var _5: Int64? - if Int(_1!) & Int(1 << 4) != 0 {_5 = reader.readInt64() } - var _6: [Api.StarsTransaction]? - if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarsTransaction.self) - } } - var _7: String? - if Int(_1!) & Int(1 << 0) != 0 {_7 = parseString(reader) } - var _8: [Api.Chat]? - if let _ = reader.readInt32() { - _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _9: [Api.User]? - if let _ = reader.readInt32() { - _9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil - let _c8 = _8 != nil - let _c9 = _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.payments.StarsStatus.starsStatus(flags: _1!, balance: _2!, subscriptions: _3, subscriptionsNextOffset: _4, subscriptionsMissingBalance: _5, history: _6, nextOffset: _7, chats: _8!, users: _9!) - } - else { - return nil - } - } - - } -} -public extension Api.payments { - enum UserStarGifts: TypeConstructorDescription { - case userStarGifts(flags: Int32, count: Int32, gifts: [Api.UserStarGift], nextOffset: String?, users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .userStarGifts(let flags, let count, let gifts, let nextOffset, let users): - if boxed { - buffer.appendInt32(1801827607) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(gifts.count)) - for item in gifts { - item.serialize(buffer, true) - } - if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .userStarGifts(let flags, let count, let gifts, let nextOffset, let users): - return ("userStarGifts", [("flags", flags as Any), ("count", count as Any), ("gifts", gifts as Any), ("nextOffset", nextOffset as Any), ("users", users as Any)]) - } - } - - public static func parse_userStarGifts(_ reader: BufferReader) -> UserStarGifts? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: [Api.UserStarGift]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.UserStarGift.self) - } - var _4: String? - if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) } - var _5: [Api.User]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.payments.UserStarGifts.userStarGifts(flags: _1!, count: _2!, gifts: _3!, nextOffset: _4, users: _5!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api34.swift b/submodules/TelegramApi/Sources/Api34.swift index 222cf9735b..e7fe762c2c 100644 --- a/submodules/TelegramApi/Sources/Api34.swift +++ b/submodules/TelegramApi/Sources/Api34.swift @@ -1,3 +1,243 @@ +public extension Api.payments { + enum StarsRevenueStats: TypeConstructorDescription { + case starsRevenueStats(revenueGraph: Api.StatsGraph, status: Api.StarsRevenueStatus, usdRate: Double) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsRevenueStats(let revenueGraph, let status, let usdRate): + if boxed { + buffer.appendInt32(-919881925) + } + revenueGraph.serialize(buffer, true) + status.serialize(buffer, true) + serializeDouble(usdRate, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starsRevenueStats(let revenueGraph, let status, let usdRate): + return ("starsRevenueStats", [("revenueGraph", revenueGraph as Any), ("status", status as Any), ("usdRate", usdRate as Any)]) + } + } + + public static func parse_starsRevenueStats(_ reader: BufferReader) -> StarsRevenueStats? { + var _1: Api.StatsGraph? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _2: Api.StarsRevenueStatus? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.StarsRevenueStatus + } + var _3: Double? + _3 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.payments.StarsRevenueStats.starsRevenueStats(revenueGraph: _1!, status: _2!, usdRate: _3!) + } + else { + return nil + } + } + + } +} +public extension Api.payments { + enum StarsRevenueWithdrawalUrl: TypeConstructorDescription { + case starsRevenueWithdrawalUrl(url: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsRevenueWithdrawalUrl(let url): + if boxed { + buffer.appendInt32(497778871) + } + serializeString(url, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starsRevenueWithdrawalUrl(let url): + return ("starsRevenueWithdrawalUrl", [("url", url as Any)]) + } + } + + public static func parse_starsRevenueWithdrawalUrl(_ reader: BufferReader) -> StarsRevenueWithdrawalUrl? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.payments.StarsRevenueWithdrawalUrl.starsRevenueWithdrawalUrl(url: _1!) + } + else { + return nil + } + } + + } +} +public extension Api.payments { + enum StarsStatus: TypeConstructorDescription { + case starsStatus(flags: Int32, balance: Int64, subscriptions: [Api.StarsSubscription]?, subscriptionsNextOffset: String?, subscriptionsMissingBalance: Int64?, history: [Api.StarsTransaction]?, nextOffset: String?, chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsStatus(let flags, let balance, let subscriptions, let subscriptionsNextOffset, let subscriptionsMissingBalance, let history, let nextOffset, let chats, let users): + if boxed { + buffer.appendInt32(-1141231252) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(balance, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(subscriptions!.count)) + for item in subscriptions! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 2) != 0 {serializeString(subscriptionsNextOffset!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {serializeInt64(subscriptionsMissingBalance!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(history!.count)) + for item in history! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starsStatus(let flags, let balance, let subscriptions, let subscriptionsNextOffset, let subscriptionsMissingBalance, let history, let nextOffset, let chats, let users): + return ("starsStatus", [("flags", flags as Any), ("balance", balance as Any), ("subscriptions", subscriptions as Any), ("subscriptionsNextOffset", subscriptionsNextOffset as Any), ("subscriptionsMissingBalance", subscriptionsMissingBalance as Any), ("history", history as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_starsStatus(_ reader: BufferReader) -> StarsStatus? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: [Api.StarsSubscription]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarsSubscription.self) + } } + var _4: String? + if Int(_1!) & Int(1 << 2) != 0 {_4 = parseString(reader) } + var _5: Int64? + if Int(_1!) & Int(1 << 4) != 0 {_5 = reader.readInt64() } + var _6: [Api.StarsTransaction]? + if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarsTransaction.self) + } } + var _7: String? + if Int(_1!) & Int(1 << 0) != 0 {_7 = parseString(reader) } + var _8: [Api.Chat]? + if let _ = reader.readInt32() { + _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _9: [Api.User]? + if let _ = reader.readInt32() { + _9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.payments.StarsStatus.starsStatus(flags: _1!, balance: _2!, subscriptions: _3, subscriptionsNextOffset: _4, subscriptionsMissingBalance: _5, history: _6, nextOffset: _7, chats: _8!, users: _9!) + } + else { + return nil + } + } + + } +} +public extension Api.payments { + enum UserStarGifts: TypeConstructorDescription { + case userStarGifts(flags: Int32, count: Int32, gifts: [Api.UserStarGift], nextOffset: String?, users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .userStarGifts(let flags, let count, let gifts, let nextOffset, let users): + if boxed { + buffer.appendInt32(1801827607) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(gifts.count)) + for item in gifts { + item.serialize(buffer, true) + } + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .userStarGifts(let flags, let count, let gifts, let nextOffset, let users): + return ("userStarGifts", [("flags", flags as Any), ("count", count as Any), ("gifts", gifts as Any), ("nextOffset", nextOffset as Any), ("users", users as Any)]) + } + } + + public static func parse_userStarGifts(_ reader: BufferReader) -> UserStarGifts? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.UserStarGift]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.UserStarGift.self) + } + var _4: String? + if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.payments.UserStarGifts.userStarGifts(flags: _1!, count: _2!, gifts: _3!, nextOffset: _4, users: _5!) + } + else { + return nil + } + } + + } +} public extension Api.payments { enum ValidatedRequestedInfo: TypeConstructorDescription { case validatedRequestedInfo(flags: Int32, id: String?, shippingOptions: [Api.ShippingOption]?) diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index 0a6196d7bb..a2de7f78eb 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -2574,6 +2574,22 @@ public extension Api.functions.bots { }) } } +public extension Api.functions.bots { + static func toggleUserEmojiStatusPermission(bot: Api.InputUser, enabled: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(115237778) + bot.serialize(buffer, true) + enabled.serialize(buffer, true) + return (FunctionDescription(name: "bots.toggleUserEmojiStatusPermission", parameters: [("bot", String(describing: bot)), ("enabled", String(describing: enabled))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.bots { static func toggleUsername(bot: Api.InputUser, username: String, active: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -2591,6 +2607,22 @@ public extension Api.functions.bots { }) } } +public extension Api.functions.bots { + static func updateUserEmojiStatus(userId: Api.InputUser, emojiStatus: Api.EmojiStatus) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-308334395) + userId.serialize(buffer, true) + emojiStatus.serialize(buffer, true) + return (FunctionDescription(name: "bots.updateUserEmojiStatus", parameters: [("userId", String(describing: userId)), ("emojiStatus", String(describing: emojiStatus))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.channels { static func checkUsername(channel: Api.InputChannel, username: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -6493,6 +6525,22 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func getPreparedInlineMessage(bot: Api.InputUser, id: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-2055291464) + bot.serialize(buffer, true) + serializeString(id, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.getPreparedInlineMessage", parameters: [("bot", String(describing: bot)), ("id", String(describing: id))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.PreparedInlineMessage? in + let reader = BufferReader(buffer) + var result: Api.messages.PreparedInlineMessage? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.PreparedInlineMessage + } + return result + }) + } +} public extension Api.functions.messages { static func getQuickReplies(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -7608,6 +7656,28 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func savePreparedInlineMessage(flags: Int32, result: Api.InputBotInlineResult, userId: Api.InputUser, peerTypes: [Api.InlineQueryPeerType]?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-232816849) + serializeInt32(flags, buffer: buffer, boxed: false) + result.serialize(buffer, true) + userId.serialize(buffer, true) + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(peerTypes!.count)) + for item in peerTypes! { + item.serialize(buffer, true) + }} + return (FunctionDescription(name: "messages.savePreparedInlineMessage", parameters: [("flags", String(describing: flags)), ("result", String(describing: result)), ("userId", String(describing: userId)), ("peerTypes", String(describing: peerTypes))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.BotPreparedInlineMessage? in + let reader = BufferReader(buffer) + var result: Api.messages.BotPreparedInlineMessage? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.BotPreparedInlineMessage + } + return result + }) + } +} public extension Api.functions.messages { static func saveRecentSticker(flags: Int32, id: Api.InputDocument, unsave: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -8775,6 +8845,24 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func botCancelStarsSubscription(flags: Int32, userId: Api.InputUser, invoiceSlug: String?, chargeId: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1475996902) + serializeInt32(flags, buffer: buffer, boxed: false) + userId.serialize(buffer, true) + if Int(flags) & Int(1 << 1) != 0 {serializeString(invoiceSlug!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {serializeString(chargeId!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "payments.botCancelStarsSubscription", parameters: [("flags", String(describing: flags)), ("userId", String(describing: userId)), ("invoiceSlug", String(describing: invoiceSlug)), ("chargeId", String(describing: chargeId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.payments { static func canPurchasePremium(purpose: Api.InputStorePaymentPurpose) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/ApiUtils/BotInfo.swift b/submodules/TelegramCore/Sources/ApiUtils/BotInfo.swift index 92ee56bc26..213f9d8441 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/BotInfo.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/BotInfo.swift @@ -13,10 +13,25 @@ extension BotMenuButton { } } +extension BotAppSettings { + init(apiBotAppSettings: Api.BotAppSettings) { + switch apiBotAppSettings { + case let .botAppSettings(_, placeholderDocument, backgroundColor, backgroundDarkColor, headerColor, headerDarkColor): + self.init( + placeholder: placeholderDocument.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }, + backgroundColor: backgroundColor, + backgroundDarkColor: backgroundDarkColor, + headerColor: headerColor, + headerDarkColor: headerDarkColor + ) + } + } +} + extension BotInfo { convenience init(apiBotInfo: Api.BotInfo) { switch apiBotInfo { - case let .botInfo(_, _, description, descriptionPhoto, descriptionDocument, apiCommands, apiMenuButton, privacyPolicyUrl): + case let .botInfo(_, _, description, descriptionPhoto, descriptionDocument, apiCommands, apiMenuButton, privacyPolicyUrl, appSettings): let photo: TelegramMediaImage? = descriptionPhoto.flatMap(telegramMediaImageFromApiPhoto) let video: TelegramMediaFile? = descriptionDocument.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) } var commands: [BotCommand] = [] @@ -32,7 +47,7 @@ extension BotInfo { if let apiMenuButton = apiMenuButton { menuButton = BotMenuButton(apiBotMenuButton: apiMenuButton) } - self.init(description: description ?? "", photo: photo, video: video, commands: commands, menuButton: menuButton, privacyPolicyUrl: privacyPolicyUrl) + self.init(description: description ?? "", photo: photo, video: video, commands: commands, menuButton: menuButton, privacyPolicyUrl: privacyPolicyUrl, appSettings: appSettings.flatMap { BotAppSettings(apiBotAppSettings: $0) }) } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift index 581d20ecfb..4d49aaa5fa 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift @@ -2,6 +2,22 @@ import Foundation import Postbox import TelegramApi +extension ReplyMarkupButtonAction.PeerTypes { + init?(apiType: Api.InlineQueryPeerType) { + switch apiType { + case .inlineQueryPeerTypePM: + self = .users + case .inlineQueryPeerTypeBotPM: + self = .bots + case .inlineQueryPeerTypeBroadcast: + self = .channels + case .inlineQueryPeerTypeChat, .inlineQueryPeerTypeMegagroup: + self = .groups + case .inlineQueryPeerTypeSameBotPM: + return nil + } + } +} extension ReplyMarkupButton { init(apiButton: Api.KeyboardButton) { diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 9503219ab2..851ad87d6d 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1706,14 +1706,14 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: updatedState.updateCachedPeerData(peer.peerId, { current in if peer.peerId.namespace == Namespaces.Peer.CloudUser, let previous = current as? CachedUserData { if let botInfo = previous.botInfo { - return previous.withUpdatedBotInfo(BotInfo(description: botInfo.description, photo: botInfo.photo, video: botInfo.video, commands: commands, menuButton: botInfo.menuButton, privacyPolicyUrl: botInfo.privacyPolicyUrl)) + return previous.withUpdatedBotInfo(BotInfo(description: botInfo.description, photo: botInfo.photo, video: botInfo.video, commands: commands, menuButton: botInfo.menuButton, privacyPolicyUrl: botInfo.privacyPolicyUrl, appSettings: botInfo.appSettings)) } } else if peer.peerId.namespace == Namespaces.Peer.CloudGroup, let previous = current as? CachedGroupData { if let index = previous.botInfos.firstIndex(where: { $0.peerId == botPeerId }) { var updatedBotInfos = previous.botInfos let previousBotInfo = updatedBotInfos[index] updatedBotInfos.remove(at: index) - updatedBotInfos.insert(CachedPeerBotInfo(peerId: botPeerId, botInfo: BotInfo(description: previousBotInfo.botInfo.description, photo: previousBotInfo.botInfo.photo, video: previousBotInfo.botInfo.video, commands: commands, menuButton: previousBotInfo.botInfo.menuButton, privacyPolicyUrl: previousBotInfo.botInfo.privacyPolicyUrl)), at: index) + updatedBotInfos.insert(CachedPeerBotInfo(peerId: botPeerId, botInfo: BotInfo(description: previousBotInfo.botInfo.description, photo: previousBotInfo.botInfo.photo, video: previousBotInfo.botInfo.video, commands: commands, menuButton: previousBotInfo.botInfo.menuButton, privacyPolicyUrl: previousBotInfo.botInfo.privacyPolicyUrl, appSettings: previousBotInfo.botInfo.appSettings)), at: index) return previous.withUpdatedBotInfos(updatedBotInfos) } } else if peer.peerId.namespace == Namespaces.Peer.CloudChannel, let previous = current as? CachedChannelData { @@ -1721,7 +1721,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: var updatedBotInfos = previous.botInfos let previousBotInfo = updatedBotInfos[index] updatedBotInfos.remove(at: index) - updatedBotInfos.insert(CachedPeerBotInfo(peerId: botPeerId, botInfo: BotInfo(description: previousBotInfo.botInfo.description, photo: previousBotInfo.botInfo.photo, video: previousBotInfo.botInfo.video, commands: commands, menuButton: previousBotInfo.botInfo.menuButton, privacyPolicyUrl: previousBotInfo.botInfo.privacyPolicyUrl)), at: index) + updatedBotInfos.insert(CachedPeerBotInfo(peerId: botPeerId, botInfo: BotInfo(description: previousBotInfo.botInfo.description, photo: previousBotInfo.botInfo.photo, video: previousBotInfo.botInfo.video, commands: commands, menuButton: previousBotInfo.botInfo.menuButton, privacyPolicyUrl: previousBotInfo.botInfo.privacyPolicyUrl, appSettings: previousBotInfo.botInfo.appSettings)), at: index) return previous.withUpdatedBotInfos(updatedBotInfos) } } @@ -1733,7 +1733,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: updatedState.updateCachedPeerData(botPeerId, { current in if let previous = current as? CachedUserData { if let botInfo = previous.botInfo { - return previous.withUpdatedBotInfo(BotInfo(description: botInfo.description, photo: botInfo.photo, video: botInfo.video, commands: botInfo.commands, menuButton: menuButton, privacyPolicyUrl: botInfo.privacyPolicyUrl)) + return previous.withUpdatedBotInfo(BotInfo(description: botInfo.description, photo: botInfo.photo, video: botInfo.video, commands: botInfo.commands, menuButton: menuButton, privacyPolicyUrl: botInfo.privacyPolicyUrl, appSettings: botInfo.appSettings)) } } return current diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_BotInfo.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_BotInfo.swift index cadd5f91bd..632f67a86b 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_BotInfo.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_BotInfo.swift @@ -45,6 +45,62 @@ public enum BotMenuButton: PostboxCoding, Hashable { } } +public struct BotAppSettings: PostboxCoding, Equatable { + public let placeholder: TelegramMediaFile? + public let backgroundColor: Int32? + public let backgroundDarkColor: Int32? + public let headerColor: Int32? + public let headerDarkColor: Int32? + + public init(placeholder: TelegramMediaFile?, backgroundColor: Int32?, backgroundDarkColor: Int32?, headerColor: Int32?, headerDarkColor: Int32?) { + self.placeholder = placeholder + self.backgroundColor = backgroundColor + self.backgroundDarkColor = backgroundDarkColor + self.headerColor = headerColor + self.headerDarkColor = headerDarkColor + } + + public init(decoder: PostboxDecoder) { + if let placeholder = decoder.decodeObjectForKey("p", decoder: { TelegramMediaFile(decoder: $0) }) as? TelegramMediaFile { + self.placeholder = placeholder + } else { + self.placeholder = nil + } + self.backgroundColor = decoder.decodeOptionalInt32ForKey("b") + self.backgroundDarkColor = decoder.decodeOptionalInt32ForKey("bd") + self.headerColor = decoder.decodeOptionalInt32ForKey("h") + self.headerDarkColor = decoder.decodeOptionalInt32ForKey("hd") + } + + public func encode(_ encoder: PostboxEncoder) { + if let placeholder = self.placeholder { + encoder.encodeObject(placeholder, forKey: "p") + } else { + encoder.encodeNil(forKey: "p") + } + if let backgroundColor = self.backgroundColor { + encoder.encodeInt32(backgroundColor, forKey: "b") + } else { + encoder.encodeNil(forKey: "b") + } + if let backgroundDarkColor = self.backgroundDarkColor { + encoder.encodeInt32(backgroundDarkColor, forKey: "bd") + } else { + encoder.encodeNil(forKey: "bd") + } + if let headerColor = self.headerColor { + encoder.encodeInt32(headerColor, forKey: "h") + } else { + encoder.encodeNil(forKey: "h") + } + if let headerDarkColor = self.headerDarkColor { + encoder.encodeInt32(headerDarkColor, forKey: "hd") + } else { + encoder.encodeNil(forKey: "hd") + } + } +} + public final class BotInfo: PostboxCoding, Equatable { public let description: String public let photo: TelegramMediaImage? @@ -52,14 +108,16 @@ public final class BotInfo: PostboxCoding, Equatable { public let commands: [BotCommand] public let menuButton: BotMenuButton public let privacyPolicyUrl: String? + public let appSettings: BotAppSettings? - public init(description: String, photo: TelegramMediaImage?, video: TelegramMediaFile?, commands: [BotCommand], menuButton: BotMenuButton, privacyPolicyUrl: String?) { + public init(description: String, photo: TelegramMediaImage?, video: TelegramMediaFile?, commands: [BotCommand], menuButton: BotMenuButton, privacyPolicyUrl: String?, appSettings: BotAppSettings?) { self.description = description self.photo = photo self.video = video self.commands = commands self.menuButton = menuButton self.privacyPolicyUrl = privacyPolicyUrl + self.appSettings = appSettings } public init(decoder: PostboxDecoder) { @@ -77,6 +135,11 @@ public final class BotInfo: PostboxCoding, Equatable { self.commands = decoder.decodeObjectArrayWithDecoderForKey("c") self.menuButton = (decoder.decodeObjectForKey("b", decoder: { BotMenuButton(decoder: $0) }) as? BotMenuButton) ?? .commands self.privacyPolicyUrl = decoder.decodeOptionalStringForKey("pp") + if let appSettings = decoder.decodeObjectForKey("as", decoder: { BotAppSettings(decoder: $0) }) as? BotAppSettings { + self.appSettings = appSettings + } else { + self.appSettings = nil + } } public func encode(_ encoder: PostboxEncoder) { @@ -98,9 +161,14 @@ public final class BotInfo: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "pp") } + if let appSettings = self.appSettings { + encoder.encodeObject(appSettings, forKey: "as") + } else { + encoder.encodeNil(forKey: "as") + } } public static func ==(lhs: BotInfo, rhs: BotInfo) -> Bool { - return lhs.description == rhs.description && lhs.commands == rhs.commands && lhs.menuButton == rhs.menuButton && lhs.photo == rhs.photo && lhs.privacyPolicyUrl == rhs.privacyPolicyUrl + return lhs.description == rhs.description && lhs.commands == rhs.commands && lhs.menuButton == rhs.menuButton && lhs.photo == rhs.photo && lhs.privacyPolicyUrl == rhs.privacyPolicyUrl && lhs.appSettings == rhs.appSettings } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index 3e5616334f..6d2800ae08 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -299,6 +299,7 @@ public struct CachedUserFlags: OptionSet { public static let premiumRequired = CachedUserFlags(rawValue: 1 << 3) public static let adsEnabled = CachedUserFlags(rawValue: 1 << 4) public static let canViewRevenue = CachedUserFlags(rawValue: 1 << 5) + public static let botCanManageEmojiStatus = CachedUserFlags(rawValue: 1 << 6) } public final class EditableBotInfo: PostboxCoding, Equatable { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index f8ef324e33..53c6ef5955 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -1126,6 +1126,34 @@ public extension TelegramEngine.EngineData.Item { } } + public struct CanManageEmojiStatus: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Bool + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedUserData { + return cachedData.flags.contains(.botCanManageEmojiStatus) + } else { + return false + } + } + } + public struct CanViewStarsRevenue: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { public typealias Result = Bool diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index 220ef1bdaa..8bbd8de7b4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -67,7 +67,7 @@ func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: P |> switchToLatest } -func _internal_requestMainWebView(postbox: Postbox, network: Network, botId: PeerId, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal { +func _internal_requestMainWebView(postbox: Postbox, network: Network, peerId: PeerId, botId: PeerId, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal { var serializedThemeParams: Api.DataJSON? if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) { serializedThemeParams = .dataJSON(data: dataString) @@ -76,7 +76,7 @@ func _internal_requestMainWebView(postbox: Postbox, network: Network, botId: Pee guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else { return .fail(.generic) } - guard let peer = transaction.getPeer(botId), let inputPeer = apiInputPeer(peer) else { + guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { return .fail(.generic) } @@ -286,7 +286,7 @@ func _internal_sendWebViewData(postbox: Postbox, network: Network, stateManager: |> switchToLatest } -func _internal_requestAppWebView(postbox: Postbox, network: Network, stateManager: AccountStateManager, peerId: PeerId, appReference: BotAppReference, payload: String?, themeParams: [String: Any]?, compact: Bool, allowWrite: Bool) -> Signal { +func _internal_requestAppWebView(postbox: Postbox, network: Network, stateManager: AccountStateManager, peerId: PeerId, appReference: BotAppReference, payload: String?, themeParams: [String: Any]?, compact: Bool, fullscreen: Bool, allowWrite: Bool) -> Signal { var serializedThemeParams: Api.DataJSON? if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) { serializedThemeParams = .dataJSON(data: dataString) @@ -321,6 +321,9 @@ func _internal_requestAppWebView(postbox: Postbox, network: Network, stateManage if compact { flags |= (1 << 7) } + if fullscreen { + flags |= (1 << 8) + } return network.request(Api.functions.messages.requestAppWebView(flags: flags, peer: inputPeer, app: app, startParam: payload, themeParams: serializedThemeParams, platform: botWebViewPlatform)) |> mapError { _ -> RequestWebViewError in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PreparedInlineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PreparedInlineMessages.swift new file mode 100644 index 0000000000..d4ce48ffd0 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PreparedInlineMessages.swift @@ -0,0 +1,40 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +public struct PreparedInlineMessage: Equatable { + let queryId: Int64 + let result: ChatContextResult + let peerTypes: [ReplyMarkupButtonAction.PeerTypes] +} + +func _internal_getPreparedInlineMessage(account: Account, botId: EnginePeer.Id, id: String) -> Signal { + return account.postbox.transaction { transaction -> Api.InputUser? in + return transaction.getPeer(botId).flatMap(apiInputUser) + } + |> mapToSignal { inputBot -> Signal in + guard let inputBot else { + return .single(nil) + } + return account.network.request(Api.functions.messages.getPreparedInlineMessage(bot: inputBot, id: id)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result else { + return .single(nil) + } + return account.postbox.transaction { transaction -> PreparedInlineMessage? in + switch result { + case let .preparedInlineMessage(queryId, result, peerTypes, users): + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: users)) + + return PreparedInlineMessage(queryId: queryId, result: ChatContextResult(apiResult: result, queryId: queryId), peerTypes: peerTypes.compactMap { ReplyMarkupButtonAction.PeerTypes(apiType: $0) }) + } + } + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index d6170a6de9..955f29e570 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -403,6 +403,10 @@ public extension TelegramEngine { public func messageReadStats(id: MessageId) -> Signal { return _internal_messageReadStats(account: self.account, id: id) } + + public func getPreparedInlineMessage(botId: EnginePeer.Id, id: String) -> Signal { + return _internal_getPreparedInlineMessage(account: self.account, botId: botId, id: id) + } public func requestCancelLiveLocation(ids: [MessageId]) -> Signal { return self.account.postbox.transaction { transaction -> Void in @@ -590,12 +594,12 @@ public extension TelegramEngine { return _internal_requestSimpleWebView(postbox: self.account.postbox, network: self.account.network, botId: botId, url: url, source: source, themeParams: themeParams) } - public func requestMainWebView(botId: PeerId, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal { - return _internal_requestMainWebView(postbox: self.account.postbox, network: self.account.network, botId: botId, source: source, themeParams: themeParams) + public func requestMainWebView(peerId: PeerId, botId: PeerId, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal { + return _internal_requestMainWebView(postbox: self.account.postbox, network: self.account.network, peerId: peerId, botId: botId, source: source, themeParams: themeParams) } - public func requestAppWebView(peerId: PeerId, appReference: BotAppReference, payload: String?, themeParams: [String: Any]?, compact: Bool, allowWrite: Bool) -> Signal { - return _internal_requestAppWebView(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, appReference: appReference, payload: payload, themeParams: themeParams, compact: compact, allowWrite: allowWrite) + public func requestAppWebView(peerId: PeerId, appReference: BotAppReference, payload: String?, themeParams: [String: Any]?, compact: Bool, fullscreen: Bool, allowWrite: Bool) -> Signal { + return _internal_requestAppWebView(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, appReference: appReference, payload: payload, themeParams: themeParams, compact: compact, fullscreen: fullscreen, allowWrite: allowWrite) } public func sendWebViewData(botId: PeerId, buttonText: String, data: String) -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 8736e95596..4acd4f6a85 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -811,6 +811,10 @@ public extension TelegramEngine { return _internal_updateBotAbout(account: self.account, peerId: peerId, about: about) } + public func toggleBotEmojiStatusAccess(peerId: PeerId, enabled: Bool) -> Signal { + return _internal_toggleBotEmojiStatusAccess(account: self.account, peerId: peerId, enabled: enabled) + } + public func updatePeerNameColorAndEmoji(peerId: EnginePeer.Id, nameColor: PeerNameColor, backgroundEmojiId: Int64?, profileColor: PeerNameColor?, profileBackgroundEmojiId: Int64?) -> Signal { return _internal_updatePeerNameColorAndEmoji(account: self.account, peerId: peerId, nameColor: nameColor, backgroundEmojiId: backgroundEmojiId, profileColor: profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateBotInfo.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateBotInfo.swift index 1ae8ef638f..ef75f3aefc 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateBotInfo.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateBotInfo.swift @@ -100,7 +100,7 @@ func _internal_updateBotDescription(account: Account, peerId: PeerId, descriptio if let botInfo = current.botInfo { var updatedBotInfo = botInfo if botInfo.description == editableBotInfo.description { - updatedBotInfo = BotInfo(description: description, photo: botInfo.photo, video: botInfo.video, commands: botInfo.commands, menuButton: botInfo.menuButton, privacyPolicyUrl: botInfo.privacyPolicyUrl) + updatedBotInfo = BotInfo(description: description, photo: botInfo.photo, video: botInfo.video, commands: botInfo.commands, menuButton: botInfo.menuButton, privacyPolicyUrl: botInfo.privacyPolicyUrl, appSettings: botInfo.appSettings) } return current.withUpdatedEditableBotInfo(editableBotInfo.withUpdatedDescription(description)).withUpdatedBotInfo(updatedBotInfo) } else { @@ -121,3 +121,43 @@ func _internal_updateBotDescription(account: Account, peerId: PeerId, descriptio |> mapError { _ -> UpdateBotInfoError in } |> switchToLatest } + +public enum ToggleBotEmojiStatusAccessError { + case generic +} + +func _internal_toggleBotEmojiStatusAccess(account: Account, peerId: PeerId, enabled: Bool) -> Signal { + return account.postbox.transaction { transaction -> Signal in + if let peer = transaction.getPeer(peerId), let inputUser = apiInputUser(peer) { + return account.network.request(Api.functions.bots.toggleUserEmojiStatusPermission(bot: inputUser, enabled: enabled ? .boolTrue : .boolFalse)) + |> mapError { _ -> ToggleBotEmojiStatusAccessError in + return .generic + } + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> Void in + if case .boolTrue = result { + transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in + if let current = current as? CachedUserData { + var updatedFlags: CachedUserFlags = current.flags + if enabled { + updatedFlags.insert(.botCanManageEmojiStatus) + } else { + updatedFlags.remove(.botCanManageEmojiStatus) + } + return current.withUpdatedFlags(updatedFlags) + } else { + return current + } + }) + } + } + |> mapError { _ -> ToggleBotEmojiStatusAccessError in } + } + } else { + return .fail(.generic) + } + } + |> mapError { _ -> ToggleBotEmojiStatusAccessError in } + |> switchToLatest + |> ignoreValues +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 9121a3cbee..83b3135fa1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -282,6 +282,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee let translationsDisabled = (userFullFlags & (1 << 23)) != 0 let adsEnabled = (userFullFlags2 & (1 << 7)) != 0 let canViewRevenue = (userFullFlags2 & (1 << 9)) != 0 + let botCanManageEmojiStatus = (userFullFlags2 & (1 << 10)) != 0 var flags: CachedUserFlags = previous.flags if premiumRequired { @@ -309,6 +310,11 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } else { flags.remove(.canViewRevenue) } + if botCanManageEmojiStatus { + flags.insert(.botCanManageEmojiStatus) + } else { + flags.remove(.botCanManageEmojiStatus) + } let callsPrivate = (userFullFlags & (1 << 5)) != 0 let canPinMessages = (userFullFlags & (1 << 7)) != 0 @@ -446,7 +452,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee var botInfos: [CachedPeerBotInfo] = [] for botInfo in chatFullBotInfo ?? [] { switch botInfo { - case let .botInfo(_, userId, _, _, _, _, _, _): + case let .botInfo(_, userId, _, _, _, _, _, _, _): if let userId = userId { let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) let parsedBotInfo = BotInfo(apiBotInfo: botInfo) @@ -636,7 +642,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee var botInfos: [CachedPeerBotInfo] = [] for botInfo in apiBotInfos { switch botInfo { - case let .botInfo(_, userId, _, _, _, _, _, _): + case let .botInfo(_, userId, _, _, _, _, _, _, _): if let userId = userId { let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) let parsedBotInfo = BotInfo(apiBotInfo: botInfo) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 7040216a1e..5a7fc07df5 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1381,7 +1381,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese context: context, parentController: parentController, updatedPresentationData: nil, - peer: .user(user), + botPeer: .user(user), + chatPeer: nil, threadId: nil, buttonText: "", url: "", @@ -1545,15 +1546,20 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese if let _ = user.botInfo { //TODO:localize items[.permissions]!.append(PeerInfoScreenHeaderItem(id: 30, text: "ALLOW ACCESS TO")) -// items[.permissions]!.append(PeerInfoScreenSwitchItem(id: 31, text: "Emoji Status", value: false, icon: UIImage(bundleImageName: "Chat/Info/Status"), isLocked: false, toggled: { value in -// -// })) + var canManageEmojiStatus = false + if let cachedData = data.cachedData as? CachedUserData, cachedData.flags.contains(.botCanManageEmojiStatus) { + canManageEmojiStatus = true + } + items[.permissions]!.append(PeerInfoScreenSwitchItem(id: 31, text: "Emoji Status", value: canManageEmojiStatus, icon: UIImage(bundleImageName: "Chat/Info/Status"), isLocked: false, toggled: { value in + let _ = (context.engine.peers.toggleBotEmojiStatusAccess(peerId: user.id, enabled: value) + |> deliverOnMainQueue).startStandalone() + })) if data.webAppPermissions?.location?.isRequested == true { items[.permissions]!.append(PeerInfoScreenSwitchItem(id: 32, text: "Geolocation", value: data.webAppPermissions?.location?.isAllowed ?? false, icon: UIImage(bundleImageName: "Chat/Info/Location"), isLocked: false, toggled: { value in let _ = updateWebAppPermissionsStateInteractively(context: context, peerId: user.id) { current in return WebAppPermissionsState(location: WebAppPermissionsState.Location(isRequested: true, isAllowed: value)) - }.start() + }.startStandalone() })) } } diff --git a/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index 00ef0e1e1e..658482861d 100644 --- a/submodules/TelegramUI/Sources/ApplicationContext.swift +++ b/submodules/TelegramUI/Sources/ApplicationContext.swift @@ -946,7 +946,7 @@ final class AuthorizedApplicationContext { } if openAppIfAny, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.rootController.viewControllers.last as? ViewController { - self.context.sharedContext.openWebApp(context: self.context, parentController: parentController, updatedPresentationData: nil, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: true, payload: nil) + self.context.sharedContext.openWebApp(context: self.context, parentController: parentController, updatedPresentationData: nil, botPeer: peer, chatPeer: nil, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: true, payload: nil) } else { self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil)) } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index 97da9a68e3..cf8220130b 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -14,7 +14,20 @@ import UndoUI import UrlHandling import TelegramPresentationData -func openWebAppImpl(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) { +func openWebAppImpl( + context: AccountContext, + parentController: ViewController, + updatedPresentationData: (initial: PresentationData, signal: Signal)?, + botPeer: EnginePeer, + chatPeer: EnginePeer?, + threadId: Int64?, + buttonText: String, + url: String, + simple: Bool, + source: ChatOpenWebViewSource, + skipTermsOfService: Bool, + payload: String? +) { let presentationData: PresentationData if let parentController = parentController as? ChatControllerImpl { presentationData = parentController.presentationData @@ -30,9 +43,9 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u botAddress = bot.addressName ?? "" botVerified = bot.isVerified } else { - botName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - botAddress = peer.addressName ?? "" - botVerified = peer.isVerified + botName = botPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + botAddress = botPeer.addressName ?? "" + botVerified = botPeer.isVerified } if source == .generic { @@ -95,7 +108,7 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u if let navigationController = parentController.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer { for controller in minimizedContainer.controllers { - if let controller = controller as? AttachmentController, let mainController = controller.mainController as? WebAppController, mainController.botId == peer.id && mainController.source == .menu { + if let controller = controller as? AttachmentController, let mainController = controller.mainController as? WebAppController, mainController.botId == botPeer.id && mainController.source == .menu { navigationController.maximizeViewController(controller, animated: true) return } @@ -105,22 +118,22 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u var fullSize = false var isFullscreen = false if isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, url: url), case .peer(_, .appStart) = internalUrl { - if url.contains("&mode=fullscreen") { + if url.contains("mode=fullscreen") { isFullscreen = true fullSize = true } else { - fullSize = !url.contains("?mode=compact") + fullSize = !url.contains("mode=compact") } } var presentImpl: ((ViewController, Any?) -> Void)? - let params = WebAppParameters(source: .menu, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: fullSize, isFullscreen: isFullscreen) + let params = WebAppParameters(source: .menu, peerId: chatPeer?.id ?? botPeer.id, botId: botPeer.id, botName: botName, botVerified: botVerified, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: fullSize, isFullscreen: isFullscreen) let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in - ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in + ChatControllerImpl.botOpenUrl(context: context, peerId: chatPeer?.id ?? botPeer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) }, commit: commit) }, requestSwitchInline: { [weak parentController] query, chatTypes, completion in - ChatControllerImpl.botRequestSwitchInline(context: context, controller: parentController as? ChatControllerImpl, peerId: peer.id, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion) + ChatControllerImpl.botRequestSwitchInline(context: context, controller: parentController as? ChatControllerImpl, peerId: chatPeer?.id ?? botPeer.id, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion) }, getInputContainerNode: { [weak parentController] in if let parentController = parentController as? ChatControllerImpl, let layout = parentController.validLayout, case .compact = layout.metrics.widthClass { return (parentController.chatDisplayNode.getWindowInputAccessoryHeight(), parentController.chatDisplayNode.inputPanelContainerNode, { @@ -160,10 +173,10 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u } } else if simple { var isInline = false - var botId = peer.id + var botId = botPeer.id var botName = botName var botAddress = "" - var botVerified = peer.isVerified + var botVerified = botPeer.isVerified if case let .inline(bot) = source { isInline = true botId = bot.id @@ -180,9 +193,14 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u } let webViewSignal: Signal - let webViewSource: RequestSimpleWebViewSource = isInline ? .inline(startParam: payload) : .generic + let webViewSource: RequestSimpleWebViewSource + if let payload { + webViewSource = .inline(startParam: payload) + } else { + webViewSource = .generic + } if url.isEmpty { - webViewSignal = context.engine.messages.requestMainWebView(botId: botId, source: webViewSource, themeParams: generateWebAppThemeParams(presentationData.theme)) + webViewSignal = context.engine.messages.requestMainWebView(peerId: chatPeer?.id ?? botId, botId: botId, source: webViewSource, themeParams: generateWebAppThemeParams(presentationData.theme)) } else { webViewSignal = context.engine.messages.requestSimpleWebView(botId: botId, url: url, source: webViewSource, themeParams: generateWebAppThemeParams(presentationData.theme)) } @@ -202,13 +220,13 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u } else { source = url.isEmpty ? .generic : .simple } - let params = WebAppParameters(source: source, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: payload, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen)) + let params = WebAppParameters(source: source, peerId: chatPeer?.id ?? botId, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: payload, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen)) let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in - ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in + ChatControllerImpl.botOpenUrl(context: context, peerId: chatPeer?.id ?? botId, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) }, commit: commit) }, requestSwitchInline: { [weak parentController] query, chatTypes, completion in - ChatControllerImpl.botRequestSwitchInline(context: context, controller: parentController as? ChatControllerImpl, peerId: peer.id, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion) + ChatControllerImpl.botRequestSwitchInline(context: context, controller: parentController as? ChatControllerImpl, peerId: chatPeer?.id ?? botId, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion) }, getNavigationController: { [weak parentController] in var navigationController: NavigationController? if let parentController = parentController as? ChatControllerImpl { @@ -239,7 +257,7 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u messageActionCallbackDisposable = MetaDisposable() } - messageActionCallbackDisposable.set(((context.engine.messages.requestWebView(peerId: peer.id, botId: peer.id, url: !url.isEmpty ? url : nil, payload: nil, themeParams: generateWebAppThemeParams(presentationData.theme), fromMenu: false, replyToMessageId: nil, threadId: threadId) + messageActionCallbackDisposable.set(((context.engine.messages.requestWebView(peerId: chatPeer?.id ?? botPeer.id, botId: botPeer.id, url: !url.isEmpty ? url : nil, payload: nil, themeParams: generateWebAppThemeParams(presentationData.theme), fromMenu: false, replyToMessageId: nil, threadId: threadId) |> afterDisposed { updateProgress() }) @@ -248,9 +266,9 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u return } var presentImpl: ((ViewController, Any?) -> Void)? - let params = WebAppParameters(source: .button, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false, fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen)) + let params = WebAppParameters(source: .button, peerId: chatPeer?.id ?? botPeer.id, botId: botPeer.id, botName: botName, botVerified: botVerified, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false, fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen)) let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in - ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in + ChatControllerImpl.botOpenUrl(context: context, peerId: chatPeer?.id ?? botPeer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) }, commit: commit) }, completion: { [weak parentController] in @@ -285,7 +303,7 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u if skipTermsOfService { openWebView() } else { - var botPeer = peer + var botPeer = botPeer if case let .inline(bot) = source { botPeer = bot } @@ -319,7 +337,7 @@ public extension ChatControllerImpl { } self.chatDisplayNode.dismissInput() - self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadId: self.chatLocation.threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: false, payload: nil) + self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, botPeer: EnginePeer(peer), chatPeer: EnginePeer(peer), threadId: self.chatLocation.threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: false, payload: nil) } static func botRequestSwitchInline(context: AccountContext, controller: ChatControllerImpl?, peerId: EnginePeer.Id, botAddress: String, query: String, chatTypes: [ReplyMarkupButtonRequestPeerType]?, completion: @escaping () -> Void) -> Void { @@ -413,7 +431,7 @@ public extension ChatControllerImpl { } } - func presentBotApp(botApp: BotApp?, botPeer: EnginePeer, payload: String?, compact: Bool, concealed: Bool = false, commit: @escaping () -> Void = {}) { + func presentBotApp(botApp: BotApp?, botPeer: EnginePeer, payload: String?, mode: ResolvedStartAppMode, concealed: Bool = false, commit: @escaping () -> Void = {}) { guard let peerId = self.chatLocation.peerId else { return } @@ -469,7 +487,7 @@ public extension ChatControllerImpl { } let botAddress = botPeer.addressName ?? "" - strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestAppWebView(peerId: peerId, appReference: .id(id: botApp.id, accessHash: botApp.accessHash), payload: payload, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme), compact: compact, allowWrite: allowWrite) + strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestAppWebView(peerId: peerId, appReference: .id(id: botApp.id, accessHash: botApp.accessHash), payload: payload, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme), compact: mode == .compact, fullscreen: mode == .fullscreen, allowWrite: allowWrite) |> afterDisposed { updateProgress() }) @@ -570,7 +588,7 @@ public extension ChatControllerImpl { } }) } else { - self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: botPeer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false, payload: payload) + self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, botPeer: botPeer, chatPeer: nil, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false, payload: payload) } } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 6ea48775d8..b44df4b217 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -9856,16 +9856,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .withBotApp(botAppStart): let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId.id)) |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in - if let strongSelf = self, let peer { - if let botApp = botAppStart.botApp { - strongSelf.presentBotApp(botApp: botApp, botPeer: peer, payload: botAppStart.payload, compact: botAppStart.compact, concealed: concealed, commit: { - dismissWebAppControllers() - commit() - }) - } else { - strongSelf.context.sharedContext.openWebApp(context: strongSelf.context, parentController: strongSelf, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false, payload: botAppStart.payload) + guard let self, let peer else { + return + } + if let botApp = botAppStart.botApp { + self.presentBotApp(botApp: botApp, botPeer: peer, payload: botAppStart.payload, mode: botAppStart.mode, concealed: concealed, commit: { + dismissWebAppControllers() commit() - } + }) + } else { + self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, botPeer: peer, chatPeer: nil, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false, payload: botAppStart.payload) + commit() } }) default: diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index bff0145832..b0364dbd23 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -668,9 +668,9 @@ func chatHistoryEntriesForView( if reverse { return (entries.reversed(), currentState) } else { - #if DEBUG - assert(entries.map(\.stableId) == entries.sorted().map(\.stableId)) - #endif +// #if DEBUG +// assert(entries.map(\.stableId) == entries.sorted().map(\.stableId)) +// #endif return (entries, currentState) } } diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index 25480b6b89..b2b58e5eae 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -165,7 +165,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam controller.presentAttachmentBot(botId: attachBotStart.botId, payload: attachBotStart.payload, justInstalled: attachBotStart.justInstalled) } if let botAppStart = params.botAppStart, case let .peer(peer) = params.chatLocation { - controller.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, compact: botAppStart.compact) + controller.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, mode: botAppStart.mode) } params.setupController(controller) found = true @@ -188,7 +188,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam } if let botAppStart = params.botAppStart, case let .peer(peer) = params.chatLocation { Queue.mainQueue().after(0.1) { - controller.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, compact: botAppStart.compact) + controller.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, mode: botAppStart.mode) } } } else { @@ -196,7 +196,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam if let botAppStart = params.botAppStart, case let .peer(peer) = params.chatLocation { Queue.mainQueue().after(0.1) { - controller.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, compact: botAppStart.compact) + controller.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, mode: botAppStart.mode) } } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 30a0afd453..a3cee89117 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2825,8 +2825,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return MiniAppListScreen(context: context, initialData: initialData as! MiniAppListScreen.InitialData) } - public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) { - openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, peer: peer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService, payload: payload) + public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) { + openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, botPeer: botPeer, chatPeer: chatPeer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService, payload: payload) } } diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index ca2d9acc38..1744c61bbc 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -50,6 +50,7 @@ public enum UndoOverlayContent { case peers(context: AccountContext, peers: [EnginePeer], title: String?, text: String, customUndoText: String?) case messageTagged(context: AccountContext, isSingleMessage: Bool, customEmoji: TelegramMediaFile, isBuiltinReaction: Bool, customUndoText: String?) case media(context: AccountContext, file: FileMediaReference, title: String?, text: String, undoText: String?, customAction: (() -> Void)?) + case progress(progress: CGFloat, title: String, text: String, undoText: String?) } public enum UndoOverlayAction { diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index d04b022b4d..6a2c38bbf2 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -1351,6 +1351,45 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { if let updatedImageSignal = updatedImageSignal { stillStickerNode.setSignal(updatedImageSignal) } + case let .progress(progress, title, text, customUndoText): + self.avatarNode = nil + self.iconNode = ASImageNode() + self.iconNode?.displayWithoutProcessing = true + self.iconNode?.displaysAsynchronously = false + self.iconNode?.image = generateTintedImage(image: UIImage(bundleImageName: "Premium/File"), color: .white) + self.iconImageSize = CGSize(width: 14.0, height: 14.0) + self.iconCheckNode = nil + self.animationNode = nil + self.animatedStickerNode = nil + + self.statusNode = RadialStatusNode(backgroundNodeColor: .clear) + + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in + return ("URL", contents) + }), textAlignment: .natural) + self.textNode.attributedText = attributedText + + + if text.contains("](") { + isUserInteractionEnabled = true + } + self.originalRemainingSeconds = 5 + + self.textNode.maximumNumberOfLines = 5 + + if let customUndoText = customUndoText { + undoText = customUndoText + displayUndo = true + } else { + displayUndo = false + } + + self.statusNode?.transitionToState(.progress(color: .white, lineWidth: nil, value: max(progress, 0.027), cancelEnabled: false, animateRotation: true), completion: {}) } self.remainingSeconds = self.originalRemainingSeconds @@ -1394,7 +1433,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } else { self.isUserInteractionEnabled = false } - case .sticker, .customEmoji, .media: + case .sticker, .customEmoji, .media, .progress: self.isUserInteractionEnabled = displayUndo case .dice: self.panelWrapperNode.clipsToBounds = true @@ -1431,6 +1470,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.slotMachineNode.flatMap(self.panelWrapperNode.addSubnode) self.avatarNode.flatMap(self.panelWrapperNode.addSubnode) self.multiAvatarsNode.flatMap(self.panelWrapperNode.addSubnode) + self.panelWrapperNode.addSubnode(self.buttonNode) self.panelWrapperNode.addSubnode(self.titleNode) self.panelWrapperNode.addSubnode(self.textNode) @@ -1629,6 +1669,15 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.renewWithCurrentContent() transition = .animated(duration: 0.1, curve: .easeInOut) + case let .progress(progress, title, text, _): + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural) + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + self.textNode.attributedText = attributedText + + self.statusNode?.transitionToState(.progress(color: .white, lineWidth: nil, value: max(progress, 0.027), cancelEnabled: false, animateRotation: true), completion: {}) default: break } @@ -1667,7 +1716,10 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { var leftInset: CGFloat = 50.0 if let iconImageSize = self.iconImageSize { - leftInset = 9.0 + iconImageSize.width + 9.0 + if case .progress = self.content { + } else { + leftInset = 9.0 + iconImageSize.width + 9.0 + } } else if let iconSize = preferredSize { if iconSize.width > leftInset { leftInset = iconSize.width - 8.0 @@ -1802,8 +1854,13 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { iconSize = CGSize() } + var isProgress = false + if case .progress = self.content { + isProgress = true + } + let iconFrame: CGRect - if self.iconImageSize != nil { + if self.iconImageSize != nil && !isProgress { iconFrame = CGRect(origin: CGPoint(x: 9.0, y: floor((contentHeight - iconSize.height) / 2.0) + verticalOffset), size: iconSize) } else { iconFrame = CGRect(origin: CGPoint(x: floor((leftInset - iconSize.width) / 2.0), y: floor((contentHeight - iconSize.height) / 2.0) + verticalOffset), size: iconSize) @@ -1885,13 +1942,17 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } transition.updateFrame(node: statusNode, frame: statusFrame) if !self.didStartStatusNode { - let statusColor: UIColor - if case .starsSent = self.content { - statusColor = self.undoTextColor + if case .progress = self.content { + } else { - statusColor = .white + let statusColor: UIColor + if case .starsSent = self.content { + statusColor = self.undoTextColor + } else { + statusColor = .white + } + statusNode.transitionToState(.secretTimeout(color: statusColor, icon: .none, beginTime: CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, timeout: Double(self.remainingSeconds), sparks: false), completion: {}) } - statusNode.transitionToState(.secretTimeout(color: statusColor, icon: .none, beginTime: CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, timeout: Double(self.remainingSeconds), sparks: false), completion: {}) self.didStartStatusNode = true } } diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 9f51d5c976..6d879d92f7 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -71,7 +71,7 @@ public enum ParsedInternalPeerUrlParameter { case channelMessage(Int32, Double?) case replyThread(Int32, Int32) case voiceChat(String?) - case appStart(String, String?, Bool) + case appStart(String, String?, ResolvedStartAppMode) case story(Int32) case boost case text(String) @@ -293,18 +293,25 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, query: String) } return .startAttach(peerName, value, choose) } else if queryItem.name == "startapp" { - var compact = false + var mode: ResolvedStartAppMode = .generic if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { - if queryItem.name == "mode", value == "compact" { - compact = true + if queryItem.name == "mode" { + switch value { + case "compact": + mode = .compact + case "fullscreen": + mode = .fullscreen + default: + break + } break } } } } - return .peer(.name(peerName), .appStart("", queryItem.value, compact)) + return .peer(.name(peerName), .appStart("", queryItem.value, mode)) } else if queryItem.name == "story" { if let id = Int32(value) { return .peer(.name(peerName), .story(id)) @@ -335,18 +342,25 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, query: String) } else if queryItem.name == "profile" { return .peer(.name(peerName), .profile) } else if queryItem.name == "startapp" { - var compact = false + var mode: ResolvedStartAppMode = .generic if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { - if queryItem.name == "mode", value == "compact" { - compact = true + if queryItem.name == "mode" { + switch value { + case "compact": + mode = .compact + case "fullscreen": + mode = .fullscreen + default: + break + } break } } } } - return .peer(.name(peerName), .appStart("", nil, compact)) + return .peer(.name(peerName), .appStart("", queryItem.value, mode)) } } } @@ -615,19 +629,28 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, query: String) } else if pathComponents.count == 2 { let appName = pathComponents[1] var startApp: String? - var compact = false + var mode: ResolvedStartAppMode = .generic if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { if queryItem.name == "startapp" { startApp = value - } else if queryItem.name == "mode", value == "compact" { - compact = true + } + if queryItem.name == "mode" { + switch value { + case "compact": + mode = .compact + case "fullscreen": + mode = .fullscreen + default: + break + } + break } } } } - return .peer(.name(peerName), .appStart(appName, startApp, compact)) + return .peer(.name(peerName), .appStart(appName, startApp, mode)) } else { return nil } @@ -745,10 +768,10 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) } } } - case let .appStart(name, payload, compact): + case let .appStart(name, payload, mode): if name.isEmpty { if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) { - return .single(.result(.peer(peer._asPeer(), .withBotApp(ChatControllerInitialBotAppStart(botApp: nil, payload: payload, justInstalled: false, compact: compact))))) + return .single(.result(.peer(peer._asPeer(), .withBotApp(ChatControllerInitialBotAppStart(botApp: nil, payload: payload, justInstalled: false, mode: mode))))) } else { return .single(.result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil)))) } @@ -760,7 +783,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) } |> mapToSignal { botApp -> Signal in if let botApp { - return .single(.result(.peer(peer._asPeer(), .withBotApp(ChatControllerInitialBotAppStart(botApp: botApp, payload: payload, justInstalled: false, compact: compact))))) + return .single(.result(.peer(peer._asPeer(), .withBotApp(ChatControllerInitialBotAppStart(botApp: botApp, payload: payload, justInstalled: false, mode: mode))))) } else { return .single(.result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil)))) } diff --git a/submodules/WebUI/Sources/FileDownload.swift b/submodules/WebUI/Sources/FileDownload.swift new file mode 100644 index 0000000000..815a109a61 --- /dev/null +++ b/submodules/WebUI/Sources/FileDownload.swift @@ -0,0 +1,75 @@ +import Foundation +import SwiftSignalKit + +final class FileDownload: NSObject, URLSessionDownloadDelegate { + private let fileSize: Int64? + private var urlSession: URLSession! + private var completion: ((URL?, Error?) -> Void)? + private var progressHandler: ((Double) -> Void)? + + init(from url: URL, fileSize: Int64?, progressHandler: @escaping (Double) -> Void, completion: @escaping (URL?, Error?) -> Void) { + self.progressHandler = progressHandler + self.fileSize = fileSize + self.completion = completion + + + super.init() + + let configuration = URLSessionConfiguration.default + urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main) + + let downloadTask = self.urlSession.downloadTask(with: url) + downloadTask.resume() + } + + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { + var totalBytesExpectedToWrite = totalBytesExpectedToWrite + if totalBytesExpectedToWrite == -1, let fileSize = self.fileSize { + totalBytesExpectedToWrite = fileSize + } + let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite) + progressHandler?(progress) + } + + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + completion?(location, nil) + } + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + if let error = error { + completion?(nil, error) + } + } + + static func getFileSize(url: String) -> Signal { + if #available(iOS 13.0, *) { + guard let url = URL(string: url) else { + return .single(nil) + } + var request = URLRequest(url: url) + request.httpMethod = "HEAD" + + return Signal { subscriber in + let task = URLSession.shared.dataTask(with: request) { _, response, error in + if let _ = error { + subscriber.putNext(nil) + subscriber.putCompletion() + return + } + var fileSize: Int64? + if let httpResponse = response as? HTTPURLResponse, let contentLength = httpResponse.value(forHTTPHeaderField: "Content-Length"), let size = Int64(contentLength) { + fileSize = size + } + subscriber.putNext(fileSize) + subscriber.putCompletion() + } + task.resume() + return ActionDisposable { + task.cancel() + } + } + } else { + return .single(nil) + } + } +} diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 48c4c656e8..fb09575cd7 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -400,7 +400,7 @@ public final class WebAppController: ViewController, AttachmentContainable { controller.dismiss() return } - let _ = (self.context.engine.messages.requestAppWebView(peerId: peer.id, appReference: .id(id: botApp.id, accessHash: botApp.accessHash), payload: appStart.payload, themeParams: generateWebAppThemeParams(self.presentationData.theme), compact: appStart.compact, allowWrite: true) + let _ = (self.context.engine.messages.requestAppWebView(peerId: peer.id, appReference: .id(id: botApp.id, accessHash: botApp.accessHash), payload: appStart.payload, themeParams: generateWebAppThemeParams(self.presentationData.theme), compact: appStart.mode == .compact, fullscreen: appStart.mode == .fullscreen, allowWrite: true) |> deliverOnMainQueue).startStandalone(next: { [weak self] result in guard let self, let parsedUrl = URL(string: result.url) else { return @@ -626,6 +626,69 @@ public final class WebAppController: ViewController, AttachmentContainable { transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: navigationBarHeight))) transition.updateFrame(node: self.topOverscrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -1000.0), size: CGSize(width: layout.size.width, height: 1000.0))) + + var contentTopInset: CGFloat = 0.0 + if controller.isFullscreen { + var added = false + let fullscreenControls: ComponentView + if let current = self.fullscreenControls { + fullscreenControls = current + } else { + fullscreenControls = ComponentView() + self.fullscreenControls = fullscreenControls + added = true + } + let controlsMargin: CGFloat = 8.0 + let componentTransition: ComponentTransition = added ? .immediate : ComponentTransition(transition) + let controlsSize = fullscreenControls.update( + transition: componentTransition, + component: AnyComponent( + FullscreenControlsComponent( + context: self.context, + title: controller.botName, + isVerified: controller.botVerified, + insets: UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: 0.0, right: layout.safeInsets.right), + hasBack: self.hasBackButton, + backPressed: { [weak self] in + guard let self else { + return + } + self.controller?.cancelPressed() + }, + minimizePressed: { [weak self] in + guard let self else { + return + } + self.controller?.requestMinimize(topEdgeOffset: nil, initialVelocity: nil) + }, + morePressed: { [weak self] node, gesture in + guard let self else { + return + } + self.controller?.morePressed(node: node, gesture: gesture) + } + ) + ), + environment: {}, + containerSize: layout.size + ) + if let view = fullscreenControls.view { + if view.superview == nil { + self.view.addSubview(view) + } + transition.updateFrame(view: view, frame: CGRect(origin: CGPoint(x: 0.0, y: (layout.statusBarHeight ?? 0.0) + controlsMargin), size: controlsSize)) + if added { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + contentTopInset = controlsSize.height + controlsMargin * 2.0 + } else if let fullscreenControls = self.fullscreenControls { + self.fullscreenControls = nil + fullscreenControls.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + fullscreenControls.view?.removeFromSuperview() + }) + } + if let webView = self.webView { var scrollInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0) var frameBottomInset: CGFloat = 0.0 @@ -683,6 +746,10 @@ public final class WebAppController: ViewController, AttachmentContainable { if let controller = self.controller { webView.updateMetrics(height: viewportFrame.height, isExpanded: controller.isContainerExpanded(), isStable: !controller.isContainerPanning(), transition: transition) + + let contentInsetsData = "{top:\(contentTopInset), bottom:0.0, left:0.0, right:0.0}" + webView.sendEvent(name: "content_safe_area_changed", data: contentInsetsData) + if self.updateWebViewWhenStable && !controller.isContainerPanning() { self.updateWebViewWhenStable = false webView.setNeedsLayout() @@ -710,66 +777,6 @@ public final class WebAppController: ViewController, AttachmentContainable { placeholderNode.updateAbsoluteRect(placeholderFrame, within: layout.size) } - if controller.isFullscreen { - var added = false - let fullscreenControls: ComponentView - if let current = self.fullscreenControls { - fullscreenControls = current - } else { - fullscreenControls = ComponentView() - self.fullscreenControls = fullscreenControls - added = true - } - - let componentTransition: ComponentTransition = added ? .immediate : ComponentTransition(transition) - let controlsSize = fullscreenControls.update( - transition: componentTransition, - component: AnyComponent( - FullscreenControlsComponent( - context: self.context, - title: controller.botName, - isVerified: controller.botVerified, - insets: UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: 0.0, right: layout.safeInsets.right), - hasBack: self.hasBackButton, - backPressed: { [weak self] in - guard let self else { - return - } - self.controller?.cancelPressed() - }, - minimizePressed: { [weak self] in - guard let self else { - return - } - self.controller?.requestMinimize(topEdgeOffset: nil, initialVelocity: nil) - }, - morePressed: { [weak self] node, gesture in - guard let self else { - return - } - self.controller?.morePressed(node: node, gesture: gesture) - } - ) - ), - environment: {}, - containerSize: layout.size - ) - if let view = fullscreenControls.view { - if view.superview == nil { - self.view.addSubview(view) - } - transition.updateFrame(view: view, frame: CGRect(origin: CGPoint(x: 0.0, y: (layout.statusBarHeight ?? 0.0) + 8.0), size: controlsSize)) - if added { - view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - } - } - } else if let fullscreenControls = self.fullscreenControls { - self.fullscreenControls = nil - fullscreenControls.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in - fullscreenControls.view?.removeFromSuperview() - }) - } - if let previousLayout = previousLayout, (previousLayout.inputHeight ?? 0.0).isZero, let inputHeight = layout.inputHeight, inputHeight > 44.0 { Queue.mainQueue().justDispatch { self.controller?.requestAttachmentMenuExpansion() @@ -891,6 +898,8 @@ public final class WebAppController: ViewController, AttachmentContainable { self.requestLayout(transition: .immediate) case "web_app_request_safe_area": self.requestLayout(transition: .immediate) + case "web_app_request_content_safe_area": + self.requestLayout(transition: .immediate) case "web_app_request_theme": self.sendThemeChangedEvent() case "web_app_expand": @@ -1370,13 +1379,24 @@ public final class WebAppController: ViewController, AttachmentContainable { case "web_app_add_to_home_screen": self.addToHomeScreen() case "web_app_check_home_screen": - self.webView?.sendEvent(name: "home_screen_checked", data: "{status: \"unknown\"}") + let data: JSON = ["status": "unknown"] + self.webView?.sendEvent(name: "home_screen_checked", data: data.string) case "web_app_request_location": self.requestLocation() case "web_app_check_location": self.checkLocation() case "web_app_open_location_settings": break + case "web_app_send_prepared_message": + if let json = json, let id = json["id"] as? String { + self.sendPreparedMessage(id: id) + } + case "web_app_request_emoji_status_access": + self.requestEmojiStatusAccess() + case "web_app_request_file_download": + if let json = json, let url = json["url"] as? String, let fileName = json["file_name"] as? String { + self.downloadFile(url: url, fileName: fileName) + } default: break } @@ -1612,13 +1632,6 @@ public final class WebAppController: ViewController, AttachmentContainable { } fileprivate func shareAccountContact() { - if "".isEmpty, let controller = self.controller { - let previewController = WebAppMessagePreviewScreen(context: controller.context, completion: { _ in }) - previewController.navigationPresentation = .flatModal - controller.parentController()?.push(previewController) - return - } - guard let context = self.controller?.context, let botId = self.controller?.botId, let botName = self.controller?.botName else { return } @@ -2120,68 +2133,147 @@ public final class WebAppController: ViewController, AttachmentContainable { } } - fileprivate func setEmojiStatus(_ fileId: Int64, expirationDate: Int32? = nil) { + fileprivate func sendPreparedMessage(id: String) { guard let controller = self.controller else { return } - - let botName = controller.botName - if let _ = expirationDate { - let _ = combineLatest( - queue: Queue.mainQueue(), - self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]), - self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)), - self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId)) - ).start(next: { [weak self] files, accountPeer, botPeer in - guard let self, let accountPeer, let controller = self.controller else { + let _ = (self.context.engine.messages.getPreparedInlineMessage(botId: controller.botId, id: id) + |> deliverOnMainQueue).start(next: { [weak self, weak controller] preparedMessage in + guard let self, let controller, let preparedMessage else { + self?.webView?.sendEvent(name: "prepared_message_failed", data: "{error: \"MESSAGE_EXPIRED\"}") + return + } + let previewController = WebAppMessagePreviewScreen(context: controller.context, botName: controller.botName, preparedMessage: preparedMessage, completion: { [weak self] result in + guard let self else { return } - guard let file = files[fileId] else { - self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"SUGGESTED_EMOJI_INVALID\"}") - return + if result { + self.webView?.sendEvent(name: "prepared_message_sent", data: nil) + } else { + self.webView?.sendEvent(name: "prepared_message_failed", data: "{error: \"USER_DECLINED\"}") } - let confirmController = WebAppSetEmojiStatusScreen( - context: self.context, - botName: controller.botName, - accountPeer: accountPeer, - file: file, - completion: { [weak self, weak controller] result in - guard let self else { - return - } - if result { - let _ = (self.context.engine.accountData.setEmojiStatus(file: file, expirationDate: expirationDate) - |> deliverOnMainQueue).start(completed: { [weak self] in - self?.webView?.sendEvent(name: "emoji_status_set", data: nil) - }) - //TODO:localize - let resultController = UndoOverlayController( - presentationData: self.presentationData, - content: .sticker(context: context, file: file, loop: false, title: nil, text: "Your emoji status updated.", undoText: nil, customAction: nil), - elevatedLayout: true, - action: { action in - if case .undo = action { - - } - return true - } - ) - controller?.present(resultController, in: .window(.root)) - } else { - self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"USER_DECLINED\"}") - } - } - ) - controller.parentController()?.push(confirmController) }) - - + previewController.navigationPresentation = .flatModal + controller.parentController()?.push(previewController) + }) + } + + fileprivate func downloadFile(url: String, fileName: String) { + guard let controller = self.controller else { return } + var title: String? + let photoExtensions = [".jpg", ".png", ".gif", ".tiff"] + let videoExtensions = [".mp4", ".mov"] + let lowercasedFilename = fileName.lowercased() + for ext in photoExtensions { + if lowercasedFilename.hasSuffix(ext) { + title = "Download Photo" + break + } + } + if title == nil { + for ext in videoExtensions { + if lowercasedFilename.hasSuffix(ext) { + title = "Download Video" + break + } + } + } + if title == nil { + title = "Download Document" + } + let _ = (FileDownload.getFileSize(url: url) + |> deliverOnMainQueue).start(next: { [weak self] fileSize in + guard let self else { + return + } + + var fileSizeString = "" + if let fileSize { + fileSizeString = " (\(dataSizeString(fileSize, formatting: DataSizeStringFormatting(presentationData: self.presentationData))))" + } + + let text: String = "**\(controller.botName)** suggests you to download **\(fileName)**\(fileSizeString)." + let alertController = standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: title, text: text, actions: [ + TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { [weak self] in + self?.webView?.sendEvent(name: "file_download_requested", data: "{status: \"cancelled\"}") + }), + TextAlertAction(type: .defaultAction, title: "Download", action: { [weak self] in + self?.startDownload(url: url, fileName: fileName, fileSize: fileSize) + }) + ], parseMarkdown: true) + controller.present(alertController, in: .window(.root)) + }) + } + + private var fileDownload: FileDownload? + private weak var fileDownloadTooltip: UndoOverlayController? + fileprivate func startDownload(url: String, fileName: String, fileSize: Int64?) { + guard let controller = self.controller else { + return + } + self.webView?.sendEvent(name: "file_download_requested", data: "{status: \"downloading\"}") + + self.fileDownload = FileDownload( + from: URL(string: url)!, + fileSize: fileSize, + progressHandler: { [weak self] progress in + guard let self else { + return + } + let text: String + if let fileSize { + let downloadedSize = Int64(Double(fileSize) * progress) + text = "\(dataSizeString(downloadedSize, formatting: DataSizeStringFormatting(presentationData: self.presentationData))) / \(dataSizeString(fileSize, formatting: DataSizeStringFormatting(presentationData: self.presentationData)))" + } else { + text = "\(Int32(progress))%" + } + + self.fileDownloadTooltip?.content = .progress( + progress: progress, + title: fileName, + text: text, + undoText: "Cancel" + ) + }, + completion: { resultUrl, _ in + + } + ) + + let text: String + if let fileSize { + text = "0 KB / \(dataSizeString(fileSize, formatting: DataSizeStringFormatting(presentationData: self.presentationData)))" + } else { + text = "0%" + } + + let tooltipController = UndoOverlayController( + presentationData: self.presentationData, + content: .progress( + progress: 0.0, + title: fileName, + text: text, + undoText: "Cancel" + ), + elevatedLayout: false, + position: .top, + action: { _ in + return true + } + ) + controller.present(tooltipController, in: .window(.root)) + self.fileDownloadTooltip = tooltipController + } + + fileprivate func requestEmojiStatusAccess() { + guard let controller = self.controller else { + return + } let _ = combineLatest( queue: Queue.mainQueue(), - self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]), self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)), self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId)), self.context.engine.stickers.loadedStickerPack(reference: .iconStatusEmoji, forceActualized: false) @@ -2194,7 +2286,62 @@ public final class WebAppController: ViewController, AttachmentContainable { } } |> take(1) - ).start(next: { [weak self] files, accountPeer, botPeer, iconStatusEmoji in + ).start(next: { [weak self] accountPeer, botPeer, iconStatusEmoji in + guard let self, let accountPeer, let controller = self.controller else { + return + } + let alertController = webAppEmojiStatusAlertController( + context: self.context, + accountPeer: accountPeer, + botName: controller.botName, + icons: iconStatusEmoji, + completion: { [weak self] result in + guard let self, let controller = self.controller else { + return + } + if result { + let context = self.context + let botId = controller.botId + let _ = (context.engine.peers.toggleBotEmojiStatusAccess(peerId: botId, enabled: true) + |> deliverOnMainQueue).startStandalone(completed: { [weak self] in + self?.webView?.sendEvent(name: "emoji_status_access_requested", data: "{status: \"allowed\"}") + }) + + //TODO:localize + if let botPeer { + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .invitedToVoiceChat(context: self.context, peer: botPeer, title: nil, text: "**\(controller.botName)** can now set your emoji status anytime.", action: "Undo", duration: 5.0), + elevatedLayout: true, + action: { action in + if case .undo = action { + let _ = (context.engine.peers.toggleBotEmojiStatusAccess(peerId: botId, enabled: false) + |> deliverOnMainQueue).startStandalone() + } + return true + } + ) + controller.present(resultController, in: .window(.root)) + } + } else { + self.webView?.sendEvent(name: "emoji_status_access_requested", data: "{status: \"cancelled\"}") + } + } + ) + controller.present(alertController, in: .window(.root)) + }) + } + + fileprivate func setEmojiStatus(_ fileId: Int64, expirationDate: Int32? = nil) { + guard let controller = self.controller else { + return + } + let _ = combineLatest( + queue: Queue.mainQueue(), + self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]), + self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)), + self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId)) + ).start(next: { [weak self] files, accountPeer, botPeer in guard let self, let accountPeer, let controller = self.controller else { return } @@ -2202,12 +2349,11 @@ public final class WebAppController: ViewController, AttachmentContainable { self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"SUGGESTED_EMOJI_INVALID\"}") return } - let alertController = webAppEmojiStatusAlertController( + let confirmController = WebAppSetEmojiStatusScreen( context: self.context, + botName: controller.botName, accountPeer: accountPeer, - botName: botName, - icons: iconStatusEmoji, - expirationDate: expirationDate, + file: file, completion: { [weak self, weak controller] result in guard let self else { return @@ -2218,26 +2364,24 @@ public final class WebAppController: ViewController, AttachmentContainable { self?.webView?.sendEvent(name: "emoji_status_set", data: nil) }) //TODO:localize - if let botPeer { - let resultController = UndoOverlayController( - presentationData: presentationData, - content: .invitedToVoiceChat(context: context, peer: botPeer, title: nil, text: "**\(botName)** can now set your emoji status anytime.", action: "Undo", duration: 5.0), - elevatedLayout: true, - action: { action in - if case .undo = action { - - } - return true + let resultController = UndoOverlayController( + presentationData: self.presentationData, + content: .sticker(context: context, file: file, loop: false, title: nil, text: "Your emoji status updated.", undoText: nil, customAction: nil), + elevatedLayout: true, + action: { action in + if case .undo = action { + } - ) - controller?.present(resultController, in: .window(.root)) - } + return true + } + ) + controller?.present(resultController, in: .window(.root)) } else { self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"USER_DECLINED\"}") } } ) - controller.present(alertController, in: .window(.root)) + controller.parentController()?.push(confirmController) }) } diff --git a/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift b/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift index 9bbdd20916..b0ada96f4a 100644 --- a/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift +++ b/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift @@ -332,7 +332,6 @@ func webAppEmojiStatusAlertController( accountPeer: EnginePeer, botName: String, icons: [TelegramMediaFile], - expirationDate: Int32?, completion: @escaping (Bool) -> Void ) -> AlertController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift index 39af0f6031..99994bedee 100644 --- a/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift +++ b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift @@ -26,13 +26,19 @@ private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let botName: String + let preparedMessage: PreparedInlineMessage let dismiss: () -> Void init( context: AccountContext, + botName: String, + preparedMessage: PreparedInlineMessage, dismiss: @escaping () -> Void ) { self.context = context + self.botName = botName + self.preparedMessage = preparedMessage self.dismiss = dismiss } @@ -225,11 +231,17 @@ private final class WebAppMessagePreviewSheetComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment private let context: AccountContext + private let botName: String + private let preparedMessage: PreparedInlineMessage init( - context: AccountContext + context: AccountContext, + botName: String, + preparedMessage: PreparedInlineMessage ) { self.context = context + self.botName = botName + self.preparedMessage = preparedMessage } static func ==(lhs: WebAppMessagePreviewSheetComponent, rhs: WebAppMessagePreviewSheetComponent) -> Bool { @@ -252,6 +264,8 @@ private final class WebAppMessagePreviewSheetComponent: CombinedComponent { component: SheetComponent( content: AnyComponent(SheetContent( context: context.component.context, + botName: context.component.botName, + preparedMessage: context.component.preparedMessage, dismiss: { animateOut.invoke(Action { _ in if let controller = controller() { @@ -307,6 +321,8 @@ public final class WebAppMessagePreviewScreen: ViewControllerComponentContainer public init( context: AccountContext, + botName: String, + preparedMessage: PreparedInlineMessage, completion: @escaping (Bool) -> Void ) { self.context = context @@ -315,7 +331,9 @@ public final class WebAppMessagePreviewScreen: ViewControllerComponentContainer super.init( context: context, component: WebAppMessagePreviewSheetComponent( - context: context + context: context, + botName: botName, + preparedMessage: preparedMessage ), navigationBarAppearance: .none, statusBarStyle: .ignore,