Various improvements

This commit is contained in:
Ilya Laktyushin 2024-11-08 20:22:19 +01:00
parent 85b6a8ffe9
commit ef4d0adbb9
16 changed files with 571 additions and 344 deletions

View file

@ -905,7 +905,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
}
} else {
let controller = legacyICloudFilePicker(theme: self.presentationData.theme, mode: .export, url: url, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in
let _ = tempFile
})
self.present(controller, nil)
}

View file

@ -1309,7 +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[-11046771] = { 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) }

View file

@ -866,13 +866,13 @@ public extension Api.messages {
}
public extension Api.messages {
enum PreparedInlineMessage: TypeConstructorDescription {
case preparedInlineMessage(queryId: Int64, result: Api.BotInlineResult, peerTypes: [Api.InlineQueryPeerType], users: [Api.User])
case preparedInlineMessage(queryId: Int64, result: Api.BotInlineResult, peerTypes: [Api.InlineQueryPeerType], cacheTime: Int32, users: [Api.User])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .preparedInlineMessage(let queryId, let result, let peerTypes, let users):
case .preparedInlineMessage(let queryId, let result, let peerTypes, let cacheTime, let users):
if boxed {
buffer.appendInt32(1636301421)
buffer.appendInt32(-11046771)
}
serializeInt64(queryId, buffer: buffer, boxed: false)
result.serialize(buffer, true)
@ -881,6 +881,7 @@ public extension Api.messages {
for item in peerTypes {
item.serialize(buffer, true)
}
serializeInt32(cacheTime, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(users.count))
for item in users {
@ -892,8 +893,8 @@ public extension Api.messages {
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)])
case .preparedInlineMessage(let queryId, let result, let peerTypes, let cacheTime, let users):
return ("preparedInlineMessage", [("queryId", queryId as Any), ("result", result as Any), ("peerTypes", peerTypes as Any), ("cacheTime", cacheTime as Any), ("users", users as Any)])
}
}
@ -908,16 +909,19 @@ public extension Api.messages {
if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InlineQueryPeerType.self)
}
var _4: [Api.User]?
var _4: Int32?
_4 = reader.readInt32()
var _5: [Api.User]?
if let _ = reader.readInt32() {
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
_5 = 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!)
let _c5 = _5 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 {
return Api.messages.PreparedInlineMessage.preparedInlineMessage(queryId: _1!, result: _2!, peerTypes: _3!, cacheTime: _4!, users: _5!)
}
else {
return nil

View file

@ -3,19 +3,23 @@ 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
init(apiType: [Api.InlineQueryPeerType]) {
var rawValue: Int32 = 0
for type in apiType {
switch type {
case .inlineQueryPeerTypePM:
rawValue |= ReplyMarkupButtonAction.PeerTypes.users.rawValue
case .inlineQueryPeerTypeBotPM:
rawValue |= ReplyMarkupButtonAction.PeerTypes.bots.rawValue
case .inlineQueryPeerTypeBroadcast:
rawValue |= ReplyMarkupButtonAction.PeerTypes.channels.rawValue
case .inlineQueryPeerTypeChat, .inlineQueryPeerTypeMegagroup:
rawValue |= ReplyMarkupButtonAction.PeerTypes.groups.rawValue
case .inlineQueryPeerTypeSameBotPM:
break
}
}
self.init(rawValue: rawValue)
}
}

View file

@ -2162,6 +2162,34 @@ public extension TelegramEngine.EngineData.Item {
}
}
public struct BotAppSettings: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = Optional<TelegramCore.BotAppSettings>
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.botInfo?.appSettings
} else {
return nil
}
}
}
public struct BotCommands: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = Optional<[BotCommand]>

View file

@ -5,9 +5,10 @@ import TelegramApi
import MtProtoKit
public struct PreparedInlineMessage: Equatable {
let queryId: Int64
let result: ChatContextResult
let peerTypes: [ReplyMarkupButtonAction.PeerTypes]
public let botId: EnginePeer.Id
public let queryId: Int64
public let result: ChatContextResult
public let peerTypes: ReplyMarkupButtonAction.PeerTypes
}
func _internal_getPreparedInlineMessage(account: Account, botId: EnginePeer.Id, id: String) -> Signal<PreparedInlineMessage?, NoError> {
@ -29,10 +30,15 @@ func _internal_getPreparedInlineMessage(account: Account, botId: EnginePeer.Id,
}
return account.postbox.transaction { transaction -> PreparedInlineMessage? in
switch result {
case let .preparedInlineMessage(queryId, result, peerTypes, users):
case let .preparedInlineMessage(queryId, result, apiPeerTypes, cacheTime, 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) })
let _ = cacheTime
return PreparedInlineMessage(
botId: botId,
queryId: queryId,
result: ChatContextResult(apiResult: result, queryId: queryId),
peerTypes: ReplyMarkupButtonAction.PeerTypes(apiType: apiPeerTypes)
)
}
}
}

View file

@ -7,6 +7,7 @@ public enum EngineOutgoingMessageContent {
case text(String, [MessageTextEntity])
case file(FileMediaReference)
case contextResult(ChatContextResultCollection, ChatContextResult)
case preparedInlineMessage(PreparedInlineMessage)
}
public final class StoryPreloadInfo {
@ -249,7 +250,9 @@ public extension TelegramEngine {
scheduleTime: Int32? = nil
) -> Signal<[MessageId?], NoError> {
var message: EnqueueMessage?
if case let .contextResult(results, result) = content {
if case let .preparedInlineMessage(preparedInlineMessage) = content {
message = self.outgoingMessageWithChatContextResult(to: peerId, threadId: nil, botId: preparedInlineMessage.botId, result: preparedInlineMessage.result, replyToMessageId: replyToMessageId, replyToStoryId: storyId, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: nil)
} else if case let .contextResult(results, result) = content {
message = self.outgoingMessageWithChatContextResult(to: peerId, threadId: nil, botId: results.botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: storyId, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: nil)
} else {
var attributes: [MessageAttribute] = []

View file

@ -1610,6 +1610,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
}
if isPreview, let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramUser, peer.firstName == nil {
hasAvatar = false
effectiveAuthor = nil
}
var isInstantVideo = false
if let forwardInfo = item.content.firstMessage.forwardInfo, forwardInfo.source == nil, forwardInfo.author?.id.namespace == Namespaces.Peer.CloudUser {
for media in item.content.firstMessage.media {

View file

@ -1555,7 +1555,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|> deliverOnMainQueue).startStandalone()
}))
if data.webAppPermissions?.location?.isRequested == true {
if data.webAppPermissions?.location?.isRequested == true || data.webAppPermissions?.location?.isAllowed == 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))
@ -5459,6 +5459,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
}
var appSettings: BotAppSettings?
if let settings = self.data?.cachedData as? CachedUserData {
appSettings = settings.botInfo?.appSettings
}
let presentationData = self.presentationData
let proceed: (Bool) -> Void = { [weak self] installed in
guard let self else {
@ -5466,7 +5471,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
let context = self.context
let peerId = self.peerId
let params = WebAppParameters(source: .settings, peerId: self.context.account.peerId, botId: bot.peer.id, botName: bot.peer.compactDisplayTitle, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: bot.flags.contains(.hasSettings), fullSize: true)
let params = WebAppParameters(source: .settings, peerId: self.context.account.peerId, botId: bot.peer.id, botName: bot.peer.compactDisplayTitle, botVerified: bot.peer.isVerified, botAddress: bot.peer.addressName ?? "", url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: bot.flags.contains(.hasSettings), fullSize: true, appSettings: appSettings)
var openUrlImpl: ((String, Bool, Bool, @escaping () -> Void) -> Void)?
var presentImpl: ((ViewController, Any?) -> Void)?

View file

@ -94,187 +94,72 @@ func openWebAppImpl(
}
}
let openWebView = { [weak parentController] in
guard let parentController else {
return
}
if source == .menu {
if let parentController = parentController as? ChatControllerImpl {
parentController.updateChatPresentationInterfaceState(interactive: false) { state in
return state.updatedForceInputCommandsHidden(true)
}
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.BotAppSettings(id: botPeer.id))
|> deliverOnMainQueue).start(next: { appSettings in
let openWebView = { [weak parentController] in
guard let parentController else {
return
}
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 == botPeer.id && mainController.source == .menu {
navigationController.maximizeViewController(controller, animated: true)
return
}
}
}
var fullSize = false
var isFullscreen = false
if isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, url: url), case .peer(_, .appStart) = internalUrl {
if url.contains("mode=fullscreen") {
isFullscreen = true
fullSize = true
} else {
fullSize = !url.contains("mode=compact")
}
}
var presentImpl: ((ViewController, Any?) -> Void)?
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: 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: 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, {
return parentController.chatDisplayNode.textInputPanelNode?.makeAttachmentMenuTransition(accessoryPanelNode: nil)
})
} else {
return nil
}
}, completion: { [weak parentController] in
if let parentController = parentController as? ChatControllerImpl {
parentController.chatDisplayNode.historyNode.scrollToEndOfHistory()
}
}, willDismiss: { [weak parentController] in
if let parentController = parentController as? ChatControllerImpl {
parentController.interfaceInteraction?.updateShowWebView { _ in
return false
}
}
}, didDismiss: { [weak parentController] in
if source == .menu {
if let parentController = parentController as? ChatControllerImpl {
parentController.updateChatPresentationInterfaceState(interactive: false) { state in
return state.updatedForceInputCommandsHidden(false)
return state.updatedForceInputCommandsHidden(true)
}
}
}, getNavigationController: { [weak parentController] in
var navigationController: NavigationController?
if let parentController = parentController as? ChatControllerImpl {
navigationController = parentController.effectiveNavigationController
}
return navigationController ?? (context.sharedContext.mainWindow?.viewController as? NavigationController)
})
controller.navigationPresentation = .flatModal
parentController.push(controller)
presentImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
} else if simple {
var isInline = false
var botId = botPeer.id
var botName = botName
var botAddress = ""
var botVerified = botPeer.isVerified
if case let .inline(bot) = source {
isInline = true
botId = bot.id
botName = bot.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
botAddress = bot.addressName ?? ""
botVerified = bot.isVerified
}
let messageActionCallbackDisposable: MetaDisposable
if let parentController = parentController as? ChatControllerImpl {
messageActionCallbackDisposable = parentController.messageActionCallbackDisposable
} else {
messageActionCallbackDisposable = MetaDisposable()
}
let webViewSignal: Signal<RequestWebViewResult, RequestWebViewError>
let webViewSource: RequestSimpleWebViewSource
if let payload {
webViewSource = .inline(startParam: payload)
} else {
webViewSource = .generic
}
if url.isEmpty {
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))
}
messageActionCallbackDisposable.set(((webViewSignal
|> afterDisposed {
updateProgress()
})
|> deliverOnMainQueue).start(next: { [weak parentController] result in
guard let parentController else {
return
}
var presentImpl: ((ViewController, Any?) -> Void)?
let source: WebAppParameters.Source
if isInline {
source = .inline
} else {
source = url.isEmpty ? .generic : .simple
}
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: 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: chatPeer?.id ?? botId, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion)
}, getNavigationController: { [weak parentController] in
var navigationController: NavigationController?
if let parentController = parentController as? ChatControllerImpl {
navigationController = parentController.effectiveNavigationController
}
return navigationController ?? (context.sharedContext.mainWindow?.viewController as? NavigationController)
})
controller.navigationPresentation = .flatModal
if let parentController = parentController as? ChatControllerImpl {
parentController.currentWebAppController = controller
}
parentController.push(controller)
presentImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
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 == botPeer.id && mainController.source == .menu {
navigationController.maximizeViewController(controller, animated: true)
return
}
}
}
}, error: { [weak parentController] error in
if let parentController {
parentController.present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
})]), in: .window(.root))
}
}))
} else {
let messageActionCallbackDisposable: MetaDisposable
if let parentController = parentController as? ChatControllerImpl {
messageActionCallbackDisposable = parentController.messageActionCallbackDisposable
} else {
messageActionCallbackDisposable = MetaDisposable()
}
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()
})
|> deliverOnMainQueue).startStandalone(next: { [weak parentController] result in
guard let parentController else {
return
var fullSize = false
var isFullscreen = false
if isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, url: url), case .peer(_, .appStart) = internalUrl {
if url.contains("mode=fullscreen") {
isFullscreen = true
fullSize = true
} else {
fullSize = !url.contains("mode=compact")
}
}
var presentImpl: ((ViewController, Any?) -> Void)?
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 params = WebAppParameters(source: .menu, peerId: chatPeer?.id ?? botPeer.id, botId: botPeer.id, botName: botName, botVerified: botVerified, botAddress: botPeer.addressName ?? "", url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: fullSize, isFullscreen: isFullscreen, appSettings: appSettings)
let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit 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: 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, {
return parentController.chatDisplayNode.textInputPanelNode?.makeAttachmentMenuTransition(accessoryPanelNode: nil)
})
} else {
return nil
}
}, completion: { [weak parentController] in
if let parentController = parentController as? ChatControllerImpl {
parentController.chatDisplayNode.historyNode.scrollToEndOfHistory()
}
}, willDismiss: { [weak parentController] in
if let parentController = parentController as? ChatControllerImpl {
parentController.interfaceInteraction?.updateShowWebView { _ in
return false
}
}
}, didDismiss: { [weak parentController] in
if let parentController = parentController as? ChatControllerImpl {
parentController.updateChatPresentationInterfaceState(interactive: false) { state in
return state.updatedForceInputCommandsHidden(false)
}
}
}, getNavigationController: { [weak parentController] in
var navigationController: NavigationController?
if let parentController = parentController as? ChatControllerImpl {
@ -283,51 +168,169 @@ func openWebAppImpl(
return navigationController ?? (context.sharedContext.mainWindow?.viewController as? NavigationController)
})
controller.navigationPresentation = .flatModal
if let parentController = parentController as? ChatControllerImpl {
parentController.currentWebAppController = controller
}
parentController.push(controller)
presentImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
}, error: { [weak parentController] error in
if let parentController {
parentController.present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
})]), in: .window(.root))
} else if simple {
var isInline = false
var botId = botPeer.id
var botName = botName
var botAddress = ""
var botVerified = botPeer.isVerified
if case let .inline(bot) = source {
isInline = true
botId = bot.id
botName = bot.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
botAddress = bot.addressName ?? ""
botVerified = bot.isVerified
}
}))
}
}
if skipTermsOfService {
openWebView()
} else {
var botPeer = botPeer
if case let .inline(bot) = source {
botPeer = bot
}
let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id)
|> deliverOnMainQueue).startStandalone(next: { [weak parentController] value in
guard let parentController else {
return
}
if value {
openWebView()
} else {
let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: updatedPresentationData, peer: botPeer, completion: { _ in
let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone()
openWebView()
}, showMore: nil, openTerms: {
if let navigationController = parentController.navigationController as? NavigationController {
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: presentationData.strings.WebApp_LaunchTermsConfirmation_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
}
let messageActionCallbackDisposable: MetaDisposable
if let parentController = parentController as? ChatControllerImpl {
messageActionCallbackDisposable = parentController.messageActionCallbackDisposable
} else {
messageActionCallbackDisposable = MetaDisposable()
}
let webViewSignal: Signal<RequestWebViewResult, RequestWebViewError>
let webViewSource: RequestSimpleWebViewSource
if let payload {
webViewSource = .inline(startParam: payload)
} else {
webViewSource = .generic
}
if url.isEmpty {
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))
}
messageActionCallbackDisposable.set(((webViewSignal
|> afterDisposed {
updateProgress()
})
parentController.present(controller, in: .window(.root))
|> deliverOnMainQueue).start(next: { [weak parentController] result in
guard let parentController else {
return
}
var presentImpl: ((ViewController, Any?) -> Void)?
let source: WebAppParameters.Source
if isInline {
source = .inline
} else {
source = url.isEmpty ? .generic : .simple
}
let params = WebAppParameters(source: source, peerId: chatPeer?.id ?? botId, botId: botId, botName: botName, botVerified: botVerified, botAddress: botPeer.addressName ?? "", url: result.url, queryId: nil, payload: payload, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen), appSettings: appSettings)
let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit 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: chatPeer?.id ?? botId, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion)
}, getNavigationController: { [weak parentController] in
var navigationController: NavigationController?
if let parentController = parentController as? ChatControllerImpl {
navigationController = parentController.effectiveNavigationController
}
return navigationController ?? (context.sharedContext.mainWindow?.viewController as? NavigationController)
})
controller.navigationPresentation = .flatModal
if let parentController = parentController as? ChatControllerImpl {
parentController.currentWebAppController = controller
}
parentController.push(controller)
presentImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
}, error: { [weak parentController] error in
if let parentController {
parentController.present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
})]), in: .window(.root))
}
}))
} else {
let messageActionCallbackDisposable: MetaDisposable
if let parentController = parentController as? ChatControllerImpl {
messageActionCallbackDisposable = parentController.messageActionCallbackDisposable
} else {
messageActionCallbackDisposable = MetaDisposable()
}
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()
})
|> deliverOnMainQueue).startStandalone(next: { [weak parentController] result in
guard let parentController else {
return
}
var presentImpl: ((ViewController, Any?) -> Void)?
let params = WebAppParameters(source: .button, peerId: chatPeer?.id ?? botPeer.id, botId: botPeer.id, botName: botName, botVerified: botVerified, botAddress: botPeer.addressName ?? "", 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), appSettings: appSettings)
let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit 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
if let parentController = parentController as? ChatControllerImpl {
parentController.chatDisplayNode.historyNode.scrollToEndOfHistory()
}
}, getNavigationController: { [weak parentController] in
var navigationController: NavigationController?
if let parentController = parentController as? ChatControllerImpl {
navigationController = parentController.effectiveNavigationController
}
return navigationController ?? (context.sharedContext.mainWindow?.viewController as? NavigationController)
})
controller.navigationPresentation = .flatModal
if let parentController = parentController as? ChatControllerImpl {
parentController.currentWebAppController = controller
}
parentController.push(controller)
presentImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
}, error: { [weak parentController] error in
if let parentController {
parentController.present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
})]), in: .window(.root))
}
}))
}
})
}
}
if skipTermsOfService {
openWebView()
} else {
var botPeer = botPeer
if case let .inline(bot) = source {
botPeer = bot
}
let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id)
|> deliverOnMainQueue).startStandalone(next: { [weak parentController] value in
guard let parentController else {
return
}
if value {
openWebView()
} else {
let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: updatedPresentationData, peer: botPeer, completion: { _ in
let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone()
openWebView()
}, showMore: nil, openTerms: {
if let navigationController = parentController.navigationController as? NavigationController {
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: presentationData.strings.WebApp_LaunchTermsConfirmation_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
}
})
parentController.present(controller, in: .window(.root))
}
})
}
})
}
public extension ChatControllerImpl {
@ -496,7 +499,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), isFullscreen: result.flags.contains(.fullScreen))
let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, botVerified: botPeer.isVerified, botAddress: botPeer.addressName ?? "", 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), appSettings: nil)
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

View file

@ -13,6 +13,7 @@ import TelegramNotices
import PresentationDataUtils
import TelegramCallsUI
import AttachmentUI
import WebUI
func updateChatPresentationInterfaceStateImpl(
selfController: ChatControllerImpl,
@ -591,7 +592,7 @@ func updateChatPresentationInterfaceStateImpl(
if selfController.presentationInterfaceState.hasBirthdayToday {
selfController.displayBirthdayTooltip()
}
if case .standard(.embedded) = selfController.presentationInterfaceState.mode, let controllerInteraction = selfController.controllerInteraction, let interfaceInteraction = selfController.interfaceInteraction {
if let titleAccessoryPanelNode = titlePanelForChatPresentationInterfaceState(selfController.presentationInterfaceState, context: selfController.context, currentPanel: selfController.customNavigationPanelNode as? ChatTitleAccessoryPanelNode, controllerInteraction: controllerInteraction, interfaceInteraction: interfaceInteraction, force: true) {
selfController.customNavigationPanelNode = titleAccessoryPanelNode as? ChatControllerCustomNavigationPanelNode
@ -600,5 +601,9 @@ func updateChatPresentationInterfaceStateImpl(
}
}
if let cachedData = selfController.peerView?.cachedData as? CachedUserData, let appSettings = cachedData.botInfo?.appSettings {
let _ = WebAppController.preloadAppPlaceholder(context: selfController.context, appSettings: appSettings).startStandalone()
}
selfController.stateUpdated?(transition)
}

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, isFullscreen: false)
let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, botVerified: bot.peer.isVerified, botAddress: bot.peer.addressName ?? "", 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

@ -20,6 +20,7 @@ swift_library(
"//submodules/AttachmentUI:AttachmentUI",
"//submodules/HexColor:HexColor",
"//submodules/PhotoResources:PhotoResources",
"//submodules/MediaResources",
"//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/UrlHandling:UrlHandling",

View file

@ -15,6 +15,7 @@ import PresentationDataUtils
import HexColor
import ShimmerEffect
import PhotoResources
import MediaResources
import LegacyComponents
import UrlHandling
import MoreButtonNode
@ -33,6 +34,7 @@ import OverlayStatusController
import TelegramUIPreferences
import CoreMotion
import DeviceLocationManager
import LegacyMediaPickerUI
private let durgerKingBotIds: [Int64] = [5104055776, 2200339955]
@ -60,6 +62,7 @@ public struct WebAppParameters {
let botId: PeerId
let botName: String
let botVerified: Bool
let botAddress: String
let url: String?
let queryId: Int64?
let payload: String?
@ -68,6 +71,7 @@ public struct WebAppParameters {
let forceHasSettings: Bool
let fullSize: Bool
let isFullscreen: Bool
let appSettings: BotAppSettings?
public init(
source: Source,
@ -75,6 +79,7 @@ public struct WebAppParameters {
botId: PeerId,
botName: String,
botVerified: Bool,
botAddress: String,
url: String?,
queryId: Int64?,
payload: String?,
@ -82,25 +87,24 @@ public struct WebAppParameters {
keepAliveSignal: Signal<Never, KeepWebViewError>?,
forceHasSettings: Bool,
fullSize: Bool,
isFullscreen: Bool = false
isFullscreen: Bool = false,
appSettings: BotAppSettings? = nil
) {
self.source = source
self.peerId = peerId
self.botId = botId
self.botName = botName
self.botVerified = botVerified
self.botAddress = botAddress
self.url = url
self.queryId = queryId
self.payload = payload
self.buttonText = buttonText
self.keepAliveSignal = keepAliveSignal
self.forceHasSettings = forceHasSettings
self.fullSize = fullSize
// #if DEBUG
// self.isFullscreen = true
// #else
self.fullSize = fullSize || isFullscreen
self.isFullscreen = isFullscreen
// #endif
self.appSettings = appSettings
}
}
@ -239,7 +243,21 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.placeholderNode = placeholderNode
let placeholder: Signal<(FileMediaReference, Bool)?, NoError>
if durgerKingBotIds.contains(controller.botId.id._internalGetInt64Value()) {
if let botAppSettings = controller.botAppSettings {
Queue.mainQueue().justDispatch {
if let backgroundColor = botAppSettings.backgroundColor {
self.appBackgroundColor = UIColor(rgb: UInt32(bitPattern: backgroundColor))
self.updateBackgroundColor(transition: .immediate)
}
if let headerColor = botAppSettings.headerColor {
self.headerColor = UIColor(rgb: UInt32(bitPattern: headerColor))
self.updateHeaderBackgroundColor(transition: .immediate)
}
}
}
if let placeholderFile = controller.botAppSettings?.placeholder {
placeholder = .single((.standalone(media: placeholderFile), false))
} else if durgerKingBotIds.contains(controller.botId.id._internalGetInt64Value()) {
placeholder = .single(nil)
|> delay(0.05, queue: Queue.mainQueue())
} else {
@ -289,7 +307,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
if let fileReference = fileReference {
let _ = freeMediaFileInteractiveFetched(account: strongSelf.context.account, userLocation: .other, fileReference: fileReference).start()
}
strongSelf.placeholderDisposable.set((svgIconImageFile(account: strongSelf.context.account, fileReference: fileReference, stickToTop: isPlaceholder)
let _ = (svgIconImageFile(account: strongSelf.context.account, fileReference: fileReference, stickToTop: isPlaceholder)
|> deliverOnMainQueue).start(next: { [weak self] transform in
if let strongSelf = self {
let imageSize: CGSize
@ -309,7 +327,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
strongSelf.placeholderNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}))
})
}))
self.iconDisposable = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId))
@ -461,9 +479,24 @@ public final class WebAppController: ViewController, AttachmentContainable {
shapes = [.image(image: image, rect: CGRect(origin: CGPoint(), size: image.size))]
placeholderSize = image.size
}
let theme = self.presentationData.theme
self.placeholderNode?.update(backgroundColor: .clear, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, horizontal: true, size: placeholderSize, mask: true)
let foregroundColor: UIColor
let shimmeringColor: UIColor
if let backgroundColor = self.appBackgroundColor {
if backgroundColor.lightness > 0.705 {
foregroundColor = backgroundColor.mixedWith(UIColor(rgb: 0x000000), alpha: 0.05)
shimmeringColor = UIColor.white.withAlphaComponent(0.2)
} else {
foregroundColor = backgroundColor.mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05)
shimmeringColor = UIColor.white.withAlphaComponent(0.4)
}
} else {
let theme = self.presentationData.theme
foregroundColor = theme.list.mediaPlaceholderColor
shimmeringColor = theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4)
}
self.placeholderNode?.update(backgroundColor: .clear, foregroundColor: foregroundColor, shimmeringColor: shimmeringColor, shapes: shapes, horizontal: true, size: placeholderSize, mask: true)
return placeholderSize
}
@ -1097,8 +1130,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
case "web_app_set_background_color":
if let json = json, let colorValue = json["color"] as? String, let color = UIColor(hexString: colorValue) {
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear)
transition.updateBackgroundColor(node: self.backgroundNode, color: color)
self.appBackgroundColor = color
self.updateBackgroundColor(transition: .animated(duration: 0.2, curve: .linear))
}
case "web_app_set_header_color":
if let json = json {
@ -1403,7 +1436,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
fileprivate var needDismissConfirmation = false
fileprivate var appBackgroundColor: UIColor?
fileprivate var headerColor: UIColor?
fileprivate var headerPrimaryTextColor: UIColor?
private var headerColorKey: String?
@ -1415,6 +1449,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
fileprivate let bottomPanelColorPromise = Promise<UIColor?>(nil)
private func updateBackgroundColor(transition: ContainedViewLayoutTransition) {
transition.updateBackgroundColor(node: self.backgroundNode, color: self.appBackgroundColor ?? .clear)
}
private func updateHeaderBackgroundColor(transition: ContainedViewLayoutTransition) {
guard let controller = self.controller else {
return
@ -2143,7 +2181,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
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
let previewController = WebAppMessagePreviewScreen(context: controller.context, botName: controller.botName, botAddress: controller.botAddress, preparedMessage: preparedMessage, completion: { [weak self] result in
guard let self else {
return
}
@ -2162,6 +2200,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
guard let controller = self.controller else {
return
}
var isMedia = true
var title: String?
let photoExtensions = [".jpg", ".png", ".gif", ".tiff"]
let videoExtensions = [".mp4", ".mov"]
@ -2182,6 +2221,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
if title == nil {
title = "Download Document"
isMedia = false
}
let _ = (FileDownload.getFileSize(url: url)
@ -2201,7 +2241,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
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)
self?.startDownload(url: url, fileName: fileName, fileSize: fileSize, isMedia: isMedia)
})
], parseMarkdown: true)
controller.present(alertController, in: .window(.root))
@ -2210,7 +2250,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
private var fileDownload: FileDownload?
private weak var fileDownloadTooltip: UndoOverlayController?
fileprivate func startDownload(url: String, fileName: String, fileSize: Int64?) {
fileprivate func startDownload(url: String, fileName: String, fileSize: Int64?, isMedia: Bool) {
guard let controller = self.controller else {
return
}
@ -2238,8 +2278,70 @@ public final class WebAppController: ViewController, AttachmentContainable {
undoText: "Cancel"
)
},
completion: { resultUrl, _ in
completion: { [weak self] resultUrl, _ in
if let resultUrl, let self {
if let tooltip = self.fileDownloadTooltip {
tooltip.dismissWithCommitAction()
}
if isMedia {
let saveToPhotos: (URL, Bool) -> Void = { url, isVideo in
var fileExtension = (resultUrl.absoluteString as NSString).pathExtension
if fileExtension.isEmpty {
fileExtension = "mp4"
}
PHPhotoLibrary.shared().performChanges({
if isVideo {
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
} else {
if let fileData = try? Data(contentsOf: url) {
PHAssetCreationRequest.forAsset().addResource(with: .photo, data: fileData, options: nil)
}
}
}, completionHandler: { _, error in
})
}
let isVideo = fileName.lowercased().hasSuffix(".mp4") || fileName.lowercased().hasSuffix(".mov")
saveToPhotos(resultUrl, isVideo)
let tooltipController = UndoOverlayController(
presentationData: self.presentationData,
content: .actionSucceeded(title: fileName, text: isMedia ? "Saved to Photos" : "Saved to Files", cancel: nil, destructive: false),
elevatedLayout: false,
position: .top,
action: { _ in
return true
}
)
controller.present(tooltipController, in: .current)
} else {
let tempFile = TempBox.shared.file(path: resultUrl.absoluteString, fileName: fileName)
let url = URL(fileURLWithPath: tempFile.path)
try? FileManager.default.copyItem(at: resultUrl, to: url)
let pickerController = legacyICloudFilePicker(theme: self.presentationData.theme, mode: .export, url: url, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { [weak self, weak controller] urls in
guard let self, let controller, !urls.isEmpty else {
return
}
let tooltipController = UndoOverlayController(
presentationData: self.presentationData,
content: .actionSucceeded(title: fileName, text: isMedia ? "Saved to Photos" : "Saved to Files", cancel: nil, destructive: false),
elevatedLayout: false,
position: .top,
action: { _ in
return true
}
)
controller.present(tooltipController, in: .current)
})
controller.present(pickerController, in: .window(.root))
//
// if let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
// let targetUrl = documentsUrl.appendingPathComponent(fileName)
// try? FileManager.default.copyItem(at: resultUrl, to: targetUrl)
// }
}
}
}
)
@ -2264,7 +2366,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
return true
}
)
controller.present(tooltipController, in: .window(.root))
controller.present(tooltipController, in: .current)
self.fileDownloadTooltip = tooltipController
}
@ -2551,6 +2653,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
public let botId: PeerId
fileprivate let botName: String
fileprivate let botVerified: Bool
fileprivate let botAppSettings: BotAppSettings?
fileprivate let botAddress: String
private let url: String?
private let queryId: Int64?
private let payload: String?
@ -2579,6 +2683,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.botId = params.botId
self.botName = params.botName
self.botVerified = params.botVerified
self.botAppSettings = params.appSettings
self.botAddress = params.botAddress
self.url = params.url
self.queryId = params.queryId
self.payload = params.payload
@ -2605,6 +2711,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
super.init(navigationBarPresentationData: navigationBarPresentationData)
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.automaticallyControlPresentationContextLayout = false
if !self.isFullscreen {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: self.cancelButtonNode)
@ -2905,6 +3012,14 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.validLayout = layout
super.containerLayoutUpdated(layout, transition: transition)
var presentationLayout = layout
if self.isFullscreen {
presentationLayout.intrinsicInsets.top = (presentationLayout.statusBarHeight ?? 0.0) + 36.0
} else {
presentationLayout.intrinsicInsets.top = 56.0
}
self.presentationContext.containerLayoutUpdated(presentationLayout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
@ -3015,6 +3130,30 @@ public final class WebAppController: ViewController, AttachmentContainable {
})
return imageView
}
public static func preloadAppPlaceholder(context: AccountContext, appSettings: BotAppSettings) -> Signal<Never, NoError> {
guard let file = appSettings.placeholder else {
return .complete()
}
let path = context.account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedPreparedSvgRepresentation())
if !FileManager.default.fileExists(atPath: path) {
let accountFullSizeData = Signal<(Data?, Bool), NoError> { subscriber in
let accountResource = context.account.postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedPreparedSvgRepresentation(), complete: false, fetch: true)
let fetchedFullSize = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: MediaResourceUserContentType(file: file), reference: .standalone(resource: file.resource))
let fetchedFullSizeDisposable = fetchedFullSize.start()
let fullSizeDisposable = accountResource.start()
return ActionDisposable {
fetchedFullSizeDisposable.dispose()
fullSizeDisposable.dispose()
}
}
return accountFullSizeData
|> ignoreValues
}
return .complete()
}
}
final class WebAppPickerContext: AttachmentMediaPickerContext {

View file

@ -17,49 +17,22 @@ 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
}
if areMediaArraysEqual(lhs.media, rhs.media) {
return false
}
if lhs.botAddress != rhs.botAddress {
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 entities: TextEntitiesMessageAttribute?
let media: [Media]
let botAddress: String
}
let context: AccountContext
@ -202,9 +175,7 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode {
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)
@ -214,40 +185,27 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode {
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))
let authorPeerId = EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0))
let botPeerId = EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1))
var peers = SimpleDictionary<PeerId, Peer>()
var messages = SimpleDictionary<MessageId, Message>()
let 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))))
}
peers[authorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)
peers[botPeerId] = TelegramUser(id: botPeerId, accessHash: nil, firstName: messageItem.botAddress, lastName: "", username: messageItem.botAddress, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)
let media = messageItem.media
var attributes: [MessageAttribute] = []
if messageItem.reply != nil {
attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false))
if let entities = messageItem.entities {
attributes.append(entities)
}
attributes.append(InlineBotMessageAttribute(peerId: nil, title: "Test Attach"))
attributes.append(InlineBotMessageAttribute(peerId: botPeerId, title: nil))
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: [:])
let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: authorPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.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))
}
@ -311,16 +269,6 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode {
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 {

View file

@ -27,17 +27,20 @@ private final class SheetContent: CombinedComponent {
let context: AccountContext
let botName: String
let botAddress: String
let preparedMessage: PreparedInlineMessage
let dismiss: () -> Void
init(
context: AccountContext,
botName: String,
botAddress: String,
preparedMessage: PreparedInlineMessage,
dismiss: @escaping () -> Void
) {
self.context = context
self.botName = botName
self.botAddress = botAddress
self.preparedMessage = preparedMessage
self.dismiss = dismiss
}
@ -101,7 +104,7 @@ private final class SheetContent: CombinedComponent {
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 amountInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("\(component.botName) 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,
@ -120,17 +123,45 @@ private final class SheetContent: CombinedComponent {
}
}
))
var text: String = ""
var entities: TextEntitiesMessageAttribute?
var media: [Media] = []
switch component.preparedMessage.result {
case let .internalReference(reference):
switch reference.message {
case let .auto(textValue, entitiesValue, _):
text = textValue
entities = entitiesValue
if let file = reference.file {
media = [file]
} else if let image = reference.image {
media = [image]
}
case let .text(textValue, entitiesValue, disableUrlPreview, previewParameters, _):
text = textValue
entities = entitiesValue
let _ = disableUrlPreview
let _ = previewParameters
case let .contact(contact, _):
media = [contact]
case let .mapLocation(map, _):
media = [map]
case let .invoice(invoice, _):
media = [invoice]
default:
break
}
case let .externalReference(reference):
let _ = reference
}
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"
text: text,
entities: entities,
media: media,
botAddress: component.botAddress
)
let listItemParams = ListViewItemLayoutParams(width: context.availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true)
@ -197,7 +228,7 @@ private final class SheetContent: CombinedComponent {
displaysProgress: false,
action: {
if let controller = controller() as? WebAppMessagePreviewScreen {
let _ = controller
controller.proceed()
}
}
),
@ -232,15 +263,18 @@ private final class WebAppMessagePreviewSheetComponent: CombinedComponent {
private let context: AccountContext
private let botName: String
private let botAddress: String
private let preparedMessage: PreparedInlineMessage
init(
context: AccountContext,
botName: String,
botAddress: String,
preparedMessage: PreparedInlineMessage
) {
self.context = context
self.botName = botName
self.botAddress = botAddress
self.preparedMessage = preparedMessage
}
@ -265,6 +299,7 @@ private final class WebAppMessagePreviewSheetComponent: CombinedComponent {
content: AnyComponent<EnvironmentType>(SheetContent(
context: context.component.context,
botName: context.component.botName,
botAddress: context.component.botAddress,
preparedMessage: context.component.preparedMessage,
dismiss: {
animateOut.invoke(Action { _ in
@ -290,12 +325,14 @@ private final class WebAppMessagePreviewSheetComponent: CombinedComponent {
dismiss: { animated in
if animated {
animateOut.invoke(Action { _ in
if let controller = controller() {
if let controller = controller() as? WebAppMessagePreviewScreen {
controller.completeWithResult(false)
controller.dismiss(completion: nil)
}
})
} else {
if let controller = controller() {
if let controller = controller() as? WebAppMessagePreviewScreen {
controller.completeWithResult(false)
controller.dismiss(completion: nil)
}
}
@ -317,15 +354,18 @@ private final class WebAppMessagePreviewSheetComponent: CombinedComponent {
public final class WebAppMessagePreviewScreen: ViewControllerComponentContainer {
private let context: AccountContext
private let preparedMessage: PreparedInlineMessage
fileprivate let completion: (Bool) -> Void
public init(
context: AccountContext,
botName: String,
botAddress: String,
preparedMessage: PreparedInlineMessage,
completion: @escaping (Bool) -> Void
) {
self.context = context
self.preparedMessage = preparedMessage
self.completion = completion
super.init(
@ -333,6 +373,7 @@ public final class WebAppMessagePreviewScreen: ViewControllerComponentContainer
component: WebAppMessagePreviewSheetComponent(
context: context,
botName: botName,
botAddress: botAddress,
preparedMessage: preparedMessage
),
navigationBarAppearance: .none,
@ -346,8 +387,43 @@ public final class WebAppMessagePreviewScreen: ViewControllerComponentContainer
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate func complete(peer: EnginePeer) {
self.completeWithResult(true)
self.dismiss()
let _ = self.context.engine.messages.enqueueOutgoingMessage(
to: peer.id,
replyTo: nil,
storyId: nil,
content: .preparedInlineMessage(preparedMessage)
).start()
}
private var completed = false
fileprivate func completeWithResult(_ result: Bool) {
guard !self.completed else {
return
}
self.completion(result)
}
fileprivate func proceed() {
let requestPeerType = self.preparedMessage.peerTypes.requestPeerTypes
let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: requestPeerType, hasContactSelector: false))
controller.peerSelected = { [weak self, weak controller] peer, _ in
guard let self else {
return
}
self.complete(peer: peer)
controller?.dismiss()
}
self.push(controller)
}
public func dismissAnimated() {
self.completeWithResult(false)
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}