diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index f24c6003c..23b7ae0c9 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -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"; diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index d360ccf65..57bb4cbf2 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -1225,6 +1225,9 @@ not_null Reactions::resolveListener() { } void Reactions::customEmojiResolveDone(not_null document) { + if (!document->sticker()) { + return; + } const auto id = ReactionId{ { document->id } }; const auto favorite = (_unresolvedFavoriteId == id); const auto i = _unresolvedTop.find(id); diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index f016a9d4d..8dfc2b4f7 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -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; }; diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp index bb90a581d..2ed3baef8 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp @@ -652,22 +652,27 @@ void CustomEmojiManager::unregisterListener(not_null listener) { } } -rpl::producer> CustomEmojiManager::resolve( - DocumentId documentId) { +auto CustomEmojiManager::resolve(DocumentId documentId) +-> rpl::producer, rpl::empty_error> { return [=](auto consumer) { auto result = rpl::lifetime(); - const auto put = [=](not_null document) { + const auto put = [=]( + not_null 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(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 document) { } } -void CustomEmojiManager::processListeners(not_null document) { +void CustomEmojiManager::processListeners( + not_null document) { const auto id = document->id; if (const auto listeners = _resolvers.take(id)) { for (const auto &listener : *listeners) { diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h index d51a52190..1d2f7940b 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h @@ -66,8 +66,8 @@ public: void resolve(DocumentId documentId, not_null listener); void unregisterListener(not_null listener); - [[nodiscard]] rpl::producer> resolve( - DocumentId documentId); + [[nodiscard]] auto resolve(DocumentId documentId) + -> rpl::producer, rpl::empty_error>; [[nodiscard]] std::unique_ptr createLoader( not_null document, diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp index a764199da..5d3025d45 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp @@ -98,7 +98,7 @@ namespace { text.size(), Data::SerializeCustomEmojiId(document)) }, }; - }); + }) | rpl::map_error_to_done(); } [[nodiscard]] rpl::producer PeerCustomStatus( diff --git a/Telegram/SourceFiles/history/view/media/history_view_custom_emoji.cpp b/Telegram/SourceFiles/history/view/media/history_view_custom_emoji.cpp index 51c69c89b..94de0a30c 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_custom_emoji.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_custom_emoji.cpp @@ -108,6 +108,9 @@ CustomEmoji::CustomEmoji( } void CustomEmoji::customEmojiResolveDone(not_null document) { + if (!document->sticker()) { + return; + } _resolving = false; const auto id = document->id; for (auto &line : _lines) { diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index a35609c38..c91b146a5 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -162,7 +162,7 @@ void TopicIconView::setupPlayer(not_null topic) { id ) | rpl::map([=](not_null document) { return document.get(); - }); + }) | rpl::map_error_to_done(); }) | rpl::flatten_latest( ) | rpl::map([=](DocumentData *document) -> rpl::producer> { diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 674170696..f7ff549c2 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -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 MakeEmojiSetStatusPreview( + not_null parent, + not_null peer, + not_null document) { + auto result = std::make_unique(parent); + + const auto size = st::chatGiveawayPeerSize; + const auto padding = st::chatGiveawayPeerPadding; + + const auto raw = result.get(); + + const auto width = raw->lifetime().make_state(); + const auto name = raw->lifetime().make_state( + raw, + rpl::single(peer->name()), + st::botEmojiStatusName); + auto emojiText = TextWithEntities(); + const auto emoji = raw->lifetime().make_state( + raw, + rpl::single(emojiText), + st::botEmojiStatusName); + const auto userpic = raw->lifetime().make_state( + 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 box, + not_null bot, + not_null document, + TimeId until, + Fn done) { + box->setNoContentMargin(true); + + auto owned = Settings::MakeEmojiStatusPreview(box, document); + const auto preview = box->addRow( + object_ptr::fromRaw(owned.release())); + preview->resize(preview->width(), st::botEmojiStatusPreviewHeight); + + const auto set = box->lifetime().make_state(); + + box->addRow(object_ptr( + box, + tr::lng_bot_emoji_status_title(), + st::botEmojiStatusTitle)); + AddSkip(box->verticalLayout()); + + box->addRow(object_ptr( + 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::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 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; diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index a879cdd2c..b3ce5cbf5 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -263,6 +263,8 @@ private: void botSharePhone(Fn 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 _panel; + rpl::lifetime _lifetime; + static base::weak_ptr PendingActivation; }; diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 6c239c49e..172420fd8 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -1773,7 +1773,29 @@ void AddSummaryPremium( } Ui::AddSkip(content, descriptionPadding.bottom()); +} +std::unique_ptr MakeEmojiStatusPreview( + not_null parent, + not_null document) { + auto result = std::make_unique(parent); + + const auto raw = result.get(); + const auto size = HistoryView::Sticker::EmojiSize(); + const auto emoji = raw->lifetime().make_state( + 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 diff --git a/Telegram/SourceFiles/settings/settings_premium.h b/Telegram/SourceFiles/settings/settings_premium.h index 65d7d18da..3a6ef6896 100644 --- a/Telegram/SourceFiles/settings/settings_premium.h +++ b/Telegram/SourceFiles/settings/settings_premium.h @@ -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 buttonCallback); +[[nodiscard]] std::unique_ptr MakeEmojiStatusPreview( + not_null parent, + not_null document); +[[nodiscard]] std::unique_ptr MakeEmojiSetStatusPreview( + not_null parent, + not_null peer, + not_null document); + } // namespace Settings diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp index 36de9475d..62e9191e1 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -824,6 +824,8 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) { 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'.")); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h index a42bf6634..7ea2be451 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h @@ -51,6 +51,12 @@ struct CustomMethodRequest { Fn callback; }; +struct SetEmojiStatusRequest { + uint64 customEmojiId = 0; + TimeId expirationDate = 0; + Fn callback; +}; + class Delegate { public: virtual Webview::ThemeParams botThemeParams() = 0; @@ -67,6 +73,7 @@ public: virtual void botAllowWriteAccess(Fn callback) = 0; virtual void botSharePhone(Fn 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 title); void sendDataMessage(const QJsonObject &args); void switchInlineQueryMessage(const QJsonObject &args); + void processEmojiStatusRequest(const QJsonObject &args); + void processEmojiStatusAccessRequest(); void processButtonMessage( std::unique_ptr