From c4decce7f0ffa3d248f5da723dfb54db365aaa03 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 2 Oct 2025 18:58:38 +0300 Subject: [PATCH] Added ability to tag with emoji recent forwarded messages to self. --- Telegram/CMakeLists.txt | 2 + .../animations/toast/saved_messages.tgs | Bin 0 -> 3533 bytes .../Resources/qrc/telegram/animations.qrc | 1 + .../chat_helpers/chat_helpers.style | 9 + .../SourceFiles/history/history_widget.cpp | 7 + Telegram/SourceFiles/history/history_widget.h | 2 + .../history_view_self_forwards_tagger.cpp | 266 ++++++++++++++++++ .../view/history_view_self_forwards_tagger.h | 66 +++++ 8 files changed, 353 insertions(+) create mode 100644 Telegram/Resources/animations/toast/saved_messages.tgs create mode 100644 Telegram/SourceFiles/history/view/history_view_self_forwards_tagger.cpp create mode 100644 Telegram/SourceFiles/history/view/history_view_self_forwards_tagger.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 2ce69477aa..f6a4509ed7 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -918,6 +918,8 @@ PRIVATE history/view/history_view_schedule_box.h history/view/history_view_scheduled_section.cpp history/view/history_view_scheduled_section.h + history/view/history_view_self_forwards_tagger.cpp + history/view/history_view_self_forwards_tagger.h history/view/history_view_send_action.cpp history/view/history_view_send_action.h history/view/history_view_service_message.cpp diff --git a/Telegram/Resources/animations/toast/saved_messages.tgs b/Telegram/Resources/animations/toast/saved_messages.tgs new file mode 100644 index 0000000000000000000000000000000000000000..6d2cac78305883b3d9f4c8dbc04a7bb61bea7d29 GIT binary patch literal 3533 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 HQ7-@heIx7s literal 0 HcmV?d00001 diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index 94365b758f..1d38ee7e4a 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -43,6 +43,7 @@ ../../animations/cake.tgs ../../animations/camera_outline.tgs ../../animations/photo_suggest_icon.tgs + ../../animations/toast/saved_messages.tgs ../../animations/dice/dice_idle.tgs ../../animations/dice/dart_idle.tgs diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index e7e68c5087..2931d8f430 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -815,6 +815,15 @@ reactPanelScrollRounded: ScrollArea(emojiScroll) { deltat: 14px; deltab: 14px; } +selfForwardsTaggerStripSkip: 46px; +selfForwardsTaggerIcon: size(32px, 32px); +selfForwardsTaggerToast: Toast(defaultToast) { + minWidth: 160px; + maxWidth: 380px; + radius: 9px; + padding: margins(54px, 12px, 19px, 12px); + iconPosition: point(15px, 6px); +} choosePeerGroupIcon: icon {{ "info/edit/create_group", lightButtonFg }}; choosePeerChannelIcon: icon {{ "info/edit/create_channel", lightButtonFg }}; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index f1e6bda1e3..b17dd7e90b 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -122,6 +122,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_item_preview.h" #include "history/view/history_view_reply.h" #include "history/view/history_view_requests_bar.h" +#include "history/view/history_view_self_forwards_tagger.h" #include "history/view/history_view_sticker_toast.h" #include "history/view/history_view_subsection_tabs.h" #include "history/view/history_view_translate_bar.h" @@ -1018,6 +1019,12 @@ HistoryWidget::HistoryWidget( } }, lifetime()); + _selfForwardsTagger = std::make_unique( + controller, + this, + [=] { return _list; }, + _scroll.data()); + if (session().supportMode()) { session().data().chatListEntryRefreshes( ) | rpl::start_with_next([=] { diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index fc46753cae..e04cb41f3f 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -100,6 +100,7 @@ struct FileChosen; namespace HistoryView { class StickerToast; class PaidReactionToast; +class SelfForwardsTagger; class TopBarWidget; class PaysStatus; class ContactStatus; @@ -894,6 +895,7 @@ private: HistoryView::InfoTooltip _topToast; std::unique_ptr _stickerToast; + std::unique_ptr _selfForwardsTagger; std::unique_ptr _chooseForReport; std::unique_ptr _paidReactionToast; diff --git a/Telegram/SourceFiles/history/view/history_view_self_forwards_tagger.cpp b/Telegram/SourceFiles/history/view/history_view_self_forwards_tagger.cpp new file mode 100644 index 0000000000..8dfcaf845a --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_self_forwards_tagger.cpp @@ -0,0 +1,266 @@ +/* +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 "history/view/history_view_self_forwards_tagger.h" + +#include "base/call_delayed.h" +#include "base/event_filter.h" +#include "base/timer_rpl.h" +#include "chat_helpers/share_message_phrase_factory.h" +#include "core/ui_integration.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/history_item.h" +#include "history/view/reactions/history_view_reactions_selector.h" +#include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" +#include "main/main_session.h" +#include "ui/rect.h" +#include "ui/effects/show_animation.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast_widget.h" +#include "ui/toast/toast.h" +#include "ui/widgets/tooltip.h" +#include "window/window_session_controller.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" + +namespace HistoryView { +namespace { + +constexpr auto kInitTimer = crl::time(3000); +constexpr auto kTimerOnLeave = crl::time(2000); + +} // namespace + +SelfForwardsTagger::SelfForwardsTagger( + not_null controller, + not_null parent, + Fn listWidget, + not_null scroll) +: _controller(controller) +, _parent(parent) +, _listWidget(std::move(listWidget)) +, _scroll(scroll) { + setup(); +} + +SelfForwardsTagger::~SelfForwardsTagger() = default; + +void SelfForwardsTagger::setup() { + _controller->session().data().recentSelfForwards( + ) | rpl::start_with_next([=](const MessageIdsList &ids) { + showSelectorForMessages(ids); + }, _lifetime); +} + +void SelfForwardsTagger::showSelectorForMessages( + const MessageIdsList &ids) { + if (ids.empty()) { + return; + } + const auto lastId = ids.back(); + const auto item = _controller->session().data().message(lastId); + if (!item) { + return; + } + using namespace Reactions; + const auto reactions = Data::LookupPossibleReactions(item, true); + if (reactions.recent.empty()) { + return; + } + + showToast( + rpl::variable( + ChatHelpers::ForwardedMessagePhrase({ + .toCount = 1, + .singleMessage = (ids.size() == 1), + .to1 = _controller->session().user(), + .toSelfWithPremiumIsEmpty = false, + })).current(), + nullptr); + + const auto toastWidget = [&]() -> Ui::RpWidget* { + if (const auto toast = _toast.get()) { + return toast->widget(); + } + return nullptr; + }(); + if (!toastWidget) { + return; + } + + const auto toastWidth = toastWidget->width(); + const auto selector = Ui::CreateChild( + toastWidget->parentWidget(), + st::reactPanelEmojiPan, + _controller->uiShow(), + reactions, + TextWithEntities(), + [](bool) {}, + IconFactory(), + [] { return false; }, + false); + selector->setBubbleUp(true); + + const auto hideAndDestroy = [ + selectorWeak = base::make_weak(selector), + toastWidgetWeak = _toast] { + const auto selector = selectorWeak.get(); + const auto toastWidget = toastWidgetWeak.get(); + if (!selector || !toastWidget) { + return; + } + Ui::Animations::HideWidgets({ toastWidget->widget(), selector }); + selector->shownValue( + ) | rpl::start_with_next([toastWidgetWeak](bool shown) { + if (!shown) { + if (const auto toast = toastWidgetWeak.get()) { + delete toast->widget(); + } + } + }, selector->lifetime()); + }; + + selector->chosen( + ) | rpl::start_with_next([=](ChosenReaction reaction) { + for (const auto &id : ids) { + if (const auto item = _controller->session().data().message( + id)) { + item->toggleReaction( + reaction.id, + HistoryReactionSource::Selector); + } + } + hideAndDestroy(); + }, selector->lifetime()); + + const auto eventFilterCallback = [=](not_null event) { + if (event->type() == QEvent::MouseButtonPress) { + hideAndDestroy(); + return base::EventFilterResult::Cancel; + } + return base::EventFilterResult::Continue; + }; + base::install_event_filter(selector, _parent, eventFilterCallback); + if (const auto list = _listWidget()) { + list->lifetime().add([=] { + hideAndDestroy(); + }); + base::install_event_filter(selector, list, eventFilterCallback); + } + + struct State { + rpl::lifetime timerLifetime; + bool expanded = false; + }; + const auto state = selector->lifetime().make_state(); + const auto restartTimer = [=](crl::time ms) { + state->timerLifetime.destroy(); + base::timer_once(ms) | rpl::start_with_next([=] { + hideAndDestroy(); + }, state->timerLifetime); + }; + + selector->willExpand() | rpl::start_with_next([=] { + state->expanded = true; + }, selector->lifetime()); + + base::install_event_filter(selector, [=](not_null event) { + if (event->type() == QEvent::MouseButtonPress) { + state->timerLifetime.destroy(); + return base::EventFilterResult::Continue; + } else if (!state->expanded && event->type() == QEvent::Enter) { + state->timerLifetime.destroy(); + return base::EventFilterResult::Continue; + } else if (!state->expanded && event->type() == QEvent::Leave) { + restartTimer(kTimerOnLeave); + return base::EventFilterResult::Continue; + } + return base::EventFilterResult::Continue; + }, selector->lifetime()); + + QObject::connect( + _toast->widget(), + &QObject::destroyed, + selector, + [=] { delete selector; }); + + const auto selectorWidth = toastWidth; + selector->countWidth(selectorWidth, selectorWidth); + selector->initGeometry(_parent->height() / 2); + + _toast->widget()->geometryValue( + ) | rpl::start_with_next([=](const QRect &rect) { + if (rect.isEmpty()) { + return; + } + selector->moveToLeft( + rect.x() + (rect.width() - selector->width()) / 2, + rect::bottom(rect) - st::selfForwardsTaggerStripSkip); + }, selector->lifetime()); + restartTimer(kInitTimer); + selector->show(); +} + +void SelfForwardsTagger::showToast( + const TextWithEntities &text, + Fn callback) { + hideToast(); + _toast = Ui::Toast::Show(_scroll, Ui::Toast::Config{ + .text = text, + .textContext = Core::TextContext({ + .session = &_controller->session(), + }), + .st = &st::selfForwardsTaggerToast, + .attach = RectPart::Top, + .infinite = true, + }); + if (const auto strong = _toast.get()) { + const auto widget = strong->widget(); + createLottieIcon(widget); + if (callback) { + QObject::connect(widget, &QObject::destroyed, callback); + } + } else if (callback) { + callback(); + } +} + +void SelfForwardsTagger::createLottieIcon( + not_null widget) { + const auto lottieWidget = Ui::CreateChild(widget); + struct State { + std::unique_ptr lottieIcon; + }; + const auto state = lottieWidget->lifetime().make_state(); + state->lottieIcon = Lottie::MakeIcon({ + .name = u"toast/saved_messages"_q, + .sizeOverride = st::selfForwardsTaggerIcon, + }); + const auto icon = state->lottieIcon.get(); + lottieWidget->resize(st::selfForwardsTaggerIcon); + lottieWidget->move(st::selfForwardsTaggerToast.iconPosition); + lottieWidget->show(); + lottieWidget->raise(); + icon->animate( + [=] { lottieWidget->update(); }, + 0, + icon->framesCount() - 1); + lottieWidget->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(lottieWidget); + icon->paint(p, 0, 0); + }, lottieWidget->lifetime()); +} + +void SelfForwardsTagger::hideToast() { + if (const auto strong = _toast.get()) { + strong->hideAnimated(); + } +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_self_forwards_tagger.h b/Telegram/SourceFiles/history/view/history_view_self_forwards_tagger.h new file mode 100644 index 0000000000..cdb6863710 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_self_forwards_tagger.h @@ -0,0 +1,66 @@ +/* +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/weak_ptr.h" + +namespace Lottie { +class Icon; +} // namespace Lottie + +namespace Data { +class Session; +} // namespace Data + +namespace Main { +class Session; +} // namespace Main + +namespace Window { +class SessionController; +} // namespace Window + +namespace Ui { +class RpWidget; +} // namespace Ui + +namespace Ui::Toast { +class Instance; +} // namespace Ui::Toast + +namespace HistoryView { + +class SelfForwardsTagger final : public base::has_weak_ptr { +public: + SelfForwardsTagger( + not_null controller, + not_null parent, + Fn listWidget, + not_null scroll); + + ~SelfForwardsTagger(); + +private: + void setup(); + void showSelectorForMessages(const MessageIdsList &ids); + void showToast(const TextWithEntities &text, Fn callback); + void createLottieIcon(not_null widget); + void hideToast(); + [[nodiscard]] QRect toastGeometry() const; + + const not_null _controller; + const not_null _parent; + const Fn _listWidget; + const not_null _scroll; + + base::weak_ptr _toast; + rpl::lifetime _lifetime; + +}; + +} // namespace HistoryView