Add reaction selector widget.

This commit is contained in:
John Preston
2025-09-23 18:32:22 +04:00
parent 0bcfcb4ea4
commit eccbb0df7a
8 changed files with 390 additions and 57 deletions
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_group_message_field.h"
#include "base/event_filter.h"
#include "boxes/premium_preview_box.h"
#include "calls/group/calls_group_messages.h"
#include "chat_helpers/compose/compose_show.h"
@@ -18,6 +19,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/stickers/data_custom_emoji.h"
#include "data/stickers/data_stickers.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "history/view/reactions/history_view_reactions_selector.h"
#include "history/view/reactions/history_view_reactions_strip.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/controls/emoji_button.h"
@@ -39,8 +43,272 @@ namespace {
constexpr auto kWarnLimit = 24;
constexpr auto kErrorLimit = 99;
using Chosen = HistoryView::Reactions::ChosenReaction;
} // namespace
class ReactionPanel final {
public:
ReactionPanel(
not_null<QWidget*> outer,
std::shared_ptr<ChatHelpers::Show> show,
rpl::producer<QRect> fieldGeometry);
~ReactionPanel();
[[nodiscard]] rpl::producer<Chosen> chosen() const;
void show();
void hide();
void raise();
void hideIfCollapsed();
void collapse();
private:
struct Hiding;
void create();
void updateShowState();
void fadeOutSelector();
void startAnimation();
const not_null<QWidget*> _outer;
const std::shared_ptr<ChatHelpers::Show> _show;
std::unique_ptr<Ui::RpWidget> _parent;
std::unique_ptr<HistoryView::Reactions::Selector> _selector;
std::vector<std::unique_ptr<Hiding>> _hiding;
rpl::event_stream<Chosen> _chosen;
Ui::Animations::Simple _showing;
rpl::variable<float64> _shownValue;
rpl::variable<QRect> _fieldGeometry;
rpl::variable<bool> _expanded;
rpl::variable<bool> _shown = false;
};
struct ReactionPanel::Hiding {
explicit Hiding(not_null<QWidget*> parent) : widget(parent) {
}
Ui::RpWidget widget;
Ui::Animations::Simple animation;
QImage frame;
};
ReactionPanel::ReactionPanel(
not_null<QWidget*> outer,
std::shared_ptr<ChatHelpers::Show> show,
rpl::producer<QRect> fieldGeometry)
: _outer(outer)
, _show(std::move(show))
, _fieldGeometry(std::move(fieldGeometry)) {
}
ReactionPanel::~ReactionPanel() = default;
auto ReactionPanel::chosen() const -> rpl::producer<Chosen> {
return _chosen.events();
}
void ReactionPanel::show() {
if (_shown.current()) {
return;
}
create();
if (!_selector) {
return;
}
const auto duration = st::defaultPanelAnimation.heightDuration
* st::defaultPopupMenu.showDuration;
_shown = true;
_showing.start([=] { updateShowState(); }, 0., 1., duration);
updateShowState();
_parent->show();
}
void ReactionPanel::hide() {
if (!_selector) {
return;
}
_selector->beforeDestroy();
if (!anim::Disabled()) {
fadeOutSelector();
}
_shown = false;
_expanded = false;
_showing.stop();
_selector = nullptr;
_parent = nullptr;
}
void ReactionPanel::raise() {
if (_parent) {
_parent->raise();
}
}
void ReactionPanel::hideIfCollapsed() {
if (!_expanded.current()) {
hide();
}
}
void ReactionPanel::collapse() {
if (_expanded.current()) {
hide();
show();
}
}
void ReactionPanel::create() {
auto reactions = Data::LookupPossibleReactions(&_show->session());
if (reactions.recent.empty()) {
return;
}
_parent = std::make_unique<Ui::RpWidget>(_outer);
_parent->show();
_parent->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
if (e->type() == QEvent::MouseButtonPress) {
const auto event = static_cast<QMouseEvent*>(e.get());
if (event->button() == Qt::LeftButton) {
if (!_selector
|| !_selector->geometry().contains(event->pos())) {
collapse();
}
}
}
}, _parent->lifetime());
_selector = std::make_unique<HistoryView::Reactions::Selector>(
_parent.get(),
st::storiesReactionsPan,
_show,
std::move(reactions),
TextWithEntities(),
[=](bool fast) { hide(); },
nullptr, // iconFactory
nullptr, // paused
true);
_selector->chosen(
) | rpl::start_with_next([=](Chosen reaction) {
if (reaction.id.custom() && !_show->session().premium()) {
ShowPremiumPreviewBox(
_show,
PremiumFeature::AnimatedEmoji);
} else {
_chosen.fire(std::move(reaction));
hide();
}
}, _selector->lifetime());
const auto desiredWidth = st::storiesReactionsWidth;
const auto maxWidth = desiredWidth * 2;
const auto width = _selector->countWidth(desiredWidth, maxWidth);
const auto margins = _selector->marginsForShadow();
const auto categoriesTop = _selector->extendTopForCategoriesAndAbout(
width);
const auto full = margins.left() + width + margins.right();
_shownValue = 0.;
rpl::combine(
_fieldGeometry.value(),
_shownValue.value(),
_expanded.value()
) | rpl::start_with_next([=](QRect field, float64 shown, bool expanded) {
const auto width = margins.left()
+ _selector->countAppearedWidth(shown)
+ margins.right();
const auto available = field.y();
const auto min = st::storiesReactionsBottomSkip
+ st::reactStripHeight;
const auto max = min
+ margins.top()
+ categoriesTop
+ st::storiesReactionsAddedTop;
const auto height = expanded ? std::min(available, max) : min;
const auto top = field.y() - height;
const auto shift = (width / 2);
const auto right = (field.x() + field.width() / 2 + shift);
_parent->setGeometry(QRect((right - width), top, full, height));
const auto innerTop = height
- st::storiesReactionsBottomSkip
- st::reactStripHeight;
const auto maxAdded = innerTop - margins.top() - categoriesTop;
const auto added = std::min(maxAdded, st::storiesReactionsAddedTop);
_selector->setSpecialExpandTopSkip(added);
_selector->initGeometry(innerTop);
}, _selector->lifetime());
_selector->willExpand(
) | rpl::start_with_next([=] {
_expanded = true;
const auto raw = _parent.get();
base::install_event_filter(raw, qApp, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::MouseButtonPress) {
const auto event = static_cast<QMouseEvent*>(e.get());
if (event->button() == Qt::LeftButton) {
if (!_selector
|| !_selector->geometry().contains(
_parent->mapFromGlobal(event->globalPos()))) {
collapse();
}
}
}
return base::EventFilterResult::Continue;
});
}, _selector->lifetime());
_selector->escapes() | rpl::start_with_next([=] {
collapse();
}, _selector->lifetime());
}
void ReactionPanel::fadeOutSelector() {
const auto geometry = Ui::MapFrom(
_outer,
_parent.get(),
_selector->geometry());
_hiding.push_back(std::make_unique<Hiding>(_outer));
const auto raw = _hiding.back().get();
raw->frame = Ui::GrabWidgetToImage(_selector.get());
raw->widget.setGeometry(geometry);
raw->widget.show();
raw->widget.paintRequest(
) | rpl::start_with_next([=] {
if (const auto opacity = raw->animation.value(0.)) {
auto p = QPainter(&raw->widget);
p.setOpacity(opacity);
p.drawImage(0, 0, raw->frame);
}
}, raw->widget.lifetime());
Ui::PostponeCall(&raw->widget, [=] {
raw->animation.start([=] {
if (raw->animation.animating()) {
raw->widget.update();
} else {
const auto i = ranges::find(
_hiding,
raw,
&std::unique_ptr<Hiding>::get);
if (i != end(_hiding)) {
_hiding.erase(i);
}
}
}, 1., 0., st::slideWrapDuration);
});
}
void ReactionPanel::updateShowState() {
const auto progress = _showing.value(_shown.current() ? 1. : 0.);
const auto opacity = 1.;
const auto appearing = _showing.animating();
const auto toggling = false;
_shownValue = progress;
_selector->updateShowState(progress, opacity, appearing, toggling);
}
MessageField::MessageField(
not_null<QWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
@@ -70,11 +338,51 @@ void MessageField::createControls(PeerData *peer) {
_field->setDocumentMargin(4.);
_field->setAdditionalMargin(style::ConvertScale(4) - 4);
_reactionPanel = std::make_unique<ReactionPanel>(
_parent,
_show,
_wrap->geometryValue());
_fieldFocused = _field->focusedChanges();
_fieldEmpty = _field->changes() | rpl::map([field = _field] {
return field->getLastText().trimmed().isEmpty();
});
rpl::combine(
_fieldFocused.value(),
_fieldEmpty.value()
) | rpl::start_with_next([=](bool focused, bool empty) {
if (!focused) {
_reactionPanel->hideIfCollapsed();
} else if (empty) {
_reactionPanel->show();
} else {
_reactionPanel->hide();
}
}, _field->lifetime());
_reactionPanel->chosen(
) | rpl::start_with_next([=](Chosen reaction) {
if (const auto customId = reaction.id.custom()) {
const auto document = _show->session().data().document(customId);
if (const auto sticker = document->sticker()) {
if (const auto alt = sticker->alt; !alt.isEmpty()) {
const auto length = int(alt.size());
const auto data = Data::SerializeCustomEmojiId(customId);
const auto tag = Ui::InputField::CustomEmojiLink(data);
_submitted.fire({ alt, { { 0, length, tag } } });
}
}
} else {
_submitted.fire({ reaction.id.emoji() });
}
_reactionPanel->hide();
}, _field->lifetime());
const auto show = _show;
const auto allow = [=](not_null<DocumentData*> emoji) {
return peer
? Data::AllowEmojiWithoutPremium(peer, emoji)
: show->session().premium();
if (peer && Data::AllowEmojiWithoutPremium(peer, emoji)) {
return true;
}
return false;
};
InitMessageFieldHandlers({
.session = &show->session(),
@@ -252,6 +560,8 @@ void MessageField::toggle(bool shown) {
} else if (shown) {
Assert(_width.current() > 0);
Ui::SendPendingMoveResizeEvents(_wrap.get());
} else if (Ui::InFocusChain(_field)) {
_parent->setFocus();
}
_shown = shown;
if (!anim::Disabled()) {
@@ -291,6 +601,9 @@ void MessageField::raise() {
if (_cache) {
_cache->raise();
}
if (_reactionPanel) {
_reactionPanel->raise();
}
if (_emojiPanel) {
_emojiPanel->raise();
}
@@ -25,6 +25,8 @@ class RpWidget;
namespace Calls::Group {
class ReactionPanel;
class MessageField final {
public:
MessageField(
@@ -64,6 +66,9 @@ private:
Ui::SendButton *_send = nullptr;
Ui::EmojiButton *_emojiToggle = nullptr;
std::unique_ptr<ChatHelpers::TabbedPanel> _emojiPanel;
std::unique_ptr<ReactionPanel> _reactionPanel;
rpl::variable<bool> _fieldFocused;
rpl::variable<bool> _fieldEmpty = true;
rpl::variable<int> _width;
rpl::variable<int> _height;
@@ -82,7 +82,6 @@ private:
style::complex_color _messageBg;
Ui::RoundRect _messageBgRect;
rpl::lifetime _effectsLifetime;
Ui::Animations::Simple _topFadeAnimation;
@@ -254,7 +254,7 @@ PossibleItemReactionsRef LookupPossibleReactions(
}
}
if (allowed.paidEnabled
&& !added.contains(Data::ReactionId::Paid())) {
&& !added.contains(ReactionId::Paid())) {
result.recent.push_back(reactions->lookupPaid());
}
} else {
@@ -300,7 +300,7 @@ PossibleItemReactionsRef LookupPossibleReactions(
}
}
if (!item->reactionsAreTags()) {
const auto toFront = [&](Data::ReactionId id) {
const auto toFront = [&](ReactionId id) {
const auto i = ranges::find(result.recent, id, &Reaction::id);
if (i != end(result.recent) && i != begin(result.recent)) {
std::rotate(begin(result.recent), i, i + 1);
@@ -308,12 +308,40 @@ PossibleItemReactionsRef LookupPossibleReactions(
};
toFront(reactions->favoriteId());
if (paidInFront) {
toFront(Data::ReactionId::Paid());
toFront(ReactionId::Paid());
}
}
return result;
}
[[nodiscard]] PossibleItemReactionsRef LookupPossibleReactions(
not_null<Main::Session*> session) {
auto result = PossibleItemReactionsRef();
const auto reactions = &session->data().reactions();
const auto &full = reactions->list(Reactions::Type::Active);
const auto &top = reactions->list(Reactions::Type::Top);
const auto &recent = reactions->list(Reactions::Type::Recent);
const auto premiumPossible = session->premiumPossible();
auto added = base::flat_set<ReactionId>();
result.recent.reserve(full.size());
for (const auto &reaction : ranges::views::concat(top, recent, full)) {
if (premiumPossible || !reaction.id.custom()) {
if (added.emplace(reaction.id).second) {
result.recent.push_back(&reaction);
}
}
}
result.customAllowed = premiumPossible;
const auto i = ranges::find(
result.recent,
reactions->favoriteId(),
&Reaction::id);
if (i != end(result.recent) && i != begin(result.recent)) {
std::rotate(begin(result.recent), i, i + 1);
}
return result;
}
PossibleItemReactions::PossibleItemReactions(
const PossibleItemReactionsRef &other)
: recent(other.recent | ranges::views::transform([](const auto &value) {
@@ -574,8 +602,8 @@ DocumentData *Reactions::chooseGenericAnimation(
const auto i = sticker
? ranges::find(
_available,
::Data::ReactionId{ { sticker->alt } },
&::Data::Reaction::id)
ReactionId{ { sticker->alt } },
&Reaction::id)
: end(_available);
if (i != end(_available) && i->aroundAnimation) {
const auto view = i->aroundAnimation->createMediaView();
@@ -61,6 +61,8 @@ struct PossibleItemReactions {
[[nodiscard]] PossibleItemReactionsRef LookupPossibleReactions(
not_null<HistoryItem*> item,
bool paidInFront = false);
[[nodiscard]] PossibleItemReactionsRef LookupPossibleReactions(
not_null<Main::Session*> session);
struct MyTagInfo {
ReactionId id;
@@ -36,7 +36,14 @@ StickerToast::StickerToast(
not_null<Window::SessionController*> controller,
not_null<QWidget*> parent,
Fn<void()> destroy)
: _controller(controller)
: StickerToast(controller->uiShow(), parent, std::move(destroy)) {
}
StickerToast::StickerToast(
std::shared_ptr<ChatHelpers::Show> show,
not_null<QWidget*> parent,
Fn<void()> destroy)
: _show(std::move(show))
, _parent(parent)
, _destroy(std::move(destroy)) {
}
@@ -104,7 +111,7 @@ void StickerToast::requestSet() {
Expects(_for != nullptr);
if (const auto sticker = _for->sticker()) {
const auto api = &_controller->session().api();
const auto api = &_show->session().api();
_setRequestId = api->request(MTPmessages_GetStickerSet(
Data::InputStickerSet(sticker->set),
MTP_int(0) // hash
@@ -112,7 +119,7 @@ void StickerToast::requestSet() {
_setRequestId = 0;
result.match([&](const MTPDmessages_stickerSet &data) {
data.vset().match([&](const MTPDstickerSet &data) {
const auto owner = &_controller->session().data();
const auto owner = &_show->session().data();
showWithTitle(owner->stickers().getSetTitle(data));
});
}, [&](const MTPDmessages_stickerSetNotModified &) {
@@ -125,7 +132,7 @@ void StickerToast::requestSet() {
}
void StickerToast::cancelRequest() {
_controller->session().api().request(base::take(_setRequestId)).cancel();
_show->session().api().request(base::take(_setRequestId)).cancel();
}
void StickerToast::showWithTitle(const QString &title) {
@@ -219,29 +226,28 @@ void StickerToast::showWithTitle(const QString &title) {
}
button->setClickedCallback([=] {
if (toSaved) {
_controller->showPeerHistory(
_controller->session().userPeerId(),
Window::SectionShow::Way::Forward);
if (const auto window = _show->resolveWindow()) {
window->showPeerHistory(
window->session().userPeerId(),
Window::SectionShow::Way::Forward);
}
hideToast();
return;
} else if (_section == Section::TopicIcon) {
Settings::ShowPremium(_controller, u"forum_topic_icon"_q);
if (const auto window = _show->resolveWindow()) {
Settings::ShowPremium(window, u"forum_topic_icon"_q);
}
return;
}
const auto id = _for->sticker()->set.id;
const auto id = _for->sticker()->set;
const auto &sets = _for->owner().stickers().sets();
const auto i = sets.find(id);
const auto i = sets.find(id.id);
if (isEmoji
&& (i != end(sets))
&& (i->second->flags & Data::StickersSetFlag::Installed)) {
ShowPremiumPreviewBox(
_controller,
PremiumFeature::AnimatedEmoji);
ShowPremiumPreviewBox(_show, PremiumFeature::AnimatedEmoji);
} else {
_controller->show(Box<StickerSetBox>(
_controller->uiShow(),
_for->sticker()->set,
setType));
_show->show(Box<StickerSetBox>(_show, id, setType));
}
hideToast();
});
@@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_widgets.h"
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Ui {
class Show;
class RpWidget;
@@ -30,6 +34,10 @@ public:
not_null<Window::SessionController*> controller,
not_null<QWidget*> parent,
Fn<void()> destroy);
StickerToast(
std::shared_ptr<ChatHelpers::Show> show,
not_null<QWidget*> parent,
Fn<void()> destroy);
~StickerToast();
enum class Section {
@@ -50,7 +58,7 @@ private:
void setupLottiePreview(not_null<Ui::RpWidget*> widget, int size);
void clearHiddenHiding();
const not_null<Window::SessionController*> _controller;
const std::shared_ptr<ChatHelpers::Show> _show;
const not_null<QWidget*> _parent;
Section _section = {};
style::Toast _st;
@@ -735,34 +735,6 @@ void WeatherView::cacheBackground() {
return { QString() + QChar(10084) };
}
[[nodiscard]] Data::PossibleItemReactionsRef LookupPossibleReactions(
not_null<Main::Session*> session) {
auto result = Data::PossibleItemReactionsRef();
const auto reactions = &session->data().reactions();
const auto &full = reactions->list(Data::Reactions::Type::Active);
const auto &top = reactions->list(Data::Reactions::Type::Top);
const auto &recent = reactions->list(Data::Reactions::Type::Recent);
const auto premiumPossible = session->premiumPossible();
auto added = base::flat_set<Data::ReactionId>();
result.recent.reserve(full.size());
for (const auto &reaction : ranges::views::concat(top, recent, full)) {
if (premiumPossible || !reaction.id.custom()) {
if (added.emplace(reaction.id).second) {
result.recent.push_back(&reaction);
}
}
}
result.customAllowed = premiumPossible;
const auto i = ranges::find(
result.recent,
reactions->favoriteId(),
&Data::Reaction::id);
if (i != end(result.recent) && i != begin(result.recent)) {
std::rotate(begin(result.recent), i, i + 1);
}
return result;
}
HistoryView::Context ReactionView::elementContext() {
return HistoryView::Context::ContactPreview;
}
@@ -907,7 +879,7 @@ void Reactions::Panel::attachToReactionButton(
}
void Reactions::Panel::create() {
auto reactions = LookupPossibleReactions(
auto reactions = Data::LookupPossibleReactions(
&_controller->uiShow()->session());
if (reactions.recent.empty()) {
return;
@@ -1153,7 +1125,7 @@ auto Reactions::attachToMenu(
desiredPosition,
st::storiesReactionsPan,
show,
LookupPossibleReactions(&show->session()),
Data::LookupPossibleReactions(&show->session()),
TextWithEntities());
if (!result) {
return result.error();