Show nice tooltips about story privacy / silence.

This commit is contained in:
John Preston 2023-07-24 17:01:13 +04:00
parent 320db83155
commit 2323aef899
7 changed files with 233 additions and 32 deletions

View file

@ -3833,6 +3833,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_stories_no_views" = "No views"; "lng_stories_no_views" = "No views";
"lng_stories_unsupported" = "This story is not supported\nby your version of Telegram."; "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_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_my_title" = "Saved Stories";
"lng_stories_archive_button" = "Stories Archive"; "lng_stories_archive_button" = "Stories Archive";

View file

@ -322,8 +322,18 @@ Controller::Controller(not_null<Delegate*> delegate)
_delegate->storiesLayerShown( _delegate->storiesLayerShown(
) | rpl::start_with_next([=](bool shown) { ) | rpl::start_with_next([=](bool shown) {
_layerShown = shown; if (_layerShown != shown) {
updatePlayingAllowed(); _layerShown = shown;
updatePlayingAllowed();
}
}, _lifetime);
_header->tooltipShownValue(
) | rpl::start_with_next([=](bool shown) {
if (_tooltipShown != shown) {
_tooltipShown = shown;
updatePlayingAllowed();
}
}, _lifetime); }, _lifetime);
const auto window = _wrap->window()->windowHandle(); const auto window = _wrap->window()->windowHandle();
@ -961,7 +971,8 @@ void Controller::updatePlayingAllowed() {
&& !_captionFullView && !_captionFullView
&& !_captionExpanded && !_captionExpanded
&& !_layerShown && !_layerShown
&& !_menuShown); && !_menuShown
&& !_tooltipShown);
} }
void Controller::setPlayingAllowed(bool allowed) { void Controller::setPlayingAllowed(bool allowed) {

View file

@ -257,6 +257,7 @@ private:
bool _hasSendText = false; bool _hasSendText = false;
bool _layerShown = false; bool _layerShown = false;
bool _menuShown = false; bool _menuShown = false;
bool _tooltipShown = false;
bool _paused = false; bool _paused = false;
FullStoryId _shown; FullStoryId _shown;

View file

@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/buttons.h" #include "ui/widgets/buttons.h"
#include "ui/widgets/continuous_sliders.h" #include "ui/widgets/continuous_sliders.h"
#include "ui/widgets/labels.h" #include "ui/widgets/labels.h"
#include "ui/widgets/tooltip.h"
#include "ui/wrap/fade_wrap.h" #include "ui/wrap/fade_wrap.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "ui/rp_widget.h" #include "ui/rp_widget.h"
@ -50,10 +51,9 @@ struct PrivacyBadge {
class UserpicBadge final : public Ui::RpWidget { class UserpicBadge final : public Ui::RpWidget {
public: public:
UserpicBadge( UserpicBadge(not_null<QWidget*> userpic, PrivacyBadge badge);
not_null<QWidget*> userpic,
PrivacyBadge badge, [[nodiscard]] QRect badgeGeometry() const;
Fn<void()> clicked);
private: private:
bool eventFilter(QObject *o, QEvent *e) override; bool eventFilter(QObject *o, QEvent *e) override;
@ -63,7 +63,6 @@ private:
const not_null<QWidget*> _userpic; const not_null<QWidget*> _userpic;
const PrivacyBadge _badgeData; const PrivacyBadge _badgeData;
const std::unique_ptr<Ui::AbstractButton> _clickable;
QRect _badge; QRect _badge;
QImage _layer; QImage _layer;
bool _grabbing = false; bool _grabbing = false;
@ -95,15 +94,10 @@ private:
return {}; return {};
} }
UserpicBadge::UserpicBadge( UserpicBadge::UserpicBadge(not_null<QWidget*> userpic, PrivacyBadge badge)
not_null<QWidget*> userpic,
PrivacyBadge badge,
Fn<void()> clicked)
: RpWidget(userpic->parentWidget()) : RpWidget(userpic->parentWidget())
, _userpic(userpic) , _userpic(userpic)
, _badgeData(badge) , _badgeData(badge) {
, _clickable(std::make_unique<Ui::AbstractButton>(parentWidget())) {
_clickable->setClickedCallback(std::move(clicked));
userpic->installEventFilter(this); userpic->installEventFilter(this);
updateGeometry(); updateGeometry();
setAttribute(Qt::WA_TransparentForMouseEvents); setAttribute(Qt::WA_TransparentForMouseEvents);
@ -113,6 +107,10 @@ UserpicBadge::UserpicBadge(
show(); show();
} }
QRect UserpicBadge::badgeGeometry() const {
return _badge;
}
bool UserpicBadge::eventFilter(QObject *o, QEvent *e) { bool UserpicBadge::eventFilter(QObject *o, QEvent *e) {
if (o != _userpic) { if (o != _userpic) {
return false; return false;
@ -173,22 +171,27 @@ void UserpicBadge::updateGeometry() {
_badge = QRect( _badge = QRect(
QPoint(width - badge.width(), height - badge.height()), QPoint(width - badge.width(), height - badge.height()),
badge); badge);
_clickable->setGeometry(_badge.translated(pos()));
update(); update();
} }
[[nodiscard]] std::unique_ptr<Ui::RpWidget> MakePrivacyBadge( struct MadePrivacyBadge {
std::unique_ptr<Ui::RpWidget> widget;
QRect geometry;
};
[[nodiscard]] MadePrivacyBadge MakePrivacyBadge(
not_null<QWidget*> userpic, not_null<QWidget*> userpic,
Data::StoryPrivacy privacy, Data::StoryPrivacy privacy) {
Fn<void()> clicked) {
const auto badge = LookupPrivacyBadge(privacy); const auto badge = LookupPrivacyBadge(privacy);
if (!badge.icon) { if (!badge.icon) {
return nullptr; return {};
} }
return std::make_unique<UserpicBadge>( auto widget = std::make_unique<UserpicBadge>(userpic, badge);
userpic, const auto geometry = widget->badgeGeometry();
badge, return {
std::move(clicked)); .widget = std::move(widget),
.geometry = geometry,
};
} }
[[nodiscard]] Timestamp ComposeTimestamp(TimeId when, TimeId now) { [[nodiscard]] Timestamp ComposeTimestamp(TimeId when, TimeId now) {
@ -277,6 +280,8 @@ void Header::show(HeaderData data) {
_info->setGeometry({ 0, 0, r, _widget->height() }); _info->setGeometry({ 0, 0, r, _widget->height() });
} }
}; };
_tooltip = nullptr;
_tooltipShown = false;
if (userChanged) { if (userChanged) {
_volume = nullptr; _volume = nullptr;
_date = nullptr; _date = nullptr;
@ -328,6 +333,8 @@ void Header::show(HeaderData data) {
_controller->layoutValue( _controller->layoutValue(
) | rpl::start_with_next([=](const Layout &layout) { ) | rpl::start_with_next([=](const Layout &layout) {
raw->setGeometry(layout.header); raw->setGeometry(layout.header);
_contentGeometry = layout.content;
updateTooltipGeometry();
}, raw->lifetime()); }, raw->lifetime());
} }
auto timestamp = ComposeDetails(data, base::unixtime::now()); auto timestamp = ComposeDetails(data, base::unixtime::now());
@ -357,8 +364,29 @@ void Header::show(HeaderData data) {
_counter = nullptr; _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<QEvent*> 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<QMouseEvent*>(e.get())->pos());
return (_privacyBadgeOver != over);
}) | rpl::start_with_next([=] {
_privacyBadgeOver = !_privacyBadgeOver;
toggleTooltip(Tooltip::Privacy, _privacyBadgeOver);
}, _privacy->lifetime());
}
if (data.video) { if (data.video) {
createPlayPause(); createPlayPause();
@ -369,6 +397,7 @@ void Header::show(HeaderData data) {
_playPause->moveToRight(playPause.x(), playPause.y(), width); _playPause->moveToRight(playPause.x(), playPause.y(), width);
const auto volume = st::storiesVolumeButtonPosition; const auto volume = st::storiesVolumeButtonPosition;
_volumeToggle->moveToRight(volume.x(), volume.y(), width); _volumeToggle->moveToRight(volume.x(), volume.y(), width);
updateTooltipGeometry();
}, _playPause->lifetime()); }, _playPause->lifetime());
_pauseState = _controller->pauseState(); _pauseState = _controller->pauseState();
@ -496,15 +525,14 @@ void Header::createVolumeToggle() {
_volumeToggle->events( _volumeToggle->events(
) | rpl::start_with_next([=](not_null<QEvent*> e) { ) | rpl::start_with_next([=](not_null<QEvent*> e) {
if (state->silent) {
return;
}
const auto type = e->type(); const auto type = e->type();
if (type == QEvent::Enter || type == QEvent::Leave) { if (type == QEvent::Enter || type == QEvent::Leave) {
const auto over = (e->type() == QEvent::Enter); const auto over = (e->type() == QEvent::Enter);
if (state->over != over) { if (state->over != over) {
state->over = over; state->over = over;
if (over) { if (state->silent) {
toggleTooltip(Tooltip::SilentVideo, over);
} else if (over) {
state->hideTimer.cancel(); state->hideTimer.cancel();
_volume->toggle(true, anim::type::normal); _volume->toggle(true, anim::type::normal);
} else if (!state->dropdownOver) { } 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<Ui::ImportantTooltip>(
_widget->parentWidget(),
Ui::MakeNiceTooltipLabel(
_widget.get(),
rpl::single(text),
st::storiesInfoTooltipMaxWidth,
st::storiesInfoTooltipLabel),
st::storiesInfoTooltip);
const auto tooltip = _tooltip.get();
const auto weak = QPointer<QWidget>(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<QWidget>(_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( void Header::rebuildVolumeControls(
not_null<Ui::RpWidget*> dropdown, not_null<Ui::RpWidget*> dropdown,
bool horizontal) { bool horizontal) {
@ -682,11 +827,14 @@ void Header::raise() {
} }
} }
bool Header::ignoreWindowMove(QPoint position) const { bool Header::ignoreWindowMove(QPoint position) const {
return _ignoreWindowMove; return _ignoreWindowMove;
} }
rpl::producer<bool> Header::tooltipShownValue() const {
return _tooltipShown.value();
}
void Header::updateDateText() { void Header::updateDateText() {
if (!_date || !_data || !_data->date) { if (!_date || !_data || !_data->date) {
return; return;

View file

@ -20,6 +20,7 @@ class FlatLabel;
class IconButton; class IconButton;
class AbstractButton; class AbstractButton;
class UserpicButton; class UserpicButton;
class ImportantTooltip;
template <typename Widget> template <typename Widget>
class FadeWrap; class FadeWrap;
} // namespace Ui } // namespace Ui
@ -55,8 +56,15 @@ public:
void raise(); void raise();
[[nodiscard]] bool ignoreWindowMove(QPoint position) const; [[nodiscard]] bool ignoreWindowMove(QPoint position) const;
[[nodiscard]] rpl::producer<bool> tooltipShownValue() const;
private: private:
enum class Tooltip {
None,
SilentVideo,
Privacy,
};
void updateDateText(); void updateDateText();
void applyPauseState(); void applyPauseState();
void createPlayPause(); void createPlayPause();
@ -64,6 +72,8 @@ private:
void rebuildVolumeControls( void rebuildVolumeControls(
not_null<Ui::RpWidget*> dropdown, not_null<Ui::RpWidget*> dropdown,
bool horizontal); bool horizontal);
void toggleTooltip(Tooltip type, bool show);
void updateTooltipGeometry();
const not_null<Controller*> _controller; const not_null<Controller*> _controller;
@ -81,9 +91,15 @@ private:
std::unique_ptr<Ui::FadeWrap<Ui::RpWidget>> _volume; std::unique_ptr<Ui::FadeWrap<Ui::RpWidget>> _volume;
rpl::variable<const style::icon*> _volumeIcon; rpl::variable<const style::icon*> _volumeIcon;
std::unique_ptr<Ui::RpWidget> _privacy; std::unique_ptr<Ui::RpWidget> _privacy;
QRect _privacyBadgeGeometry;
std::optional<HeaderData> _data; std::optional<HeaderData> _data;
std::unique_ptr<Ui::ImportantTooltip> _tooltip = { nullptr };
rpl::variable<bool> _tooltipShown = false;
QRect _contentGeometry;
Tooltip _tooltipType = {};
base::Timer _dateUpdateTimer; base::Timer _dateUpdateTimer;
bool _ignoreWindowMove = false; bool _ignoreWindowMove = false;
bool _privacyBadgeOver = false;
}; };

View file

@ -891,3 +891,18 @@ storiesVolumeSlider: MediaSlider {
seekSize: size(12px, 12px); seekSize: size(12px, 12px);
duration: mediaviewOverDuration; 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;

@ -1 +1 @@
Subproject commit ad852f0f4ab271de4db799d01fa8b7032eb33b11 Subproject commit bd1e8f7c47c3e99493adf9653d684c86a0a51941