diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index c9394f3a0b..67774b1788 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7407,3 +7407,6 @@ Sorry for the inconvenience."; "WebApp.AddToAttachmentText" = "%@ asks your permission to be added as an option to your attachments menu so you access it from any chat."; "WebApp.AddToAttachmentAdd" = "Add"; + +"WebApp.AddToAttachmentAlreadyAddedError" = "This bot is already added in the attachment menu."; + diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 8075aa471b..7670f41087 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -188,10 +188,10 @@ public struct ResolvedBotAdminRights: OptionSet { public static let pinMessages = ResolvedBotAdminRights(rawValue: 128) public static let promoteMembers = ResolvedBotAdminRights(rawValue: 256) public static let manageVideoChats = ResolvedBotAdminRights(rawValue: 512) - public static let manageChat = ResolvedBotAdminRights(rawValue: 1024) - public static let canBeAnonymous = ResolvedBotAdminRights(rawValue: 2048) + public static let canBeAnonymous = ResolvedBotAdminRights(rawValue: 1024) + public static let manageChat = ResolvedBotAdminRights(rawValue: 2048) - public var chatAdminRights: TelegramChatAdminRightsFlags { + public var chatAdminRights: TelegramChatAdminRightsFlags? { var flags = TelegramChatAdminRightsFlags() if self.contains(ResolvedBotAdminRights.changeInfo) { @@ -224,6 +224,11 @@ public struct ResolvedBotAdminRights: OptionSet { if self.contains(ResolvedBotAdminRights.canBeAnonymous) { flags.insert(.canBeAnonymous) } + + if flags.isEmpty && !self.contains(ResolvedBotAdminRights.manageChat) { + return nil + } + return flags } } @@ -253,6 +258,7 @@ public enum ResolvedUrl { case settings(ResolvedUrlSettingsSection) case joinVoiceChat(PeerId, String?) case importStickers + case setAttach(PeerId) } public enum NavigateToChatKeepStack { diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 3d6ea10739..69384f5683 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -17,7 +17,7 @@ public enum AttachmentButtonType: Equatable { case location case contact case poll - case app(String) + case app(PeerId, String, TelegramMediaFile?) } public protocol AttachmentContainable: ViewController { @@ -97,7 +97,7 @@ public class AttachmentController: ViewController { private let context: AccountContext private let updatedPresentationData: (initial: PresentationData, signal: Signal)? private let chatLocation: ChatLocation - private let buttons: [AttachmentButtonType] + private var buttons: [AttachmentButtonType] public var mediaPickerContext: AttachmentMediaPickerContext? { get { @@ -564,11 +564,13 @@ public class AttachmentController: ViewController { completion(nil, nil) } - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, chatLocation: ChatLocation, buttons: [AttachmentButtonType]) { + private var buttonsDisposable: Disposable? + + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, chatLocation: ChatLocation, buttons: Signal<[AttachmentButtonType], NoError>) { self.context = context self.updatedPresentationData = updatedPresentationData self.chatLocation = chatLocation - self.buttons = buttons + self.buttons = [] super.init(navigationBarPresentationData: nil) @@ -581,10 +583,21 @@ public class AttachmentController: ViewController { strongSelf.node.scrollToTop() } } + + self.buttonsDisposable = (buttons + |> deliverOnMainQueue).start(next: { [weak self] buttons in + if let strongSelf = self { + let previousButtons = strongSelf.buttons + strongSelf.buttons = buttons + if let layout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, transition: !previousButtons.isEmpty ? .animated(duration: 0.2, curve: .easeInOut) : .immediate) + } + } + }) } deinit { - print() + self.buttonsDisposable?.dispose() } public required init(coder aDecoder: NSCoder) { @@ -621,9 +634,12 @@ public class AttachmentController: ViewController { return false } + private var validLayout: ContainerViewLayout? + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - + + self.validLayout = layout self.node.containerLayoutUpdated(layout, transition: transition) } } diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 3437cd0aa0..0eedc58c0c 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -88,7 +88,7 @@ private final class AttachButtonComponent: CombinedComponent { case .poll: name = strings.Attachment_Poll imageName = "Chat/Attach Menu/Poll" - case let .app(appName): + case let .app(_, appName, _): name = appName imageName = "Chat List/Tabs/IconSettings" } diff --git a/submodules/CounterContollerTitleView/Sources/CounterContollerTitleView.swift b/submodules/CounterContollerTitleView/Sources/CounterContollerTitleView.swift index 3e6872815e..455947e152 100644 --- a/submodules/CounterContollerTitleView/Sources/CounterContollerTitleView.swift +++ b/submodules/CounterContollerTitleView/Sources/CounterContollerTitleView.swift @@ -15,24 +15,33 @@ public struct CounterContollerTitle: Equatable { } public final class CounterContollerTitleView: UIView { - private var theme: PresentationTheme private let titleNode: ImmediateTextNode private let subtitleNode: ImmediateTextNode public var title: CounterContollerTitle = CounterContollerTitle(title: "", counter: "") { didSet { if self.title != oldValue { - self.titleNode.attributedText = NSAttributedString(string: self.title.title, font: Font.semibold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) - self.subtitleNode.attributedText = NSAttributedString(string: self.title.counter, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) - - self.accessibilityLabel = self.title.title - self.accessibilityValue = self.title.counter - - self.setNeedsLayout() + self.update() } } } + public var theme: PresentationTheme { + didSet { + self.update() + } + } + + private func update() { + self.titleNode.attributedText = NSAttributedString(string: self.title.title, font: Font.semibold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + self.subtitleNode.attributedText = NSAttributedString(string: self.title.counter, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + + self.accessibilityLabel = self.title.title + self.accessibilityValue = self.title.counter + + self.setNeedsLayout() + } + public init(theme: PresentationTheme) { self.theme = theme diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 107a679804..923fbcc0a1 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -292,6 +292,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1885586395] = { return Api.Update.parse_updatePendingJoinRequests($0) } dict[299870598] = { return Api.Update.parse_updateBotChatInviteRequester($0) } dict[357013699] = { return Api.Update.parse_updateMessageReactions($0) } + dict[397910539] = { return Api.Update.parse_updateAttachMenuBots($0) } dict[1951948721] = { return Api.Update.parse_updateReadFeed($0) } dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) } dict[1558266229] = { return Api.PopularContact.parse_popularContact($0) } @@ -323,6 +324,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1144565411] = { return Api.KeyboardButton.parse_keyboardButtonRequestPoll($0) } dict[-376962181] = { return Api.KeyboardButton.parse_inputKeyboardButtonUserProfile($0) } dict[814112961] = { return Api.KeyboardButton.parse_keyboardButtonUserProfile($0) } + dict[326529584] = { return Api.KeyboardButton.parse_keyboardButtonWebView($0) } dict[383348795] = { return Api.ContactStatus.parse_contactStatus($0) } dict[997004590] = { return Api.users.UserFull.parse_userFull($0) } dict[1679398724] = { return Api.SecureFile.parse_secureFileEmpty($0) } @@ -391,6 +393,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1798033689] = { return Api.ChannelMessagesFilter.parse_channelMessagesFilterEmpty($0) } dict[-847783593] = { return Api.ChannelMessagesFilter.parse_channelMessagesFilter($0) } dict[-219353309] = { return Api.ChatAdminWithInvites.parse_chatAdminWithInvites($0) } + dict[-729926056] = { return Api.AttachMenuBot.parse_attachMenuBot($0) } dict[2004110666] = { return Api.DialogFilterSuggested.parse_dialogFilterSuggested($0) } dict[326715557] = { return Api.auth.PasswordRecovery.parse_passwordRecovery($0) } dict[-1803769784] = { return Api.messages.BotResults.parse_botResults($0) } @@ -425,6 +428,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[341499403] = { return Api.Contact.parse_contact($0) } dict[-1078332329] = { return Api.help.PassportConfig.parse_passportConfigNotModified($0) } dict[-1600596305] = { return Api.help.PassportConfig.parse_passportConfig($0) } + dict[202659196] = { return Api.WebViewResult.parse_webViewResultUrl($0) } + dict[-1312107643] = { return Api.WebViewResult.parse_webViewResultConfirmationRequired($0) } dict[1648543603] = { return Api.FileHash.parse_fileHash($0) } dict[295067450] = { return Api.BotInlineResult.parse_botInlineResult($0) } dict[400266251] = { return Api.BotInlineResult.parse_botInlineMediaResult($0) } @@ -844,6 +849,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1462213465] = { return Api.InputBotInlineResult.parse_inputBotInlineResultPhoto($0) } dict[-459324] = { return Api.InputBotInlineResult.parse_inputBotInlineResultDocument($0) } dict[1336154098] = { return Api.InputBotInlineResult.parse_inputBotInlineResultGame($0) } + dict[-237467044] = { return Api.AttachMenuBots.parse_attachMenuBotsNotModified($0) } + dict[1011024320] = { return Api.AttachMenuBots.parse_attachMenuBots($0) } dict[1352683077] = { return Api.account.PrivacyRules.parse_privacyRules($0) } dict[-123988] = { return Api.PrivacyRule.parse_privacyValueAllowContacts($0) } dict[1698855810] = { return Api.PrivacyRule.parse_privacyValueAllowAll($0) } @@ -962,6 +969,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[512177195] = { return Api.Document.parse_document($0) } dict[-1707344487] = { return Api.messages.HighScores.parse_highScores($0) } dict[-1493633966] = { return Api.WebAuthorization.parse_webAuthorization($0) } + dict[-1428220517] = { return Api.messages.WebViewResult.parse_webViewResult($0) } dict[-1052885936] = { return Api.ImportedContact.parse_importedContact($0) } dict[1042605427] = { return Api.payments.BankCardData.parse_bankCardData($0) } return dict @@ -1223,6 +1231,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.ChatAdminWithInvites: _1.serialize(buffer, boxed) + case let _1 as Api.AttachMenuBot: + _1.serialize(buffer, boxed) case let _1 as Api.DialogFilterSuggested: _1.serialize(buffer, boxed) case let _1 as Api.auth.PasswordRecovery: @@ -1247,6 +1257,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.help.PassportConfig: _1.serialize(buffer, boxed) + case let _1 as Api.WebViewResult: + _1.serialize(buffer, boxed) case let _1 as Api.FileHash: _1.serialize(buffer, boxed) case let _1 as Api.BotInlineResult: @@ -1629,6 +1641,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.InputBotInlineResult: _1.serialize(buffer, boxed) + case let _1 as Api.AttachMenuBots: + _1.serialize(buffer, boxed) case let _1 as Api.account.PrivacyRules: _1.serialize(buffer, boxed) case let _1 as Api.PrivacyRule: @@ -1699,6 +1713,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.WebAuthorization: _1.serialize(buffer, boxed) + case let _1 as Api.messages.WebViewResult: + _1.serialize(buffer, boxed) case let _1 as Api.ImportedContact: _1.serialize(buffer, boxed) case let _1 as Api.payments.BankCardData: diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index 0773d24fad..fb914b0348 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -2702,5 +2702,51 @@ public struct messages { } } + public enum WebViewResult: TypeConstructorDescription { + case webViewResult(result: Api.BotInlineResult, users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .webViewResult(let result, let users): + if boxed { + buffer.appendInt32(-1428220517) + } + result.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 .webViewResult(let result, let users): + return ("webViewResult", [("result", result), ("users", users)]) + } + } + + public static func parse_webViewResult(_ reader: BufferReader) -> WebViewResult? { + var _1: Api.BotInlineResult? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.BotInlineResult + } + var _2: [Api.User]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.WebViewResult.webViewResult(result: _1!, users: _2!) + } + else { + return nil + } + } + + } } } diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index 233faf6382..41496ba101 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -4936,6 +4936,7 @@ public extension Api { case updatePendingJoinRequests(peer: Api.Peer, requestsPending: Int32, recentRequesters: [Int64]) case updateBotChatInviteRequester(peer: Api.Peer, date: Int32, userId: Int64, about: String, invite: Api.ExportedChatInvite, qts: Int32) case updateMessageReactions(peer: Api.Peer, msgId: Int32, reactions: Api.MessageReactions) + case updateAttachMenuBots case updateReadFeed(flags: Int32, filterId: Int32, maxPosition: Api.FeedPosition, unreadCount: Int32?, unreadMutedCount: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -5781,6 +5782,12 @@ public extension Api { peer.serialize(buffer, true) serializeInt32(msgId, buffer: buffer, boxed: false) reactions.serialize(buffer, true) + break + case .updateAttachMenuBots: + if boxed { + buffer.appendInt32(397910539) + } + break case .updateReadFeed(let flags, let filterId, let maxPosition, let unreadCount, let unreadMutedCount): if boxed { @@ -5989,6 +5996,8 @@ public extension Api { return ("updateBotChatInviteRequester", [("peer", peer), ("date", date), ("userId", userId), ("about", about), ("invite", invite), ("qts", qts)]) case .updateMessageReactions(let peer, let msgId, let reactions): return ("updateMessageReactions", [("peer", peer), ("msgId", msgId), ("reactions", reactions)]) + case .updateAttachMenuBots: + return ("updateAttachMenuBots", []) case .updateReadFeed(let flags, let filterId, let maxPosition, let unreadCount, let unreadMutedCount): return ("updateReadFeed", [("flags", flags), ("filterId", filterId), ("maxPosition", maxPosition), ("unreadCount", unreadCount), ("unreadMutedCount", unreadMutedCount)]) } @@ -7722,6 +7731,9 @@ public extension Api { return nil } } + public static func parse_updateAttachMenuBots(_ reader: BufferReader) -> Update? { + return Api.Update.updateAttachMenuBots + } public static func parse_updateReadFeed(_ reader: BufferReader) -> Update? { var _1: Int32? _1 = reader.readInt32() @@ -8253,6 +8265,7 @@ public extension Api { case keyboardButtonRequestPoll(flags: Int32, quiz: Api.Bool?, text: String) case inputKeyboardButtonUserProfile(text: String, userId: Api.InputUser) case keyboardButtonUserProfile(text: String, userId: Int64) + case keyboardButtonWebView(text: String, url: String) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -8351,6 +8364,13 @@ public extension Api { serializeString(text, buffer: buffer, boxed: false) serializeInt64(userId, buffer: buffer, boxed: false) break + case .keyboardButtonWebView(let text, let url): + if boxed { + buffer.appendInt32(326529584) + } + serializeString(text, buffer: buffer, boxed: false) + serializeString(url, buffer: buffer, boxed: false) + break } } @@ -8382,6 +8402,8 @@ public extension Api { return ("inputKeyboardButtonUserProfile", [("text", text), ("userId", userId)]) case .keyboardButtonUserProfile(let text, let userId): return ("keyboardButtonUserProfile", [("text", text), ("userId", userId)]) + case .keyboardButtonWebView(let text, let url): + return ("keyboardButtonWebView", [("text", text), ("url", url)]) } } @@ -8585,6 +8607,20 @@ public extension Api { return nil } } + public static func parse_keyboardButtonWebView(_ reader: BufferReader) -> KeyboardButton? { + 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.KeyboardButton.keyboardButtonWebView(text: _1!, url: _2!) + } + else { + return nil + } + } } public enum ContactStatus: TypeConstructorDescription { @@ -10366,6 +10402,46 @@ public extension Api { } } + } + public enum AttachMenuBot: TypeConstructorDescription { + case attachMenuBot(botId: Int64, attachMenuIcon: Api.Document) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .attachMenuBot(let botId, let attachMenuIcon): + if boxed { + buffer.appendInt32(-729926056) + } + serializeInt64(botId, buffer: buffer, boxed: false) + attachMenuIcon.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .attachMenuBot(let botId, let attachMenuIcon): + return ("attachMenuBot", [("botId", botId), ("attachMenuIcon", attachMenuIcon)]) + } + } + + public static func parse_attachMenuBot(_ reader: BufferReader) -> AttachMenuBot? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Api.Document? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Document + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.AttachMenuBot.attachMenuBot(botId: _1!, attachMenuIcon: _2!) + } + else { + return nil + } + } + } public enum DialogFilterSuggested: TypeConstructorDescription { case dialogFilterSuggested(filter: Api.DialogFilter, description: String) @@ -11276,6 +11352,76 @@ public extension Api { } } + } + public enum WebViewResult: TypeConstructorDescription { + case webViewResultUrl(queryId: Int64, url: String) + case webViewResultConfirmationRequired(bot: Api.AttachMenuBot, users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .webViewResultUrl(let queryId, let url): + if boxed { + buffer.appendInt32(202659196) + } + serializeInt64(queryId, buffer: buffer, boxed: false) + serializeString(url, buffer: buffer, boxed: false) + break + case .webViewResultConfirmationRequired(let bot, let users): + if boxed { + buffer.appendInt32(-1312107643) + } + bot.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 .webViewResultUrl(let queryId, let url): + return ("webViewResultUrl", [("queryId", queryId), ("url", url)]) + case .webViewResultConfirmationRequired(let bot, let users): + return ("webViewResultConfirmationRequired", [("bot", bot), ("users", users)]) + } + } + + public static func parse_webViewResultUrl(_ reader: BufferReader) -> WebViewResult? { + var _1: Int64? + _1 = reader.readInt64() + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.WebViewResult.webViewResultUrl(queryId: _1!, url: _2!) + } + else { + return nil + } + } + public static func parse_webViewResultConfirmationRequired(_ reader: BufferReader) -> WebViewResult? { + var _1: Api.AttachMenuBot? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.AttachMenuBot + } + var _2: [Api.User]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.WebViewResult.webViewResultConfirmationRequired(bot: _1!, users: _2!) + } + else { + return nil + } + } + } public enum FileHash: TypeConstructorDescription { case fileHash(offset: Int32, limit: Int32, hash: Buffer) @@ -21600,6 +21746,72 @@ public extension Api { } } + } + public enum AttachMenuBots: TypeConstructorDescription { + case attachMenuBotsNotModified + case attachMenuBots(hash: Int64, bots: [Api.AttachMenuBot], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .attachMenuBotsNotModified: + if boxed { + buffer.appendInt32(-237467044) + } + + break + case .attachMenuBots(let hash, let bots, let users): + if boxed { + buffer.appendInt32(1011024320) + } + serializeInt64(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(bots.count)) + for item in bots { + 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 .attachMenuBotsNotModified: + return ("attachMenuBotsNotModified", []) + case .attachMenuBots(let hash, let bots, let users): + return ("attachMenuBots", [("hash", hash), ("bots", bots), ("users", users)]) + } + } + + public static func parse_attachMenuBotsNotModified(_ reader: BufferReader) -> AttachMenuBots? { + return Api.AttachMenuBots.attachMenuBotsNotModified + } + public static func parse_attachMenuBots(_ reader: BufferReader) -> AttachMenuBots? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.AttachMenuBot]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.AttachMenuBot.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.AttachMenuBots.attachMenuBots(hash: _1!, bots: _2!, users: _3!) + } + else { + return nil + } + } + } public enum PrivacyRule: TypeConstructorDescription { case privacyValueAllowContacts diff --git a/submodules/TelegramApi/Sources/Api4.swift b/submodules/TelegramApi/Sources/Api4.swift index 5282f9492c..a545ef2e68 100644 --- a/submodules/TelegramApi/Sources/Api4.swift +++ b/submodules/TelegramApi/Sources/Api4.swift @@ -4849,6 +4849,84 @@ public extension Api { return result }) } + + public static func getAttachMenuBots(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(385663691) + serializeInt64(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.getAttachMenuBots", parameters: [("hash", hash)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.AttachMenuBots? in + let reader = BufferReader(buffer) + var result: Api.AttachMenuBots? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.AttachMenuBots + } + return result + }) + } + + public static func toggleBotInAttachMenu(bot: Api.InputUser, enabled: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(451818415) + bot.serialize(buffer, true) + enabled.serialize(buffer, true) + return (FunctionDescription(name: "messages.toggleBotInAttachMenu", parameters: [("bot", bot), ("enabled", 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 static func requestWebView(flags: Int32, peer: Api.InputPeer, bot: Api.InputUser, url: String?, themeParams: Api.DataJSON?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1585704741) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + bot.serialize(buffer, true) + if Int(flags) & Int(1 << 0) != 0 {serializeString(url!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {themeParams!.serialize(buffer, true)} + return (FunctionDescription(name: "messages.requestWebView", parameters: [("flags", flags), ("peer", peer), ("bot", bot), ("url", url), ("themeParams", themeParams)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.WebViewResult? in + let reader = BufferReader(buffer) + var result: Api.WebViewResult? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.WebViewResult + } + return result + }) + } + + public static func setWebViewResult(queryId: Int64, result: Api.InputBotInlineResult) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-467873507) + serializeInt64(queryId, buffer: buffer, boxed: false) + result.serialize(buffer, true) + return (FunctionDescription(name: "messages.setWebViewResult", parameters: [("queryId", queryId), ("result", result)]), 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 static func getWebViewResult(peer: Api.InputPeer, bot: Api.InputUser, queryId: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(582402580) + peer.serialize(buffer, true) + bot.serialize(buffer, true) + serializeInt64(queryId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.getWebViewResult", parameters: [("peer", peer), ("bot", bot), ("queryId", queryId)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.WebViewResult? in + let reader = BufferReader(buffer) + var result: Api.messages.WebViewResult? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.WebViewResult + } + return result + }) + } } public struct channels { public static func readHistory(channel: Api.InputChannel, maxId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index b60f7b1d69..98c0c1f47b 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -1091,6 +1091,7 @@ public class Account { self.managedOperationsDisposable.add(managedSynchronizeEmojiKeywordsOperations(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedApplyPendingScheduledMessagesActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedSynchronizeAvailableReactions(postbox: self.postbox, network: self.network).start()) + self.managedOperationsDisposable.add(managedSynchronizeAttachMenuBots(postbox: self.postbox, network: self.network).start()) if !supplementary { self.managedOperationsDisposable.add(managedChatListFilters(postbox: self.postbox, network: self.network, accountPeerId: self.peerId).start()) diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index 3fb09a98ef..b70ca8da48 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -112,6 +112,7 @@ enum AccountStateMutationOperation { case UpdateGroupCallParticipants(id: Int64, accessHash: Int64, participants: [Api.GroupCallParticipant], version: Int32) case UpdateGroupCall(peerId: PeerId, call: Api.GroupCall) case UpdateAutoremoveTimeout(peer: Api.Peer, value: CachedPeerAutoremoveTimeout.Value?) + case UpdateAttachMenuBots } struct HoleFromPreviousState { @@ -496,9 +497,13 @@ struct AccountMutableState { self.addOperation(.UpdateChatListFilter(id: id, filter: filter)) } + mutating func addUpdateAttachMenuBots() { + self.addOperation(.UpdateAttachMenuBots) + } + mutating func addOperation(_ operation: AccountStateMutationOperation) { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots: break case let .AddMessages(messages, location): for message in messages { diff --git a/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift index a922ed23a2..00158c8d14 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift @@ -42,6 +42,8 @@ extension ReplyMarkupButton { self.init(title: text, titleWhenForwarded: nil, action: .openUserProfile(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)))) case let .inputKeyboardButtonUserProfile(text, _): self.init(title: text, titleWhenForwarded: nil, action: .openUserProfile(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0)))) + case let .keyboardButtonWebView(text, url): + self.init(title: text, titleWhenForwarded: nil, action: .openWebView(url: url)) } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift index 38bc91b459..6f4b420577 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift @@ -64,6 +64,9 @@ extension TelegramUser { if (flags & (1 << 21)) != 0 { botFlags.insert(.requiresGeolocationForInlineRequests) } + if (flags & (1 << 27)) != 0 { + botFlags.insert(.canBeAddedToAttachMenu) + } botInfo = BotUserInfo(flags: botFlags, inlinePlaceholder: botInlinePlaceholder) } @@ -118,6 +121,9 @@ extension TelegramUser { if (flags & (1 << 21)) != 0 { botFlags.insert(.requiresGeolocationForInlineRequests) } + if (flags & (1 << 27)) != 0 { + botFlags.insert(.canBeAddedToAttachMenu) + } botInfo = BotUserInfo(flags: botFlags, inlinePlaceholder: botInlinePlaceholder) } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 15771dd1a2..ebfdb7f41a 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1512,6 +1512,8 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo }) case let .updateMessageReactions(peer, msgId, reactions): updatedState.updateMessageReactions(MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: msgId), reactions: reactions, eventTimestamp: updatesDate) + case .updateAttachMenuBots: + updatedState.addUpdateAttachMenuBots() default: break } @@ -2304,7 +2306,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) var currentAddScheduledMessages: OptimizeAddMessagesState? for operation in operations { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots: if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location)) } @@ -2417,6 +2419,7 @@ func replayFinalState( var peerActivityTimestamps: [PeerId: Int32] = [:] var syncChatListFilters = false var deletedMessageIds: [DeletedMessageId] = [] + var syncAttachMenuBots = false var holesFromPreviousStateMessageIds: [MessageId] = [] var clearHolesFromPreviousStateForChannelMessagesWithPts: [PeerIdAndMessageNamespace: Int32] = [:] @@ -3322,6 +3325,8 @@ func replayFinalState( return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) }) + case .UpdateAttachMenuBots: + syncAttachMenuBots = true } } @@ -3555,6 +3560,10 @@ func replayFinalState( } } + if syncAttachMenuBots { +// addSynchronizeAttachMenuBotsOperation(transaction: transaction) + } + for groupId in invalidateGroupStats { transaction.setNeedsPeerGroupMessageStatsSynchronization(groupId: groupId, namespace: Namespaces.Message.Cloud) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 5cb52c2d64..2cc45c0cd7 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -83,6 +83,7 @@ public struct Namespaces { public static let cachedSendAsPeers: Int8 = 18 public static let availableReactions: Int8 = 19 public static let resolvedByPhonePeers: Int8 = 20 + public static let attachMenuBots: Int8 = 21 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift index 815f8139cc..46ee14e87f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift @@ -12,6 +12,7 @@ public enum ReplyMarkupButtonAction: PostboxCoding, Equatable { case urlAuth(url: String, buttonId: Int32) case setupPoll(isQuiz: Bool?) case openUserProfile(peerId: PeerId) + case openWebView(url: String) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("v", orElse: 0) { @@ -37,6 +38,8 @@ public enum ReplyMarkupButtonAction: PostboxCoding, Equatable { self = .setupPoll(isQuiz: decoder.decodeOptionalInt32ForKey("isq").flatMap { $0 != 0 }) case 10: self = .openUserProfile(peerId: PeerId(decoder.decodeInt64ForKey("peerId", orElse: 0))) + case 11: + self = .openWebView(url: decoder.decodeStringForKey("u", orElse: "")) default: self = .text } @@ -79,6 +82,9 @@ public enum ReplyMarkupButtonAction: PostboxCoding, Equatable { case let .openUserProfile(peerId): encoder.encodeInt32(10, forKey: "v") encoder.encodeInt64(peerId.toInt64(), forKey: "peerId") + case let .openWebView(url): + encoder.encodeInt32(11, forKey: "v") + encoder.encodeString(url, forKey: "u") } } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift index 766e3b9385..38b1116597 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift @@ -31,6 +31,7 @@ public struct BotUserInfoFlags: OptionSet { public static let hasAccessToChatHistory = BotUserInfoFlags(rawValue: (1 << 0)) public static let worksWithGroups = BotUserInfoFlags(rawValue: (1 << 1)) public static let requiresGeolocationForInlineRequests = BotUserInfoFlags(rawValue: (1 << 3)) + public static let canBeAddedToAttachMenu = BotUserInfoFlags(rawValue: (1 << 4)) } public struct BotUserInfo: PostboxCoding, Equatable { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift new file mode 100644 index 0000000000..83ca3ef76e --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift @@ -0,0 +1,255 @@ +import Foundation +import TelegramApi +import Postbox +import SwiftSignalKit + +public final class AttachMenuBots: Equatable, Codable { + public final class Bot: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case peerId + case icon + } + + public let peerId: PeerId + public let icon: TelegramMediaFile + + public init( + peerId: PeerId, + icon: TelegramMediaFile + ) { + self.peerId = peerId + self.icon = icon + } + + public static func ==(lhs: Bot, rhs: Bot) -> Bool { + if lhs.peerId != rhs.peerId { + return false + } + if lhs.icon != rhs.icon { + return false + } + return true + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let peerIdValue = try container.decode(Int64.self, forKey: .peerId) + self.peerId = PeerId(peerIdValue) + + let iconData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .icon) + self.icon = TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: iconData.data))) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.peerId.toInt64(), forKey: .peerId) + try container.encode(PostboxEncoder().encodeObjectToRawData(self.icon), forKey: .icon) + } + } + + private enum CodingKeys: String, CodingKey { + case hash + case bots + } + + public let hash: Int64 + public let bots: [Bot] + + public init( + hash: Int64, + bots: [Bot] + ) { + self.hash = hash + self.bots = bots + } + + public static func ==(lhs: AttachMenuBots, rhs: AttachMenuBots) -> Bool { + if lhs.hash != rhs.hash { + return false + } + if lhs.bots != rhs.bots { + return false + } + return true + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.hash = try container.decode(Int64.self, forKey: .hash) + self.bots = try container.decode([Bot].self, forKey: .bots) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.hash, forKey: .hash) + try container.encode(self.bots, forKey: .bots) + } +} + +private func cachedAttachMenuBots(postbox: Postbox) -> Signal { + return postbox.transaction { transaction -> AttachMenuBots? in + return cachedAttachMenuBots(transaction: transaction) + } +} + +private func cachedAttachMenuBots(transaction: Transaction) -> AttachMenuBots? { + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: 0) + + let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.attachMenuBots, key: key))?.get(AttachMenuBots.self) + if let cached = cached { + return cached + } else { + return nil + } +} + +private func setCachedAttachMenuBots(transaction: Transaction, attachMenuBots: AttachMenuBots) { + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: 0) + + let entryId = ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.attachMenuBots, key: key) + if let entry = CodableEntry(attachMenuBots) { + transaction.putItemCacheEntry(id: entryId, entry: entry, collectionSpec: ItemCacheCollectionSpec(lowWaterItemCount: 10, highWaterItemCount: 10)) + } else { + transaction.removeItemCacheEntry(id: entryId) + } +} + +func managedSynchronizeAttachMenuBots(postbox: Postbox, network: Network) -> Signal { + let poll = Signal { subscriber in + let signal: Signal = cachedAttachMenuBots(postbox: postbox) + |> mapToSignal { current in + return (network.request(Api.functions.messages.getAttachMenuBots(hash: current?.hash ?? 0)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result = result else { + return .complete() + } + return postbox.transaction { transaction in + switch result { + case let .attachMenuBots(hash, bots, users): + var peers: [Peer] = [] + for user in users { + let telegramUser = TelegramUser(user: user) + peers.append(telegramUser) + } + updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in + return updated + }) + + var resultBots: [AttachMenuBots.Bot] = [] + for bot in bots { + switch bot { + case let .attachMenuBot(botId, attachMenuIcon): + if let icon = telegramMediaFileFromApiDocument(attachMenuIcon) { + resultBots.append(AttachMenuBots.Bot(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), icon: icon)) + } + } + } + + let attachMenuBots = AttachMenuBots(hash: hash, bots: resultBots) + setCachedAttachMenuBots(transaction: transaction, attachMenuBots: attachMenuBots) + case .attachMenuBotsNotModified: + break + } + } |> ignoreValues + }) + } + + return signal.start(completed: { + subscriber.putCompletion() + }) + } + + return ( + poll + |> then( + .complete() + |> suspendAwareDelay(2.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()) + ) + ) + |> restart +} + +func _internal_addBotToAttachMenu(postbox: Postbox, network: Network, peerId: PeerId) -> Signal { + return postbox.transaction { transaction -> Signal in + guard let peer = transaction.getPeer(peerId), let inputUser = apiInputUser(peer) else { + return .complete() + } + return network.request(Api.functions.messages.toggleBotInAttachMenu(bot: inputUser, enabled: .boolTrue)) + |> map { value -> Bool in + switch value { + case .boolTrue: + return true + default: + return false + } + } + |> `catch` { error -> Signal in + return .single(false) + } + |> afterCompleted { + let _ = (managedSynchronizeAttachMenuBots(postbox: postbox, network: network) + |> take(1)).start() + } + } + |> switchToLatest +} + +func _internal_removeBotFromAttachMenu(postbox: Postbox, network: Network, peerId: PeerId) -> Signal { + return postbox.transaction { transaction -> Signal in + guard let peer = transaction.getPeer(peerId), let inputUser = apiInputUser(peer) else { + return .complete() + } + return network.request(Api.functions.messages.toggleBotInAttachMenu(bot: inputUser, enabled: .boolFalse)) + |> map { value -> Bool in + switch value { + case .boolTrue: + return true + default: + return false + } + } + |> `catch` { error -> Signal in + return .single(false) + } + |> afterCompleted { + let _ = (managedSynchronizeAttachMenuBots(postbox: postbox, network: network) + |> take(1)).start() + } + } + |> switchToLatest +} + +public struct AttachMenuBot { + public let peer: Peer + public let icon: TelegramMediaFile + + init(peer: Peer, icon: TelegramMediaFile) { + self.peer = peer + self.icon = icon + } +} + +func _internal_attachMenuBots(postbox: Postbox) -> Signal<[AttachMenuBot], NoError> { + return postbox.transaction { transaction -> [AttachMenuBot] in + guard let cachedBots = cachedAttachMenuBots(transaction: transaction)?.bots else { + return [] + } + var resultBots: [AttachMenuBot] = [] + for bot in cachedBots { + if let peer = transaction.getPeer(bot.peerId) { + resultBots.append(AttachMenuBot(peer: peer, icon: bot.icon)) + } + } + return resultBots + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift new file mode 100644 index 0000000000..7f00f8d269 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -0,0 +1,95 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +public enum RequestWebViewResult { + case webViewResult(queryId: Int64, url: String) + case requestConfirmation +} + +public enum RequestWebViewError { + case generic +} + +func _internal_requestWebView(postbox: Postbox, network: Network, peerId: PeerId, botId: PeerId, url: String?, themeParams: [String: Any]?) -> Signal { + var serializedThemeParams: Api.DataJSON? + if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) { + serializedThemeParams = .dataJSON(data: dataString) + } + + return postbox.transaction { transaction -> Signal in + guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer), let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else { + return .fail(.generic) + } + + var flags: Int32 = 0 + if let _ = url { + flags |= (1 << 0) + } + if let _ = serializedThemeParams { + flags |= (1 << 1) + } + return network.request(Api.functions.messages.requestWebView(flags: flags, peer: inputPeer, bot: inputUser, url: url, themeParams: serializedThemeParams)) + |> mapError { _ -> RequestWebViewError in + return .generic + } + |> mapToSignal { result -> Signal in + switch result { + case let .webViewResultConfirmationRequired(_, users): + return postbox.transaction { transaction -> RequestWebViewResult in + var peers: [Peer] = [] + for user in users { + let telegramUser = TelegramUser(user: user) + peers.append(telegramUser) + } + updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in + return updated + }) + return .requestConfirmation + } + |> castError(RequestWebViewError.self) + case let .webViewResultUrl(queryId, url): + return .single(.webViewResult(queryId: queryId, url: url)) + } + } + } + |> castError(RequestWebViewError.self) + |> switchToLatest +} + +public enum GetWebViewResultError { + case generic +} + +func _internal_getWebViewResult(postbox: Postbox, network: Network, peerId: PeerId, botId: PeerId, queryId: Int64) -> Signal { + return postbox.transaction { transaction -> Signal in + guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer), let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else { + return .fail(.generic) + } + return network.request(Api.functions.messages.getWebViewResult(peer: inputPeer, bot: inputUser, queryId: queryId)) + |> mapError { _ -> GetWebViewResultError in + return .generic + } + |> mapToSignal { result -> Signal in + return postbox.transaction { transaction -> ChatContextResult in + switch result { + case let .webViewResult(result, users): + var peers: [Peer] = [] + for user in users { + let telegramUser = TelegramUser(user: user) + peers.append(telegramUser) + } + updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in + return updated + }) + return ChatContextResult(apiResult: result, queryId: queryId) + } + } + |> castError(GetWebViewResultError.self) + } + } + |> castError(GetWebViewResultError.self) + |> switchToLatest +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift index 16ab23533f..49a6885152 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift @@ -2,19 +2,19 @@ import Foundation import Postbox import SwiftSignalKit -func _internal_enqueueOutgoingMessageWithChatContextResult(account: Account, to peerId: PeerId, results: ChatContextResultCollection, result: ChatContextResult, replyToMessageId: MessageId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> Bool { - guard let message = outgoingMessageWithChatContextResult(to: peerId, results: results, result: result, replyToMessageId: replyToMessageId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId) else { +func _internal_enqueueOutgoingMessageWithChatContextResult(account: Account, to peerId: PeerId, botId: PeerId, result: ChatContextResult, replyToMessageId: MessageId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> Bool { + guard let message = outgoingMessageWithChatContextResult(to: peerId, botId: botId, result: result, replyToMessageId: replyToMessageId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId) else { return false } let _ = enqueueMessages(account: account, peerId: peerId, messages: [message]).start() return true } -private func outgoingMessageWithChatContextResult(to peerId: PeerId, results: ChatContextResultCollection, result: ChatContextResult, replyToMessageId: MessageId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> EnqueueMessage? { +private func outgoingMessageWithChatContextResult(to peerId: PeerId, botId: PeerId, result: ChatContextResult, replyToMessageId: MessageId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> EnqueueMessage? { var attributes: [MessageAttribute] = [] attributes.append(OutgoingChatContextResultMessageAttribute(queryId: result.queryId, id: result.id, hideVia: hideVia)) if !hideVia { - attributes.append(InlineBotMessageAttribute(peerId: results.botId, title: nil)) + attributes.append(InlineBotMessageAttribute(peerId: botId, title: nil)) } if let scheduleTime = scheduleTime { attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime)) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 4e454657b6..6e7ca00545 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -160,8 +160,8 @@ public extension TelegramEngine { return _internal_exportMessageLink(account: self.account, peerId: peerId, messageId: messageId, isThread: isThread) } - public func enqueueOutgoingMessageWithChatContextResult(to peerId: PeerId, results: ChatContextResultCollection, result: ChatContextResult, replyToMessageId: MessageId? = nil, hideVia: Bool = false, silentPosting: Bool = false, scheduleTime: Int32? = nil, correlationId: Int64? = nil) -> Bool { - return _internal_enqueueOutgoingMessageWithChatContextResult(account: self.account, to: peerId, results: results, result: result, replyToMessageId: replyToMessageId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId) + public func enqueueOutgoingMessageWithChatContextResult(to peerId: PeerId, botId: PeerId, result: ChatContextResult, replyToMessageId: MessageId? = nil, hideVia: Bool = false, silentPosting: Bool = false, scheduleTime: Int32? = nil, correlationId: Int64? = nil) -> Bool { + return _internal_enqueueOutgoingMessageWithChatContextResult(account: self.account, to: peerId, botId: botId, result: result, replyToMessageId: replyToMessageId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId) } public func requestChatContextResults(botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal { @@ -317,5 +317,25 @@ public extension TelegramEngine { public func translate(text: String, fromLang: String?, toLang: String) -> Signal { return _internal_translate(network: self.account.network, text: text, fromLang: fromLang, toLang: toLang) } + + public func requestWebView(peerId: PeerId, botId: PeerId, url: String?, themeParams: [String: Any]?) -> Signal { + return _internal_requestWebView(postbox: self.account.postbox, network: self.account.network, peerId: peerId, botId: botId, url: url, themeParams: themeParams) + } + + public func getWebViewResult(peerId: PeerId, botId: PeerId, queryId: Int64) -> Signal { + return _internal_getWebViewResult(postbox: self.account.postbox, network: self.account.network, peerId: peerId, botId: botId, queryId: queryId) + } + + public func addBotToAttachMenu(peerId: PeerId) -> Signal { + return _internal_addBotToAttachMenu(postbox: self.account.postbox, network: self.account.network, peerId: peerId) + } + + public func removeBotFromAttachMenu(peerId: PeerId) -> Signal { + return _internal_removeBotFromAttachMenu(postbox: self.account.postbox, network: self.account.network, peerId: peerId) + } + + public func attachMenuBots() -> Signal<[AttachMenuBot], NoError> { + return _internal_attachMenuBots(postbox: self.account.postbox) + } } } diff --git a/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift index ee3274981a..e21bec652d 100644 --- a/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift @@ -215,6 +215,9 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { self.controllerInteraction.openPollCreation(isQuiz) case let .openUserProfile(peerId): self.controllerInteraction.openPeer(peerId, .info, nil, nil) + case let .openWebView(startParam): + print(startParam) + break } if dismissIfOnce { if let message = self.message { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 08e352840f..50802c5d03 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -10415,11 +10415,30 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - var availableTabs: [AttachmentButtonType] = [.gallery, .file, .location, .contact] + var availableButtons: [AttachmentButtonType] = [.gallery, .file, .location, .contact] if canSendPolls { - availableTabs.insert(.poll, at: availableTabs.count - 1) + availableButtons.insert(.poll, at: availableButtons.count - 1) + } + + let presentationData = self.presentationData + + let buttons: Signal<[AttachmentButtonType], NoError> + if let _ = peer as? TelegramUser { + buttons = .single(availableButtons) + |> then( + self.context.engine.messages.attachMenuBots() + |> map { attachMenuBots in + var buttons = availableButtons + for bot in attachMenuBots.reversed() { + let peerTitle = EnginePeer(bot.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + buttons.insert(.app(bot.peer.id, peerTitle, bot.icon), at: 1) + } + return buttons + } + ) + } else { + buttons = .single(availableButtons) } -// availableTabs.insert(.app("Web App"), at: 1) let inputText = self.presentationInterfaceState.interfaceState.effectiveInputState.inputText @@ -10427,7 +10446,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let currentFilesController = Atomic(value: nil) let currentLocationController = Atomic(value: nil) - let attachmentController = AttachmentController(context: self.context, updatedPresentationData: self.updatedPresentationData, chatLocation: self.chatLocation, buttons: availableTabs) + let attachmentController = AttachmentController(context: self.context, updatedPresentationData: self.updatedPresentationData, chatLocation: self.chatLocation, buttons: buttons) attachmentController.requestController = { [weak self, weak attachmentController] type, completion in guard let strongSelf = self else { return @@ -10684,8 +10703,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let controller = strongSelf.configurePollCreation() completion(controller, nil) strongSelf.controllerNavigationDisposable.set(nil) - case .app: - let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, url: "", message: nil) + case let .app(botId, botName, _): + let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peer.id, botId: botId, botName: botName, url: nil) completion(controller, nil) strongSelf.controllerNavigationDisposable.set(nil) } @@ -12222,7 +12241,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let replyMessageId = self.presentationInterfaceState.interfaceState.replyMessageId - if self.context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: peerId, results: results, result: result, replyToMessageId: replyMessageId, hideVia: hideVia, silentPosting: silentPosting) { + if self.context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: peerId, botId: results.botId, result: result, replyToMessageId: replyMessageId, hideVia: hideVia, silentPosting: silentPosting) { self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index fa774fe32b..e864b9f359 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -2568,13 +2568,25 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.historyNode.view.drawHierarchy(in: CGRect(origin: CGPoint(), size: unscaledSize), afterScreenUpdates: false) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: -1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + if let emptyNode = self.emptyNode { + emptyNode.view.drawHierarchy(in: CGRect(origin: CGPoint(), size: unscaledSize), afterScreenUpdates: false) + } + UIGraphicsPopContext() }).flatMap(applyScreenshotEffectToImage) let blurredHistoryNode = ASImageNode() blurredHistoryNode.image = image blurredHistoryNode.frame = self.historyNode.frame self.blurredHistoryNode = blurredHistoryNode - self.historyNode.supernode?.insertSubnode(blurredHistoryNode, aboveSubnode: self.historyNode) + if let emptyNode = self.emptyNode { + emptyNode.supernode?.insertSubnode(blurredHistoryNode, aboveSubnode: emptyNode) + } else { + self.historyNode.supernode?.insertSubnode(blurredHistoryNode, aboveSubnode: self.historyNode) + } } } else { if let blurredHistoryNode = self.blurredHistoryNode { diff --git a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift index 57ec1c7151..3a35ccd26d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift @@ -112,7 +112,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { iconImage = incoming ? graphics.chatBubbleActionButtonIncomingPaymentIconImage : graphics.chatBubbleActionButtonOutgoingPaymentIconImage case .openUserProfile: iconImage = incoming ? graphics.chatBubbleActionButtonIncomingProfileIconImage : graphics.chatBubbleActionButtonOutgoingProfileIconImage - case .openWebApp: + case .openWebView: iconImage = incoming ? graphics.chatBubbleActionButtonIncomingWebAppIconImage : graphics.chatBubbleActionButtonOutgoingWebAppIconImage default: iconImage = nil diff --git a/submodules/TelegramUI/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Sources/ChatMessageItemView.swift index c111678fb1..d8dd36e520 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItemView.swift @@ -854,6 +854,9 @@ public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol break case let .openUserProfile(peerId): item.controllerInteraction.openPeer(peerId, .info, nil, nil) + case let .openWebView(startParam): + print(startParam) + break } } } diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 105475eb41..e89f628475 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -937,6 +937,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }), .window(.root), nil) case .importStickers: break + case .setAttach: + break } } })) diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index c605239805..6541927c7f 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -25,6 +25,7 @@ import UndoUI import ImportStickerPackUI import PeerInfoUI import Markdown +import WebUI private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer { if case .default = navigation { @@ -546,5 +547,29 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur present(controller, nil) } } + case let .setAttach(peerId): + let presentError: (String) -> Void = { errorText in + present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + } + let _ = (context.engine.messages.attachMenuBots() + |> deliverOnMainQueue).start(next: { attachMenuBots in + if let _ = attachMenuBots.firstIndex(where: { $0.peer.id == peerId }) { + presentError(presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError) + } else { + let _ = (context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ) + |> deliverOnMainQueue).start(next: { peer in + if let peer = peer, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.canBeAddedToAttachMenu) { + let controller = addWebAppToAttachmentController(sharedContext: context.sharedContext, peerName: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), completion: { + let _ = context.engine.messages.addBotToAttachMenu(peerId: peerId).start() + }) + present(controller, nil) + } else { + presentError(presentationData.strings.Login_UnknownError) + } + }) + } + }) } } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index c75f110003..e42d0e4930 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -614,6 +614,8 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur var game: String? var post: String? var voiceChat: String? + var attach: String? + var setAttach: String? if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { @@ -633,9 +635,13 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur post = value } else if ["voicechat", "videochat", "livestream"].contains(queryItem.name) { voiceChat = value + } else if queryItem.name == "attach" { + attach = value } } else if ["voicechat", "videochat", "livestream"].contains(queryItem.name) { voiceChat = "" + } else if queryItem.name == "setattach" { + setAttach = "" } } } @@ -662,6 +668,10 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } else { result += "?voicechat=" } + } else if let attach = attach { + result += "?attach=\(attach)" + } else if let _ = setAttach { + result += "?setattach" } convertedUrl = result } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 768b7d40e0..3084e13b98 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -926,24 +926,25 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } } + if let encryptionKeyFingerprint = data.encryptionKeyFingerprint { + items[.peerInfo]!.append(PeerInfoScreenDisclosureEncryptionKeyItem(id: 5, text: presentationData.strings.Profile_EncryptionKey, fingerprint: encryptionKeyFingerprint, action: { + interaction.openEncryptionKey() + })) + } + + if user.botInfo != nil, !user.isVerified { + items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 6, text: presentationData.strings.ReportPeer_Report, action: { + interaction.openReport(false) + })) + } + if let botInfo = user.botInfo, botInfo.flags.contains(.worksWithGroups) { - items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 5, text: presentationData.strings.Bot_AddToChat, color: .accent, action: { + items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 7, text: presentationData.strings.Bot_AddToChat, color: .accent, action: { interaction.openAddBotToGroup() })) - items[.peerInfo]!.append(PeerInfoScreenCommentItem(id: 6, text: presentationData.strings.Bot_AddToChatInfo)) + items[.peerInfo]!.append(PeerInfoScreenCommentItem(id: 8, text: presentationData.strings.Bot_AddToChatInfo)) } - } - - if let encryptionKeyFingerprint = data.encryptionKeyFingerprint { - items[.peerInfo]!.append(PeerInfoScreenDisclosureEncryptionKeyItem(id: 6, text: presentationData.strings.Profile_EncryptionKey, fingerprint: encryptionKeyFingerprint, action: { - interaction.openEncryptionKey() - })) - } - - if user.botInfo != nil, !user.isVerified { - items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 5, text: presentationData.strings.ReportPeer_Report, action: { - interaction.openReport(false) - })) + } } else if let channel = data.peer as? TelegramChannel { let ItemUsername = 1 diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 26328af18c..601629c8cd 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -42,7 +42,7 @@ extension ResolvedBotAdminRights { if components.contains("pin_messages") { rawValue |= ResolvedBotAdminRights.pinMessages.rawValue } - if components.contains("promote_members") { + if components.contains("promotѲe_members") { rawValue |= ResolvedBotAdminRights.promoteMembers.rawValue } if components.contains("manage_video_chats") { @@ -66,6 +66,7 @@ extension ResolvedBotAdminRights { public enum ParsedInternalPeerUrlParameter { case botStart(String) case groupBotStart(String, ResolvedBotAdminRights?) + case attachBotStart(String) case channelMessage(Int32, Double?) case replyThread(Int32, Int32) case voiceChat(String?) @@ -86,6 +87,7 @@ public enum ParsedInternalUrl { case wallpaper(WallpaperUrlParameter) case theme(String) case phone(String) + case setAttach(String) } private enum ParsedUrl { @@ -182,7 +184,9 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { } else { for queryItem in queryItems { if let value = queryItem.value { - if queryItem.name == "start" { + if queryItem.name == "attach" { + return .peerName(peerName, .attachBotStart(value)) + } else if queryItem.name == "start" { return .peerName(peerName, .botStart(value)) } else if queryItem.name == "startgroup" { var botAdminRights: ResolvedBotAdminRights? @@ -200,6 +204,8 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { } } else if ["voicechat", "videochat", "livestream"].contains(queryItem.name) { return .peerName(peerName, .voiceChat(nil)) + } else if queryItem.name == "setattach" { + return .setAttach(peerName) } } } @@ -431,6 +437,19 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) return .single(.botStart(peerId: peer.id, payload: payload)) case let .groupBotStart(payload, adminRights): return .single(.groupBotStart(peerId: peer.id, payload: payload, adminRights: adminRights)) + case let .attachBotStart(name): + return context.engine.peers.resolvePeerByName(name: name) + |> take(1) + |> mapToSignal { botPeer -> Signal in + return .single(botPeer?._asPeer()) + } + |> mapToSignal { botPeer -> Signal in + if let _ = botPeer { + return .single(.peer(peer.id, .chat(textInputState: nil, subject: nil, peekData: nil))) + } else { + return .single(.peer(peer.id, .chat(textInputState: nil, subject: nil, peekData: nil))) + } + } case let .channelMessage(id, timecode): return .single(.channelMessage(peerId: peer.id, messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: id), timecode: timecode)) case let .replyThread(id, replyId): @@ -523,6 +542,19 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) return .single(.wallpaper(parameter)) case let .theme(slug): return .single(.theme(slug)) + case let .setAttach(name): + return context.engine.peers.resolvePeerByName(name: name) + |> take(1) + |> mapToSignal { peer -> Signal in + return .single(peer?._asPeer()) + } + |> mapToSignal { peer -> Signal in + if let peer = peer { + return .single(.setAttach(peer.id)) + } else { + return .single(.inaccessiblePeer) + } + } } } diff --git a/submodules/WebUI/BUILD b/submodules/WebUI/BUILD index 33468d677f..259d81455d 100644 --- a/submodules/WebUI/BUILD +++ b/submodules/WebUI/BUILD @@ -15,9 +15,11 @@ swift_library( "//submodules/Display:Display", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/AccountContext:AccountContext", "//submodules/AttachmentUI:AttachmentUI", "//submodules/CounterContollerTitleView:CounterContollerTitleView", + "//submodules/HexColor:HexColor", ], visibility = [ "//visibility:public", diff --git a/submodules/WebUI/Sources/WebAppAlertContentNode.swift b/submodules/WebUI/Sources/WebAppAlertContentNode.swift index 35752610e4..351e2eb370 100644 --- a/submodules/WebUI/Sources/WebAppAlertContentNode.swift +++ b/submodules/WebUI/Sources/WebAppAlertContentNode.swift @@ -12,6 +12,7 @@ import AppBundle private final class WebAppAlertContentNode: AlertContentNode { private let strings: PresentationStrings + private let peerName: String private let textNode: ASTextNode private let iconNode: ASImageNode @@ -26,8 +27,9 @@ private final class WebAppAlertContentNode: AlertContentNode { return self.isUserInteractionEnabled } - init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction]) { + init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, peerName: String, actions: [TextAlertAction]) { self.strings = strings + self.peerName = peerName self.textNode = ASTextNode() self.textNode.maximumNumberOfLines = 0 @@ -72,7 +74,7 @@ private final class WebAppAlertContentNode: AlertContentNode { } override func updateTheme(_ theme: AlertControllerTheme) { - self.textNode.attributedText = NSAttributedString(string: strings.WebApp_AddToAttachmentText("Web App").string, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: strings.WebApp_AddToAttachmentText(self.peerName).string, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Bot Payments/BotLogo"), color: theme.accentColor) self.actionNodesSeparator.backgroundColor = theme.separatorColor @@ -197,7 +199,7 @@ private final class WebAppAlertContentNode: AlertContentNode { } } -func addWebAppToAttachmentController(sharedContext: SharedAccountContext) -> AlertController { +public func addWebAppToAttachmentController(sharedContext: SharedAccountContext, peerName: String, completion: @escaping () -> Void) -> AlertController { let presentationData = sharedContext.currentPresentationData.with { $0 } let theme = presentationData.theme let strings = presentationData.strings @@ -209,9 +211,10 @@ func addWebAppToAttachmentController(sharedContext: SharedAccountContext) -> Ale }), TextAlertAction(type: .defaultAction, title: presentationData.strings.WebApp_AddToAttachmentAdd, action: { dismissImpl?(true) + completion() })] - contentNode = WebAppAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, actions: actions) + contentNode = WebAppAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, peerName: peerName, actions: actions) let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) dismissImpl = { [weak controller] animated in diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 961a506ba1..f4e3a61f33 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -3,6 +3,7 @@ import UIKit import WebKit import Display import AsyncDisplayKit +import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData @@ -10,6 +11,8 @@ import AccountContext import AttachmentUI import CounterContollerTitleView import ContextUI +import PresentationDataUtils +import HexColor private class WeakGameScriptMessageHandler: NSObject, WKScriptMessageHandler { private let f: (WKScriptMessage) -> () @@ -25,6 +28,17 @@ private class WeakGameScriptMessageHandler: NSObject, WKScriptMessageHandler { } } +private func generateThemeParams(_ presentationTheme: PresentationTheme) -> [String: Any] { + return [ + "bg_color": Int32(bitPattern: presentationTheme.list.plainBackgroundColor.rgb), + "text_color": Int32(bitPattern: presentationTheme.list.itemPrimaryTextColor.rgb), + "hint_color": Int32(bitPattern: presentationTheme.list.blocksBackgroundColor.rgb), + "link_color": Int32(bitPattern: presentationTheme.list.itemAccentColor.rgb), + "button_color": Int32(bitPattern: presentationTheme.list.itemCheckColors.fillColor.rgb), + "button_text_color": Int32(bitPattern: presentationTheme.list.itemCheckColors.foregroundColor.rgb) + ] +} + public final class WebAppController: ViewController, AttachmentContainable { public var requestAttachmentMenuExpansion: () -> Void = { } public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } @@ -32,18 +46,20 @@ public final class WebAppController: ViewController, AttachmentContainable { public var cancelPanGesture: () -> Void = { } private class Node: ViewControllerTracingNode { + private weak var controller: WebAppController? + private var webView: WKWebView? private let context: AccountContext var presentationData: PresentationData private let present: (ViewController, Any?) -> Void - private let message: EngineMessage? + private var queryId: Int64? - init(context: AccountContext, presentationData: PresentationData, url: String, present: @escaping (ViewController, Any?) -> Void, message: EngineMessage?) { + init(context: AccountContext, controller: WebAppController, presentationData: PresentationData, peerId: PeerId, botId: PeerId, url: String?, present: @escaping (ViewController, Any?) -> Void) { self.context = context + self.controller = controller self.presentationData = presentationData self.present = present - self.message = message super.init() @@ -92,14 +108,26 @@ public final class WebAppController: ViewController, AttachmentContainable { self.view.addSubview(webView) self.webView = webView - if let parsedUrl = URL(string: url) { - webView.load(URLRequest(url: parsedUrl)) - } + let _ = (context.engine.messages.requestWebView(peerId: peerId, botId: botId, url: url, themeParams: generateThemeParams(presentationData.theme)) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + switch result { + case let .webViewResult(queryId, url): + if let parsedUrl = URL(string: url) { + strongSelf.queryId = queryId + strongSelf.webView?.load(URLRequest(url: parsedUrl)) + } + case .requestConfirmation: + break + } + }) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { if let webView = self.webView { - webView.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height - navigationBarHeight))) + webView.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height - navigationBarHeight - layout.intrinsicInsets.bottom))) } } @@ -113,34 +141,6 @@ public final class WebAppController: ViewController, AttachmentContainable { }) } - private func shareData() -> (EnginePeer, String)? { - guard let message = self.message else { - return nil - } - var botPeer: EnginePeer? - var gameName: String? - for media in message.media { - if let game = media as? TelegramMediaGame { - inner: for attribute in message.attributes { - if let attribute = attribute as? InlineBotMessageAttribute, let peerId = attribute.peerId { - botPeer = message.peers[peerId].flatMap(EnginePeer.init) - break inner - } - } - if botPeer == nil { - botPeer = message.author - } - - gameName = game.name - } - } - if let botPeer = botPeer, let gameName = gameName { - return (botPeer, gameName) - } - - return nil - } - private func handleScriptMessage(_ message: WKScriptMessage) { guard let body = message.body as? [String: Any] else { return @@ -150,15 +150,61 @@ public final class WebAppController: ViewController, AttachmentContainable { return } - if eventName == "share_game" || eventName == "share_score" { - if let (botPeer, gameName) = self.shareData(), let addressName = botPeer.addressName, !addressName.isEmpty, !gameName.isEmpty { - if eventName == "share_score" { - - } else { - + switch eventName { + case "webview_send_result_message": + self.handleSendResultMessage() + case "webview_close": + self.controller?.dismiss() + default: + break + } + } + + func sendEvent(name: String, data: String) { + let script = "window.TelegramGameProxy.receiveEvent(\"\(name)\", \(data))" + self.webView?.evaluateJavaScript(script, completionHandler: { _, _ in + + }) + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + + let themeParams = generateThemeParams(presentationData.theme) + var themeParamsString = "{" + for (key, value) in themeParams { + if let value = value as? Int32 { + let color = UIColor(rgb: UInt32(bitPattern: value)) + + if themeParamsString.count > 1 { + themeParamsString.append(", ") } + themeParamsString.append("\"\(key)\": \"#\(color.hexString)\"") } } + themeParamsString.append("}") + self.sendEvent(name: "theme_changed", data: themeParamsString) + } + + private func handleSendResultMessage() { + guard let controller = self.controller, let queryId = self.queryId else { + return + } + + let _ = (self.context.engine.messages.getWebViewResult(peerId: controller.peerId, botId: controller.botId, queryId: queryId) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self, let controller = strongSelf.controller else { + return + } + + controller.present(textAlertController(context: strongSelf.context, updatedPresentationData: controller.updatedPresentationData, title: nil, text: "Send result?", actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { [weak self] in + guard let strongSelf = self, let controller = strongSelf.controller else { + return + } + let _ = strongSelf.context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: controller.peerId, botId: controller.botId, result: result) + controller.dismiss() + })]), in: .window(.root)) + }) } } @@ -167,19 +213,25 @@ public final class WebAppController: ViewController, AttachmentContainable { return self.displayNode as! Node } + private var titleView: CounterContollerTitleView? private let moreButtonNode: MoreButtonNode private let context: AccountContext - private let url: String - private let message: EngineMessage? + private let peerId: PeerId + private let botId: PeerId + private let url: String? private var presentationData: PresentationData + fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal)? + private var presentationDataDisposable: Disposable? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, url: String, message: EngineMessage?) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, botId: PeerId, botName: String, url: String?) { self.context = context + self.peerId = peerId + self.botId = botId self.url = url - self.message = message + self.updatedPresentationData = updatedPresentationData self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } var theme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme) @@ -199,14 +251,30 @@ public final class WebAppController: ViewController, AttachmentContainable { self.navigationItem.rightBarButtonItem?.target = self let titleView = CounterContollerTitleView(theme: self.presentationData.theme) - titleView.title = CounterContollerTitle(title: "Web App", counter: self.presentationData.strings.Bot_GenericBotStatus) + titleView.title = CounterContollerTitle(title: botName, counter: self.presentationData.strings.Bot_GenericBotStatus) self.navigationItem.titleView = titleView + self.titleView = titleView self.moreButtonNode.action = { [weak self] _, gesture in if let strongSelf = self { strongSelf.morePressed(node: strongSelf.moreButtonNode.contextSourceNode, gesture: gesture) } } + + self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData) + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + strongSelf.presentationData = presentationData + + var theme = NavigationBarTheme(rootControllerTheme: presentationData.theme) + theme = theme.withUpdatedBackgroundColor(presentationData.theme.list.plainBackgroundColor) + let navigationBarPresentationData = NavigationBarPresentationData(theme: theme, strings: NavigationBarStrings(back: "", close: "")) + strongSelf.navigationBar?.updatePresentationData(navigationBarPresentationData) + strongSelf.titleView?.theme = presentationData.theme + + strongSelf.controllerNode.updatePresentationData(presentationData) + } + }) } required public init(coder aDecoder: NSCoder) { @@ -215,6 +283,7 @@ public final class WebAppController: ViewController, AttachmentContainable { deinit { assert(true) + self.presentationDataDisposable?.dispose() } @objc private func cancelPressed() { @@ -226,20 +295,22 @@ public final class WebAppController: ViewController, AttachmentContainable { } @objc private func morePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) { + let context = self.context var items: [ContextMenuItem] = [] - items.append(.action(ContextMenuActionItem(text: "Open Bot", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Bots"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, f in - f(.default) - - guard let strongSelf = self else { - return - } - let controller = addWebAppToAttachmentController(sharedContext: strongSelf.context.sharedContext) - strongSelf.present(controller, in: .window(.root)) - }))) + + if self.botId != self.peerId { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebApp_OpenBot, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Bots"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.default) + +// if let strongSelf = self { +// strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: strongSelf., context: strongSelf.context, chatLocation: .peer(id: strongSelf.peerId))) +// } + }))) + } - items.append(.action(ContextMenuActionItem(text: "Reload Page", icon: { theme in + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebApp_ReloadPage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) @@ -247,11 +318,15 @@ public final class WebAppController: ViewController, AttachmentContainable { }))) - items.append(.action(ContextMenuActionItem(text: "Remove Bot", textColor: .destructive, icon: { theme in + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebApp_RemoveBot, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) - }, action: { _, f in + }, action: { [weak self] _, f in f(.default) + if let strongSelf = self { + let _ = context.engine.messages.removeBotFromAttachMenu(peerId: strongSelf.botId).start() + strongSelf.dismiss() + } }))) let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .reference(WebAppContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) @@ -259,9 +334,9 @@ public final class WebAppController: ViewController, AttachmentContainable { } override public func loadDisplayNode() { - self.displayNode = Node(context: self.context, presentationData: self.presentationData, url: self.url, present: { [weak self] c, a in + self.displayNode = Node(context: self.context, controller: self, presentationData: self.presentationData, peerId: self.peerId, botId: self.botId, url: self.url, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) - }, message: self.message) + }) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {