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 0000000000..6d2cac7830
Binary files /dev/null and b/Telegram/Resources/animations/toast/saved_messages.tgs differ
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