Merge commit '85b6a8ffe96bf8a5b1c7c5b5180024bc07770ba2'

This commit is contained in:
Isaac 2024-11-08 16:29:59 +01:00
commit 98433a4a6f
92 changed files with 5311 additions and 880 deletions

View File

@ -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";

View File

@ -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<MiniAppListScreenInitialData, NoError>
func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController
func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?)
func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?)
func makeDebugSettingsController(context: AccountContext?) -> ViewController?

View File

@ -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
}
}

View File

@ -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

View File

@ -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

View File

@ -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)
@ -1457,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))
}

View File

@ -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: "",

View File

@ -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
}

View File

@ -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;

View File

@ -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)

View File

@ -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;

View File

@ -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)

View File

@ -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) }
@ -487,7 +488,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 +900,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 +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[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) }
@ -1265,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) }
@ -1306,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) }
@ -1510,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:
@ -2296,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:
@ -2354,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:

View File

@ -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
}
}
}
}

View File

@ -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

View File

@ -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
}
}
}
}

View File

@ -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

View File

@ -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, 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)
@ -969,6 +970,16 @@ public extension Api {
stopped.serialize(buffer, true)
serializeInt32(qts, buffer: buffer, boxed: false)
break
case .updateBotSubscriptionExpire(let userId, let payload, let invoiceSlug, let untilDate, let qts):
if boxed {
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
case .updateBotWebhookJSON(let data):
if boxed {
buffer.appendInt32(-2095595325)
@ -2041,6 +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 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):
@ -2732,6 +2745,29 @@ 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: 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
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
}
}
public static func parse_updateBotWebhookJSON(_ reader: BufferReader) -> Update? {
var _1: Api.DataJSON?
if let signature = reader.readInt32() {

View File

@ -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?)

View File

@ -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
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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]?)

View File

@ -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<Api.Bool>) {
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<Api.Bool>) {
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<Api.Bool>) {
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<Api.Bool>) {
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<Api.messages.PreparedInlineMessage>) {
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<Api.messages.QuickReplies>) {
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<Api.messages.BotPreparedInlineMessage>) {
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<Api.Bool>) {
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<Api.Bool>) {
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<Api.Bool>) {
let buffer = Buffer()

View File

@ -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) })
}
}
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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, _):

View File

@ -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

View File

@ -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! {

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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
@ -114,8 +115,9 @@ public final class TelegramMediaInvoice: Media, Equatable {
self.currency = currency
self.totalAmount = totalAmount
self.startParam = startParam
self.flags = flags
self.extendedMedia = extendedMedia
self.subscriptionPeriod = subscriptionPeriod
self.flags = flags
self.version = version
}
@ -126,8 +128,9 @@ 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.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? {
self.receiptMessageId = MessageId(peerId: PeerId(receiptMessageIdPeerId), namespace: receiptMessageIdNamespace, id: receiptMessageIdId)
@ -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
)

View File

@ -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

View File

@ -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))
}
}
@ -64,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<RequestWebViewResult, RequestWebViewError> {
func _internal_requestMainWebView(postbox: Postbox, network: Network, peerId: PeerId, botId: PeerId, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal<RequestWebViewResult, RequestWebViewError> {
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)
@ -73,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)
}
@ -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<Never, KeepWebViewError>?
if let queryId {
keepAlive = keepWebViewSignal(network: network, stateManager: stateManager, flags: flags, peer: inputPeer, bot: inputBot, queryId: queryId, replyToMessageId: replyToMessageId, threadId: threadId, sendAs: nil)
@ -276,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<RequestWebViewResult, RequestWebViewError> {
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<RequestWebViewResult, RequestWebViewError> {
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)
@ -311,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
@ -323,6 +336,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))
}
}

View File

@ -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<PreparedInlineMessage?, NoError> {
return account.postbox.transaction { transaction -> Api.InputUser? in
return transaction.getPeer(botId).flatMap(apiInputUser)
}
|> mapToSignal { inputBot -> Signal<PreparedInlineMessage?, NoError> 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<Api.messages.PreparedInlineMessage?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<PreparedInlineMessage?, NoError> 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) })
}
}
}
}
}

View File

@ -403,6 +403,10 @@ public extension TelegramEngine {
public func messageReadStats(id: MessageId) -> Signal<MessageReadStats?, NoError> {
return _internal_messageReadStats(account: self.account, id: id)
}
public func getPreparedInlineMessage(botId: EnginePeer.Id, id: String) -> Signal<PreparedInlineMessage?, NoError> {
return _internal_getPreparedInlineMessage(account: self.account, botId: botId, id: id)
}
public func requestCancelLiveLocation(ids: [MessageId]) -> Signal<Never, NoError> {
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<RequestWebViewResult, RequestWebViewError> {
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<RequestWebViewResult, RequestWebViewError> {
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<RequestWebViewResult, RequestWebViewError> {
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<RequestWebViewResult, RequestWebViewError> {
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<Never, SendWebViewDataError> {

View File

@ -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)
}
}
}
@ -400,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 }
@ -798,6 +800,7 @@ func _internal_requestBotPaymentReceipt(account: Account, messageId: MessageId)
totalAmount: totalAmount,
startParam: "",
extendedMedia: nil,
subscriptionPeriod: parsedInvoice.subscriptionPeriod,
flags: [],
version: TelegramMediaInvoice.lastVersion
)
@ -820,6 +823,7 @@ func _internal_requestBotPaymentReceipt(account: Account, messageId: MessageId)
totalAmount: totalAmount,
startParam: "",
extendedMedia: nil,
subscriptionPeriod: parsedInvoice.subscriptionPeriod,
flags: [],
version: TelegramMediaInvoice.lastVersion
)

View File

@ -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)

View File

@ -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<Never, ToggleBotEmojiStatusAccessError> {
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<Void, UpdatePeerNameColorAndEmojiError> {
return _internal_updatePeerNameColorAndEmoji(account: self.account, peerId: peerId, nameColor: nameColor, backgroundEmojiId: backgroundEmojiId, profileColor: profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId)
}

View File

@ -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<Never, ToggleBotEmojiStatusAccessError> {
return account.postbox.transaction { transaction -> Signal<Void, ToggleBotEmojiStatusAccessError> 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<Void, ToggleBotEmojiStatusAccessError> 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
}

View File

@ -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)

View File

@ -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))

View File

@ -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..<endIndex])
} else {
balanceText = String(balanceText[balanceText.startIndex..<balanceText.endIndex])
}
let integerPartString = balanceText[..<dotIndex.lowerBound]
if let integerPart = Int32(integerPartString) {

View File

@ -1283,14 +1283,14 @@ 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(
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,

View File

@ -44,6 +44,7 @@ swift_library(
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
"//submodules/InAppPurchaseManager",
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/ProgressNavigationButtonNode",
],
visibility = [
"//visibility:public",

View File

@ -30,6 +30,7 @@ import AudioToolbox
import TextFormat
import InAppPurchaseManager
import BlurredBackgroundComponent
import ProgressNavigationButtonNode
final class GiftSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -203,7 +204,7 @@ final class GiftSetupScreenComponent: Component {
self.buttonSeparator.opacity = Float(bottomPanelAlpha)
}
func proceed() {
@objc private func proceed() {
guard let component = self.component else {
return
}
@ -215,7 +216,7 @@ final class GiftSetupScreenComponent: Component {
}
}
func proceedWithPremiumGift() {
private func proceedWithPremiumGift() {
guard let component = self.component, case let .premium(product) = component.subject, let storeProduct = product.storeProduct, let inAppPurchaseManager = component.context.inAppPurchaseManager else {
return
}
@ -304,7 +305,7 @@ final class GiftSetupScreenComponent: Component {
})
}
func proceedWithStarGift() {
private func proceedWithStarGift() {
guard let component = self.component, case let .starGift(starGift) = component.subject, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else {
return
}
@ -450,6 +451,8 @@ final class GiftSetupScreenComponent: Component {
self.isUpdating = false
}
let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? ""
if self.component == nil {
let _ = (component.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId),
@ -588,26 +591,16 @@ final class GiftSetupScreenComponent: Component {
self.component = component
self.state = state
let alphaTransition: ComponentTransition
if !transition.animation.isImmediate {
alphaTransition = .easeInOut(duration: 0.25)
} else {
alphaTransition = .immediate
}
if themeUpdated {
self.backgroundColor = environment.theme.list.blocksBackgroundColor
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let _ = alphaTransition
let _ = presentationData
let navigationTitleSize = self.navigationTitle.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.Gift_Send_Title, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
text: .plain(NSAttributedString(string: environment.strings.Gift_Send_TitleTo(peerName).string, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center
)),
environment: {},
@ -720,7 +713,6 @@ final class GiftSetupScreenComponent: Component {
))))
self.resetText = nil
let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? ""
let introFooter: AnyComponent<Empty>?
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

View File

@ -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 {

View File

@ -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

View File

@ -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<WebAppPermissionsState?, NoError> = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<WebAppPermissionsState?, NoError> 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
))
}
}

View File

@ -1209,6 +1209,7 @@ private enum InfoSection: Int, CaseIterable {
case personalChannel
case peerInfo
case balances
case permissions
case peerInfoTrailing
case peerMembers
}
@ -1380,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: "",
@ -1540,7 +1542,28 @@ 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"))
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))
}.startStandalone()
}))
}
}
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()

View File

@ -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))

View File

@ -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 {

View File

@ -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<Empty>()
private let amountView = ComponentView<Empty>()
private var animationNode: AnimatedStickerNode?
private var lockView: UIImageView?
private var countView = ComponentView<Empty>()
private let countView = ComponentView<Empty>()
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)

View File

@ -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))

View File

@ -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<Empty>
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)),

View File

@ -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)
@ -303,8 +306,15 @@ private final class SheetContent: CombinedComponent {
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 let _ = component.invoice.subscriptionPeriod {
isSubscription = true
}
let star = star.update(
@ -314,7 +324,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 +361,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 +380,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 +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 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
@ -499,7 +534,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 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)
}

View File

@ -30,7 +30,6 @@ swift_library(
"//submodules/MediaPickerUI",
"//submodules/LegacyMediaPickerUI",
"//submodules/LocationUI",
"//submodules/WebUI",
"//submodules/TelegramUI/Components/ChatScheduleTimeController",
"//submodules/TelegramUI/Components/ChatTimerScreen",
"//submodules/TextFormat",

View File

@ -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<PresentationData, NoError>) = (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<PresentationData, NoError>) = (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
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "location.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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 Üû^Œ°å<C2B0>ìcÌ4³H74}è’ã'-Jé©TÒ³ó1Ý௡ל<C397>ë3$ʳ' »ŒZ2ÀLàrü¹¾<C2B9>Æsˆ˜ÉÔˆZ™I8•.(ÂoDäJ}¤Ö”tž­Fî”Ef)ñÑZjê#²P<C2B2>m!j©ÎµÞ&,Ì©÷È“¸Hª ¥<>JžµŽX¬™Ëìv*gb|9‡¶æÞÙGpûκÿƒ³N?Çfü@žµ׹¡þ®ãg>ÃY}Ï1<E280BA>³ocës?{ÍÁø½`¿´y °_îÚƒù·^nß(wo^Ë6ÉG0ïÆ™uc¶óÆ}ÜÆÍîùNíÇë- £èkW+·ã|úl<C3BA>ð?|„¯ð/|ã¨#‡ðâ7wRàLÍñ‰Ð;iÙW wÒj¶ãJN &¹èBÀ(—MQ(ã÷vPY7¢<37>1¶S¯¬*s#Þ‚*³KœÂA¤Ò <C392>Kn¤—sêèk¤ùk8˜¬b;ÅHíÇ<C3AD>¹°áÍ­äÖ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

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "status (3).pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "miniappminimize_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "miniappverify_14.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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":[]}

View File

@ -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":[]}

View File

@ -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))
}

View File

@ -14,7 +14,20 @@ import UndoUI
import UrlHandling
import TelegramPresentationData
func openWebAppImpl(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) {
func openWebAppImpl(
context: AccountContext,
parentController: ViewController,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
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
}
@ -103,18 +116,24 @@ 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: 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, {
@ -154,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
@ -174,9 +193,14 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u
}
let webViewSignal: Signal<RequestWebViewResult, RequestWebViewError>
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))
}
@ -196,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))
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 {
@ -233,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()
})
@ -242,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))
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
@ -279,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
}
@ -313,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 {
@ -407,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
}
@ -463,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()
})
@ -472,7 +496,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
@ -564,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)
}
}
}

View File

@ -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)
@ -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:

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -301,14 +301,14 @@ 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(
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,

View File

@ -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<PresentationData, NoError>)?, 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<PresentationData, NoError>)?, 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)
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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<ResolveInternalUrlResult, NoError> 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))))
}

View File

@ -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",

View File

@ -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<Int64?, NoError> {
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)
}
}
}

View File

@ -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<Empty>()
private let leftButton = HighlightTrackingButton()
private let titleClippingView = UIView()
private let title = ComponentView<Empty>()
private let credibility = ComponentView<Empty>()
private let buttonTitle = ComponentView<Empty>()
private let minimizeButton = ComponentView<Empty>()
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<Empty>, 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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -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<Never, KeepWebViewError>?
let forceHasSettings: Bool
let fullSize: Bool
let isFullscreen: Bool
public init(
source: Source,
@ -77,7 +81,8 @@ public struct WebAppParameters {
buttonText: String?,
keepAliveSignal: Signal<Never, KeepWebViewError>?,
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<Empty>?
fileprivate let loadingProgressPromise = Promise<CGFloat?>(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() {
@ -380,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
@ -593,11 +613,82 @@ 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)))
var contentTopInset: CGFloat = 0.0
if controller.isFullscreen {
var added = false
let fullscreenControls: ComponentView<Empty>
if let current = self.fullscreenControls {
fullscreenControls = current
} else {
fullscreenControls = ComponentView<Empty>()
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
@ -606,8 +697,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 +708,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,30 +721,40 @@ 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)
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()
}
}
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 {
@ -691,6 +794,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 +895,11 @@ 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_content_safe_area":
self.requestLayout(transition: .immediate)
case "web_app_request_theme":
self.sendThemeChangedEvent()
case "web_app_expand":
@ -939,7 +1050,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 +1349,54 @@ 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":
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
}
@ -1469,11 +1632,9 @@ 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 {
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 +1645,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 +2018,523 @@ 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 sendPreparedMessage(id: String) {
guard let controller = self.controller else {
return
}
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
}
if result {
self.webView?.sendEvent(name: "prepared_message_sent", data: nil)
} else {
self.webView?.sendEvent(name: "prepared_message_failed", data: "{error: \"USER_DECLINED\"}")
}
})
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.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] 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
}
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)
})
}
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<CLLocation, NoError> { 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 +2549,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 +2559,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
private let keepAliveSignal: Signal<Never, KeepWebViewError>?
private let replyToMessageId: MessageId?
private let threadId: Int64?
public var isFullscreen: Bool
private var presentationData: PresentationData
fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
@ -1909,6 +2587,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 +2606,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 +2703,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 +2713,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 +2852,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 +2960,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 +2970,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

View File

@ -0,0 +1,364 @@
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],
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
}

View File

@ -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
}

View File

@ -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<Void, NoError>?, (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<PeerId, Peer>()
var messages = SimpleDictionary<MessageId, Message>()
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)
}
}

View File

@ -0,0 +1,355 @@
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 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
}
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
private let botName: String
private let preparedMessage: PreparedInlineMessage
init(
context: AccountContext,
botName: String,
preparedMessage: PreparedInlineMessage
) {
self.context = context
self.botName = botName
self.preparedMessage = preparedMessage
}
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<Void>.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(SheetContent(
context: context.component.context,
botName: context.component.botName,
preparedMessage: context.component.preparedMessage,
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,
botName: String,
preparedMessage: PreparedInlineMessage,
completion: @escaping (Bool) -> Void
) {
self.context = context
self.completion = completion
super.init(
context: context,
component: WebAppMessagePreviewSheetComponent(
context: context,
botName: botName,
preparedMessage: preparedMessage
),
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<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}

View File

@ -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<WebAppPermissionsState?, NoError> {
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<Never, NoError> {
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<Never, NoError> {
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<Never, NoError> in
return updateWebAppPermissionsState(context: context, peerId: peerId, state: f(current))
}
}

View File

@ -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<Void>.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(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<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.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()
})
}

View File

@ -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?