diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index d67d771fb..4bdea7355 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_channel.h" #include "data/data_chat_filters.h" #include "data/data_cloud_themes.h" +#include "data/data_emoji_statuses.h" #include "data/data_group_call.h" #include "data/data_drafts.h" #include "data/data_histories.h" @@ -2388,7 +2389,7 @@ void Updates::feedUpdate(const MTPUpdate &update) { } break; case mtpc_updateRecentEmojiStatuses: { - // #TODO emoji_status + session().data().emojiStatuses().refreshRecentDelayed(); } break; case mtpc_updateRecentReactions: { diff --git a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp index 191750bc0..fe17c0cff 100644 --- a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp +++ b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp @@ -419,7 +419,7 @@ void AddReactionCustomIcon( Fn repaint; }; const auto state = stateLifetime->make_state(); - static constexpr auto tag = Data::CustomEmojiManager::SizeTag::Large; + static constexpr auto tag = Data::CustomEmojiManager::SizeTag::Normal; state->custom = controller->session().data().customEmojiManager().create( customId, [=] { state->repaint(); }, @@ -445,6 +445,7 @@ void AddReactionCustomIcon( paintCallback, std::move(destroys), stateLifetime); + state->repaint = crl::guard(widget, [=] { widget->update(); }); } void ReactionsSettingsBox( diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index d6738880b..702db3212 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -454,6 +454,13 @@ EmojiListWidget::~EmojiListWidget() { base::take(_customEmoji); } +void EmojiListWidget::provideRecent( + const std::vector &customRecentList) { + clearSelection(); + fillRecentFrom(customRecentList); + resizeToWidth(width()); +} + void EmojiListWidget::repaintCustom(uint64 setId) { if (!_repaintsScheduled.emplace(setId).second) { return; @@ -730,24 +737,18 @@ void EmojiListWidget::ensureLoaded(int section) { } void EmojiListWidget::fillRecent() { - if (_mode != Mode::Full && _mode != Mode::EmojiStatus) { - return; // #TODO emoji_status + if (_mode != Mode::Full) { + return; } _recent.clear(); _recentCustomIds.clear(); const auto &list = Core::App().settings().recentEmoji(); _recent.reserve(std::min(int(list.size()), Core::kRecentEmojiLimit) + 1); - if (_mode == Mode::EmojiStatus) { - const auto star = QString::fromUtf8("\xe2\xad\x90\xef\xb8\x8f"); - _recent.push_back({ .id = { Ui::Emoji::Find(star) } }); - } const auto test = session().isTestMode(); for (const auto &one : list) { const auto document = std::get_if(&one.id.data); - if (_mode == Mode::EmojiStatus && !document) { - continue; - } else if (document && document->test != test) { + if (document && document->test != test) { continue; } _recent.push_back({ @@ -770,11 +771,16 @@ void EmojiListWidget::fillRecentFrom(const std::vector &list) { _recent.reserve(list.size()); for (const auto &id : list) { - _recent.push_back({ - .custom = resolveCustomRecent(id), - .id = { RecentEmojiDocument{.id = id, .test = test } }, - }); - _recentCustomIds.emplace(id); + if (!id && _mode == Mode::EmojiStatus) { + const auto star = QString::fromUtf8("\xe2\xad\x90\xef\xb8\x8f"); + _recent.push_back({ .id = { Ui::Emoji::Find(star) } }); + } else { + _recent.push_back({ + .custom = resolveCustomRecent(id), + .id = { RecentEmojiDocument{.id = id, .test = test } }, + }); + _recentCustomIds.emplace(id); + } } } @@ -1445,8 +1451,8 @@ void EmojiListWidget::processPanelHideFinished() { } void EmojiListWidget::refreshRecent() { - if (_mode != Mode::Full && _mode != Mode::EmojiStatus) { - return; // #TODO emoji_status + if (_mode != Mode::Full) { + return; } clearSelection(); fillRecent(); diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h index e733e47d4..6bb9ac7c7 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h @@ -113,6 +113,8 @@ public: -> rpl::producer>; [[nodiscard]] rpl::producer<> jumpedToPremium() const; + void provideRecent(const std::vector &customRecentList); + void paintExpanding( QPainter &p, QRect clip, diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp index 127995814..06b89693a 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp @@ -872,6 +872,16 @@ void TabbedSelector::showPromoForPremiumEmoji() { }, lifetime()); } +void TabbedSelector::provideRecentEmoji( + const std::vector &customRecentList) { + for (const auto &tab : _tabs) { + if (tab.type() == SelectorTab::Emoji) { + const auto emoji = static_cast(tab.widget()); + emoji->provideRecent(customRecentList); + } + } +} + void TabbedSelector::checkRestrictedPeer() { if (_currentPeer) { const auto error = (_currentTabType == SelectorTab::Stickers) diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h index f93509c43..a891b04ca 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h @@ -109,6 +109,7 @@ public: void refreshStickers(); void setCurrentPeer(PeerData *peer); void showPromoForPremiumEmoji(); + void provideRecentEmoji(const std::vector &customRecentList); void hideFinished(); void showStarted(); diff --git a/Telegram/SourceFiles/data/data_emoji_statuses.cpp b/Telegram/SourceFiles/data/data_emoji_statuses.cpp new file mode 100644 index 000000000..1e8c66551 --- /dev/null +++ b/Telegram/SourceFiles/data/data_emoji_statuses.cpp @@ -0,0 +1,173 @@ +/* +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 "data/data_emoji_statuses.h" +// +#include "main/main_session.h" +#include "data/data_user.h" +#include "data/data_session.h" +#include "base/timer_rpl.h" +#include "base/call_delayed.h" +#include "apiwrap.h" + +namespace Data { +namespace { + +constexpr auto kRefreshDefaultListEach = 60 * 60 * crl::time(1000); +constexpr auto kRecentRequestTimeout = 10 * crl::time(1000); + +[[nodiscard]] DocumentId Parse(const MTPEmojiStatus &status) { + return status.match([&](const MTPDemojiStatus &data) { + return DocumentId(data.vdocument_id().v); + }, [](const MTPDemojiStatusEmpty &) { + return DocumentId(); + }); +} + +[[nodiscard]] std::vector ListFromMTP( + const MTPDaccount_emojiStatuses &data) { + const auto &list = data.vstatuses().v; + auto result = std::vector(); + result.reserve(list.size()); + for (const auto &status : list) { + const auto id = Parse(status); + if (!id) { + LOG(("API Error: emojiStatusEmpty in account.emojiStatuses.")); + } else { + result.push_back(id); + } + } + return result; +} + +} // namespace + +EmojiStatuses::EmojiStatuses(not_null owner) +: _owner(owner) +, _defaultRefreshTimer([=] { refreshDefault(); }) { + refreshDefault(); + + base::timer_each( + kRefreshDefaultListEach + ) | rpl::start_with_next([=] { + refreshDefault(); + }, _lifetime); +} + +EmojiStatuses::~EmojiStatuses() = default; + +Main::Session &EmojiStatuses::session() const { + return _owner->session(); +} + +void EmojiStatuses::refreshRecent() { + requestRecent(); +} + +void EmojiStatuses::refreshDefault() { + requestDefault(); +} + +void EmojiStatuses::refreshRecentDelayed() { + if (_recentRequestId || _recentRequestScheduled) { + return; + } + _recentRequestScheduled = true; + base::call_delayed(kRecentRequestTimeout, &_owner->session(), [=] { + if (_recentRequestScheduled) { + requestRecent(); + } + }); +} + +const std::vector &EmojiStatuses::list(Type type) const { + switch (type) { + case Type::Recent: return _recent; + case Type::Default: return _default; + } + Unexpected("Type in EmojiStatuses::list."); +} + +rpl::producer<> EmojiStatuses::recentUpdates() const { + return _recentUpdated.events(); +} + +rpl::producer<> EmojiStatuses::defaultUpdates() const { + return _defaultUpdated.events(); +} + +void EmojiStatuses::requestRecent() { + if (_recentRequestId) { + return; + } + auto &api = _owner->session().api(); + _recentRequestScheduled = false; + _recentRequestId = api.request(MTPaccount_GetRecentEmojiStatuses( + MTP_long(_recentHash) + )).done([=](const MTPaccount_EmojiStatuses &result) { + _recentRequestId = 0; + result.match([&](const MTPDaccount_emojiStatuses &data) { + updateRecent(data); + }, [](const MTPDaccount_emojiStatusesNotModified&) { + }); + }).fail([=] { + _recentRequestId = 0; + _recentHash = 0; + }).send(); +} + +void EmojiStatuses::requestDefault() { + if (_defaultRequestId) { + return; + } + auto &api = _owner->session().api(); + _defaultRequestId = api.request(MTPaccount_GetDefaultEmojiStatuses( + MTP_long(_recentHash) + )).done([=](const MTPaccount_EmojiStatuses &result) { + _defaultRequestId = 0; + result.match([&](const MTPDaccount_emojiStatuses &data) { + updateDefault(data); + }, [&](const MTPDaccount_emojiStatusesNotModified &) { + }); + }).fail([=] { + _defaultRequestId = 0; + _defaultHash = 0; + }).send(); +} + +void EmojiStatuses::updateRecent(const MTPDaccount_emojiStatuses &data) { + _recentHash = data.vhash().v; + _recent = ListFromMTP(data); + _recentUpdated.fire({}); +} + +void EmojiStatuses::updateDefault(const MTPDaccount_emojiStatuses &data) { + _defaultHash = data.vhash().v; + _default = ListFromMTP(data); + _defaultUpdated.fire({}); +} + +void EmojiStatuses::set(DocumentId id) { + auto &api = _owner->session().api(); + if (_sentRequestId) { + api.request(base::take(_sentRequestId)).cancel(); + } + _owner->session().user()->setEmojiStatus(id); + _sentRequestId = api.request(MTPaccount_UpdateEmojiStatus( + id ? MTP_emojiStatus(MTP_long(id)) : MTP_emojiStatusEmpty() + )).done([=] { + _sentRequestId = 0; + }).fail([=] { + _sentRequestId = 0; + }).send(); +} + +bool EmojiStatuses::setting() const { + return _sentRequestId != 0;; +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_emoji_statuses.h b/Telegram/SourceFiles/data/data_emoji_statuses.h new file mode 100644 index 000000000..c1293e332 --- /dev/null +++ b/Telegram/SourceFiles/data/data_emoji_statuses.h @@ -0,0 +1,79 @@ +/* +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 "base/timer.h" + +namespace Ui::Text { +class CustomEmoji; +} // namespace Ui::Text + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +class DocumentMedia; +class Session; + +class EmojiStatuses final { +public: + explicit EmojiStatuses(not_null owner); + ~EmojiStatuses(); + + [[nodiscard]] Session &owner() const { + return *_owner; + } + [[nodiscard]] Main::Session &session() const; + + void refreshRecent(); + void refreshRecentDelayed(); + void refreshDefault(); + + enum class Type { + Recent, + Default, + }; + [[nodiscard]] const std::vector &list(Type type) const; + + [[nodiscard]] rpl::producer<> recentUpdates() const; + [[nodiscard]] rpl::producer<> defaultUpdates() const; + + void set(DocumentId id); + [[nodiscard]] bool setting() const; + +private: + void requestRecent(); + void requestDefault(); + + void updateRecent(const MTPDaccount_emojiStatuses &data); + void updateDefault(const MTPDaccount_emojiStatuses &data); + + const not_null _owner; + + std::vector _recent; + std::vector _default; + rpl::event_stream<> _recentUpdated; + rpl::event_stream<> _defaultUpdated; + + mtpRequestId _recentRequestId = 0; + bool _recentRequestScheduled = false; + uint64 _recentHash = 0; + + base::Timer _defaultRefreshTimer; + mtpRequestId _defaultRequestId = 0; + uint64 _defaultHash = 0; + + mtpRequestId _sentRequestId = 0; + + rpl::lifetime _lifetime; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index b4d27ccc2..3d3a8f640 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -38,7 +38,7 @@ constexpr auto kSizeForDownscale = 64; constexpr auto kRecentRequestTimeout = 10 * crl::time(1000); constexpr auto kRecentReactionsLimit = 40; constexpr auto kTopRequestDelay = 60 * crl::time(1000); -constexpr auto kTopReactionsLimit = 10; +constexpr auto kTopReactionsLimit = 14; [[nodiscard]] QString ReactionIdToLog(const ReactionId &id) { if (const auto custom = id.custom()) { @@ -107,7 +107,7 @@ PossibleItemReactionsRef LookupPossibleReactions( }(); auto added = base::flat_set(); const auto add = [&](auto predicate) { - auto &&all = ranges::views::concat(recent, top, full); + auto &&all = ranges::views::concat(top, recent, full); for (const auto &reaction : all) { if (predicate(reaction)) { if (added.emplace(reaction.id).second) { diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 7fa56e4ac..37a371576 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -60,6 +60,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_send_action.h" #include "data/data_sponsored_messages.h" #include "data/data_message_reactions.h" +#include "data/data_emoji_statuses.h" #include "data/data_cloud_themes.h" #include "data/data_streaming.h" #include "data/data_media_rotation.h" @@ -252,6 +253,7 @@ Session::Session(not_null session) , _stickers(std::make_unique(this)) , _sponsoredMessages(std::make_unique(this)) , _reactions(std::make_unique(this)) +, _emojiStatuses(std::make_unique(this)) , _notifySettings(std::make_unique(this)) , _customEmojiManager(std::make_unique(this)) { _cache->open(_session->local().cacheKey()); diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index b8173c8f7..c08ce4a7a 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -52,6 +52,7 @@ class ScheduledMessages; class SendActionManager; class SponsoredMessages; class Reactions; +class EmojiStatuses; class ChatFilters; class CloudThemes; class Streaming; @@ -118,6 +119,9 @@ public: [[nodiscard]] Reactions &reactions() const { return *_reactions; } + [[nodiscard]] EmojiStatuses &emojiStatuses() const { + return *_emojiStatuses; + } [[nodiscard]] NotifySettings ¬ifySettings() const { return *_notifySettings; } @@ -981,6 +985,7 @@ private: const std::unique_ptr _stickers; std::unique_ptr _sponsoredMessages; const std::unique_ptr _reactions; + const std::unique_ptr _emojiStatuses; const std::unique_ptr _notifySettings; const std::unique_ptr _customEmojiManager; diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp index eff1c7ccf..40449fe4e 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp @@ -33,6 +33,7 @@ constexpr auto kExpandDuration = crl::time(300); constexpr auto kScaleDuration = crl::time(120); constexpr auto kFullDuration = kExpandDuration + kScaleDuration; constexpr auto kExpandDelay = crl::time(40); +constexpr auto kDefaultColumns = 8; class StripEmoji final : public Ui::Text::CustomEmoji { public: @@ -105,19 +106,59 @@ Selector::Selector( const Data::PossibleItemReactionsRef &reactions, IconFactory iconFactory, Fn close) +: Selector( + parent, + parentController, + reactions, + (reactions.customAllowed + ? ChatHelpers::EmojiListMode::FullReactions + : ChatHelpers::EmojiListMode::RecentReactions), + {}, + iconFactory, + close) { +} + +Selector::Selector( + not_null parent, + not_null parentController, + ChatHelpers::EmojiListMode mode, + std::vector recent, + Fn close) +: Selector( + parent, + parentController, + { .customAllowed = true }, + mode, + std::move(recent), + nullptr, + close) { +} + +Selector::Selector( + not_null parent, + not_null parentController, + const Data::PossibleItemReactionsRef &reactions, + ChatHelpers::EmojiListMode mode, + std::vector recent, + IconFactory iconFactory, + Fn close) : RpWidget(parent) , _parentController(parentController.get()) , _reactions(reactions) +, _recent(std::move(recent)) +, _listMode(mode) , _jumpedToPremium([=] { close(false); }) , _cachedRound( QSize(2 * st::reactStripSkip + st::reactStripSize, st::reactStripHeight), st::reactionCornerShadow, st::reactStripHeight) -, _strip( - QRect(0, 0, st::reactStripSize, st::reactStripSize), - st::reactStripImage, - crl::guard(this, [=] { update(_inner); }), - std::move(iconFactory)) +, _strip(iconFactory + ? std::make_unique( + QRect(0, 0, st::reactStripSize, st::reactStripSize), + st::reactStripImage, + crl::guard(this, [=] { update(_inner); }), + std::move(iconFactory)) + : nullptr) , _size(st::reactStripSize) , _skipx(countSkipLeft()) , _skipy((st::reactStripHeight - st::reactStripSize) / 2) { @@ -129,10 +170,14 @@ Selector::Selector( }, lifetime()); } +int Selector::recentCount() const { + return int(_strip ? _reactions.recent.size() : _recent.size()); +} + int Selector::countSkipLeft() const { const auto addedToMax = _reactions.customAllowed || _reactions.morePremiumAvailable; - const auto max = int(_reactions.recent.size()) + (addedToMax ? 1 : 0); + const auto max = recentCount() + (addedToMax ? 1 : 0); const auto width = max * _size; return std::max( (st::reactStripMinWidth - (max * _size)) / 2, @@ -142,13 +187,13 @@ int Selector::countSkipLeft() const { int Selector::countWidth(int desiredWidth, int maxWidth) { const auto addedToMax = _reactions.customAllowed || _reactions.morePremiumAvailable; - const auto max = int(_reactions.recent.size()) + (addedToMax ? 1 : 0); + const auto max = recentCount() + (addedToMax ? 1 : 0); const auto possibleColumns = std::min( (desiredWidth - 2 * _skipx + _size - 1) / _size, (maxWidth - 2 * _skipx) / _size); - _columns = std::min(possibleColumns, max); + _columns = _strip ? std::min(possibleColumns, max) : kDefaultColumns; _small = (possibleColumns - _columns > 1); - _recentRows = (_reactions.recent.size() + _recentRows = (recentCount() + (_reactions.morePremiumAvailable ? 1 : 0) + _columns - 1) / _columns; const auto added = (_columns < max || _reactions.customAllowed) @@ -156,22 +201,24 @@ int Selector::countWidth(int desiredWidth, int maxWidth) { : _reactions.morePremiumAvailable ? Strip::AddedButton::Premium : Strip::AddedButton::None; - const auto &real = _reactions.recent; - auto list = std::vector>(); - list.reserve(_columns); - if (const auto cut = max - _columns) { - const auto from = begin(real); - const auto till = end(real) - (cut + (addedToMax ? 0 : 1)); - for (auto i = from; i != till; ++i) { - list.push_back(&*i); - } - } else { - for (const auto &reaction : real) { - list.push_back(&reaction); + if (_strip) { + const auto &real = _reactions.recent; + auto list = std::vector>(); + list.reserve(_columns); + if (const auto cut = max - _columns) { + const auto from = begin(real); + const auto till = end(real) - (cut + (addedToMax ? 0 : 1)); + for (auto i = from; i != till; ++i) { + list.push_back(&*i); + } + } else { + for (const auto &reaction : real) { + list.push_back(&reaction); + } } + _strip->applyList(list, added); + _strip->clearAppearAnimations(false); } - _strip.applyList(list, added); - _strip.clearAppearAnimations(false); return std::max(2 * _skipx + _columns * _size, desiredWidth); } @@ -211,6 +258,10 @@ void Selector::initGeometry(int innerTop) { { 0, _collapsedTopSkip, 0, 0 } ).translated(left, top)); _inner = _outer.marginsRemoved(extents); + + if (!_strip) { + expand(); + } } void Selector::updateShowState( @@ -239,6 +290,8 @@ void Selector::updateShowState( } void Selector::paintAppearing(QPainter &p) { + Expects(_strip != nullptr); + p.setOpacity(_appearOpacity); const auto factor = style::DevicePixelRatio(); @@ -256,7 +309,7 @@ void Selector::paintAppearing(QPainter &p) { const auto size = QSize(fullWidth, _outer.height()); q.translate(_inner.topLeft() - QPoint(0, _collapsedTopSkip)); - _strip.paint( + _strip->paint( q, { _skipx, _skipy }, { _size, 0 }, @@ -305,11 +358,13 @@ void Selector::paintBackgroundToBuffer() { } void Selector::paintCollapsed(QPainter &p) { + Expects(_strip != nullptr); + if (_paintBuffer.isNull()) { paintBackgroundToBuffer(); } p.drawImage(_outer.topLeft(), _paintBuffer); - _strip.paint( + _strip->paint( p, _inner.topLeft() + QPoint(_skipx, _skipy), { _size, 0 }, @@ -441,9 +496,9 @@ void Selector::paintBubble(QPainter &p, int innerWidth) { void Selector::paintEvent(QPaintEvent *e) { auto p = Painter(this); - if (_appearing) { + if (_strip && _appearing) { paintAppearing(p); - } else if (!_expanded) { + } else if (_strip && !_expanded) { paintCollapsed(p); } else if (const auto progress = _expanding.value(kFullDuration) ; progress < kFullDuration) { @@ -454,12 +509,15 @@ void Selector::paintEvent(QPaintEvent *e) { } void Selector::mouseMoveEvent(QMouseEvent *e) { + if (!_strip) { + return; + } setSelected(lookupSelectedIndex(e->pos())); } int Selector::lookupSelectedIndex(QPoint position) const { const auto p = position - _inner.topLeft() - QPoint(_skipx, _skipy); - const auto max = _strip.count(); + const auto max = _strip->count(); const auto index = p.x() / _size; if (p.x() >= 0 && p.y() >= 0 && p.y() < _inner.height() && index < max) { return index; @@ -468,10 +526,12 @@ int Selector::lookupSelectedIndex(QPoint position) const { } void Selector::setSelected(int index) { + Expects(_strip != nullptr); + if (index >= 0 && _expandScheduled) { return; } - _strip.setSelected(index); + _strip->setSelected(index); const auto over = (index >= 0); if (_over != over) { _over = over; @@ -485,19 +545,25 @@ void Selector::setSelected(int index) { } void Selector::leaveEventHook(QEvent *e) { + if (!_strip) { + return; + } setSelected(-1); } void Selector::mousePressEvent(QMouseEvent *e) { + if (!_strip) { + return; + } _pressed = lookupSelectedIndex(e->pos()); } void Selector::mouseReleaseEvent(QMouseEvent *e) { - if (_pressed != lookupSelectedIndex(e->pos())) { + if (!_strip || _pressed != lookupSelectedIndex(e->pos())) { return; } _pressed = -1; - const auto selected = _strip.selected(); + const auto selected = _strip->selected(); if (selected == Strip::AddedButton::Premium) { _premiumPromoChosen.fire({}); } else if (selected == Strip::AddedButton::Expand) { @@ -547,30 +613,35 @@ void Selector::expand() { } void Selector::cacheExpandIcon() { + if (!_strip) { + return; + } _expandIconCache = _cachedRound.PrepareImage({ _size, _size }); _expandIconCache.fill(Qt::transparent); auto q = QPainter(&_expandIconCache); - _strip.paintOne(q, _strip.count() - 1, { 0, 0 }, 1.); + _strip->paintOne(q, _strip->count() - 1, { 0, 0 }, 1.); } void Selector::createList(not_null controller) { using namespace ChatHelpers; - auto recent = std::vector(); + auto recent = _recent; auto defaultReactionIds = base::flat_map(); - recent.reserve(_reactions.recent.size()); - auto index = 0; - const auto inStrip = _strip.count(); - for (const auto &reaction : _reactions.recent) { - if (const auto id = reaction.id.custom()) { - recent.push_back(id); - } else { - recent.push_back(reaction.selectAnimation->id); - defaultReactionIds.emplace(recent.back(), reaction.id.emoji()); - } - if (index + 1 < inStrip) { - _defaultReactionInStripMap.emplace(recent.back(), index++); - } - }; + if (_strip) { + recent.reserve(recentCount()); + auto index = 0; + const auto inStrip = _strip->count(); + for (const auto &reaction : _reactions.recent) { + if (const auto id = reaction.id.custom()) { + recent.push_back(id); + } else { + recent.push_back(reaction.selectAnimation->id); + defaultReactionIds.emplace(recent.back(), reaction.id.emoji()); + } + if (index + 1 < inStrip) { + _defaultReactionInStripMap.emplace(recent.back(), index++); + } + }; + } const auto manager = &controller->session().data().customEmojiManager(); _stripPaintOneShift = [&] { // See EmojiListWidget custom emoji position resolving. @@ -603,9 +674,10 @@ void Selector::createList(not_null controller) { : manager->create(id, std::move(repaint), tag); const auto i = _defaultReactionInStripMap.find(id); if (i != end(_defaultReactionInStripMap)) { + Assert(_strip != nullptr); return std::make_unique( std::move(result), - &_strip, + _strip.get(), -_stripPaintOneShift, i->second); } @@ -623,9 +695,7 @@ void Selector::createList(not_null controller) { _list = _scroll->setOwnedWidget( object_ptr(_scroll, EmojiListDescriptor{ .session = &controller->session(), - .mode = (_reactions.customAllowed - ? EmojiListMode::FullReactions - : EmojiListMode::RecentReactions), + .mode = _listMode, .controller = controller, .paused = [] { return false; }, .customRecentList = std::move(recent), @@ -770,6 +840,65 @@ bool AdjustMenuGeometryForSelector( return menu->prepareGeometryFor(desiredPosition); } +AttachSelectorResult MakeJustSelectorMenu( + not_null menu, + not_null controller, + QPoint desiredPosition, + ChatHelpers::EmojiListMode mode, + std::vector recent, + Fn chosen) { + const auto selector = Ui::CreateChild( + menu.get(), + controller, + mode, + std::move(recent), + [=](bool fast) { menu->hideMenu(fast); }); + if (!AdjustMenuGeometryForSelector(menu, desiredPosition, selector)) { + return AttachSelectorResult::Failed; + } + const auto selectorInnerTop = menu->preparedPadding().top() + - st::reactStripExtend.top(); + selector->initGeometry(selectorInnerTop); + selector->show(); + + selector->chosen() | rpl::start_with_next([=](ChosenReaction reaction) { + menu->hideMenu(); + chosen(std::move(reaction)); + }, selector->lifetime()); + + const auto correctTop = selector->y(); + menu->showStateValue( + ) | rpl::start_with_next([=](Ui::PopupMenu::ShowState state) { + const auto origin = menu->preparedOrigin(); + using Origin = Ui::PanelAnimation::Origin; + if (origin == Origin::BottomLeft || origin == Origin::BottomRight) { + const auto add = state.appearing + ? (menu->rect().marginsRemoved( + menu->preparedPadding() + ).height() - state.appearingHeight) + : 0; + selector->move(selector->x(), correctTop + add); + } + selector->updateShowState( + state.widthProgress * state.heightProgress, + state.opacity, + state.appearing, + state.toggling); + }, selector->lifetime()); + + const auto weak = base::make_weak(controller.get()); + controller->enableGifPauseReason( + Window::GifPauseReason::MediaPreview); + QObject::connect(menu.get(), &QObject::destroyed, [weak] { + if (const auto strong = weak.get()) { + strong->disableGifPauseReason( + Window::GifPauseReason::MediaPreview); + } + }); + + return AttachSelectorResult::Attached; +} + AttachSelectorResult AttachSelectorToMenu( not_null menu, not_null controller, diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h index 3b7ab83eb..c9a881d46 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h @@ -22,6 +22,7 @@ namespace ChatHelpers { class TabbedPanel; class EmojiListWidget; class StickersListFooter; +enum class EmojiListMode; } // namespace ChatHelpers namespace Window { @@ -43,6 +44,12 @@ public: const Data::PossibleItemReactionsRef &reactions, IconFactory iconFactory, Fn close); + Selector( + not_null parent, + not_null parentController, + ChatHelpers::EmojiListMode mode, + std::vector recent, + Fn close); int countWidth(int desiredWidth, int maxWidth); [[nodiscard]] QMargins extentsForShadow() const; @@ -74,6 +81,15 @@ private: int finalBottom = 0; }; + Selector( + not_null parent, + not_null parentController, + const Data::PossibleItemReactionsRef &reactions, + ChatHelpers::EmojiListMode mode, + std::vector recent, + IconFactory iconFactory, + Fn close); + void paintEvent(QPaintEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override; void leaveEventHook(QEvent *e) override; @@ -89,6 +105,7 @@ private: void paintBubble(QPainter &p, int innerWidth); void paintBackgroundToBuffer(); + [[nodiscard]] int recentCount() const; [[nodiscard]] int countSkipLeft() const; [[nodiscard]] int lookupSelectedIndex(QPoint position) const; void setSelected(int index); @@ -100,12 +117,14 @@ private: const base::weak_ptr _parentController; const Data::PossibleItemReactions _reactions; + const std::vector _recent; + const ChatHelpers::EmojiListMode _listMode; Fn _jumpedToPremium; base::flat_map _defaultReactionInStripMap; Ui::RoundAreaWithShadow _cachedRound; QPoint _defaultReactionShift; QPoint _stripPaintOneShift; - Strip _strip; + std::unique_ptr _strip; rpl::event_stream _chosen; rpl::event_stream<> _premiumPromoChosen; @@ -149,6 +168,15 @@ enum class AttachSelectorResult { Failed, Attached, }; + +AttachSelectorResult MakeJustSelectorMenu( + not_null menu, + not_null controller, + QPoint desiredPosition, + ChatHelpers::EmojiListMode mode, + std::vector recent, + Fn chosen); + AttachSelectorResult AttachSelectorToMenu( not_null menu, not_null controller, diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index 2c25cfdb4..25af9d71b 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_session.h" #include "data/data_document.h" +#include "data/data_emoji_statuses.h" #include "data/stickers/data_custom_emoji.h" #include "editor/photo_editor_layer_widget.h" #include "info/profile/info_profile_values.h" @@ -231,6 +232,20 @@ void BadgeView::move(int left, int top, int bottom) { void EmojiStatusPanel::show( not_null controller, not_null button) { + const auto self = controller->session().user(); + const auto &statuses = controller->session().data().emojiStatuses(); + const auto &other = statuses.list(Data::EmojiStatuses::Type::Default); + auto list = statuses.list(Data::EmojiStatuses::Type::Recent); + list.insert(begin(list), 0); + list.reserve(list.size() + other.size() + 1); + for (const auto &otherId : other) { + if (!ranges::contains(list, otherId)) { + list.push_back(otherId); + } + } + if (!ranges::contains(list, self->emojiStatusId())) { + list.push_back(self->emojiStatusId()); + } if (!_panel) { create(controller); @@ -242,6 +257,7 @@ void EmojiStatusPanel::show( button->removeEventFilter(_panel.get()); }, _panel->lifetime()); } + _panel->selector()->provideRecentEmoji(list); const auto parent = _panel->parentWidget(); const auto global = button->mapToGlobal(QPoint()); const auto local = parent->mapFromGlobal(global); @@ -280,10 +296,7 @@ void EmojiStatusPanel::create( std::move(statusChosen), _panel->selector()->emojiChosen() | rpl::map_to(DocumentId()) ) | rpl::start_with_next([=](DocumentId id) { - controller->session().user()->setEmojiStatus(id); - controller->session().api().request(MTPaccount_UpdateEmojiStatus( - id ? MTP_emojiStatus(MTP_long(id)) : MTP_emojiStatusEmpty() - )).send(); + controller->session().data().emojiStatuses().set(id); _panel->hideAnimated(); }, _panel->lifetime()); diff --git a/Telegram/SourceFiles/settings/settings_chat.cpp b/Telegram/SourceFiles/settings/settings_chat.cpp index 30838894b..b0ba26dbf 100644 --- a/Telegram/SourceFiles/settings/settings_chat.cpp +++ b/Telegram/SourceFiles/settings/settings_chat.cpp @@ -919,14 +919,13 @@ void SetupMessages( selected ) | rpl::start_with_next([=, idValue = std::move(idValue)]( const Data::ReactionId &id) { + const auto index = state->icons.flag ? 1 : 0; + const auto iconSize = st::settingsReactionRightIcon; const auto &reactions = controller->session().data().reactions(); - for (const auto &r : reactions.list(Data::Reactions::Type::All)) { - if (id != r.id) { - continue; - } - const auto index = state->icons.flag ? 1 : 0; - state->icons.lifetimes[index] = rpl::lifetime(); - const auto iconSize = st::settingsReactionRightIcon; + const auto &list = reactions.list(Data::Reactions::Type::All); + const auto i = ranges::find(list, id, &Data::Reaction::id); + state->icons.lifetimes[index] = rpl::lifetime(); + if (i != end(list)) { AddReactionAnimatedIcon( inner, buttonRight->geometryValue( @@ -936,17 +935,30 @@ void SetupMessages( r.top() + (r.height() - iconSize) / 2); }), iconSize, - r, + *i, buttonRight->events( ) | rpl::filter([=](not_null event) { return event->type() == QEvent::Enter; }) | rpl::to_empty, rpl::duplicate(idValue) | rpl::skip(1) | rpl::to_empty, &state->icons.lifetimes[index]); - state->icons.flag = !state->icons.flag; - toggleButtonRight(true); - break; + } else if (const auto customId = id.custom()) { + AddReactionCustomIcon( + inner, + buttonRight->geometryValue( + ) | rpl::map([=](const QRect &r) { + return QPoint( + r.left() + (r.width() - iconSize) / 2, + r.top() + (r.height() - iconSize) / 2); + }), + iconSize, + controller, + customId, + rpl::duplicate(idValue) | rpl::skip(1) | rpl::to_empty, + &state->icons.lifetimes[index]); } + state->icons.flag = !state->icons.flag; + toggleButtonRight(true); }, buttonRight->lifetime()); react->geometryValue(