diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 573a6dbf54..a0a500bff6 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1293,6 +1293,8 @@ PRIVATE settings/business/settings_away_message.h settings/business/settings_shortcut_messages.cpp settings/business/settings_shortcut_messages.h + settings/business/settings_chat_intro.cpp + settings/business/settings_chat_intro.h settings/business/settings_chatbots.cpp settings/business/settings_chatbots.h settings/business/settings_greeting.cpp diff --git a/Telegram/Resources/icons/settings/premium/intro.png b/Telegram/Resources/icons/settings/premium/intro.png new file mode 100644 index 0000000000..ecb6673fc4 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/intro.png differ diff --git a/Telegram/Resources/icons/settings/premium/intro@2x.png b/Telegram/Resources/icons/settings/premium/intro@2x.png new file mode 100644 index 0000000000..43fb942276 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/intro@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/intro@3x.png b/Telegram/Resources/icons/settings/premium/intro@3x.png new file mode 100644 index 0000000000..35e11ed24a Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/intro@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 5aeac265d4..b87b38d0f5 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2185,6 +2185,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_business_about_away_messages" = "Define messages that are automatically sent when you are off."; "lng_business_subtitle_chatbots" = "Chatbots"; "lng_business_about_chatbots" = "Add any third party chatbots that will process customer interactions."; +"lng_business_subtitle_chat_intro" = "Intro"; +"lng_business_about_chat_intro" = "Customize the message people see before they start a chat with you."; "lng_location_title" = "Location"; "lng_location_about" = "Display the location of your business on your account."; @@ -2297,6 +2299,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_chatbot_menu_remove" = "Remove bot from this chat"; "lng_chatbot_menu_revoke" = "Revoke access to this chat"; +"lng_chat_intro_title" = "Intro"; +"lng_chat_intro_subtitle" = "Customize your intro"; +"lng_chat_intro_default_title" = "No messages here yet..."; +"lng_chat_intro_default_message" = "Send a message or tap on the greeting below"; +"lng_chat_intro_enter_title" = "Enter Title"; +"lng_chat_intro_enter_message" = "Enter Message"; +"lng_chat_intro_choose_sticker" = "Choose Sticker"; +"lng_chat_intro_random_sticker" = "Random"; +"lng_chat_intro_about" = "You can customize the message people see before they start a chat with you."; +"lng_chat_intro_reset" = "Reset to Default"; + "lng_boost_channel_button" = "Boost Channel"; "lng_boost_group_button" = "Boost Group"; "lng_boost_again_button" = "Boost Again"; diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index 72da2a4893..3f94c8e5cc 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -109,6 +109,18 @@ rpl::producer<> Premium::cloudSetUpdated() const { return _cloudSetUpdated.events(); } +auto Premium::helloStickers() const +-> const std::vector> & { + if (_helloStickers.empty()) { + const_cast(this)->reloadHelloStickers(); + } + return _helloStickers; +} + +rpl::producer<> Premium::helloStickersUpdated() const { + return _helloStickersUpdated.events(); +} + int64 Premium::monthlyAmount() const { return _monthlyAmount; } @@ -225,6 +237,33 @@ void Premium::reloadCloudSet() { }).send(); } +void Premium::reloadHelloStickers() { + if (_helloStickersRequestId) { + return; + } + _helloStickersRequestId = _api.request(MTPmessages_GetStickers( + MTP_string("\xf0\x9f\x91\x8b\xe2\xad\x90\xef\xb8\x8f"), + MTP_long(_helloStickersHash) + )).done([=](const MTPmessages_Stickers &result) { + _helloStickersRequestId = 0; + result.match([&](const MTPDmessages_stickersNotModified &) { + }, [&](const MTPDmessages_stickers &data) { + _helloStickersHash = data.vhash().v; + const auto owner = &_session->data(); + _helloStickers.clear(); + for (const auto &sticker : data.vstickers().v) { + const auto document = owner->processDocument(sticker); + if (document->sticker()) { + _helloStickers.push_back(document); + } + } + _helloStickersUpdated.fire({}); + }); + }).fail([=] { + _helloStickersRequestId = 0; + }).send(); +} + void Premium::checkGiftCode( const QString &slug, Fn done) { @@ -609,4 +648,24 @@ RequirePremiumState ResolveRequiresPremiumToWrite( return RequirePremiumState::Unknown; } +rpl::producer RandomHelloStickerValue( + not_null session) { + const auto premium = &session->api().premium(); + const auto random = [=] { + const auto &v = premium->helloStickers(); + Assert(!v.empty()); + return v[base::RandomIndex(v.size())].get(); + }; + const auto &v = premium->helloStickers(); + if (!v.empty()) { + return rpl::single(random()); + } + return rpl::single( + nullptr + ) | rpl::then(premium->helloStickersUpdated( + ) | rpl::filter([=] { + return !premium->helloStickers().empty(); + }) | rpl::take(1) | rpl::map(random)); +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_premium.h b/Telegram/SourceFiles/api/api_premium.h index e1c3a7b412..9bacca92d5 100644 --- a/Telegram/SourceFiles/api/api_premium.h +++ b/Telegram/SourceFiles/api/api_premium.h @@ -85,6 +85,10 @@ public: -> const std::vector> &; [[nodiscard]] rpl::producer<> cloudSetUpdated() const; + [[nodiscard]] auto helloStickers() const + -> const std::vector> &; + [[nodiscard]] rpl::producer<> helloStickersUpdated() const; + [[nodiscard]] int64 monthlyAmount() const; [[nodiscard]] QString monthlyCurrency() const; @@ -111,6 +115,7 @@ private: void reloadPromo(); void reloadStickers(); void reloadCloudSet(); + void reloadHelloStickers(); void requestPremiumRequiredSlice(); const not_null _session; @@ -133,6 +138,11 @@ private: std::vector> _cloudSet; rpl::event_stream<> _cloudSetUpdated; + mtpRequestId _helloStickersRequestId = 0; + uint64 _helloStickersHash = 0; + std::vector> _helloStickers; + rpl::event_stream<> _helloStickersUpdated; + int64 _monthlyAmount = 0; QString _monthlyCurrency; @@ -215,4 +225,7 @@ enum class RequirePremiumState { not_null peer, History *maybeHistory); +[[nodiscard]] rpl::producer RandomHelloStickerValue( + not_null session); + } // namespace Api diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index a658640638..c0d26766a6 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -808,34 +808,6 @@ int ColorSelector::resizeGetHeight(int newWidth) { return st; } -struct ButtonWithEmoji { - not_null st; - int emojiWidth = 0; - int noneWidth = 0; - int added = 0; -}; - -[[nodiscard]] ButtonWithEmoji ButtonStyleWithRightEmoji( - not_null parent) { - const auto ratio = style::DevicePixelRatio(); - const auto emojiWidth = Data::FrameSizeFromTag({}) / ratio; - - const auto noneWidth = st::normalFont->width( - tr::lng_settings_color_emoji_off(tr::now)); - - const auto added = st::normalFont->spacew; - const auto rightAdded = std::max(noneWidth, emojiWidth); - return { - .st = ButtonStyleWithAddedPadding( - parent, - st::peerAppearanceButton, - QMargins(0, 0, added + rightAdded, 0)), - .emojiWidth = emojiWidth, - .noneWidth = noneWidth, - .added = added, - }; -} - [[nodiscard]] object_ptr CreateEmojiIconButton( not_null parent, std::shared_ptr show, @@ -844,7 +816,9 @@ struct ButtonWithEmoji { rpl::producer colorIndexValue, rpl::producer emojiIdValue, Fn emojiIdChosen) { - const auto button = ButtonStyleWithRightEmoji(parent); + const auto button = ButtonStyleWithRightEmoji( + parent, + tr::lng_settings_color_emoji_off(tr::now)); auto result = Settings::CreateButtonWithIcon( parent, tr::lng_settings_color_emoji(), @@ -962,7 +936,9 @@ struct ButtonWithEmoji { rpl::producer statusIdValue, Fn statusIdChosen, bool group) { - const auto button = ButtonStyleWithRightEmoji(parent); + const auto button = ButtonStyleWithRightEmoji( + parent, + tr::lng_settings_color_emoji_off(tr::now)); const auto &phrase = group ? tr::lng_edit_channel_status_group : tr::lng_edit_channel_status; @@ -1073,7 +1049,9 @@ struct ButtonWithEmoji { not_null channel) { Expects(channel->mgInfo != nullptr); - const auto button = ButtonStyleWithRightEmoji(parent); + const auto button = ButtonStyleWithRightEmoji( + parent, + tr::lng_settings_color_emoji_off(tr::now)); auto result = Settings::CreateButtonWithIcon( parent, tr::lng_group_emoji(), @@ -1509,3 +1487,25 @@ void CheckBoostLevel( cancel(); }).send(); } + +ButtonWithEmoji ButtonStyleWithRightEmoji( + not_null parent, + const QString &noneString, + const style::SettingsButton &parentSt) { + const auto ratio = style::DevicePixelRatio(); + const auto emojiWidth = Data::FrameSizeFromTag({}) / ratio; + + const auto noneWidth = st::normalFont->width(noneString); + + const auto added = st::normalFont->spacew; + const auto rightAdded = std::max(noneWidth, emojiWidth); + return { + .st = ButtonStyleWithAddedPadding( + parent, + parentSt, + QMargins(0, 0, added + rightAdded, 0)), + .emojiWidth = emojiWidth, + .noneWidth = noneWidth, + .added = added, + }; +} diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h index 6b72844494..cf83bfc74f 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h @@ -7,6 +7,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +namespace style { +struct SettingsButton; +} // namespace style + +namespace st { +extern const style::SettingsButton &peerAppearanceButton; +} // namespace st + namespace ChatHelpers { class Show; } // namespace ChatHelpers @@ -17,6 +25,7 @@ class ChatStyle; class ChatTheme; class VerticalLayout; struct AskBoostReason; +class RpWidget; } // namespace Ui void EditPeerColorBox( @@ -36,3 +45,14 @@ void CheckBoostLevel( not_null peer, Fn(int level)> askMore, Fn cancel); + +struct ButtonWithEmoji { + not_null st; + int emojiWidth = 0; + int noneWidth = 0; + int added = 0; +}; +[[nodiscard]] ButtonWithEmoji ButtonStyleWithRightEmoji( + not_null parent, + const QString &noneString, + const style::SettingsButton &parentSt = st::peerAppearanceButton); diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index 76df0c2d7b..dc977468cd 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -144,6 +144,8 @@ void PreloadSticker(const std::shared_ptr &media) { return tr::lng_business_subtitle_away_messages(); case PremiumFeature::BusinessBots: return tr::lng_business_subtitle_chatbots(); + case PremiumFeature::ChatIntro: + return tr::lng_business_subtitle_chat_intro(); } Unexpected("PremiumFeature in SectionTitle."); } @@ -201,6 +203,8 @@ void PreloadSticker(const std::shared_ptr &media) { return tr::lng_business_about_away_messages(); case PremiumFeature::BusinessBots: return tr::lng_business_about_chatbots(); + case PremiumFeature::ChatIntro: + return tr::lng_business_about_chat_intro(); } Unexpected("PremiumFeature in SectionTitle."); } @@ -528,6 +532,7 @@ struct VideoPreviewDocument { case PremiumFeature::GreetingMessage: return "greeting_message"; case PremiumFeature::AwayMessage: return "away_message"; case PremiumFeature::BusinessBots: return "business_bots"; + case PremiumFeature::ChatIntro: return "chat_intro"; } return ""; }(); diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h index 80400a6eed..fa520caafe 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.h +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -74,6 +74,7 @@ enum class PremiumFeature { GreetingMessage, AwayMessage, BusinessBots, + ChatIntro, kCount, }; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_lottie.h b/Telegram/SourceFiles/chat_helpers/stickers_lottie.h index 156d0a9bb7..17ae65c9dc 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_lottie.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_lottie.h @@ -66,7 +66,7 @@ enum class StickerLottieSize : uint8 { EmojiInteractionReserved5, EmojiInteractionReserved6, EmojiInteractionReserved7, - PremiumReactionPreview, + ChatIntroHelloSticker, }; [[nodiscard]] uint8 LottieCacheKeyShift( uint8 replacementsTag, diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp index aef9d7406f..5b32ea14c3 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp @@ -378,6 +378,9 @@ TabbedSelector::TabbedSelector( tabs.reserve(2); tabs.push_back(createTab(SelectorTab::Stickers, 0)); tabs.push_back(createTab(SelectorTab::Masks, 1)); + } else if (_mode == Mode::StickersOnly) { + tabs.reserve(1); + tabs.push_back(createTab(SelectorTab::Stickers, 0)); } else { tabs.reserve(1); tabs.push_back(createTab(SelectorTab::Emoji, 0)); @@ -385,10 +388,10 @@ TabbedSelector::TabbedSelector( return tabs; }()) , _currentTabType(full() - ? session().settings().selectorTab() - : mediaEditor() - ? SelectorTab::Stickers - : SelectorTab::Emoji) + ? session().settings().selectorTab() + : (mediaEditor() || _mode == Mode::StickersOnly) + ? SelectorTab::Stickers + : SelectorTab::Emoji) , _hasEmojiTab(ranges::contains(_tabs, SelectorTab::Emoji, &Tab::type)) , _hasStickersTab(ranges::contains(_tabs, SelectorTab::Stickers, &Tab::type)) , _hasGifsTab(ranges::contains(_tabs, SelectorTab::Gifs, &Tab::type)) @@ -661,7 +664,7 @@ void TabbedSelector::updateTabsSliderGeometry() { if (!_tabsSlider) { return; } - const auto w = mediaEditor() && hasMasksTab() && masks()->mySetsEmpty() + const auto w = (mediaEditor() && hasMasksTab() && masks()->mySetsEmpty()) ? width() / 2 : width(); _tabsSlider->resizeToWidth(w); diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h index 3b2f45d5dc..adfee53619 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h @@ -79,6 +79,7 @@ using InlineChosen = InlineBots::ResultSelected; enum class TabbedSelectorMode { Full, EmojiOnly, + StickersOnly, MediaEditor, EmojiStatus, ChannelStatus, diff --git a/Telegram/SourceFiles/data/business/data_business_common.cpp b/Telegram/SourceFiles/data/business/data_business_common.cpp index 7fc092115d..bdc8ffe7d3 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.cpp +++ b/Telegram/SourceFiles/data/business/data_business_common.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/business/data_business_common.h" +#include "data/data_document.h" #include "data/data_session.h" #include "data/data_user.h" @@ -333,4 +334,22 @@ WorkingIntervals ReplaceDayIntervals( return result.normalized(); } +ChatIntro FromMTP( + not_null owner, + const tl::conditional &intro) { + auto result = ChatIntro(); + if (intro) { + const auto &data = intro->data(); + result.title = qs(data.vtitle()); + result.description = qs(data.vdescription()); + if (const auto document = data.vsticker()) { + result.sticker = owner->processDocument(*document); + if (!result.sticker->sticker()) { + result.sticker = nullptr; + } + } + } + return result; +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h index 3793f0b607..86754c56f3 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.h +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -252,4 +252,22 @@ struct GreetingSettings { not_null owner, const tl::conditional &message); +struct ChatIntro { + QString title; + QString description; + DocumentData *sticker = nullptr; + + explicit operator bool() const { + return !title.isEmpty() || !description.isEmpty(); + } + + friend inline bool operator==( + const ChatIntro &a, + const ChatIntro &b) = default; +}; + +[[nodiscard]] ChatIntro FromMTP( + not_null owner, + const tl::conditional &intro); + } // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_info.cpp b/Telegram/SourceFiles/data/business/data_business_info.cpp index 1e7beef884..66c7e0be9e 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.cpp +++ b/Telegram/SourceFiles/data/business/data_business_info.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "base/unixtime.h" #include "data/business/data_business_common.h" +#include "data/data_document.h" #include "data/data_session.h" #include "data/data_user.h" #include "main/main_session.h" @@ -183,6 +184,54 @@ rpl::producer<> BusinessInfo::greetingSettingsChanged() const { return _greetingSettingsChanged.events(); } +void BusinessInfo::saveChatIntro(ChatIntro data, Fn fail) { + const auto &was = _chatIntro; + if (was == data) { + return; + } else { + const auto session = &_owner->session(); + using Flag = MTPaccount_UpdateBusinessIntro::Flag; + session->api().request(MTPaccount_UpdateBusinessIntro( + MTP_flags(data ? Flag::f_intro : Flag()), + MTP_inputBusinessIntro( + MTP_flags(data.sticker ? MTPDinputBusinessIntro::Flag::f_sticker : MTPDinputBusinessIntro::Flag()), + MTP_string(data.title), + MTP_string(data.description), + (data.sticker + ? data.sticker->mtpInput() + : MTP_inputDocumentEmpty())) + )).fail([=](const MTP::Error &error) { + _chatIntro = was; + _chatIntroChanged.fire({}); + if (fail) { + fail(error.type()); + } + }).send(); + } + _chatIntro = std::move(data); + _chatIntroChanged.fire({}); +} + +void BusinessInfo::applyChatIntro(ChatIntro data) { + if (_chatIntro == data) { + return; + } + _chatIntro = data; + _chatIntroChanged.fire({}); +} + +ChatIntro BusinessInfo::chatIntro() const { + return _chatIntro.value_or(ChatIntro()); +} + +bool BusinessInfo::chatIntroLoaded() const { + return _chatIntro.has_value(); +} + +rpl::producer<> BusinessInfo::chatIntroChanged() const { + return _chatIntroChanged.events(); +} + void BusinessInfo::preload() { preloadTimezones(); } diff --git a/Telegram/SourceFiles/data/business/data_business_info.h b/Telegram/SourceFiles/data/business/data_business_info.h index 933d869107..bdae99e35f 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.h +++ b/Telegram/SourceFiles/data/business/data_business_info.h @@ -36,6 +36,12 @@ public: [[nodiscard]] bool greetingSettingsLoaded() const; [[nodiscard]] rpl::producer<> greetingSettingsChanged() const; + void saveChatIntro(ChatIntro data, Fn fail); + void applyChatIntro(ChatIntro data); + [[nodiscard]] ChatIntro chatIntro() const; + [[nodiscard]] bool chatIntroLoaded() const; + [[nodiscard]] rpl::producer<> chatIntroChanged() const; + void preloadTimezones(); [[nodiscard]] bool timezonesLoaded() const; [[nodiscard]] rpl::producer timezonesValue() const; @@ -51,6 +57,9 @@ private: std::optional _greetingSettings; rpl::event_stream<> _greetingSettingsChanged; + std::optional _chatIntro; + rpl::event_stream<> _chatIntroChanged; + mtpRequestId _timezonesRequestId = 0; int32 _timezonesHash = 0; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 313ed21bdb..a1219380cd 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -601,6 +601,8 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { FromMTP(&user->owner(), update.vbusiness_away_message())); user->owner().businessInfo().applyGreetingSettings( FromMTP(&user->owner(), update.vbusiness_greeting_message())); + user->owner().businessInfo().applyChatIntro( + FromMTP(&user->owner(), update.vbusiness_intro())); } user->owner().stories().apply(user, update.vstories()); diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.cpp b/Telegram/SourceFiles/history/view/history_view_about_view.cpp index c017d8842a..0252ccc689 100644 --- a/Telegram/SourceFiles/history/view/history_view_about_view.cpp +++ b/Telegram/SourceFiles/history/view/history_view_about_view.cpp @@ -7,10 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/history_view_about_view.h" +#include "chat_helpers/stickers_lottie.h" #include "core/click_handler_types.h" +#include "data/business/data_business_common.h" +#include "data/data_document.h" +#include "data/data_session.h" #include "data/data_user.h" #include "history/view/media/history_view_service_box.h" #include "history/view/media/history_view_sticker_player_abstract.h" +#include "history/view/media/history_view_sticker.h" #include "history/view/history_view_element.h" #include "history/history.h" #include "history/history_item.h" @@ -64,6 +69,43 @@ private: }; +class ChatIntroBox final : public ServiceBoxContent { +public: + ChatIntroBox(not_null parent, Data::ChatIntro data); + ~ChatIntroBox(); + + int width() override; + int top() override; + QSize size() override; + QString title() override; + TextWithEntities subtitle() override; + int buttonSkip() override; + rpl::producer button() override; + void draw( + Painter &p, + const PaintContext &context, + const QRect &geometry) override; + ClickHandlerPtr createViewLink() override; + + bool hideServiceText() override { + return true; + } + + void stickerClearLoopPlayed() override; + std::unique_ptr stickerTakePlayer( + not_null data, + const Lottie::ColorReplacements *replacements) override; + + bool hasHeavyPart() override; + void unloadHeavyPart() override; + +private: + const not_null _parent; + const Data::ChatIntro _data; + mutable std::optional _sticker; + +}; + PremiumRequiredBox::PremiumRequiredBox(not_null parent) : _parent(parent) { } @@ -133,6 +175,99 @@ bool PremiumRequiredBox::hasHeavyPart() { void PremiumRequiredBox::unloadHeavyPart() { } +ChatIntroBox::ChatIntroBox(not_null parent, Data::ChatIntro data) +: _parent(parent) +, _data(data) { + if (const auto document = data.sticker) { + if (const auto sticker = document->sticker()) { + const auto skipPremiumEffect = false; + _sticker.emplace(_parent, document, skipPremiumEffect, _parent); + _sticker->setDiceIndex(sticker->alt, 0); + _sticker->setGiftBoxSticker(true); + _sticker->initSize(); + _sticker->setCustomEmojiPart( + st::chatIntroStickerSize, + ChatHelpers::StickerLottieSize::ChatIntroHelloSticker); + } + } +} + +ChatIntroBox::~ChatIntroBox() = default; + +int ChatIntroBox::width() { + return st::chatIntroWidth; +} + +int ChatIntroBox::top() { + return st::msgServiceGiftBoxButtonMargins.top(); +} + +QSize ChatIntroBox::size() { + return { st::msgServicePhotoWidth, st::msgServicePhotoWidth }; +} + +QString ChatIntroBox::title() { + return _data ? _data.title : tr::lng_chat_intro_default_title(tr::now); +} + +int ChatIntroBox::buttonSkip() { + return st::storyMentionButtonSkip; +} + +rpl::producer ChatIntroBox::button() { + return nullptr; +} + +TextWithEntities ChatIntroBox::subtitle() { + return { + (_data + ? _data.description + : tr::lng_chat_intro_default_message(tr::now)) + }; +} + +ClickHandlerPtr ChatIntroBox::createViewLink() { + return std::make_shared([=](ClickContext context) { + const auto my = context.other.value(); + if (const auto controller = my.sessionWindow.get()) { + Settings::ShowPremium(controller, u"require_premium"_q); + } + }); +} + +void ChatIntroBox::draw( + Painter &p, + const PaintContext &context, + const QRect &geometry) { + if (_sticker) { + _sticker->draw(p, context, geometry); + } +} + +void ChatIntroBox::stickerClearLoopPlayed() { + if (_sticker) { + _sticker->stickerClearLoopPlayed(); + } +} + +std::unique_ptr ChatIntroBox::stickerTakePlayer( + not_null data, + const Lottie::ColorReplacements *replacements) { + return _sticker + ? _sticker->stickerTakePlayer(data, replacements) + : nullptr; +} + +bool ChatIntroBox::hasHeavyPart() { + return _sticker && _sticker->hasHeavyPart(); +} + +void ChatIntroBox::unloadHeavyPart() { + if (_sticker) { + _sticker->unloadHeavyPart(); + } +} + } // namespace AboutView::AboutView( @@ -142,6 +277,10 @@ AboutView::AboutView( , _delegate(delegate) { } +AboutView::~AboutView() { + setItem({}, nullptr); +} + not_null AboutView::history() const { return _history; } @@ -187,6 +326,37 @@ bool AboutView::refresh() { return true; } +void AboutView::make(Data::ChatIntro data) { + const auto item = _history->makeMessage({ + .id = _history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeAboutView + | MessageFlag::FakeHistoryItem + | MessageFlag::Local), + .from = _history->peer->id, + }, PreparedServiceText{ { data.description } }); + + setItem(AdminLog::OwnedItem(_delegate, item), data.sticker); + + _item->overrideMedia(std::make_unique( + _item.get(), + std::make_unique(_item.get(), data))); +} + +void AboutView::setItem(AdminLog::OwnedItem item, DocumentData *sticker) { + if (const auto was = _item ? _item->data().get() : nullptr) { + if (_sticker) { + was->history()->owner().unregisterDocumentItem(_sticker, was); + } + } + _item = std::move(item); + _sticker = sticker; + if (const auto now = _item ? _item->data().get() : nullptr) { + if (_sticker) { + now->history()->owner().registerDocumentItem(_sticker, now); + } + } +} + AdminLog::OwnedItem AboutView::makeAboutBot(not_null info) { const auto textWithEntities = TextUtilities::ParseEntities( info->description, diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.h b/Telegram/SourceFiles/history/view/history_view_about_view.h index bf162f4516..cd1528b759 100644 --- a/Telegram/SourceFiles/history/view/history_view_about_view.h +++ b/Telegram/SourceFiles/history/view/history_view_about_view.h @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/admin_log/history_admin_log_item.h" +namespace Data { +struct ChatIntro; +} // namespace Data + namespace HistoryView { class AboutView final : public ClickHandlerHost { @@ -16,6 +20,7 @@ public: AboutView( not_null history, not_null delegate); + ~AboutView(); [[nodiscard]] not_null history() const; [[nodiscard]] Element *view() const; @@ -23,16 +28,20 @@ public: bool refresh(); + void make(Data::ChatIntro data); + int top = 0; int height = 0; private: [[nodiscard]] AdminLog::OwnedItem makeAboutBot(not_null info); [[nodiscard]] AdminLog::OwnedItem makePremiumRequired(); + void setItem(AdminLog::OwnedItem item, DocumentData *sticker); const not_null _history; const not_null _delegate; AdminLog::OwnedItem _item; + DocumentData *_sticker = nullptr; int _version = 0; }; diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp index 6eb5d8e405..858b89fbcc 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp @@ -884,6 +884,9 @@ BusinessBotStatus::Bar::Bar(QWidget *parent) , _settings(this, st::historyBusinessBotSettings) { _name->setAttribute(Qt::WA_TransparentForMouseEvents); _status->setAttribute(Qt::WA_TransparentForMouseEvents); + _togglePaused->setFullRadius(true); + _togglePaused->setTextTransform( + Ui::RoundButton::TextTransform::NoTransform); _settings->setClickedCallback([=] { showMenu(); }); @@ -984,7 +987,9 @@ int BusinessBotStatus::Bar::resizeGetHeight(int newWidth) { } auto available = newWidth - _settings->width() - st.namePosition.x(); if (!_togglePaused->isHidden()) { - _togglePaused->moveToRight(_settings->width(), 0); + _togglePaused->moveToRight( + _settings->width(), + (st.height - _togglePaused->height()) / 2); available -= _togglePaused->width(); } _name->resizeToWidth(available); diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index a0146f1bce..50df285803 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -114,7 +114,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#2357bf25 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int = Message; +message#2357bf25 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int = Message; messageService#2b085862 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; @@ -227,7 +227,7 @@ inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; -userFull#670bbc9c flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro = UserFull; +userFull#ecdadceb flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro birthday:flags2.5?Birthday = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; @@ -517,6 +517,7 @@ inputPrivacyKeyPhoneNumber#352dafa = InputPrivacyKey; inputPrivacyKeyAddedByPhone#d1219bdd = InputPrivacyKey; inputPrivacyKeyVoiceMessages#aee69d68 = InputPrivacyKey; inputPrivacyKeyAbout#3823cc40 = InputPrivacyKey; +inputPrivacyKeyBirthday#d65a11cc = InputPrivacyKey; privacyKeyStatusTimestamp#bc2eab30 = PrivacyKey; privacyKeyChatInvite#500e6dfa = PrivacyKey; @@ -528,6 +529,7 @@ privacyKeyPhoneNumber#d19ae46d = PrivacyKey; privacyKeyAddedByPhone#42ffd42b = PrivacyKey; privacyKeyVoiceMessages#697f414 = PrivacyKey; privacyKeyAbout#a486b761 = PrivacyKey; +privacyKeyBirthday#2000a518 = PrivacyKey; inputPrivacyValueAllowContacts#d09e07b = InputPrivacyRule; inputPrivacyValueAllowAll#184b35ce = InputPrivacyRule; @@ -1710,6 +1712,8 @@ account.connectedBots#17d7f87b connected_bots:Vector users:Vector< messages.dialogFilters#2ad93719 flags:# tags_enabled:flags.0?true filters:Vector = messages.DialogFilters; +birthday#6c8e1e06 flags:# day:int month:int year:flags.0?int = Birthday; + botBusinessConnection#896433b4 flags:# can_reply:flags.0?true disabled:flags.1?true connection_id:string user_id:long dc_id:int date:int = BotBusinessConnection; inputBusinessIntro#9c469cd flags:# title:string description:string sticker:flags.0?InputDocument = InputBusinessIntro; @@ -1727,6 +1731,10 @@ inputBusinessBotRecipients#c4e5921e flags:# existing_chats:flags.0?true new_chat businessBotRecipients#b88cf373 flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true users:flags.4?Vector exclude_users:flags.6?Vector = BusinessBotRecipients; +contactBirthday#1d998733 contact_id:long birthday:Birthday = ContactBirthday; + +contacts.contactBirthdays#114ff30d contacts:Vector users:Vector = contacts.ContactBirthdays; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1863,6 +1871,7 @@ account.getBotBusinessConnection#76a86270 connection_id:string = Updates; account.updateBusinessIntro#a614d034 flags:# intro:flags.0?InputBusinessIntro = Bool; account.toggleConnectedBotPaused#646e1097 peer:InputPeer paused:Bool = Bool; account.disablePeerConnectedBot#5e437ed9 peer:InputPeer = Bool; +account.updateBirthday#cc6e0c11 flags:# birthday:flags.0?Birthday = Bool; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#b60f5918 id:InputUser = users.UserFull; @@ -1894,6 +1903,7 @@ contacts.exportContactToken#f8654027 = ExportedContactToken; contacts.importContactToken#13005788 token:string = User; contacts.editCloseFriends#ba6705f0 id:Vector = Bool; contacts.setBlocked#94c65c76 flags:# my_stories_from:flags.0?true id:Vector limit:int = Bool; +contacts.getBirthdays#daeda864 = contacts.ContactBirthdays; messages.getMessages#63c66506 id:Vector = messages.Messages; messages.getDialogs#a0f4cb4f flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.Dialogs; diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index 0caa86b349..afaeb3a2b9 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -31,7 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Settings { namespace { -class AwayMessage : public BusinessSection { +class AwayMessage final : public BusinessSection { public: AwayMessage( QWidget *parent, diff --git a/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp b/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp new file mode 100644 index 0000000000..28a223039b --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp @@ -0,0 +1,564 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "settings/business/settings_chat_intro.h" + +#include "api/api_premium.h" +#include "boxes/peers/edit_peer_color_box.h" // ButtonStyleWithRightEmoji +#include "chat_helpers/tabbed_panel.h" +#include "chat_helpers/tabbed_selector.h" +#include "core/application.h" +#include "data/business/data_business_info.h" +#include "data/data_document.h" +#include "data/data_session.h" +#include "history/view/history_view_about_view.h" +#include "history/view/history_view_element.h" +#include "history/history.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/business/settings_recipients_helper.h" +#include "ui/chat/chat_style.h" +#include "ui/chat/chat_theme.h" +#include "ui/effects/path_shift_gradient.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/widgets/buttons.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/painter.h" +#include "ui/vertical_list.h" +#include "window/themes/window_theme.h" +#include "window/section_widget.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +using namespace HistoryView; + +class PreviewDelegate final : public DefaultElementDelegate { +public: + PreviewDelegate( + not_null parent, + not_null st, + Fn update); + + bool elementAnimationsPaused() override; + not_null elementPathShiftGradient() override; + Context elementContext() override; + +private: + const not_null _parent; + const std::unique_ptr _pathGradient; + +}; + +class PreviewWrap final : public Ui::RpWidget { +public: + PreviewWrap( + not_null parent, + not_null session, + rpl::producer value); + ~PreviewWrap(); + +private: + void paintEvent(QPaintEvent *e) override; + + void resizeTo(int width); + void prepare(rpl::producer value); + + const not_null _history; + const std::unique_ptr _theme; + const std::unique_ptr _style; + const std::unique_ptr _delegate; + + std::unique_ptr _view; + QPoint _position; + +}; + +class StickerPanel final { +public: + StickerPanel(); + ~StickerPanel(); + + struct Descriptor { + not_null controller; + not_null button; + DocumentId ensureAddedId = 0; + }; + void show(Descriptor &&descriptor); + void repaint(); + + [[nodiscard]] bool hasFocus() const; + + struct CustomChosen { + not_null sticker; + }; + [[nodiscard]] rpl::producer someCustomChosen() const { + return _someCustomChosen.events(); + } + +private: + void create(const Descriptor &descriptor); + + base::unique_qptr _panel; + QPointer _panelButton; + rpl::event_stream _someCustomChosen; + +}; + +class ChatIntro final : public BusinessSection { +public: + ChatIntro( + QWidget *parent, + not_null controller); + ~ChatIntro(); + + [[nodiscard]] bool closeByOutsideClick() const override; + [[nodiscard]] rpl::producer title() override; + + void setInnerFocus() override { + _setFocus(); + } + +private: + void setupContent(not_null controller); + void save(); + + Fn _setFocus; + + rpl::variable _intro; + +}; + +[[nodiscard]] object_ptr CreateIntroStickerButton( + not_null parent, + std::shared_ptr show, + rpl::producer stickerValue, + Fn stickerChosen) { + const auto button = ButtonStyleWithRightEmoji( + parent, + tr::lng_chat_intro_random_sticker(tr::now), + st::settingsButtonNoIcon); + auto result = Settings::CreateButtonWithIcon( + parent, + tr::lng_chat_intro_choose_sticker(), + *button.st); + const auto raw = result.data(); + + const auto right = Ui::CreateChild(raw); + right->show(); + + struct State { + StickerPanel panel; + DocumentId stickerId = 0; + }; + const auto state = right->lifetime().make_state(); + state->panel.someCustomChosen( + ) | rpl::start_with_next([=](StickerPanel::CustomChosen chosen) { + stickerChosen(chosen.sticker); + }, raw->lifetime()); + + const auto session = &show->session(); + std::move( + stickerValue + ) | rpl::start_with_next([=](DocumentData *sticker) { + state->stickerId = sticker ? sticker->id : 0; + right->resize( + (sticker ? button.emojiWidth : button.noneWidth) + button.added, + right->height()); + right->update(); + }, right->lifetime()); + + rpl::combine( + raw->sizeValue(), + right->widthValue() + ) | rpl::start_with_next([=](QSize outer, int width) { + right->resize(width, outer.height()); + const auto skip = st::settingsButton.padding.right(); + right->moveToRight(skip - button.added, 0, outer.width()); + }, right->lifetime()); + + right->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(right); + const auto height = right->height(); + if (false) { + // #TODO paint small sticker + } else { + const auto &font = st::normalFont; + p.setFont(font); + p.setPen(st::windowActiveTextFg); + p.drawText( + QPoint( + button.added, + (height - font->height) / 2 + font->ascent), + tr::lng_chat_intro_random_sticker(tr::now)); + } + }, right->lifetime()); + + raw->setClickedCallback([=] { + const auto controller = show->resolveWindow( + ChatHelpers::WindowUsage::PremiumPromo); + if (controller) { + state->panel.show({ + .controller = controller, + .button = right, + .ensureAddedId = state->stickerId, + }); + } + }); + + return result; +} + +PreviewDelegate::PreviewDelegate( + not_null parent, + not_null st, + Fn update) +: _parent(parent) +, _pathGradient(MakePathShiftGradient(st, update)) { +} + +bool PreviewDelegate::elementAnimationsPaused() { + return _parent->window()->isActiveWindow(); +} + +auto PreviewDelegate::elementPathShiftGradient() +-> not_null { + return _pathGradient.get(); +} + +Context PreviewDelegate::elementContext() { + return Context::History; +} + +PreviewWrap::PreviewWrap( + not_null parent, + not_null session, + rpl::producer value) +: RpWidget(parent) +, _history(session->data().history(session->userPeerId())) +, _theme(Window::Theme::DefaultChatThemeOn(lifetime())) +, _style(std::make_unique( + _history->session().colorIndicesValue())) +, _delegate(std::make_unique( + parent, + _style.get(), + [=] { update(); })) +, _position(0, st::msgMargin.bottom()) { + _style->apply(_theme.get()); + + session->data().viewRepaintRequest( + ) | rpl::start_with_next([=](not_null view) { + if (view == _view->view()) { + update(); + } + }, lifetime()); + + prepare(std::move(value)); +} + +PreviewWrap::~PreviewWrap() { + _view = nullptr; +} + +void PreviewWrap::prepare(rpl::producer value) { + _view = std::make_unique( + _history.get(), + _delegate.get()); + + std::move(value) | rpl::start_with_next([=](Data::ChatIntro intro) { + _view->make(std::move(intro)); + if (width() >= st::msgMinWidth) { + resizeTo(width()); + } + update(); + }, lifetime()); + + widthValue( + ) | rpl::filter([=](int width) { + return width >= st::msgMinWidth; + }) | rpl::start_with_next([=](int width) { + resizeTo(width); + }, lifetime()); +} + +void PreviewWrap::resizeTo(int width) { + const auto height = _position.y() + + _view->view()->resizeGetHeight(width) + + _position.y() + + st::msgServiceMargin.top() + + st::msgServiceGiftBoxTopSkip + - st::msgServiceMargin.bottom(); + resize(width, height); +} + +void PreviewWrap::paintEvent(QPaintEvent *e) { + auto p = Painter(this); + + const auto clip = e->rect(); + if (!clip.isEmpty()) { + p.setClipRect(clip); + Window::SectionWidget::PaintBackground( + p, + _theme.get(), + QSize(width(), window()->height()), + clip); + } + + auto context = _theme->preparePaintContext( + _style.get(), + rect(), + e->rect(), + !window()->isActiveWindow()); + p.translate(_position); + _view->view()->draw(p, context); +} + +StickerPanel::StickerPanel() = default; + +StickerPanel::~StickerPanel() = default; + +void StickerPanel::show(Descriptor &&descriptor) { + const auto controller = descriptor.controller; + if (!_panel) { + create(descriptor); + + _panel->shownValue( + ) | rpl::filter([=] { + return (_panelButton != nullptr); + }) | rpl::start_with_next([=](bool shown) { + if (shown) { + _panelButton->installEventFilter(_panel.get()); + } else { + _panelButton->removeEventFilter(_panel.get()); + } + }, _panel->lifetime()); + } + const auto button = descriptor.button; + if (const auto previous = _panelButton.data()) { + if (previous != button) { + previous->removeEventFilter(_panel.get()); + } + } + _panelButton = button; + const auto feed = [=, now = descriptor.ensureAddedId]( + std::vector list) { + list.insert(begin(list), 0); + if (now && !ranges::contains(list, now)) { + list.push_back(now); + } + _panel->selector()->provideRecentEmoji(list); + }; + const auto parent = _panel->parentWidget(); + const auto global = button->mapToGlobal(QPoint()); + const auto local = parent->mapFromGlobal(global); + _panel->moveBottomRight( + local.y() + (st::normalFont->height / 2), + local.x() + button->width() * 3); + _panel->toggleAnimated(); +} + +bool StickerPanel::hasFocus() const { + return _panel && Ui::InFocusChain(_panel.get()); +} + +void StickerPanel::repaint() { + _panel->selector()->update(); +} + +void StickerPanel::create(const Descriptor &descriptor) { + using Selector = ChatHelpers::TabbedSelector; + using Descriptor = ChatHelpers::TabbedSelectorDescriptor; + using Mode = ChatHelpers::TabbedSelector::Mode; + const auto controller = descriptor.controller; + const auto body = controller->window().widget()->bodyWidget(); + _panel = base::make_unique_q( + body, + controller, + object_ptr( + nullptr, + Descriptor{ + .show = controller->uiShow(), + .st = st::backgroundEmojiPan, + .level = Window::GifPauseReason::Layer, + .mode = Mode::StickersOnly, + .features = { + .megagroupSet = false, + .stickersSettings = false, + .openStickerSets = false, + }, + })); + _panel->setDropDown(false); + _panel->setDesiredHeightValues( + 1., + st::emojiPanMinHeight / 2, + st::emojiPanMinHeight); + _panel->hide(); + + _panel->selector()->fileChosen( + ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { + _someCustomChosen.fire({ data.document }); + _panel->hideAnimated(); + }, _panel->lifetime()); +} + +ChatIntro::ChatIntro( + QWidget *parent, + not_null controller) +: BusinessSection(parent, controller) { + setupContent(controller); +} + +ChatIntro::~ChatIntro() { + if (!Core::Quitting()) { + save(); + } +} + +bool ChatIntro::closeByOutsideClick() const { + return false; +} + +rpl::producer ChatIntro::title() { + return tr::lng_chat_intro_title(); +} + +[[nodiscard]] rpl::producer IntroWithRandomSticker( + not_null session, + rpl::producer intro) { + return std::move(intro) | rpl::map([=](Data::ChatIntro intro) + -> rpl::producer { + if (intro.sticker) { + return rpl::single(std::move(intro)); + } + return Api::RandomHelloStickerValue( + session + ) | rpl::map([=](DocumentData *sticker) { + auto copy = intro; + copy.sticker = sticker; + return copy; + }); + }) | rpl::flatten_latest(); +} + +void ChatIntro::setupContent( + not_null controller) { + using namespace rpl::mappers; + + const auto content = Ui::CreateChild(this); + const auto info = &controller->session().data().businessInfo(); + const auto current = info->chatIntro(); + + _intro = info->chatIntro(); + const auto change = [=](Fn modify) { + auto intro = _intro.current(); + modify(intro); + _intro = intro; + }; + + const auto preview = content->add( + object_ptr( + content, + &controller->session(), + IntroWithRandomSticker(&controller->session(), _intro.value())), + {}); + + const auto title = content->add( + object_ptr( + content, + st::settingsChatIntroField, + tr::lng_chat_intro_enter_title(), + current.title), + st::settingsChatIntroFieldMargins); + const auto description = content->add( + object_ptr( + content, + st::settingsChatIntroField, + tr::lng_chat_intro_enter_message(), + current.description), + st::settingsChatIntroFieldMargins); + content->add(CreateIntroStickerButton( + content, + controller->uiShow(), + _intro.value() | rpl::map([](const Data::ChatIntro &intro) { + return intro.sticker; + }) | rpl::distinct_until_changed(), + [=](DocumentData *sticker) { + change([&](Data::ChatIntro &intro) { + intro.sticker = sticker; + }); + })); + Ui::AddSkip(content); + + title->changes() | rpl::start_with_next([=] { + change([&](Data::ChatIntro &intro) { + intro.title = title->getLastText(); + }); + }, title->lifetime()); + + description->changes() | rpl::start_with_next([=] { + change([&](Data::ChatIntro &intro) { + intro.description = description->getLastText(); + }); + }, description->lifetime()); + + _setFocus = [=] { + title->setFocusFast(); + }; + + Ui::AddDividerText( + content, + tr::lng_chat_intro_about(), + st::peerAppearanceDividerTextMargin); + Ui::AddSkip(content); + + const auto resetWrap = content->add( + object_ptr>( + content, + object_ptr( + content, + tr::lng_chat_intro_reset(), + st::settingsAttentionButton + ))); + resetWrap->toggleOn( + _intro.value() | rpl::map([](const Data::ChatIntro &intro) { + return !!intro; + })); + resetWrap->entity()->setClickedCallback([=] { + _intro = Data::ChatIntro(); + }); + + Ui::ResizeFitChild(this, content); +} + +void ChatIntro::save() { + const auto show = controller()->uiShow(); + const auto fail = [=](QString error) { + if (error == u"BUSINESS_RECIPIENTS_EMPTY"_q) { + show->showToast(tr::lng_greeting_recipients_empty(tr::now)); + } + }; + controller()->session().data().businessInfo().saveChatIntro( + _intro.current(), + fail); +} + +} // namespace + +Type ChatIntroId() { + return ChatIntro::Id(); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_chat_intro.h b/Telegram/SourceFiles/settings/business/settings_chat_intro.h new file mode 100644 index 0000000000..f44105dd1c --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_chat_intro.h @@ -0,0 +1,16 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "settings/settings_type.h" + +namespace Settings { + +[[nodiscard]] Type ChatIntroId(); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp index 5b59d4987c..cb11074abd 100644 --- a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp @@ -46,7 +46,7 @@ struct BotState { LookupState state = LookupState::Empty; }; -class Chatbots : public BusinessSection { +class Chatbots final : public BusinessSection { public: Chatbots( QWidget *parent, diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 2ab04e1e6b..2060cdd93a 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -110,6 +110,7 @@ settingsBusinessIconReplies: icon {{ "settings/premium/business/business_quick", settingsBusinessIconGreeting: icon {{ "settings/premium/status", settingsIconFg }}; settingsBusinessIconAway: icon {{ "settings/premium/business/business_away", settingsIconFg }}; settingsBusinessIconChatbots: icon {{ "settings/premium/business/business_chatbots", settingsIconFg }}; +settingsBusinessIconChatIntro: icon {{ "settings/premium/intro", settingsIconFg }}; settingsPremiumNewBadge: FlatLabel(defaultFlatLabel) { style: TextStyle(semiboldTextStyle) { @@ -638,3 +639,7 @@ settingsChatbotsNotFound: FlatLabel(defaultFlatLabel) { } settingsChatbotsDeleteIcon: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFg }}; settingsChatbotsDeleteIconOver: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFgOver }}; + +settingsChatIntroField: InputField(defaultMultiSelectSearchField) { +} +settingsChatIntroFieldMargins: margins(20px, 8px, 20px, 8px); diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp index c9123c5284..0e185869fc 100644 --- a/Telegram/SourceFiles/settings/settings_business.cpp +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_app_config.h" #include "main/main_session.h" #include "settings/business/settings_away_message.h" +#include "settings/business/settings_chat_intro.h" #include "settings/business/settings_chatbots.h" #include "settings/business/settings_greeting.h" #include "settings/business/settings_location.h" @@ -69,6 +70,7 @@ using Order = std::vector; u"business_hours"_q, u"business_location"_q, u"business_bots"_q, + u"intro"_q, }; } @@ -128,6 +130,15 @@ using Order = std::vector; PremiumFeature::BusinessBots, }, }, + { + u"intro"_q, + Entry{ + &st::settingsBusinessIconChatIntro, + tr::lng_business_subtitle_chat_intro(), + tr::lng_business_about_chat_intro(), + PremiumFeature::ChatIntro, + }, + }, }; } @@ -227,9 +238,9 @@ void AddBusinessSummary( icons.reserve(int(entryMap.size())); { const auto &account = controller->session().account(); - const auto mtpOrder = account.appConfig().get( + const auto mtpOrder = /*account.appConfig().get( "business_promo_order", - FallbackOrder()); + FallbackOrder())*/FallbackOrder(); AssertIsDebug(); const auto processEntry = [&](Entry &entry) { icons.push_back(entry.icon); addRow(entry); @@ -375,6 +386,7 @@ void Business::setupContent() { case PremiumFeature::GreetingMessage: return GreetingId(); case PremiumFeature::QuickReplies: return QuickRepliesId(); case PremiumFeature::BusinessBots: return ChatbotsId(); + case PremiumFeature::ChatIntro: return ChatIntroId(); } Unexpected("Feature in showFeature."); }()); @@ -396,6 +408,8 @@ void Business::setupContent() { return owner->shortcutMessages().shortcutsLoaded(); case PremiumFeature::BusinessBots: return owner->chatbots().loaded(); + case PremiumFeature::ChatIntro: + return owner->session().user()->isFullLoaded(); } Unexpected("Feature in isReady."); }; @@ -670,6 +684,8 @@ std::vector BusinessFeaturesOrder( return PremiumFeature::BusinessLocation; } else if (s == u"business_bots"_q) { return PremiumFeature::BusinessBots; + } else if (s == u"chat_intro"_q) { + return PremiumFeature::ChatIntro; } return PremiumFeature::kCount; }) | ranges::views::filter([](PremiumFeature feature) { diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index c2ebb2f2ac..a18af8cd40 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1061,3 +1061,6 @@ boostsMessageIconPadding: margins(0px, 2px, 0px, 0px); historyIvIcon: icon{{ "boosts/boost_mini2", windowFg }}; historyIvIconPadding: margins(2px, 2px, 2px, 0px); + +chatIntroStickerSize: 96px; +chatIntroWidth: 224px;