2
0
mirror of https://github.com/telegramdesktop/tdesktop synced 2025-08-30 22:16:14 +00:00

Support age verification for sensitive media.

This commit is contained in:
John Preston
2025-07-21 16:47:41 +04:00
parent fee892f9a2
commit ccb8c43961
16 changed files with 416 additions and 56 deletions

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Filled / filled_verify_age</title>
<g id="Filled-/-filled_verify_age" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M36,34.9473684 C33.5842105,34.9473684 31.5592105,34.1302632 29.925,32.4960526 C28.2907895,30.8618421 27.4736842,28.8368421 27.4736842,26.4210526 C27.4736842,24.0526316 28.2907895,22.0394737 29.925,20.3815789 C31.5592105,18.7236842 33.5842105,17.8947368 36,17.8947368 C38.3684211,17.8947368 40.3815789,18.7236842 42.0394737,20.3815789 C43.6973684,22.0394737 44.5263158,24.0526316 44.5263158,26.4210526 C44.5263158,28.8368421 43.6973684,30.8618421 42.0394737,32.4960526 C40.3815789,34.1302632 38.3684211,34.9473684 36,34.9473684 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path>
<path d="M18.9473684,49.6842105 L18.9473684,47.1263158 C18.9473684,46.1315789 19.1960526,45.1960526 19.6934211,44.3197368 C20.1907895,43.4434211 20.8657895,42.7447368 21.7184211,42.2236842 C23.8973684,40.9447368 26.1828947,39.9736842 28.575,39.3105263 C30.9671053,38.6473684 33.4421053,38.3157895 36,38.3157895 C38.5578947,38.3157895 41.0328947,38.6473684 43.425,39.3105263 C45.8171053,39.9736842 48.1026316,40.9447368 50.2815789,42.2236842 C51.1342105,42.7447368 51.8092105,43.4434211 52.3065789,44.3197368 C52.8039474,45.1960526 53.0526316,46.1315789 53.0526316,47.1263158 L53.0526316,49.6842105 C53.0526316,50.4894737 52.7802632,51.1644737 52.2355263,51.7092105 C51.6907895,52.2539474 51.0157895,52.5263158 50.2105263,52.5263158 L21.7894737,52.5263158 C20.9842105,52.5263158 20.3092105,52.2539474 19.7644737,51.7092105 C19.2197368,51.1644737 18.9473684,50.4894737 18.9473684,49.6842105 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path>
<path d="M11.6842105,66 C10.1210526,66 8.78289474,65.4434211 7.66973684,64.3302632 C6.55657895,63.2171053 6,61.8789474 6,60.3157895 L6,51.7894737 C6,50.9842105 6.27236842,50.3092105 6.81710526,49.7644737 C7.36184211,49.2197368 8.03684211,48.9473684 8.84210526,48.9473684 C9.64736842,48.9473684 10.3223684,49.2197368 10.8671053,49.7644737 C11.4118421,50.3092105 11.6842105,50.9842105 11.6842105,51.7894737 L11.6842105,60.3157895 L20.2105263,60.3157895 C21.0157895,60.3157895 21.6907895,60.5881579 22.2355263,61.1328947 C22.7802632,61.6776316 23.0526316,62.3526316 23.0526316,63.1578947 C23.0526316,63.9631579 22.7802632,64.6381579 22.2355263,65.1828947 C21.6907895,65.7276316 21.0157895,66 20.2105263,66 L11.6842105,66 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path>
<path d="M6,20.2105263 L6,11.6842105 C6,10.1210526 6.55657895,8.78289474 7.66973684,7.66973684 C8.78289474,6.55657895 10.1210526,6 11.6842105,6 L20.2105263,6 C21.0157895,6 21.6907895,6.27236842 22.2355263,6.81710526 C22.7802632,7.36184211 23.0526316,8.03684211 23.0526316,8.84210526 C23.0526316,9.64736842 22.7802632,10.3223684 22.2355263,10.8671053 C21.6907895,11.4118421 21.0157895,11.6842105 20.2105263,11.6842105 L11.6842105,11.6842105 L11.6842105,20.2105263 C11.6842105,21.0157895 11.4118421,21.6907895 10.8671053,22.2355263 C10.3223684,22.7802632 9.64736842,23.0526316 8.84210526,23.0526316 C8.03684211,23.0526316 7.36184211,22.7802632 6.81710526,22.2355263 C6.27236842,21.6907895 6,21.0157895 6,20.2105263 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path>
<path d="M60.3157895,66 L51.7894737,66 C50.9842105,66 50.3092105,65.7276316 49.7644737,65.1828947 C49.2197368,64.6381579 48.9473684,63.9631579 48.9473684,63.1578947 C48.9473684,62.3526316 49.2197368,61.6776316 49.7644737,61.1328947 C50.3092105,60.5881579 50.9842105,60.3157895 51.7894737,60.3157895 L60.3157895,60.3157895 L60.3157895,51.7894737 C60.3157895,50.9842105 60.5881579,50.3092105 61.1328947,49.7644737 C61.6776316,49.2197368 62.3526316,48.9473684 63.1578947,48.9473684 C63.9631579,48.9473684 64.6381579,49.2197368 65.1828947,49.7644737 C65.7276316,50.3092105 66,50.9842105 66,51.7894737 L66,60.3157895 C66,61.8789474 65.4434211,63.2171053 64.3302632,64.3302632 C63.2171053,65.4434211 61.8789474,66 60.3157895,66 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path>
<path d="M60.3157895,20.2105263 L60.3157895,11.6842105 L51.7894737,11.6842105 C50.9842105,11.6842105 50.3092105,11.4118421 49.7644737,10.8671053 C49.2197368,10.3223684 48.9473684,9.64736842 48.9473684,8.84210526 C48.9473684,8.03684211 49.2197368,7.36184211 49.7644737,6.81710526 C50.3092105,6.27236842 50.9842105,6 51.7894737,6 L60.3157895,6 C61.8789474,6 63.2171053,6.55657895 64.3302632,7.66973684 C65.4434211,8.78289474 66,10.1210526 66,11.6842105 L66,20.2105263 C66,21.0157895 65.7276316,21.6907895 65.1828947,22.2355263 C64.6381579,22.7802632 63.9631579,23.0526316 63.1578947,23.0526316 C62.3526316,23.0526316 61.6776316,22.7802632 61.1328947,22.2355263 C60.5881579,21.6907895 60.3157895,21.0157895 60.3157895,20.2105263 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -13,6 +13,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"cloud_lng_topup_purpose_subs" = "Buy **Stars** to keep your channel subscriptions.";
"cloud_lng_age_verify_about_gb#one" = "To access such content, you must confirm that you are at least **{count}** year old as required by UK law.";
"cloud_lng_age_verify_about_gb#other" = "To access such content, you must confirm that you are at least **{count}** years old as required by UK law.";
"cloud_lng_passport_in_ar" = "Arabic";
"cloud_lng_passport_in_az" = "Azerbaijani";
"cloud_lng_passport_in_bg" = "Bulgarian";

View File

@@ -6756,6 +6756,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_frozen_text3" = "Appeal via {link} before {date}, or your account will be deleted.";
"lng_frozen_appeal_button" = "Submit an Appeal";
"lng_age_verify_title" = "Age Verification";
"lng_age_verify_mobile" = "Please open this media in the official Telegram app for Android or iOS to verify your age.";
"lng_age_verify_here" = "This is a one-time process using your phone's camera. Your selfie will not be stored by Telegram.";
"lng_age_verify_button" = "Verify My Age";
"lng_age_verify_sorry_title" = "Age Check Failed";
"lng_age_verify_sorry_text" = "Sorry, you can't view 18+ content.";
"lng_context_bank_card_copy" = "Copy Card Number";
"lng_context_bank_card_copied" = "Card number copied to clipboard.";

View File

@@ -25,7 +25,7 @@ SensitiveContent::SensitiveContent(not_null<ApiWrap*> api)
}
void SensitiveContent::preload() {
if (!_loaded) {
if (!_loaded && !_loadRequestId) {
reload();
}
}
@@ -37,7 +37,6 @@ void SensitiveContent::reload(bool force) {
}
return;
}
_loaded = true;
_loadRequestId = _api.request(MTPaccount_GetContentSettings(
)).done([=](const MTPaccount_ContentSettings &result) {
_loadRequestId = 0;
@@ -50,6 +49,10 @@ void SensitiveContent::reload(bool force) {
_enabled = enabled;
_canChange = canChange;
}
if (!_loaded) {
_loaded = true;
_loadedChanged.fire({});
}
if (base::take(_appConfigReloadForce) || changed) {
_appConfigReloadTimer.callOnce(kRefreshAppConfigTimeout);
}
@@ -61,6 +64,19 @@ void SensitiveContent::reload(bool force) {
}).send();
}
bool SensitiveContent::loaded() const {
return _loaded;
}
rpl::producer<bool> SensitiveContent::loadedValue() const {
if (_loaded) {
return rpl::single(true);
}
return rpl::single(false) | rpl::then(
_loadedChanged.events() | rpl::map_to(true)
);
}
bool SensitiveContent::enabledCurrent() const {
return _enabled.current();
}
@@ -69,6 +85,10 @@ rpl::producer<bool> SensitiveContent::enabled() const {
return _enabled.value();
}
bool SensitiveContent::canChangeCurrent() const {
return _canChange.current();
}
rpl::producer<bool> SensitiveContent::canChange() const {
return _canChange.value();
}

View File

@@ -26,12 +26,16 @@ public:
void reload(bool force = false);
void update(bool enabled);
[[nodiscard]] bool loaded() const;
[[nodiscard]] rpl::producer<bool> loadedValue() const;
[[nodiscard]] bool enabledCurrent() const;
[[nodiscard]] rpl::producer<bool> enabled() const;
[[nodiscard]] bool canChangeCurrent() const;
[[nodiscard]] rpl::producer<bool> canChange() const;
private:
const not_null<Main::Session*> _session;
rpl::event_stream<> _loadedChanged;
MTP::Sender _api;
mtpRequestId _loadRequestId = 0;
mtpRequestId _saveRequestId = 0;

View File

@@ -1399,6 +1399,9 @@ UserData *Session::userByPhone(const QString &phone) const {
PeerData *Session::peerByUsername(const QString &username) const {
const auto uname = username.trimmed();
if (uname.isEmpty()) {
return nullptr;
}
for (const auto &[peerId, peer] : _peers) {
if (peer->isLoaded()
&& !peer->username().compare(uname, Qt::CaseInsensitive)) {

View File

@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_sensitive_content.h"
#include "api/api_views.h"
#include "apiwrap.h"
#include "inline_bots/bot_attach_web_view.h"
#include "ui/boxes/confirm_box.h"
#include "ui/layers/generic_box.h"
#include "ui/text/format_values.h"
@@ -18,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/checkbox.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/painter.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "data/data_document.h"
#include "data/data_session.h"
@@ -34,18 +36,77 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_item.h"
#include "history/history.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "mainwindow.h"
#include "media/streaming/media_streaming_utility.h"
#include "payments/payments_checkout_process.h"
#include "payments/payments_non_panel_process.h"
#include "settings/settings_common.h"
#include "webrtc/webrtc_environment.h"
#include "webview/webview_interface.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
namespace HistoryView {
namespace {
constexpr auto kMediaUnlockedTooltipDuration = 5 * crl::time(1000);
const auto kVerifyAgeAboutPrefix = "cloud_lng_age_verify_about_";
rpl::producer<TextWithEntities> AgeVerifyAbout(
not_null<Main::Session*> session) {
const auto appConfig = &session->appConfig();
return rpl::single(
rpl::empty
) | rpl::then(
Lang::Updated()
) | rpl::map([=] {
const auto country = appConfig->ageVerifyCountry().toLower();
const auto age = appConfig->ageVerifyMinAge();
const auto [shift, string] = Lang::Plural(
Lang::kPluralKeyBaseForCloudValue,
age,
lt_count);
const auto postfixes = {
"#zero",
"#one",
"#two",
"#few",
"#many",
"#other"
};
Assert(shift >= 0 && shift < postfixes.size());
const auto postfix = *(begin(postfixes) + shift);
return Ui::Text::RichLangValue(u"To access such content, you must confirm that you are at least **{count}** years old as required by UK law."_q.replace(u"{count}"_q, string));
return Ui::Text::RichLangValue(Lang::GetNonDefaultValue(
kVerifyAgeAboutPrefix + country.toUtf8() + postfix AssertIsDebug(test_this)
).replace(u"{count}"_q, string));
});
}
[[nodiscard]] object_ptr<Ui::RpWidget> AgeVerifyIcon(
not_null<QWidget*> parent) {
const auto padding = st::settingsAgeVerifyIconPadding;
const auto full = st::settingsAgeVerifyIcon.size().grownBy(padding);
auto result = object_ptr<Ui::RpWidget>(parent);
const auto raw = result.data();
raw->resize(full);
raw->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(raw);
const auto x = (raw->width() - full.width()) / 2;
auto hq = PainterHighQualityEnabler(p);
p.setBrush(st::windowBgActive);
p.setPen(Qt::NoPen);
const auto inner = QRect(QPoint(x, 0), full);
p.drawEllipse(inner);
st::settingsAgeVerifyIcon.paintInCenter(p, inner);
}, raw->lifetime());
return result;
}
} // namespace
@@ -278,61 +339,246 @@ ClickHandlerPtr MakePaidMediaLink(not_null<HistoryItem*> item) {
});
}
void ShowAgeVerification(
std::shared_ptr<Ui::Show> show,
not_null<UserData*> bot,
Fn<void()> reveal) {
show->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setNoContentMargin(true);
box->setStyle(st::settingsAgeVerifyBox);
box->setWidth(st::boxWideWidth);
box->addRow(AgeVerifyIcon(box), st::settingsAgeVerifyIconMargin);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_age_verify_title(),
st::settingsAgeVerifyTitle),
st::boxRowPadding + st::settingsAgeVerifyMargin);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
AgeVerifyAbout(&bot->session()),
st::settingsAgeVerifyText),
st::boxRowPadding + st::settingsAgeVerifyMargin);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_age_verify_here(Ui::Text::RichLangValue),
st::settingsAgeVerifyText),
st::boxRowPadding + st::settingsAgeVerifyMargin);
const auto weak = QPointer<Ui::GenericBox>(box);
const auto done = crl::guard(&bot->session(), [=](int age) {
const auto min = bot->session().appConfig().ageVerifyMinAge();
if (age >= min) {
reveal();
bot->session().api().sensitiveContent().update(true);
} else {
show->showToast({
.title = tr::lng_age_verify_sorry_title(tr::now),
.text = tr::lng_age_verify_sorry_text(tr::now),
.duration = Ui::Toast::kDefaultDuration * 3,
});
}
if (const auto strong = weak.data()) {
strong->closeBox();
}
});
const auto button = box->addButton(tr::lng_age_verify_button(), [=] {
bot->session().attachWebView().open({
.bot = bot,
.context = { .maySkipConfirmation = true },
.source = InlineBots::WebViewSourceAgeVerification{
.done = done,
},
});
});
box->widthValue(
) | rpl::start_with_next([=](int width) {
const auto &padding = st::settingsAgeVerifyBox.buttonPadding;
button->resizeToWidth(width
- padding.left()
- padding.right());
button->moveToLeft(padding.left(), padding.top());
}, button->lifetime());
}));
}
void ShowAgeVerificationMobile(
std::shared_ptr<Ui::Show> show,
not_null<Main::Session*> session) {
show->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setTitle(tr::lng_age_verify_title());
box->setWidth(st::boxWideWidth);
const auto size = st::settingsCloudPasswordIconSize;
auto icon = Settings::CreateLottieIcon(
box->verticalLayout(),
{
.name = u"phone"_q,
.sizeOverride = { size, size },
},
st::peerAppearanceIconPadding);
box->showFinishes(
) | rpl::start_with_next([animate = std::move(icon.animate)] {
animate(anim::repeat::once);
}, box->lifetime());
box->addRow(std::move(icon.widget));
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
AgeVerifyAbout(session),
st::settingsAgeVerifyText),
st::boxRowPadding + st::settingsAgeVerifyMargin);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_age_verify_mobile(Ui::Text::RichLangValue),
st::settingsAgeVerifyText),
st::boxRowPadding + st::settingsAgeVerifyMargin);
box->addButton(tr::lng_box_ok(), [=] {
box->closeBox();
});
}));
}
void ShowAgeVerificationRequired(
std::shared_ptr<Ui::Show> show,
not_null<Main::Session*> session,
Fn<void()> reveal) {
struct State {
Fn<void()> check;
rpl::lifetime lifetime;
std::optional<PeerData*> bot;
};
const auto state = std::make_shared<State>();
const auto username = session->appConfig().ageVerifyBotUsername();
const auto bot = session->data().peerByUsername(username);
if (username.isEmpty() || bot) {
state->bot = bot;
} else {
session->api().request(MTPcontacts_ResolveUsername(
MTP_flags(0),
MTP_string(username),
MTPstring()
)).done([=](const MTPcontacts_ResolvedPeer &result) {
const auto &data = result.data();
session->data().processUsers(data.vusers());
session->data().processChats(data.vchats());
const auto botId = peerFromMTP(data.vpeer());
state->bot = session->data().peerLoaded(botId);
state->check();
}).fail([=] {
state->bot = nullptr;
state->check();
}).send();
}
state->check = [=] {
const auto sensitive = &session->api().sensitiveContent();
const auto ready = sensitive->loaded();
if (!ready) {
state->lifetime = sensitive->loadedValue(
) | rpl::filter(
rpl::mappers::_1
) | rpl::take(1) | rpl::start_with_next(state->check);
return;
} else if (!state->bot.has_value()) {
return;
}
const auto has = Core::App().mediaDevices().recordAvailability();
const auto available = Webview::Availability();
const auto bot = (*state->bot)->asUser();
if (available.error == Webview::Available::Error::None
&& has == Webrtc::RecordAvailability::VideoAndAudio
&& sensitive->canChangeCurrent()
&& bot
&& bot->isBot()
&& bot->botInfo->hasMainApp) {
ShowAgeVerification(show, bot, reveal);
} else {
ShowAgeVerificationMobile(show, session);
}
state->lifetime.destroy();
state->check = nullptr;
};
state->check();
}
void ShowSensitiveConfirm(
std::shared_ptr<Ui::Show> show,
not_null<Main::Session*> session,
Fn<void()> reveal) {
show->show(Box([=](not_null<Ui::GenericBox*> box) {
struct State {
rpl::variable<bool> canChange;
Ui::Checkbox *checkbox = nullptr;
};
const auto state = box->lifetime().make_state<State>();
const auto sensitive = &session->api().sensitiveContent();
state->canChange = sensitive->canChange();
const auto done = [=](Fn<void()> close) {
if (state->canChange.current()
&& state->checkbox->checked()) {
show->showToast({
.text = tr::lng_sensitive_toast(
tr::now,
Ui::Text::RichLangValue),
.adaptive = true,
.duration = 5 * crl::time(1000),
});
sensitive->update(true);
} else {
reveal();
}
close();
};
Ui::ConfirmBox(box, {
.text = tr::lng_sensitive_text(Ui::Text::RichLangValue),
.confirmed = done,
.confirmText = tr::lng_sensitive_view(),
.title = tr::lng_sensitive_title(),
});
const auto skip = st::defaultCheckbox.margin.bottom();
const auto wrap = box->addRow(
object_ptr<Ui::SlideWrap<Ui::Checkbox>>(
box,
object_ptr<Ui::Checkbox>(
box,
tr::lng_sensitive_always(tr::now),
false)),
st::boxRowPadding + QMargins(0, 0, 0, skip));
wrap->toggleOn(state->canChange.value());
wrap->finishAnimating();
state->checkbox = wrap->entity();
}));
}
ClickHandlerPtr MakeSensitiveMediaLink(
ClickHandlerPtr reveal,
not_null<HistoryItem*> item) {
const auto session = &item->history()->session();
session->api().sensitiveContent().preload();
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
const auto plain = [reveal, context] {
reveal->onClick(context);
};
const auto my = context.other.value<ClickHandlerContext>();
const auto controller = my.sessionWindow.get();
const auto show = controller ? controller->uiShow() : my.show;
if (!show) {
reveal->onClick(context);
return;
plain();
} else if (session->appConfig().ageVerifyNeeded()) {
ShowAgeVerificationRequired(show, session, plain);
} else {
ShowSensitiveConfirm(show, session, plain);
}
show->show(Box([=](not_null<Ui::GenericBox*> box) {
struct State {
rpl::variable<bool> canChange;
Ui::Checkbox *checkbox = nullptr;
};
const auto state = box->lifetime().make_state<State>();
const auto sensitive = &session->api().sensitiveContent();
state->canChange = sensitive->canChange();
const auto done = [=](Fn<void()> close) {
if (state->canChange.current()
&& state->checkbox->checked()) {
show->showToast({
.text = tr::lng_sensitive_toast(
tr::now,
Ui::Text::RichLangValue),
.adaptive = true,
.duration = 5 * crl::time(1000),
});
sensitive->update(true);
} else {
reveal->onClick(context);
}
close();
};
Ui::ConfirmBox(box, {
.text = tr::lng_sensitive_text(Ui::Text::RichLangValue),
.confirmed = done,
.confirmText = tr::lng_sensitive_view(),
.title = tr::lng_sensitive_title(),
});
const auto skip = st::defaultCheckbox.margin.bottom();
const auto wrap = box->addRow(
object_ptr<Ui::SlideWrap<Ui::Checkbox>>(
box,
object_ptr<Ui::Checkbox>(
box,
tr::lng_sensitive_always(tr::now),
false)),
st::boxRowPadding + QMargins(0, 0, 0, skip));
wrap->toggleOn(state->canChange.value());
wrap->finishAnimating();
state->checkbox = wrap->entity();
}));
});
}

View File

@@ -931,6 +931,8 @@ void WebViewInstance::resolve() {
requestMain();
});
}
}, [&](WebViewSourceAgeVerification) {
requestMain();
});
}
@@ -1997,6 +1999,12 @@ void WebViewInstance::botDownloadFile(
}).send();
}
void WebViewInstance::botVerifyAge(int age) {
if (v::is<WebViewSourceAgeVerification>(_source)) {
v::get<WebViewSourceAgeVerification>(_source).done(age);
}
}
void WebViewInstance::botOpenPrivacyPolicy() {
const auto bot = _bot;
const auto weak = _context.controller;

View File

@@ -163,6 +163,16 @@ struct WebViewSourceBotProfile {
WebViewSourceBotProfile) = default;
};
struct WebViewSourceAgeVerification {
Fn<void(int)> done;
friend inline bool operator==(
WebViewSourceAgeVerification,
WebViewSourceAgeVerification) {
return true;
}
};
struct WebViewSource : std::variant<
WebViewSourceButton,
WebViewSourceSwitch,
@@ -173,7 +183,8 @@ struct WebViewSource : std::variant<
WebViewSourceAttachMenu,
WebViewSourceBotMenu,
WebViewSourceGame,
WebViewSourceBotProfile> {
WebViewSourceBotProfile,
WebViewSourceAgeVerification> {
using variant::variant;
};
@@ -291,6 +302,7 @@ private:
Ui::BotWebView::SetEmojiStatusRequest request) override;
void botDownloadFile(
Ui::BotWebView::DownloadFileRequest request) override;
void botVerifyAge(int age) override;
void botOpenPrivacyPolicy() override;
void botClose() override;

View File

@@ -1004,14 +1004,11 @@ PluralResult Plural(
const auto t = f;
const auto useNonDefaultPlural = (ChoosePlural != ChoosePluralDefault)
&& Lang::details::IsNonDefaultPlural(keyBase);
const auto shift = (useNonDefaultPlural ? ChoosePlural : ChoosePluralDefault)(
(integer ? i : -1),
i,
v,
w,
f,
t);
&& (keyBase == kPluralKeyBaseForCloudValue
|| Lang::details::IsNonDefaultPlural(keyBase));
const auto shift = (useNonDefaultPlural
? ChoosePlural
: ChoosePluralDefault)((integer ? i : -1), i, v, w, f, t);
if (integer) {
const auto round = qRound(value);
if (type == lt_count_short) {

View File

@@ -38,6 +38,7 @@ struct PluralResult {
int keyShift = 0;
QString replacement;
};
inline constexpr auto kPluralKeyBaseForCloudValue = ushort(-1);
PluralResult Plural(
ushort keyBase,
float64 value,

View File

@@ -208,6 +208,22 @@ TimeId AppConfig::suggestedPostAgeMin() const {
return get<int>(u"stars_suggested_post_age_min"_q, 86400);
}
bool AppConfig::ageVerifyNeeded() const {
return get<bool>(u"need_age_video_verification"_q, false);
}
QString AppConfig::ageVerifyCountry() const {
return get<QString>(u"verify_age_country"_q, QString());
}
int AppConfig::ageVerifyMinAge() const {
return get<int>(u"verify_age_min"_q, 18);
}
QString AppConfig::ageVerifyBotUsername() const {
return get<QString>(u"verify_age_bot_username"_q, QString());
}
void AppConfig::refresh(bool force) {
if (_requestId || !_api) {
if (force) {

View File

@@ -102,6 +102,11 @@ public:
[[nodiscard]] int suggestedPostDelayMax() const;
[[nodiscard]] TimeId suggestedPostAgeMin() const;
[[nodiscard]] bool ageVerifyNeeded() const;
[[nodiscard]] QString ageVerifyCountry() const;
[[nodiscard]] int ageVerifyMinAge() const;
[[nodiscard]] QString ageVerifyBotUsername() const;
void refresh(bool force = false);
private:

View File

@@ -705,3 +705,18 @@ settingsCreditsButton: SettingsButton(settingsButton) {
}
settingsButtonIconGift: icon {{ "settings/gift", menuIconColor }};
settingsButtonIconEarn: icon {{ "settings/earn", menuIconColor }};
settingsAgeVerifyBox: Box(filterInviteBox) {
buttonPadding: margins(24px, 24px, 24px, 24px);
}
settingsAgeVerifyText: FlatLabel(defaultFlatLabel) {
minWidth: 200px;
align: align(top);
}
settingsAgeVerifyMargin: margins(0px, 6px, 0px, 6px);
settingsAgeVerifyTitle: FlatLabel(boxTitle) {
align: align(top);
}
settingsAgeVerifyIcon: icon {{ "settings/filled_verify_age-48x48", windowFgActive }};
settingsAgeVerifyIconPadding: margins(16px, 16px, 16px, 16px);
settingsAgeVerifyIconMargin: margins(0px, 24px, 0px, 14px);

View File

@@ -1011,6 +1011,16 @@ bool Panel::createWebview(const Webview::ThemeParams &params) {
secureStorageFailed(arguments);
} else if (command == "web_app_secure_storage_clear") {
secureStorageFailed(arguments);
} else if (command == "web_app_verify_age") {
const auto passed = arguments["passed"];
const auto detected = arguments["age"];
const auto valid = passed.isBool()
&& passed.toBool()
&& detected.isDouble();
const auto age = valid
? int(std::floor(detected.toDouble()))
: 0;
_delegate->botVerifyAge(age);
} else if (command == "share_score") {
_delegate->botHandleMenuButton(MenuButton::ShareGame);
}

View File

@@ -105,6 +105,7 @@ public:
virtual void botDownloadFile(DownloadFileRequest request) = 0;
virtual void botSendPreparedMessage(
SendPreparedMessageRequest request) = 0;
virtual void botVerifyAge(int age) = 0;
virtual void botOpenPrivacyPolicy() = 0;
virtual void botClose() = 0;
};