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