Replaced menu of who reacted with full window preview of reaction.

This commit is contained in:
23rd
2026-08-15 08:05:38 +03:00
parent 706272d4cc
commit 41bae1e417
7 changed files with 419 additions and 62 deletions

View File

@@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_item_components.h"
#include "history/history_item_helpers.h"
#include "history/history_item_text.h"
#include "history/view/history_view_reaction_preview.h"
#include "history/view/history_view_schedule_box.h"
#include "history/view/media/history_view_media.h"
#include "history/view/media/menu/history_view_poll_menu.h"
@@ -35,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "info/info_memento.h"
#include "iv/iv_rich_message_html_export.h"
#include "ui/effects/ripple_animation.h"
#include "ui/widgets/dropdown_menu.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/menu/menu_action.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
@@ -1551,6 +1553,180 @@ void EditTagBox(
return result;
}
[[nodiscard]] Fn<void(Ui::WhoReadParticipant)> MakeParticipantChosen(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item) {
const auto itemId = item->fullId();
const auto originPeer = item->history()->peer;
return [=](Ui::WhoReadParticipant who) {
const auto participant = originPeer->owner().peer(PeerId(who.id));
Reactions::ShowReactionParticipantInfo(
controller,
participant,
originPeer,
itemId.msg,
who.dateReacted);
};
}
[[nodiscard]] Fn<void()> MakeShowAllReactionsChosen(
not_null<Window::SessionController*> controller,
FullMsgId itemId,
Data::ReactionId id) {
return [=] {
if (const auto item = controller->session().data().message(itemId)) {
controller->showSection(std::make_shared<Info::Memento>(
nullptr,
itemId,
HistoryView::Reactions::DefaultSelectedTab(item, id)));
}
};
}
template <typename Menu>
[[nodiscard]] int AppendReactionPackAction(
not_null<Menu*> menu,
const Data::ReactionId &id,
not_null<Window::SessionController*> controller) {
const auto custom = id.custom();
if (!custom) {
return 0;
}
const auto owner = &controller->session().data();
const auto sticker = owner->document(custom)->sticker();
if (!sticker || !sticker->set.id) {
return 0;
}
AddEmojiPacksAction(
menu,
{ sticker->set },
EmojiPacksSource::Reaction,
controller);
return 2;
}
[[nodiscard]] QString WidestReactionDate(TimeId around) {
const auto parsed = base::unixtime::parse(around);
const auto day = langDayOfMonthShort(parsed.date());
const auto &font = st::whoReadDateStyle.font;
auto result = QString();
const auto check = [&](const QString &date) {
if (font->width(date) > font->width(result)) {
result = date;
}
};
for (const auto &time : {
QLocale().toString(parsed.time(), QLocale::ShortFormat),
QLocale().toString(QTime(23, 59), QLocale::ShortFormat),
}) {
check(tr::lng_mediaview_today(tr::now, lt_time, time));
check(tr::lng_mediaview_yesterday(tr::now, lt_time, time));
check(tr::lng_mediaview_date_time(
tr::now,
lt_date,
day,
lt_time,
time));
}
return result;
}
[[nodiscard]] auto ReactionPreviewPreloaderEntries(
not_null<HistoryItem*> item,
const Data::ReactionId &id)
-> std::vector<Ui::WhoReactedEntryData> {
const auto entryHeight = st::defaultWhoRead.photoSkip * 2
+ st::defaultWhoRead.photoSize;
const auto &list = item->reactions();
const auto i = ranges::find(list, id, &Data::MessageReaction::id);
const auto count = std::clamp(
(i != end(list)) ? i->count : 1,
1,
st::whoReadDropdownMenuMaxHeight / entryHeight + 1);
const auto &recent = item->recentReactions();
const auto j = recent.find(id);
const auto known = (j != end(recent)) ? int(j->second.size()) : 0;
const auto date = WidestReactionDate(item->date());
const auto customEntityData = Data::ReactionEntityData(id);
auto result = std::vector<Ui::WhoReactedEntryData>();
result.reserve(count);
for (auto k = 0; k != count; ++k) {
result.push_back({
.text = (k < known) ? j->second[k].peer->name() : QString(),
.date = date,
.customEntityData = customEntityData,
});
}
return result;
}
[[nodiscard]] bool ShowReactionPreviewMenu(
not_null<QWidget*> context,
not_null<HistoryItem*> item,
const Data::ReactionId &id,
not_null<Window::SessionController*> controller) {
if (id.paid()) {
return false;
}
struct State {
int addedToBottom = 0;
};
const auto itemId = item->fullId();
const auto participantChosen = MakeParticipantChosen(controller, item);
const auto showAllChosen = MakeShowAllReactionsChosen(
controller,
itemId,
id);
const auto preloaderEntries = ReactionPreviewPreloaderEntries(item, id);
return ShowReactionPreview(controller, itemId, id, false, [=](
ReactionPreviewMenu preview) {
const auto menu = not_null(preview.menu);
const auto refreshGeometry = preview.refreshGeometry;
const auto moderateReactionChosen = MakeModerateReactionChosen(
controller,
itemId,
item->history()->peer,
preview.hide);
using ListMenu = Ui::WhoReactedListMenu;
const auto filler = menu->lifetime().make_state<ListMenu>(
Data::ReactedMenuFactory(&controller->session()),
participantChosen,
showAllChosen,
moderateReactionChosen);
const auto state = menu->lifetime().make_state<State>();
const auto appendBottom = [=] {
state->addedToBottom = AppendReactionPackAction(
menu,
id,
controller);
};
filler->populatePreloader(
menu,
preloaderEntries,
appendBottom);
refreshGeometry();
Api::WhoReacted(
item,
id,
context,
st::defaultWhoRead
) | rpl::filter([=](const Ui::WhoReadContent &content) {
return content.state != Ui::WhoReadState::Unknown;
}) | rpl::on_next([=](Ui::WhoReadContent &&content) {
filler->populate(
menu,
content,
nullptr,
state->addedToBottom,
appendBottom);
refreshGeometry();
}, menu->lifetime());
});
}
} // namespace
std::optional<QString> CurrentVoiceTimecode(FullMsgId itemId) {
@@ -2576,22 +2752,15 @@ void ShowWhoReactedMenu(
if (item->reactionsAreTags()) {
ShowTagMenu(menu, position, context, item, id, controller);
return;
} else if (ShowReactionPreviewMenu(context, item, id, controller)) {
return;
}
struct State {
int addedToBottom = 0;
};
const auto itemId = item->fullId();
const auto participantChosen = [=](Ui::WhoReadParticipant who) {
const auto originPeer = item->history()->peer;
const auto participant = originPeer->owner().peer(PeerId(who.id));
Reactions::ShowReactionParticipantInfo(
controller,
participant,
originPeer,
itemId.msg,
who.dateReacted);
};
const auto participantChosen = MakeParticipantChosen(controller, item);
const auto moderateReactionChosen = MakeModerateReactionChosen(
controller,
itemId,
@@ -2601,15 +2770,10 @@ void ShowWhoReactedMenu(
(*menu)->hideMenu();
}
});
const auto showAllChosen = [=, itemId = item->fullId()]{
if (const auto item = controller->session().data().message(itemId)) {
controller->showSection(std::make_shared<Info::Memento>(
nullptr,
itemId,
HistoryView::Reactions::DefaultSelectedTab(item, id)));
}
};
const auto owner = &controller->session().data();
const auto showAllChosen = MakeShowAllReactionsChosen(
controller,
itemId,
id);
const auto filler = lifetime.make_state<Ui::WhoReactedListMenu>(
Data::ReactedMenuFactory(&controller->session()),
participantChosen,
@@ -2626,19 +2790,10 @@ void ShowWhoReactedMenu(
}) | rpl::on_next([=, &lifetime](Ui::WhoReadContent &&content) {
const auto creating = !*menu;
const auto appendBottom = [=] {
state->addedToBottom = 0;
if (const auto custom = id.custom()) {
if (const auto set = owner->document(custom)->sticker()) {
if (set->set.id) {
state->addedToBottom = 2;
AddEmojiPacksAction(
menu->get(),
{ set->set },
EmojiPacksSource::Reaction,
controller);
}
}
}
state->addedToBottom = AppendReactionPackAction(
not_null(menu->get()),
id,
controller);
};
if (creating) {
*menu = base::make_unique_q<Ui::PopupMenu>(
@@ -2695,8 +2850,9 @@ std::vector<StickerSetIdentifier> CollectEmojiPacks(
return result;
}
void AddEmojiPacksAction(
not_null<Ui::PopupMenu*> menu,
template <typename Menu>
void AddEmojiPacksActionTo(
not_null<Menu*> menu,
std::vector<StickerSetIdentifier> packIds,
EmojiPacksSource source,
not_null<Window::SessionController*> controller) {
@@ -2770,7 +2926,7 @@ void AddEmojiPacksAction(
}();
auto button = base::make_unique_q<Ui::Menu::MultilineAction>(
menu->menu(),
menu->st().menu,
menu->menu()->st(),
st::historyHasCustomEmoji,
st::historyHasCustomEmojiPosition,
std::move(text));
@@ -2792,6 +2948,22 @@ void AddEmojiPacksAction(
menu->addAction(std::move(button));
}
void AddEmojiPacksAction(
not_null<Ui::PopupMenu*> menu,
std::vector<StickerSetIdentifier> packIds,
EmojiPacksSource source,
not_null<Window::SessionController*> controller) {
AddEmojiPacksActionTo(menu, std::move(packIds), source, controller);
}
void AddEmojiPacksAction(
not_null<Ui::DropdownMenu*> menu,
std::vector<StickerSetIdentifier> packIds,
EmojiPacksSource source,
not_null<Window::SessionController*> controller) {
AddEmojiPacksActionTo(menu, std::move(packIds), source, controller);
}
void AddEmojiPacksAction(
not_null<Ui::PopupMenu*> menu,
not_null<HistoryItem*> item,

View File

@@ -21,6 +21,7 @@ class SessionShow;
} // namespace Main
namespace Ui {
class DropdownMenu;
class PopupMenu;
class Show;
enum class ReportReason;
@@ -149,6 +150,11 @@ void AddEmojiPacksAction(
std::vector<StickerSetIdentifier> packIds,
EmojiPacksSource source,
not_null<Window::SessionController*> controller);
void AddEmojiPacksAction(
not_null<Ui::DropdownMenu*> menu,
std::vector<StickerSetIdentifier> packIds,
EmojiPacksSource source,
not_null<Window::SessionController*> controller);
void AddEmojiPacksAction(
not_null<Ui::PopupMenu*> menu,
not_null<HistoryItem*> item,

View File

@@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/text/text_utilities.h"
#include "ui/ui_utility.h"
#include "window/window_media_preview.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
@@ -124,17 +125,16 @@ template <typename MediaData>
return { state, hideAll };
}
void SetupPreviewMenu(
Fn<void()> SetupPreviewMenu(
not_null<Window::SessionController*> controller,
const PreviewOverlay &overlay,
const style::DropdownMenu &menuSt,
Fn<void(not_null<Ui::DropdownMenu*>)> fillMenu) {
const auto &state = overlay.state;
const auto mainwidget = controller->widget()->bodyWidget();
if (fillMenu) {
state->mediaPreview->setHideEmoji(true);
auto menu = object_ptr<Ui::DropdownMenu>(
mainwidget,
st::dropdownMenuWithIcons);
auto menu = object_ptr<Ui::DropdownMenu>(mainwidget, menuSt);
menu->setAutoHiding(false);
menu->setHiddenCallback(
crl::guard(state->clickable.get(), overlay.hideAll));
@@ -153,24 +153,38 @@ void SetupPreviewMenu(
};
const auto mediaPreviewRaw = state->mediaPreview.get();
mainwidget->sizeValue() | rpl::on_next([=](QSize size) {
const auto updateLayout = [=] {
const auto size = mainwidget->size();
mediaPreviewRaw->setGeometry(Rect(size));
if (wrapRaw) {
const auto menuRaw = wrapRaw->entity();
menuRaw->showFast();
const auto gap = st::defaultMenu.itemPadding.top();
const auto menuH = menuRaw->height();
const auto shift = -(gap + menuH) / 2;
mediaPreviewRaw->setContentShift(shift);
const auto menuX = (size.width() - menuRaw->width()) / 2;
const auto menuY = mediaPreviewRaw->contentBottom() + gap;
wrapRaw->move(menuX, menuY);
wrapRaw->show(anim::type::normal);
wrapRaw->raise();
const auto menuRaw = wrapRaw ? wrapRaw->entity() : nullptr;
if (!menuRaw || menuRaw->empty()) {
return;
} else if (wrapRaw->isHidden()) {
Ui::SendPendingMoveResizeEvents(wrapRaw);
}
menuRaw->showFast();
const auto gap = st::defaultMenu.itemPadding.top();
const auto menuH = menuRaw->height();
const auto shift = -(gap + menuH) / 2;
mediaPreviewRaw->setContentShift(shift);
const auto menuX = (size.width() - menuRaw->width()) / 2;
const auto menuY = mediaPreviewRaw->contentBottom() + gap;
wrapRaw->move(menuX, menuY);
wrapRaw->show(anim::type::normal);
wrapRaw->raise();
};
mainwidget->sizeValue() | rpl::on_next([=](QSize) {
updateLayout();
}, mediaPreviewRaw->lifetime());
if (wrapRaw) {
wrapRaw->entity()->sizeValue(
) | rpl::skip(1) | rpl::on_next([=](QSize) {
updateLayout();
}, wrapRaw->lifetime());
}
return updateLayout;
}
} // namespace
@@ -183,6 +197,7 @@ bool ShowStickerPreview(
SetupPreviewMenu(
controller,
CreatePreviewOverlay(controller, origin, document),
st::dropdownMenuWithIcons,
std::move(fillMenu));
return true;
}
@@ -195,6 +210,7 @@ bool ShowPhotoPreview(
SetupPreviewMenu(
controller,
CreatePreviewOverlay(controller, origin, photo),
st::dropdownMenuWithIcons,
std::move(fillMenu));
return true;
}
@@ -203,7 +219,8 @@ bool ShowReactionPreview(
not_null<Window::SessionController*> controller,
FullMsgId origin,
Data::ReactionId reactionId,
bool emojiPreview) {
bool emojiPreview,
Fn<void(ReactionPreviewMenu)> setupMenu) {
auto document = (DocumentData*)(nullptr);
if (const auto custom = reactionId.custom()) {
document = controller->session().data().document(custom);
@@ -219,8 +236,36 @@ bool ShowReactionPreview(
const auto &state = overlay.state;
const auto mainwidget = controller->widget()->bodyWidget();
const auto shadowExtend = st::boxRoundShadow.extend;
if (setupMenu) {
const auto layout = std::make_shared<Fn<void()>>();
const auto refreshGeometry = [=] {
if (const auto callback = *layout) {
callback();
}
};
*layout = SetupPreviewMenu(
controller,
overlay,
st::whoReadDropdownMenu,
[&](not_null<Ui::DropdownMenu*> menu) {
const auto maxHeight = st::whoReadDropdownMenuMaxHeight;
menu->setMaxHeight(std::clamp(
(mainwidget->height()
- st::maxStickerSize
- st::whoReadDropdownMenuSkip * 2),
maxHeight / 4,
maxHeight));
setupMenu({
.menu = menu,
.refreshGeometry = refreshGeometry,
.hide = overlay.hideAll,
});
});
return true;
}
const auto shadowExtend = st::boxRoundShadow.extend;
if (reactionId.custom() && document->sticker()) {
const auto setId = document->sticker()->set;
const auto packName

View File

@@ -25,6 +25,13 @@ class SessionController;
namespace HistoryView {
struct ReactionPreviewMenu {
Ui::DropdownMenu *menu = nullptr;
Fn<void()> refreshGeometry;
Fn<void()> hide;
};
bool ShowStickerPreview(
not_null<Window::SessionController*> controller,
FullMsgId origin,
@@ -46,6 +53,7 @@ bool ShowReactionPreview(
not_null<Window::SessionController*> controller,
FullMsgId origin,
Data::ReactionId reactionId,
bool emojiPreview = false);
bool emojiPreview = false,
Fn<void(ReactionPreviewMenu)> setupMenu = nullptr);
} // namespace HistoryView

View File

@@ -647,6 +647,16 @@ whoReadMenu: PopupMenu(popupMenuExpandedSeparator) {
scrollPadding: margins(0px, 6px, 0px, 4px);
maxHeight: 400px;
}
whoReadDropdownMenu: DropdownMenu(dropdownMenuWithIcons) {
wrap: InnerDropdown(defaultInnerDropdown) {
scrollPadding: margins(0px, 6px, 0px, 4px);
}
menu: Menu(menuWithIcons) {
separator: expandedMenuSeparator;
}
}
whoReadDropdownMenuMaxHeight: 400px;
whoReadDropdownMenuSkip: 24px;
whoReadNameWithDateTop: 3px;
whoReadDateTop: 20px;
whoReadDateSkip: 15px;

View File

@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/call_delayed.h"
#include "ui/widgets/menu/menu_action.h"
#include "ui/widgets/dropdown_menu.h"
#include "ui/widgets/popup_menu.h"
#include "ui/effects/ripple_animation.h"
#include "ui/chat/group_call_userpics.h"
@@ -1162,7 +1163,25 @@ void WhoReactedEntryAction::paint(Painter &&p) {
_textWidth,
width());
}
if (_type == WhoReactedType::RefRecipient
if (preloader) {
if (withDate) {
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(preloaderBrush);
const auto &font = st::whoReadDateStyle.font;
const auto height = font->height / 2;
const auto width = std::min(
st::whoReadDateSkip + _date.maxWidth(),
_textWidth);
p.drawRoundedRect(
st::defaultWhoRead.nameLeft,
st::whoReadDateTop + (font->height - height) / 2,
width,
height,
height / 2.,
height / 2.);
}
} else if (_type == WhoReactedType::RefRecipient
|| _type == WhoReactedType::RefRecipientNow) {
p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);
_date.drawLeftElided(
@@ -1298,6 +1317,7 @@ WhoReactedListMenu::WhoReactedListMenu(
void WhoReactedListMenu::clear() {
_actions.clear();
_minimalWidth = 0;
}
void WhoReactedListMenu::populate(
@@ -1306,13 +1326,80 @@ void WhoReactedListMenu::populate(
Fn<void()> refillTopActions,
int addedToBottom,
Fn<void()> appendBottomActions) {
populateTo(
menu,
content,
std::move(refillTopActions),
addedToBottom,
std::move(appendBottomActions));
}
void WhoReactedListMenu::populate(
not_null<DropdownMenu*> menu,
const WhoReadContent &content,
Fn<void()> refillTopActions,
int addedToBottom,
Fn<void()> appendBottomActions) {
populateTo(
menu,
content,
std::move(refillTopActions),
addedToBottom,
std::move(appendBottomActions));
}
void WhoReactedListMenu::populatePreloader(
not_null<DropdownMenu*> menu,
std::vector<WhoReactedEntryData> entries,
Fn<void()> appendBottomActions) {
for (auto &entry : entries) {
entry.type = WhoReactedType::Preloader;
entry.userpic = QImage();
entry.callback = nullptr;
entry.closeCallback = nullptr;
auto item = base::make_unique_q<WhoReactedEntryAction>(
menu->menu(),
_customEmojiFactory,
menu->menu()->st(),
std::move(entry));
accumulate_max(_minimalWidth, item->minWidth());
_actions.push_back(item.get());
menu->addAction(std::move(item));
}
applyMinimalWidth();
if (appendBottomActions) {
appendBottomActions();
}
}
void WhoReactedListMenu::applyMinimalWidth() {
if (!_minimalWidth) {
return;
}
for (const auto &action : _actions) {
if (action->minWidth() < _minimalWidth) {
action->setMinWidth(_minimalWidth);
}
}
}
template <typename Menu>
void WhoReactedListMenu::populateTo(
not_null<Menu*> menu,
const WhoReadContent &content,
Fn<void()> refillTopActions,
int addedToBottom,
Fn<void()> appendBottomActions) {
constexpr auto kRebuildOnAnyChange = !std::is_same_v<Menu, PopupMenu>;
const auto reactions = ranges::count_if(
content.participants,
[](const auto &p) { return !p.customEntityData.isEmpty(); });
const auto addShowAll = (content.fullReactionsCount > reactions);
const auto actionsCount = int(content.participants.size())
+ (addShowAll ? 1 : 0);
if (_actions.size() > actionsCount) {
if (kRebuildOnAnyChange
? (_actions.size() != actionsCount)
: (_actions.size() > actionsCount)) {
_actions.clear();
menu->clearActions();
if (refillTopActions) {
@@ -1331,11 +1418,17 @@ void WhoReactedListMenu::populate(
menu->menu()->st(),
std::move(data));
_actions.push_back(item.get());
const auto count = int(menu->actions().size());
if (addedToBottom > 0 && addedToBottom <= count) {
menu->insertAction(count - addedToBottom, std::move(item));
} else {
if constexpr (kRebuildOnAnyChange) {
menu->addAction(std::move(item));
} else {
const auto count = int(menu->actions().size());
if (addedToBottom > 0 && addedToBottom <= count) {
menu->insertAction(
count - addedToBottom,
std::move(item));
} else {
menu->addAction(std::move(item));
}
}
}
++index;
@@ -1369,6 +1462,7 @@ void WhoReactedListMenu::populate(
.callback = _showAllChosen,
});
}
applyMinimalWidth();
if (!addedToBottom && appendBottomActions) {
appendBottomActions();
}

View File

@@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Ui {
class DropdownMenu;
class PopupMenu;
struct WhoReadParticipant {
@@ -162,14 +163,35 @@ public:
Fn<void()> refillTopActions = nullptr,
int addedToBottom = 0,
Fn<void()> appendBottomActions = nullptr);
void populate(
not_null<DropdownMenu*> menu,
const WhoReadContent &content,
Fn<void()> refillTopActions = nullptr,
int addedToBottom = 0,
Fn<void()> appendBottomActions = nullptr);
void populatePreloader(
not_null<DropdownMenu*> menu,
std::vector<WhoReactedEntryData> entries,
Fn<void()> appendBottomActions = nullptr);
private:
template <typename Menu>
void populateTo(
not_null<Menu*> menu,
const WhoReadContent &content,
Fn<void()> refillTopActions,
int addedToBottom,
Fn<void()> appendBottomActions);
void applyMinimalWidth();
const Text::CustomEmojiFactory _customEmojiFactory;
const Fn<void(WhoReadParticipant)> _participantChosen;
const Fn<void()> _showAllChosen;
const Fn<void(WhoReadParticipant)> _moderateReactionChosen;
std::vector<not_null<WhoReactedEntryAction*>> _actions;
int _minimalWidth = 0;
};