From 2323aef899df81a66645ad443f423d5d98ce4311 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 24 Jul 2023 17:01:13 +0400 Subject: [PATCH] Show nice tooltips about story privacy / silence. --- Telegram/Resources/langs/lang.strings | 10 + .../stories/media_stories_controller.cpp | 17 +- .../media/stories/media_stories_controller.h | 1 + .../media/stories/media_stories_header.cpp | 204 +++++++++++++++--- .../media/stories/media_stories_header.h | 16 ++ .../SourceFiles/media/view/media_view.style | 15 ++ Telegram/lib_ui | 2 +- 7 files changed, 233 insertions(+), 32 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 8ebc76005..85149cb42 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3833,6 +3833,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_stories_no_views" = "No views"; "lng_stories_unsupported" = "This story is not supported\nby your version of Telegram."; "lng_stories_cant_reply" = "You can't reply to this story."; +"lng_stories_about_silent" = "This video has no sound."; +"lng_stories_about_close_friends" = "You're seeing this story because {user} added you to their list of **Close Friends**."; +"lng_stories_about_contacts" = "Only {user}'s contacts can view this story."; +"lng_stories_about_selected_contacts" = "Only some contacts {user} selected can view this story."; +"lng_stories_about_close_friends_my" = "Only your list of **Close Friends** can view this story."; +"lng_stories_about_contacts_my" = "Only your contacts can view this story."; +"lng_stories_about_selected_contacts_my" = "Only some contacts you selected can view this story."; +"lng_stories_click_to_view" = "Click here to view updates from {users}."; +"lng_stories_click_to_view_and_one" = "{accumulated}, {user}"; +"lng_stories_click_to_view_and_last" = "{accumulated} and {user}"; "lng_stories_my_title" = "Saved Stories"; "lng_stories_archive_button" = "Stories Archive"; diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 2c2efc7d6..a5cf70746 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -322,8 +322,18 @@ Controller::Controller(not_null delegate) _delegate->storiesLayerShown( ) | rpl::start_with_next([=](bool shown) { - _layerShown = shown; - updatePlayingAllowed(); + if (_layerShown != shown) { + _layerShown = shown; + updatePlayingAllowed(); + } + }, _lifetime); + + _header->tooltipShownValue( + ) | rpl::start_with_next([=](bool shown) { + if (_tooltipShown != shown) { + _tooltipShown = shown; + updatePlayingAllowed(); + } }, _lifetime); const auto window = _wrap->window()->windowHandle(); @@ -961,7 +971,8 @@ void Controller::updatePlayingAllowed() { && !_captionFullView && !_captionExpanded && !_layerShown - && !_menuShown); + && !_menuShown + && !_tooltipShown); } void Controller::setPlayingAllowed(bool allowed) { diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 760835a81..3aa3a49a2 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -257,6 +257,7 @@ private: bool _hasSendText = false; bool _layerShown = false; bool _menuShown = false; + bool _tooltipShown = false; bool _paused = false; FullStoryId _shown; diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp index 5dc7f6b15..8da578b9b 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/buttons.h" #include "ui/widgets/continuous_sliders.h" #include "ui/widgets/labels.h" +#include "ui/widgets/tooltip.h" #include "ui/wrap/fade_wrap.h" #include "ui/painter.h" #include "ui/rp_widget.h" @@ -50,10 +51,9 @@ struct PrivacyBadge { class UserpicBadge final : public Ui::RpWidget { public: - UserpicBadge( - not_null userpic, - PrivacyBadge badge, - Fn clicked); + UserpicBadge(not_null userpic, PrivacyBadge badge); + + [[nodiscard]] QRect badgeGeometry() const; private: bool eventFilter(QObject *o, QEvent *e) override; @@ -63,7 +63,6 @@ private: const not_null _userpic; const PrivacyBadge _badgeData; - const std::unique_ptr _clickable; QRect _badge; QImage _layer; bool _grabbing = false; @@ -95,15 +94,10 @@ private: return {}; } -UserpicBadge::UserpicBadge( - not_null userpic, - PrivacyBadge badge, - Fn clicked) +UserpicBadge::UserpicBadge(not_null userpic, PrivacyBadge badge) : RpWidget(userpic->parentWidget()) , _userpic(userpic) -, _badgeData(badge) -, _clickable(std::make_unique(parentWidget())) { - _clickable->setClickedCallback(std::move(clicked)); +, _badgeData(badge) { userpic->installEventFilter(this); updateGeometry(); setAttribute(Qt::WA_TransparentForMouseEvents); @@ -113,6 +107,10 @@ UserpicBadge::UserpicBadge( show(); } +QRect UserpicBadge::badgeGeometry() const { + return _badge; +} + bool UserpicBadge::eventFilter(QObject *o, QEvent *e) { if (o != _userpic) { return false; @@ -173,22 +171,27 @@ void UserpicBadge::updateGeometry() { _badge = QRect( QPoint(width - badge.width(), height - badge.height()), badge); - _clickable->setGeometry(_badge.translated(pos())); update(); } -[[nodiscard]] std::unique_ptr MakePrivacyBadge( +struct MadePrivacyBadge { + std::unique_ptr widget; + QRect geometry; +}; + +[[nodiscard]] MadePrivacyBadge MakePrivacyBadge( not_null userpic, - Data::StoryPrivacy privacy, - Fn clicked) { + Data::StoryPrivacy privacy) { const auto badge = LookupPrivacyBadge(privacy); if (!badge.icon) { - return nullptr; + return {}; } - return std::make_unique( - userpic, - badge, - std::move(clicked)); + auto widget = std::make_unique(userpic, badge); + const auto geometry = widget->badgeGeometry(); + return { + .widget = std::move(widget), + .geometry = geometry, + }; } [[nodiscard]] Timestamp ComposeTimestamp(TimeId when, TimeId now) { @@ -277,6 +280,8 @@ void Header::show(HeaderData data) { _info->setGeometry({ 0, 0, r, _widget->height() }); } }; + _tooltip = nullptr; + _tooltipShown = false; if (userChanged) { _volume = nullptr; _date = nullptr; @@ -328,6 +333,8 @@ void Header::show(HeaderData data) { _controller->layoutValue( ) | rpl::start_with_next([=](const Layout &layout) { raw->setGeometry(layout.header); + _contentGeometry = layout.content; + updateTooltipGeometry(); }, raw->lifetime()); } auto timestamp = ComposeDetails(data, base::unixtime::now()); @@ -357,8 +364,29 @@ void Header::show(HeaderData data) { _counter = nullptr; } - _privacy = MakePrivacyBadge(_userpic.get(), data.privacy, [=] { - }); + auto made = MakePrivacyBadge(_userpic.get(), data.privacy); + _privacy = std::move(made.widget); + _privacyBadgeOver = false; + _privacyBadgeGeometry = _privacy + ? Ui::MapFrom(_info.get(), _privacy.get(), made.geometry) + : QRect(); + if (_privacy) { + _info->setMouseTracking(true); + _info->events( + ) | rpl::filter([=](not_null e) { + const auto type = e->type(); + if (type != QEvent::Leave && type != QEvent::MouseMove) { + return false; + } + const auto over = (type == QEvent::MouseMove) + && _privacyBadgeGeometry.contains( + static_cast(e.get())->pos()); + return (_privacyBadgeOver != over); + }) | rpl::start_with_next([=] { + _privacyBadgeOver = !_privacyBadgeOver; + toggleTooltip(Tooltip::Privacy, _privacyBadgeOver); + }, _privacy->lifetime()); + } if (data.video) { createPlayPause(); @@ -369,6 +397,7 @@ void Header::show(HeaderData data) { _playPause->moveToRight(playPause.x(), playPause.y(), width); const auto volume = st::storiesVolumeButtonPosition; _volumeToggle->moveToRight(volume.x(), volume.y(), width); + updateTooltipGeometry(); }, _playPause->lifetime()); _pauseState = _controller->pauseState(); @@ -496,15 +525,14 @@ void Header::createVolumeToggle() { _volumeToggle->events( ) | rpl::start_with_next([=](not_null e) { - if (state->silent) { - return; - } const auto type = e->type(); if (type == QEvent::Enter || type == QEvent::Leave) { const auto over = (e->type() == QEvent::Enter); if (state->over != over) { state->over = over; - if (over) { + if (state->silent) { + toggleTooltip(Tooltip::SilentVideo, over); + } else if (over) { state->hideTimer.cancel(); _volume->toggle(true, anim::type::normal); } else if (!state->dropdownOver) { @@ -565,6 +593,123 @@ void Header::createVolumeToggle() { } } +void Header::toggleTooltip(Tooltip type, bool show) { + const auto guard = gsl::finally([&] { + _tooltipShown = (_tooltip != nullptr); + }); + if (const auto was = _tooltip.release()) { + was->toggleAnimated(false); + } + if (!show) { + return; + } + const auto text = [&]() -> TextWithEntities { + using Privacy = Data::StoryPrivacy; + const auto boldName = Ui::Text::Bold(_data->user->shortName()); + const auto self = _data->user->isSelf(); + switch (type) { + case Tooltip::SilentVideo: + return { tr::lng_stories_about_silent(tr::now) }; + case Tooltip::Privacy: switch (_data->privacy) { + case Privacy::CloseFriends: + return self + ? tr::lng_stories_about_close_friends_my( + tr::now, + Ui::Text::RichLangValue) + : tr::lng_stories_about_close_friends( + tr::now, + lt_user, + boldName, + Ui::Text::RichLangValue); + case Privacy::Contacts: + return self + ? tr::lng_stories_about_contacts_my( + tr::now, + Ui::Text::RichLangValue) + : tr::lng_stories_about_contacts( + tr::now, + lt_user, + boldName, + Ui::Text::RichLangValue); + case Privacy::SelectedContacts: + return self + ? tr::lng_stories_about_selected_contacts_my( + tr::now, + Ui::Text::RichLangValue) + : tr::lng_stories_about_selected_contacts( + tr::now, + lt_user, + boldName, + Ui::Text::RichLangValue); + } + } + return {}; + }(); + if (text.empty()) { + return; + } + _tooltipType = type; + _tooltip = std::make_unique( + _widget->parentWidget(), + Ui::MakeNiceTooltipLabel( + _widget.get(), + rpl::single(text), + st::storiesInfoTooltipMaxWidth, + st::storiesInfoTooltipLabel), + st::storiesInfoTooltip); + const auto tooltip = _tooltip.get(); + const auto weak = QPointer(tooltip); + const auto destroy = [=] { + delete weak.data(); + }; + tooltip->setAttribute(Qt::WA_TransparentForMouseEvents); + tooltip->setHiddenCallback(destroy); + updateTooltipGeometry(); + tooltip->toggleAnimated(true); +} + +void Header::updateTooltipGeometry() { + if (!_tooltip) { + return; + } + const auto geometry = [&] { + switch (_tooltipType) { + case Tooltip::SilentVideo: + return Ui::MapFrom( + _widget->parentWidget(), + _volumeToggle.get(), + _volumeToggle->rect()); + case Tooltip::Privacy: + return Ui::MapFrom( + _widget->parentWidget(), + _info.get(), + _privacyBadgeGeometry.marginsAdded( + st::storiesInfoTooltip.padding)); + } + return QRect(); + }(); + if (geometry.isEmpty()) { + toggleTooltip(Tooltip::None, false); + return; + } + const auto weak = QPointer(_tooltip.get()); + const auto countPosition = [=](QSize size) { + const auto result = geometry.bottomLeft() + - QPoint(size.width() / 2, 0); + const auto inner = _contentGeometry.marginsRemoved( + st::storiesInfoTooltip.padding); + if (size.width() > inner.width()) { + return QPoint( + inner.x() + (inner.width() - size.width()) / 2, + result.y()); + } else if (result.x() < inner.x()) { + return QPoint(inner.x(), result.y()); + } + return result; + }; + _tooltip->pointAt(geometry, RectPart::Bottom, countPosition); +} + void Header::rebuildVolumeControls( not_null dropdown, bool horizontal) { @@ -682,11 +827,14 @@ void Header::raise() { } } - bool Header::ignoreWindowMove(QPoint position) const { return _ignoreWindowMove; } +rpl::producer Header::tooltipShownValue() const { + return _tooltipShown.value(); +} + void Header::updateDateText() { if (!_date || !_data || !_data->date) { return; diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.h b/Telegram/SourceFiles/media/stories/media_stories_header.h index 12a66b1a1..4c67526e2 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_header.h +++ b/Telegram/SourceFiles/media/stories/media_stories_header.h @@ -20,6 +20,7 @@ class FlatLabel; class IconButton; class AbstractButton; class UserpicButton; +class ImportantTooltip; template class FadeWrap; } // namespace Ui @@ -55,8 +56,15 @@ public: void raise(); [[nodiscard]] bool ignoreWindowMove(QPoint position) const; + [[nodiscard]] rpl::producer tooltipShownValue() const; private: + enum class Tooltip { + None, + SilentVideo, + Privacy, + }; + void updateDateText(); void applyPauseState(); void createPlayPause(); @@ -64,6 +72,8 @@ private: void rebuildVolumeControls( not_null dropdown, bool horizontal); + void toggleTooltip(Tooltip type, bool show); + void updateTooltipGeometry(); const not_null _controller; @@ -81,9 +91,15 @@ private: std::unique_ptr> _volume; rpl::variable _volumeIcon; std::unique_ptr _privacy; + QRect _privacyBadgeGeometry; std::optional _data; + std::unique_ptr _tooltip = { nullptr }; + rpl::variable _tooltipShown = false; + QRect _contentGeometry; + Tooltip _tooltipType = {}; base::Timer _dateUpdateTimer; bool _ignoreWindowMove = false; + bool _privacyBadgeOver = false; }; diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index d463e8adc..2a855721c 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -891,3 +891,18 @@ storiesVolumeSlider: MediaSlider { seekSize: size(12px, 12px); duration: mediaviewOverDuration; } +storiesInfoTooltipLabel: FlatLabel(defaultImportantTooltipLabel) { + style: TextStyle(defaultTextStyle) { + font: font(11px); + linkFont: font(11px); + linkFontOver: font(11px underline); + } + minWidth: 36px; +} +storiesInfoTooltip: ImportantTooltip(defaultImportantTooltip) { + bg: importantTooltipBg; + padding: margins(10px, 3px, 10px, 5px); + radius: 4px; + arrow: 4px; +} +storiesInfoTooltipMaxWidth: 360px; diff --git a/Telegram/lib_ui b/Telegram/lib_ui index ad852f0f4..bd1e8f7c4 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit ad852f0f4ab271de4db799d01fa8b7032eb33b11 +Subproject commit bd1e8f7c47c3e99493adf9653d684c86a0a51941