Support miniapp fullscreen API.

This commit is contained in:
John Preston 2024-11-08 12:02:59 +04:00
parent c987872be8
commit 21487641c1
7 changed files with 123 additions and 14 deletions

View file

@ -618,6 +618,7 @@ bool ResolveUsernameOrPhone(
.startAutoSubmit = myContext.botStartAutoSubmit, .startAutoSubmit = myContext.botStartAutoSubmit,
.botAppName = (appname.isEmpty() ? postParam : appname), .botAppName = (appname.isEmpty() ? postParam : appname),
.botAppForceConfirmation = myContext.mayShowConfirmation, .botAppForceConfirmation = myContext.mayShowConfirmation,
.botAppFullScreen = (params.value(u"mode"_q) == u"fullscreen"_q),
.attachBotUsername = params.value(u"attach"_q), .attachBotUsername = params.value(u"attach"_q),
.attachBotToggleCommand = (params.contains(u"startattach"_q) .attachBotToggleCommand = (params.contains(u"startattach"_q)
? params.value(u"startattach"_q) ? params.value(u"startattach"_q)

View file

@ -914,6 +914,7 @@ void WebViewInstance::requestButton() {
using Flag = MTPmessages_RequestWebView::Flag; using Flag = MTPmessages_RequestWebView::Flag;
_requestId = _session->api().request(MTPmessages_RequestWebView( _requestId = _session->api().request(MTPmessages_RequestWebView(
MTP_flags(Flag::f_theme_params MTP_flags(Flag::f_theme_params
| (_context.fullscreen ? Flag::f_fullscreen : Flag(0))
| (_button.url.isEmpty() ? Flag(0) : Flag::f_url) | (_button.url.isEmpty() ? Flag(0) : Flag::f_url)
| (_button.startCommand.isEmpty() | (_button.startCommand.isEmpty()
? Flag(0) ? Flag(0)
@ -936,7 +937,11 @@ void WebViewInstance::requestButton() {
: MTP_inputPeerEmpty()) : MTP_inputPeerEmpty())
)).done([=](const MTPWebViewResult &result) { )).done([=](const MTPWebViewResult &result) {
const auto &data = result.data(); const auto &data = result.data();
show(qs(data.vurl()), data.vquery_id().value_or_empty()); show({
.url = qs(data.vurl()),
.queryId = data.vquery_id().value_or_empty(),
.fullscreen = data.is_fullscreen(),
});
}).fail([=](const MTP::Error &error) { }).fail([=](const MTP::Error &error) {
_parentShow->showToast(error.type()); _parentShow->showToast(error.type());
if (error.type() == u"BOT_INVALID"_q) { if (error.type() == u"BOT_INVALID"_q) {
@ -950,6 +955,7 @@ void WebViewInstance::requestSimple() {
using Flag = MTPmessages_RequestSimpleWebView::Flag; using Flag = MTPmessages_RequestSimpleWebView::Flag;
_requestId = _session->api().request(MTPmessages_RequestSimpleWebView( _requestId = _session->api().request(MTPmessages_RequestSimpleWebView(
MTP_flags(Flag::f_theme_params MTP_flags(Flag::f_theme_params
| (_context.fullscreen ? Flag::f_fullscreen : Flag(0))
| (v::is<WebViewSourceSwitch>(_source) | (v::is<WebViewSourceSwitch>(_source)
? (Flag::f_url | Flag::f_from_switch_webview) ? (Flag::f_url | Flag::f_from_switch_webview)
: v::is<WebViewSourceMainMenu>(_source) : v::is<WebViewSourceMainMenu>(_source)
@ -964,7 +970,11 @@ void WebViewInstance::requestSimple() {
MTP_dataJSON(MTP_bytes(botThemeParams().json)), MTP_dataJSON(MTP_bytes(botThemeParams().json)),
MTP_string("tdesktop") MTP_string("tdesktop")
)).done([=](const MTPWebViewResult &result) { )).done([=](const MTPWebViewResult &result) {
show(qs(result.data().vurl())); const auto &data = result.data();
show({
.url = qs(data.vurl()),
.fullscreen = data.is_fullscreen(),
});
}).fail([=](const MTP::Error &error) { }).fail([=](const MTP::Error &error) {
_parentShow->showToast(error.type()); _parentShow->showToast(error.type());
close(); close();
@ -975,6 +985,7 @@ void WebViewInstance::requestMain() {
using Flag = MTPmessages_RequestMainWebView::Flag; using Flag = MTPmessages_RequestMainWebView::Flag;
_requestId = _session->api().request(MTPmessages_RequestMainWebView( _requestId = _session->api().request(MTPmessages_RequestMainWebView(
MTP_flags(Flag::f_theme_params MTP_flags(Flag::f_theme_params
| (_context.fullscreen ? Flag::f_fullscreen : Flag(0))
| (_button.startCommand.isEmpty() | (_button.startCommand.isEmpty()
? Flag() ? Flag()
: Flag::f_start_param) : Flag::f_start_param)
@ -989,7 +1000,11 @@ void WebViewInstance::requestMain() {
MTP_dataJSON(MTP_bytes(botThemeParams().json)), MTP_dataJSON(MTP_bytes(botThemeParams().json)),
MTP_string("tdesktop") MTP_string("tdesktop")
)).done([=](const MTPWebViewResult &result) { )).done([=](const MTPWebViewResult &result) {
show(qs(result.data().vurl())); const auto &data = result.data();
show({
.url = qs(data.vurl()),
.fullscreen = data.is_fullscreen(),
});
}).fail([=](const MTP::Error &error) { }).fail([=](const MTP::Error &error) {
_parentShow->showToast(error.type()); _parentShow->showToast(error.type());
close(); close();
@ -1003,6 +1018,7 @@ void WebViewInstance::requestApp(bool allowWrite) {
using Flag = MTPmessages_RequestAppWebView::Flag; using Flag = MTPmessages_RequestAppWebView::Flag;
const auto app = _app; const auto app = _app;
const auto flags = Flag::f_theme_params const auto flags = Flag::f_theme_params
| (_context.fullscreen ? Flag::f_fullscreen : Flag(0))
| (_appStartParam.isEmpty() ? Flag(0) : Flag::f_start_param) | (_appStartParam.isEmpty() ? Flag(0) : Flag::f_start_param)
| (allowWrite ? Flag::f_write_allowed : Flag(0)); | (allowWrite ? Flag::f_write_allowed : Flag(0));
_requestId = _session->api().request(MTPmessages_RequestAppWebView( _requestId = _session->api().request(MTPmessages_RequestAppWebView(
@ -1014,7 +1030,11 @@ void WebViewInstance::requestApp(bool allowWrite) {
MTP_string("tdesktop") MTP_string("tdesktop")
)).done([=](const MTPWebViewResult &result) { )).done([=](const MTPWebViewResult &result) {
_requestId = 0; _requestId = 0;
show(qs(result.data().vurl())); const auto &data = result.data();
show({
.url = qs(data.vurl()),
.fullscreen = data.is_fullscreen(),
});
}).fail([=](const MTP::Error &error) { }).fail([=](const MTP::Error &error) {
_requestId = 0; _requestId = 0;
if (error.type() == u"BOT_INVALID"_q) { if (error.type() == u"BOT_INVALID"_q) {
@ -1093,7 +1113,7 @@ void WebViewInstance::maybeChooseAndRequestButton(PeerTypes supported) {
close(); close();
} }
void WebViewInstance::show(const QString &url, uint64 queryId) { void WebViewInstance::show(ShowArgs &&args) {
auto title = Info::Profile::NameValue(_bot); auto title = Info::Profile::NameValue(_bot);
auto titleBadge = _bot->isVerified() auto titleBadge = _bot->isVerified()
? object_ptr<Ui::RpWidget>(_parentShow->toastParent()) ? object_ptr<Ui::RpWidget>(_parentShow->toastParent())
@ -1131,18 +1151,19 @@ void WebViewInstance::show(const QString &url, uint64 queryId) {
|| v::is<WebViewSourceAttachMenu>(_source) || v::is<WebViewSourceAttachMenu>(_source)
|| (attached != end(bots) || (attached != end(bots)
&& (attached->inAttachMenu || attached->inMainMenu)); && (attached->inAttachMenu || attached->inMainMenu));
_panelUrl = url; _panelUrl = args.url;
_panel = Ui::BotWebView::Show({ _panel = Ui::BotWebView::Show({
.url = url, .url = args.url,
.storageId = _session->local().resolveStorageIdBots(), .storageId = _session->local().resolveStorageIdBots(),
.title = std::move(title), .title = std::move(title),
.titleBadge = std::move(titleBadge), .titleBadge = std::move(titleBadge),
.bottom = rpl::single('@' + _bot->username()), .bottom = rpl::single('@' + _bot->username()),
.delegate = static_cast<Ui::BotWebView::Delegate*>(this), .delegate = static_cast<Ui::BotWebView::Delegate*>(this),
.menuButtons = buttons, .menuButtons = buttons,
.fullscreen = args.fullscreen,
.allowClipboardRead = allowClipboardRead, .allowClipboardRead = allowClipboardRead,
}); });
started(queryId); started(args.queryId);
if (const auto strong = PendingActivation.get()) { if (const auto strong = PendingActivation.get()) {
if (strong == this) { if (strong == this) {
@ -1618,20 +1639,25 @@ void AttachWebView::openByUsername(
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
const Api::SendAction &action, const Api::SendAction &action,
const QString &botUsername, const QString &botUsername,
const QString &startCommand) { const QString &startCommand,
bool fullscreen) {
if (botUsername.isEmpty() if (botUsername.isEmpty()
|| (_botUsername == botUsername && _startCommand == startCommand)) { || (_botUsername == botUsername
&& _startCommand == startCommand
&& _fullScreenRequested == fullscreen)) {
return; return;
} }
cancel(); cancel();
_botUsername = botUsername; _botUsername = botUsername;
_startCommand = startCommand; _startCommand = startCommand;
_fullScreenRequested = fullscreen;
const auto weak = base::make_weak(controller); const auto weak = base::make_weak(controller);
const auto show = controller->uiShow(); const auto show = controller->uiShow();
resolveUsername(show, crl::guard(weak, [=](not_null<PeerData*> peer) { resolveUsername(show, crl::guard(weak, [=](not_null<PeerData*> peer) {
_botUsername = QString(); _botUsername = QString();
const auto token = base::take(_startCommand); const auto token = base::take(_startCommand);
const auto fullscreen = base::take(_fullScreenRequested);
const auto bot = peer->asUser(); const auto bot = peer->asUser();
if (!bot || !bot->isBot()) { if (!bot || !bot->isBot()) {
@ -1646,6 +1672,7 @@ void AttachWebView::openByUsername(
.context = { .context = {
.controller = controller, .controller = controller,
.action = action, .action = action,
.fullscreen = fullscreen,
}, },
.button = { .startCommand = token }, .button = { .startCommand = token },
.source = InlineBots::WebViewSourceLinkAttachMenu{}, .source = InlineBots::WebViewSourceLinkAttachMenu{},

View file

@ -187,6 +187,7 @@ struct WebViewContext {
base::weak_ptr<Window::SessionController> controller; base::weak_ptr<Window::SessionController> controller;
Dialogs::EntryState dialogsEntryState; Dialogs::EntryState dialogsEntryState;
std::optional<Api::SendAction> action; std::optional<Api::SendAction> action;
bool fullscreen = false;
bool maySkipConfirmation = false; bool maySkipConfirmation = false;
}; };
@ -234,7 +235,12 @@ private:
void confirmOpen(Fn<void()> done); void confirmOpen(Fn<void()> done);
void confirmAppOpen(bool writeAccess, Fn<void(bool allowWrite)> done); void confirmAppOpen(bool writeAccess, Fn<void(bool allowWrite)> done);
void show(const QString &url, uint64 queryId = 0); struct ShowArgs {
QString url;
uint64 queryId = 0;
bool fullscreen = false;
};
void show(ShowArgs &&args);
void showGame(); void showGame();
void started(uint64 queryId); void started(uint64 queryId);
@ -291,7 +297,8 @@ public:
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
const Api::SendAction &action, const Api::SendAction &action,
const QString &botUsername, const QString &botUsername,
const QString &startCommand); const QString &startCommand,
bool fullscreen);
void cancel(); void cancel();
@ -360,6 +367,7 @@ private:
QString _botUsername; QString _botUsername;
QString _startCommand; QString _startCommand;
bool _fullScreenRequested = false;
mtpRequestId _requestId = 0; mtpRequestId _requestId = 0;

View file

@ -38,6 +38,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtGui/QGuiApplication> #include <QtGui/QGuiApplication>
#include <QtGui/QClipboard> #include <QtGui/QClipboard>
#include <QtGui/QWindow> #include <QtGui/QWindow>
#include <QtGui/QScreen>
#include <QtGui/qpa/qplatformscreen.h>
namespace Ui::BotWebView { namespace Ui::BotWebView {
namespace { namespace {
@ -369,11 +371,13 @@ Panel::Panel(
object_ptr<Ui::RpWidget> titleBadge, object_ptr<Ui::RpWidget> titleBadge,
not_null<Delegate*> delegate, not_null<Delegate*> delegate,
MenuButtons menuButtons, MenuButtons menuButtons,
bool fullscreen,
bool allowClipboardRead) bool allowClipboardRead)
: _storageId(storageId) : _storageId(storageId)
, _delegate(delegate) , _delegate(delegate)
, _menuButtons(menuButtons) , _menuButtons(menuButtons)
, _widget(std::make_unique<SeparatePanel>()) , _widget(std::make_unique<SeparatePanel>())
, _fullscreen(fullscreen)
, _allowClipboardRead(allowClipboardRead) { , _allowClipboardRead(allowClipboardRead) {
_widget->setWindowFlag(Qt::WindowStaysOnTopHint, false); _widget->setWindowFlag(Qt::WindowStaysOnTopHint, false);
_widget->setInnerSize(st::botWebViewPanelSize, true); _widget->setInnerSize(st::botWebViewPanelSize, true);
@ -382,7 +386,9 @@ Panel::Panel(
) | rpl::start_with_next([=](bool fullscreen) { ) | rpl::start_with_next([=](bool fullscreen) {
_widget->toggleFullScreen(fullscreen); _widget->toggleFullScreen(fullscreen);
layoutButtons(); layoutButtons();
sendFullScreen();
sendSafeArea(); sendSafeArea();
sendContentSafeArea();
}, _widget->lifetime()); }, _widget->lifetime());
_widget->closeRequests( _widget->closeRequests(
@ -762,6 +768,36 @@ bool Panel::createWebview(const Webview::ThemeParams &params) {
sendViewport(); sendViewport();
} else if (command == "web_app_request_safe_area") { } else if (command == "web_app_request_safe_area") {
sendSafeArea(); sendSafeArea();
} else if (command == "web_app_request_content_safe_area") {
sendContentSafeArea();
} else if (command == "web_app_request_fullscreen") {
if (!_fullscreen.current()) {
_fullscreen = true;
} else {
sendFullScreen();
}
} else if (command == "web_app_exit_fullscreen") {
if (_fullscreen.current()) {
_fullscreen = false;
} else {
sendFullScreen();
}
} else if (command == "web_app_check_home_screen") {
postEvent("home_screen_checked", "{ status: \"unsupported\" }");
} else if (command == "web_app_start_accelerometer") {
postEvent("accelerometer_failed", "{ error: \"UNSUPPORTED\" }");
} else if (command == "web_app_start_device_orientation") {
postEvent(
"device_orientation_failed",
"{ error: \"UNSUPPORTED\" }");
} else if (command == "web_app_start_gyroscope") {
postEvent("gyroscope_failed", "{ error: \"UNSUPPORTED\" }");
} else if (command == "web_app_check_location") {
postEvent("location_checked", "{ available: false }");
} else if (command == "web_app_request_location") {
postEvent("location_requested", "{ available: false }");
} else if (command == "web_app_biometry_get_info") {
postEvent("biometry_info_received", "{ available: false }");
} else if (command == "web_app_open_tg_link") { } else if (command == "web_app_open_tg_link") {
openTgLink(arguments); openTgLink(arguments);
} else if (command == "web_app_open_link") { } else if (command == "web_app_open_link") {
@ -846,11 +882,36 @@ void Panel::sendViewport() {
"is_expanded: true }"); "is_expanded: true }");
} }
void Panel::sendFullScreen() {
postEvent("fullscreen_changed", _fullscreen.current()
? "{ is_fullscreen: true }"
: "{ is_fullscreen: false }");
}
void Panel::sendSafeArea() { void Panel::sendSafeArea() {
postEvent("safe_area_changed", postEvent("safe_area_changed",
"{ top: 0, right: 0, bottom: 0, left: 0 }"); "{ top: 0, right: 0, bottom: 0, left: 0 }");
} }
void Panel::sendContentSafeArea() {
const auto shift = st::separatePanelClose.rippleAreaPosition.y();
const auto top = _fullscreen.current()
? (shift + st::fullScreenPanelClose.height + (shift / 2))
: 0;
const auto scaled = top * style::DevicePixelRatio();
auto report = 0;
if (const auto screen = QGuiApplication::primaryScreen()) {
const auto dpi = screen->logicalDotsPerInch();
const auto ratio = screen->devicePixelRatio();
const auto basePair = screen->handle()->logicalBaseDpi();
const auto base = (basePair.first + basePair.second) * 0.5;
const auto systemScreenScale = dpi * ratio / base;
report = int(base::SafeRound(scaled / systemScreenScale));
}
postEvent("content_safe_area_changed",
u"{ top: %1, right: 0, bottom: 0, left: 0 }"_q.arg(report));
}
void Panel::setTitle(rpl::producer<QString> title) { void Panel::setTitle(rpl::producer<QString> title) {
_widget->setTitle(std::move(title)); _widget->setTitle(std::move(title));
} }
@ -1662,6 +1723,7 @@ std::unique_ptr<Panel> Show(Args &&args) {
std::move(args.titleBadge), std::move(args.titleBadge),
args.delegate, args.delegate,
args.menuButtons, args.menuButtons,
args.fullscreen,
args.allowClipboardRead); args.allowClipboardRead);
const auto params = args.delegate->botThemeParams(); const auto params = args.delegate->botThemeParams();
if (!result->showWebview(args.url, params, std::move(args.bottom))) { if (!result->showWebview(args.url, params, std::move(args.bottom))) {

View file

@ -79,6 +79,7 @@ public:
object_ptr<Ui::RpWidget> titleBadge, object_ptr<Ui::RpWidget> titleBadge,
not_null<Delegate*> delegate, not_null<Delegate*> delegate,
MenuButtons menuButtons, MenuButtons menuButtons,
bool fullscreen,
bool allowClipboardRead); bool allowClipboardRead);
~Panel(); ~Panel();
@ -148,6 +149,8 @@ private:
void closeWithConfirmation(); void closeWithConfirmation();
void sendViewport(); void sendViewport();
void sendSafeArea(); void sendSafeArea();
void sendContentSafeArea();
void sendFullScreen();
using EventData = std::variant<QString, QJsonObject>; using EventData = std::variant<QString, QJsonObject>;
void postEvent(const QString &event); void postEvent(const QString &event);
@ -198,6 +201,7 @@ struct Args {
rpl::producer<QString> bottom; rpl::producer<QString> bottom;
not_null<Delegate*> delegate; not_null<Delegate*> delegate;
MenuButtons menuButtons; MenuButtons menuButtons;
bool fullscreen = false;
bool allowClipboardRead = false; bool allowClipboardRead = false;
}; };
[[nodiscard]] std::unique_ptr<Panel> Show(Args &&args); [[nodiscard]] std::unique_ptr<Panel> Show(Args &&args);

View file

@ -637,6 +637,7 @@ void SessionNavigation::showPeerByLinkResolved(
.context = { .context = {
.controller = parentController(), .controller = parentController(),
.action = action, .action = action,
.fullscreen = info.botAppFullScreen,
.maySkipConfirmation = !info.botAppForceConfirmation, .maySkipConfirmation = !info.botAppForceConfirmation,
}, },
.button = { .startCommand = info.startToken }, .button = { .startCommand = info.startToken },
@ -697,14 +698,18 @@ void SessionNavigation::showPeerByLinkResolved(
parentController(), parentController(),
Api::SendAction(history), Api::SendAction(history),
attachBotUsername, attachBotUsername,
info.attachBotToggleCommand.value_or(QString())); info.attachBotToggleCommand.value_or(QString()),
info.botAppFullScreen);
}); });
} else if (bot && info.attachBotMainOpen) { } else if (bot && info.attachBotMainOpen) {
const auto startCommand = info.attachBotToggleCommand.value_or( const auto startCommand = info.attachBotToggleCommand.value_or(
QString()); QString());
bot->session().attachWebView().open({ bot->session().attachWebView().open({
.bot = bot, .bot = bot,
.context = { .controller = parentController() }, .context = {
.controller = parentController(),
.fullscreen = info.botAppFullScreen,
},
.button = { .startCommand = startCommand }, .button = { .startCommand = startCommand },
.source = InlineBots::WebViewSourceLinkBotProfile{ .source = InlineBots::WebViewSourceLinkBotProfile{
.token = startCommand, .token = startCommand,
@ -728,6 +733,7 @@ void SessionNavigation::showPeerByLinkResolved(
? Api::SendAction( ? Api::SendAction(
contextUser->owner().history(contextUser)) contextUser->owner().history(contextUser))
: std::optional<Api::SendAction>()), : std::optional<Api::SendAction>()),
.fullscreen = info.botAppFullScreen,
}, },
.button = { .startCommand = *info.attachBotToggleCommand }, .button = { .startCommand = *info.attachBotToggleCommand },
.source = InlineBots::WebViewSourceLinkAttachMenu{ .source = InlineBots::WebViewSourceLinkAttachMenu{

View file

@ -49,6 +49,7 @@ struct PeerByLinkInfo {
bool joinChannel = false; bool joinChannel = false;
QString botAppName; QString botAppName;
bool botAppForceConfirmation = false; bool botAppForceConfirmation = false;
bool botAppFullScreen = false;
QString attachBotUsername; QString attachBotUsername;
std::optional<QString> attachBotToggleCommand; std::optional<QString> attachBotToggleCommand;
bool attachBotMainOpen = false; bool attachBotMainOpen = false;