Implement emoji status set from miniapps.

This commit is contained in:
John Preston 2024-11-08 16:05:39 +04:00
parent 21487641c1
commit 4198203a7f
15 changed files with 281 additions and 11 deletions

View file

@ -3418,6 +3418,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_bot_add_to_side_menu_done" = "Bot added to the main menu.";
"lng_bot_no_scan_qr" = "QR Codes for bots are not supported on Desktop. Please use one of Telegram's mobile apps.";
"lng_bot_no_share_story" = "Sharing to Stories is not supported on Desktop. Please use one of Telegram's mobile apps.";
"lng_bot_emoji_status_confirm" = "Confirm";
"lng_bot_emoji_status_title" = "Set Emoji Status";
"lng_bot_emoji_status_text" = "Do you want to set this emoji status suggested by {bot}?";
"lng_bot_status_users#one" = "{count} monthly user";
"lng_bot_status_users#other" = "{count} monthly users";

View file

@ -1225,6 +1225,9 @@ not_null<CustomEmojiManager::Listener*> Reactions::resolveListener() {
}
void Reactions::customEmojiResolveDone(not_null<DocumentData*> document) {
if (!document->sticker()) {
return;
}
const auto id = ReactionId{ { document->id } };
const auto favorite = (_unresolvedFavoriteId == id);
const auto i = _unresolvedTop.find(id);

View file

@ -47,6 +47,7 @@ struct BotInfo {
bool cantJoinGroups : 1 = false;
bool supportsAttachMenu : 1 = false;
bool canEditInformation : 1 = false;
bool canManageEmojiStatus : 1 = false;
bool supportsBusiness : 1 = false;
bool hasMainApp : 1 = false;
};

View file

@ -652,22 +652,27 @@ void CustomEmojiManager::unregisterListener(not_null<Listener*> listener) {
}
}
rpl::producer<not_null<DocumentData*>> CustomEmojiManager::resolve(
DocumentId documentId) {
auto CustomEmojiManager::resolve(DocumentId documentId)
-> rpl::producer<not_null<DocumentData*>, rpl::empty_error> {
return [=](auto consumer) {
auto result = rpl::lifetime();
const auto put = [=](not_null<DocumentData*> document) {
const auto put = [=](
not_null<DocumentData*> document,
bool resolved = true) {
if (!document->sticker()) {
if (resolved) {
consumer.put_error({});
}
return false;
}
consumer.put_next_copy(document);
return true;
};
if (!put(owner().document(documentId))) {
const auto listener = new CallbackListener(put);
if (!put(owner().document(documentId), false)) {
const auto listener = result.make_state<CallbackListener>(put);
resolve(documentId, listener);
result.add([=] {
unregisterListener(listener);
delete listener;
});
}
return result;
@ -763,6 +768,9 @@ void CustomEmojiManager::request() {
requestFinished();
}).fail([=] {
LOG(("API Error: Failed to get documents for emoji."));
for (const auto &id : ids) {
processListeners(_owner->document(id.v));
}
requestFinished();
}).send();
}
@ -792,7 +800,8 @@ void CustomEmojiManager::processLoaders(not_null<DocumentData*> document) {
}
}
void CustomEmojiManager::processListeners(not_null<DocumentData*> document) {
void CustomEmojiManager::processListeners(
not_null<DocumentData*> document) {
const auto id = document->id;
if (const auto listeners = _resolvers.take(id)) {
for (const auto &listener : *listeners) {

View file

@ -66,8 +66,8 @@ public:
void resolve(DocumentId documentId, not_null<Listener*> listener);
void unregisterListener(not_null<Listener*> listener);
[[nodiscard]] rpl::producer<not_null<DocumentData*>> resolve(
DocumentId documentId);
[[nodiscard]] auto resolve(DocumentId documentId)
-> rpl::producer<not_null<DocumentData*>, rpl::empty_error>;
[[nodiscard]] std::unique_ptr<Ui::CustomEmoji::Loader> createLoader(
not_null<DocumentData*> document,

View file

@ -98,7 +98,7 @@ namespace {
text.size(),
Data::SerializeCustomEmojiId(document)) },
};
});
}) | rpl::map_error_to_done();
}
[[nodiscard]] rpl::producer<TextWithEntities> PeerCustomStatus(

View file

@ -108,6 +108,9 @@ CustomEmoji::CustomEmoji(
}
void CustomEmoji::customEmojiResolveDone(not_null<DocumentData*> document) {
if (!document->sticker()) {
return;
}
_resolving = false;
const auto id = document->id;
for (auto &line : _lines) {

View file

@ -162,7 +162,7 @@ void TopicIconView::setupPlayer(not_null<Data::ForumTopic*> topic) {
id
) | rpl::map([=](not_null<DocumentData*> document) {
return document.get();
});
}) | rpl::map_error_to_done();
}) | rpl::flatten_latest(
) | rpl::map([=](DocumentData *document)
-> rpl::producer<std::shared_ptr<StickerPlayer>> {

View file

@ -26,12 +26,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_changes.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_emoji_statuses.h"
#include "data/data_file_origin.h"
#include "data/data_peer_bot_command.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/data_web_page.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/stickers/data_stickers.h"
#include "history/history.h"
#include "history/history_item.h"
#include "info/profile/info_profile_values.h"
@ -43,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mainwidget.h"
#include "payments/payments_checkout_process.h"
#include "payments/payments_non_panel_process.h"
#include "settings/settings_premium.h"
#include "storage/storage_account.h"
#include "storage/storage_domain.h"
#include "ui/basic_click_handlers.h"
@ -428,6 +431,124 @@ void FillBotUsepic(
Ui::IconWithTitle(box->verticalLayout(), userpic, title, aboutLabel);
}
std::unique_ptr<Ui::RpWidget> MakeEmojiSetStatusPreview(
not_null<QWidget*> parent,
not_null<PeerData*> peer,
not_null<DocumentData*> document) {
auto result = std::make_unique<Ui::RpWidget>(parent);
const auto size = st::chatGiveawayPeerSize;
const auto padding = st::chatGiveawayPeerPadding;
const auto raw = result.get();
const auto width = raw->lifetime().make_state<int>();
const auto name = raw->lifetime().make_state<Ui::FlatLabel>(
raw,
rpl::single(peer->name()),
st::botEmojiStatusName);
auto emojiText = TextWithEntities();
const auto emoji = raw->lifetime().make_state<Ui::FlatLabel>(
raw,
rpl::single(emojiText),
st::botEmojiStatusName);
const auto userpic = raw->lifetime().make_state<Ui::UserpicButton>(
raw,
peer,
st::botEmojiStatusUserpic);
raw->resize(size, size);
raw->sizeValue() | rpl::start_with_next([=](QSize outer) {
const auto full = outer.width();
const auto decorations = size
+ padding.left()
+ padding.right()
+ emoji->width()
+ st::normalFont->spacew;
const auto inner = full - decorations;
const auto use = std::min(inner, name->textMaxWidth());
*width = use + decorations;
const auto left = (full - *width) / 2;
if (inner > 0) {
userpic->moveToLeft(left, 0, outer.width());
emoji->moveToLeft(
left + *width - padding.right() - emoji->width(),
padding.top(),
outer.width());
name->resizeToWidth(use);
name->moveToLeft(
left + size + padding.left(),
padding.top(),
outer.width());
}
}, raw->lifetime());
raw->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(raw);
const auto left = (raw->width() - *width) / 2;
const auto skip = size / 2;
p.setClipRect(left + skip, 0, *width - skip, size);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::windowBgOver);
p.drawRoundedRect(left, 0, *width, size, skip, skip);
}, raw->lifetime());
return result;
}
void ConfirmEmojiStatusBox(
not_null<Ui::GenericBox*> box,
not_null<UserData*> bot,
not_null<DocumentData*> document,
TimeId until,
Fn<void(bool)> done) {
box->setNoContentMargin(true);
auto owned = Settings::MakeEmojiStatusPreview(box, document);
const auto preview = box->addRow(
object_ptr<Ui::RpWidget>::fromRaw(owned.release()));
preview->resize(preview->width(), st::botEmojiStatusPreviewHeight);
const auto set = box->lifetime().make_state<bool>();
box->addRow(object_ptr<Ui::FlatLabel>(
box,
tr::lng_bot_emoji_status_title(),
st::botEmojiStatusTitle));
AddSkip(box->verticalLayout());
box->addRow(object_ptr<Ui::FlatLabel>(
box,
tr::lng_bot_emoji_status_text(
lt_bot,
rpl::single(Ui::Text::Bold(bot->name())),
Ui::Text::RichLangValue),
st::botEmojiStatusText));
AddSkip(box->verticalLayout());
auto ownedSet = MakeEmojiSetStatusPreview(
box,
document->session().user(),
document);
box->addRow(
object_ptr<Ui::RpWidget>::fromRaw(ownedSet.release()));
box->addButton(tr::lng_bot_emoji_status_confirm(), [=] {
document->owner().emojiStatuses().set(document->id, until);
*set = true;
box->closeBox();
done(true);
});
box->addButton(tr::lng_cancel(), [=] {
const auto was = *set;
box->closeBox();
if (!was) {
done(false);
}
});
}
class BotAction final : public Ui::Menu::ItemBase {
public:
BotAction(
@ -1514,6 +1635,32 @@ void WebViewInstance::botInvokeCustomMethod(
}).send();
}
void WebViewInstance::botSetEmojiStatus(
Ui::BotWebView::SetEmojiStatusRequest request) {
const auto bot = _bot;
const auto panel = _panel.get();
const auto callback = request.callback;
const auto until = request.expirationDate;
if (!panel) {
callback(u"UNKNOWN_ERROR"_q);
return;
}
_session->data().customEmojiManager().resolve(
request.customEmojiId
) | rpl::start_with_next_error([=](not_null<DocumentData*> document) {
const auto sticker = document->sticker();
if (!sticker || sticker->setType != Data::StickersType::Emoji) {
callback(u"SUGGESTED_EMOJI_INVALID"_q);
return;
}
const auto done = [=](bool success) {
callback(success ? QString() : u"USER_DECLINED"_q);
};
panel->showBox(
Box(ConfirmEmojiStatusBox, bot, document, until, done));
}, [=] { callback(u"SUGGESTED_EMOJI_INVALID"_q); }, panel->lifetime());
}
void WebViewInstance::botOpenPrivacyPolicy() {
const auto bot = _bot;
const auto weak = _context.controller;

View file

@ -263,6 +263,8 @@ private:
void botSharePhone(Fn<void(bool shared)> callback) override;
void botInvokeCustomMethod(
Ui::BotWebView::CustomMethodRequest request) override;
void botSetEmojiStatus(
Ui::BotWebView::SetEmojiStatusRequest request) override;
void botOpenPrivacyPolicy() override;
void botClose() override;
@ -283,6 +285,8 @@ private:
QString _panelUrl;
std::unique_ptr<Ui::BotWebView::Panel> _panel;
rpl::lifetime _lifetime;
static base::weak_ptr<WebViewInstance> PendingActivation;
};

View file

@ -1773,7 +1773,29 @@ void AddSummaryPremium(
}
Ui::AddSkip(content, descriptionPadding.bottom());
}
std::unique_ptr<Ui::RpWidget> MakeEmojiStatusPreview(
not_null<QWidget*> parent,
not_null<DocumentData*> document) {
auto result = std::make_unique<Ui::RpWidget>(parent);
const auto raw = result.get();
const auto size = HistoryView::Sticker::EmojiSize();
const auto emoji = raw->lifetime().make_state<EmojiStatusTopBar>(
document,
[=](QRect r) { raw->update(std::move(r)); },
size);
raw->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(raw);
emoji->paint(p);
}, raw->lifetime());
raw->sizeValue() | rpl::start_with_next([=](QSize size) {
emoji->setCenter(QPointF(size.width() / 2., size.height() / 2.));
}, raw->lifetime());
return result;
}
} // namespace Settings

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "settings/settings_type.h"
class DocumentData;
enum class PremiumFeature;
namespace style {
@ -108,5 +109,13 @@ void AddSummaryPremium(
const QString &ref,
Fn<void(PremiumFeature)> buttonCallback);
[[nodiscard]] std::unique_ptr<Ui::RpWidget> MakeEmojiStatusPreview(
not_null<QWidget*> parent,
not_null<DocumentData*> document);
[[nodiscard]] std::unique_ptr<Ui::RpWidget> MakeEmojiSetStatusPreview(
not_null<QWidget*> parent,
not_null<PeerData*> peer,
not_null<DocumentData*> document);
} // namespace Settings

View file

@ -824,6 +824,8 @@ bool Panel::createWebview(const Webview::ThemeParams &params) {
processHeaderColor(arguments);
} else if (command == "web_app_set_bottom_bar_color") {
processBottomBarColor(arguments);
} else if (command == "web_app_set_emoji_status") {
processEmojiStatusRequest(arguments);
} else if (command == "share_score") {
_delegate->botHandleMenuButton(MenuButton::ShareGame);
}
@ -963,6 +965,41 @@ void Panel::switchInlineQueryMessage(const QJsonObject &args) {
_delegate->botSwitchInlineQuery(types, query);
}
void Panel::processEmojiStatusRequest(const QJsonObject &args) {
if (args.isEmpty()) {
_delegate->botClose();
return;
}
const auto emojiId = args["custom_emoji_id"].toString().toULongLong();
const auto expirationDate = TimeId(base::SafeRound(
args["expiration_date"].toDouble()));
if (!emojiId) {
postEvent(
"emoji_status_failed",
"{ error: \"SUGGESTED_EMOJI_INVALID\" }");
return;
} else if (expirationDate < 0) {
postEvent(
"emoji_status_failed",
"{ error: \"EXPIRATION_DATE_INVALID\" }");
return;
}
auto callback = crl::guard(this, [=](QString error) {
if (error.isEmpty()) {
postEvent("emoji_status_set");
} else {
postEvent(
"emoji_status_failed",
u"{ error: \"%1\" }"_q.arg(error));
}
});
_delegate->botSetEmojiStatus({
.customEmojiId = emojiId,
.expirationDate = expirationDate,
.callback = std::move(callback),
});
}
void Panel::openTgLink(const QJsonObject &args) {
if (args.isEmpty()) {
LOG(("BotWebView Error: Bad arguments in 'web_app_open_tg_link'."));

View file

@ -51,6 +51,12 @@ struct CustomMethodRequest {
Fn<void(CustomMethodResult)> callback;
};
struct SetEmojiStatusRequest {
uint64 customEmojiId = 0;
TimeId expirationDate = 0;
Fn<void(QString)> callback;
};
class Delegate {
public:
virtual Webview::ThemeParams botThemeParams() = 0;
@ -67,6 +73,7 @@ public:
virtual void botAllowWriteAccess(Fn<void(bool allowed)> callback) = 0;
virtual void botSharePhone(Fn<void(bool shared)> callback) = 0;
virtual void botInvokeCustomMethod(CustomMethodRequest request) = 0;
virtual void botSetEmojiStatus(SetEmojiStatusRequest request) = 0;
virtual void botOpenPrivacyPolicy() = 0;
virtual void botClose() = 0;
};
@ -123,6 +130,8 @@ private:
void setTitle(rpl::producer<QString> title);
void sendDataMessage(const QJsonObject &args);
void switchInlineQueryMessage(const QJsonObject &args);
void processEmojiStatusRequest(const QJsonObject &args);
void processEmojiStatusAccessRequest();
void processButtonMessage(
std::unique_ptr<Button> &button,
const QJsonObject &args);

View file

@ -1155,3 +1155,26 @@ msgSelectionCheck: RoundCheckbox(defaultPeerListCheck) {
}
sponsoredMessageBarMaxHeight: 156px;
botEmojiStatusPreviewHeight: 148px;
botEmojiStatusTitle: FlatLabel(boxTitle) {
style: TextStyle(defaultTextStyle) {
font: font(16px semibold);
lineHeight: 20px;
}
minWidth: 256px;
maxHeight: 0px;
align: align(top);
}
botEmojiStatusText: FlatLabel(defaultFlatLabel) {
style: boxTextStyle;
minWidth: 256px;
maxHeight: 0px;
align: align(top);
}
botEmojiStatusUserpic: UserpicButton(defaultUserpicButton) {
size: size(chatGiveawayPeerSize, chatGiveawayPeerSize);
photoSize: chatGiveawayPeerSize;
}
botEmojiStatusName: FlatLabel(defaultFlatLabel) {
}