From b259c566b7ead259624cbc915c12582bbb8d4e0a Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 11 Apr 2024 11:49:18 +0400 Subject: [PATCH] Top peer unread badges and online indicators. --- Telegram/Resources/langs/lang.strings | 8 + Telegram/SourceFiles/dialogs/dialogs.style | 10 + .../dialogs/ui/dialogs_suggestions.cpp | 43 +++- .../dialogs/ui/top_peers_strip.cpp | 191 ++++++++++++++++-- .../SourceFiles/dialogs/ui/top_peers_strip.h | 5 +- 5 files changed, 229 insertions(+), 28 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index f3be3dc3d..f6c4e47a5 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -5087,6 +5087,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_limit_upload_increase_speed" = "by **{percent}**"; "lng_limit_upload_subscribe_link" = "Telegram Premium"; +"lng_recent_title" = "Recent"; +"lng_recent_clear" = "Clear"; +"lng_recent_clear_sure" = "Do you want to clear your search history?"; +"lng_recent_remove" = "Remove from Recent"; +"lng_recent_hide_top" = "Hide all"; +"lng_recent_hide_sure" = "Are you sure you want to hide frequent contacts list?\n\nYou can always return this list in Settings > Privacy > Suggest Frequent Contacts."; +"lng_recent_hide_button" = "Hide"; + // Wnd specific "lng_wnd_choose_program_menu" = "Choose Default Program..."; diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index d0d340b09..8d219e71c 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -595,6 +595,7 @@ topPeers: DialogsStories(dialogsStoriesFull) { photo: 46px; photoLeft: 10px; photoTop: 8px; + nameLeft: 4px; } dialogsStoriesList: DialogsStoriesList { @@ -628,6 +629,15 @@ dialogsStoriesTooltipHide: IconButton(defaultIconButton) { searchedBarHeight: 32px; searchedBarFont: normalFont; searchedBarPosition: point(17px, 7px); +searchedBarLabel: FlatLabel(defaultFlatLabel) { + textFg: searchedBarFg; + margin: margins(17px, 7px, 17px, 7px); +} +searchedBarLink: LinkButton(defaultLinkButton) { + color: searchedBarFg; + overColor: searchedBarFg; + padding: margins(17px, 7px, 17px, 7px); +} dialogsSearchTagSkip: point(8px, 4px); dialogsSearchTagBottom: 10px; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp index 9c9e90bc4..53ef01cd1 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp @@ -9,12 +9,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/components/top_peers.h" #include "data/data_user.h" +#include "lang/lang_keys.h" #include "main/main_session.h" +#include "ui/widgets/buttons.h" #include "ui/widgets/elastic_scroll.h" #include "ui/widgets/labels.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" #include "ui/dynamic_thumbnails.h" +#include "styles/style_dialogs.h" #include "styles/style_layers.h" namespace Dialogs { @@ -51,27 +54,45 @@ void Suggestions::resizeEvent(QResizeEvent *e) { } object_ptr Suggestions::setupDivider() { - auto result = object_ptr( + auto result = object_ptr( this, - object_ptr( - this, - rpl::single(u"Recent"_q), - st::boxDividerLabel), - st::defaultBoxDividerLabelPadding); - + st::searchedBarHeight); + const auto raw = result.data(); + const auto label = Ui::CreateChild( + raw, + tr::lng_recent_title(), + st::searchedBarLabel); + const auto clear = Ui::CreateChild( + raw, + tr::lng_recent_clear(tr::now), + st::searchedBarLink); + rpl::combine( + raw->sizeValue(), + clear->widthValue() + ) | rpl::start_with_next([=](QSize size, int width) { + const auto x = st::searchedBarPosition.x(); + const auto y = st::searchedBarPosition.y(); + clear->moveToRight(0, 0, size.width()); + label->resizeToWidth(size.width() - x - width); + label->moveToLeft(x, y, size.width()); + }, raw->lifetime()); + raw->paintRequest() | rpl::start_with_next([=](QRect clip) { + QPainter(raw).fillRect(clip, st::searchedBarBg); + }, raw->lifetime()); return result; } TopPeersList TopPeersContent(not_null session) { - auto result = TopPeersList(); - for (const auto &peer : session->topPeers().list()) { - result.entries.push_back(TopPeersEntry{ + auto base = TopPeersList(); + const auto top = session->topPeers().list(); + for (const auto &peer : top) { + base.entries.push_back(TopPeersEntry{ .id = peer->id.value, .name = peer->shortName(), .userpic = Ui::MakeUserpicThumbnail(peer), }); } - return result; + return base; } } // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp b/Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp index 65adee797..12726cee6 100644 --- a/Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp +++ b/Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp @@ -7,11 +7,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "dialogs/ui/top_peers_strip.h" +#include "ui/effects/ripple_animation.h" #include "ui/text/text.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/popup_menu.h" #include "ui/dynamic_image.h" #include "ui/painter.h" +#include "ui/unread_badge_paint.h" #include "styles/style_dialogs.h" #include "styles/style_widgets.h" @@ -23,7 +25,16 @@ struct TopPeersStrip::Entry { uint64 id = 0; Ui::Text::String name; std::shared_ptr userpic; - bool subscribed = false; + std::unique_ptr ripple; + Ui::Animations::Simple onlineShown; + QImage userpicFrame; + float64 userpicFrameOnline = 0.; + QString badgeString; + uint32 badge : 28 = 0; + uint32 userpicFrameDirty : 1 = 0; + uint32 subscribed : 1 = 0; + uint32 online : 1 = 0; + uint32 muted : 1 = 0; }; TopPeersStrip::TopPeersStrip( @@ -39,7 +50,9 @@ TopPeersStrip::TopPeersStrip( setMouseTracking(true); } -TopPeersStrip::~TopPeersStrip() = default; +TopPeersStrip::~TopPeersStrip() { + unsubscribeUserpics(true); +} void TopPeersStrip::resizeEvent(QResizeEvent *e) { updateScrollMax(); @@ -73,6 +86,7 @@ void TopPeersStrip::wheelEvent(QWheelEvent *e) { const auto next = std::clamp(used, 0, _scrollLeftMax); if (next != now) { _scrollLeft = next; + unsubscribeUserpics(); updateSelected(); update(); } @@ -114,11 +128,45 @@ void TopPeersStrip::checkDragging() { _scrollLeftMax); if (newLeft != _scrollLeft) { _scrollLeft = newLeft; + unsubscribeUserpics(); update(); } } } +void TopPeersStrip::unsubscribeUserpics(bool all) { + const auto &st = st::topPeers; + const auto single = st.photoLeft * 2 + st.photo; + auto x = -_scrollLeft; + for (auto &entry : _entries) { + if (all || x + single <= 0 || x >= width()) { + if (entry.subscribed) { + entry.userpic->subscribeToUpdates(nullptr); + entry.subscribed = false; + } + entry.userpicFrame = QImage(); + entry.onlineShown.stop(); + entry.ripple = nullptr; + } + x += single; + } +} + +void TopPeersStrip::subscribeUserpic(Entry &entry) { + const auto raw = entry.userpic.get(); + entry.userpic->subscribeToUpdates([=] { + const auto i = ranges::find( + _entries, + raw, + [&](const Entry &entry) { return entry.userpic.get(); }); + if (i != end(_entries)) { + i->userpicFrameDirty = 1; + } + update(); + }); + entry.subscribed = true; +} + void TopPeersStrip::mouseReleaseEvent(QMouseEvent *e) { _lastMousePosition = e->globalPos(); const auto guard = gsl::finally([&] { @@ -143,6 +191,7 @@ void TopPeersStrip::updateScrollMax() { const auto widthFull = int(_entries.size()) * single; _scrollLeftMax = std::max(widthFull - width(), 0); _scrollLeft = std::clamp(_scrollLeft, 0, _scrollLeftMax); + unsubscribeUserpics(); update(); } @@ -181,10 +230,11 @@ void TopPeersStrip::apply(const TopPeersList &list) { for (auto &entry : _entries) { if (entry.subscribed) { entry.userpic->subscribeToUpdates(nullptr); - entry.subscribed = 0; + entry.subscribed = false; } } _entries = std::move(now); + unsubscribeUserpics(); if (!_entries.empty()) { _empty = false; } @@ -201,37 +251,146 @@ void TopPeersStrip::apply(Entry &entry, const TopPeersEntry &data) { if (entry.userpic.get() != data.userpic.get()) { if (entry.subscribed) { entry.userpic->subscribeToUpdates(nullptr); - entry.subscribed = 0; } entry.userpic = data.userpic; + if (entry.subscribed) { + subscribeUserpic(entry); + } + } + if (entry.online != data.online) { + entry.online = data.online; + if (!entry.subscribed) { + entry.onlineShown.stop(); + } else { + entry.onlineShown.start( + [=] { update(); }, + entry.online ? 0. : 1., + entry.online ? 1. : 0., + st::dialogsOnlineBadgeDuration); + } + } + if (entry.badge != data.badge) { + entry.badge = data.badge; + entry.badgeString = QString(); + entry.userpicFrameDirty = 1; + } + if (entry.muted != data.muted) { + entry.muted = data.muted; + if (entry.badge) { + entry.userpicFrameDirty = 1; + } } } void TopPeersStrip::paintEvent(QPaintEvent *e) { auto p = Painter(this); - auto x = -_scrollLeft; const auto &st = st::topPeers; const auto line = st.lineTwice / 2; const auto single = st.photoLeft * 2 + st.photo; - for (auto &entry : _entries) { + + const auto from = std::min(_scrollLeft / single, int(_entries.size())); + const auto till = std::max( + (_scrollLeft + width() + single - 1) / single + 1, + from); + + auto x = -_scrollLeft + from * single; + for (auto i = from; i != till; ++i) { + auto &entry = _entries[i]; if (!entry.subscribed) { - entry.userpic->subscribeToUpdates([=] { - update(); - }); - entry.subscribed = 1; + subscribeUserpic(entry); } - const auto image = entry.userpic->image(st.photo); - p.drawImage( - QRect(x + st.photoLeft, st.photoTop, st.photo, st.photo), - image); + paintUserpic(p, i, x); const auto nameLeft = x + st.nameLeft; - entry.name.drawElided(p, nameLeft, st.nameTop, single, 1, style::al_top); - + const auto nameWidth = single - 2 * st.nameLeft; + entry.name.drawElided( + p, + nameLeft, + st.nameTop, + nameWidth, + 1, + style::al_top); x += single; } } +void TopPeersStrip::paintUserpic(Painter &p, int index, int x) { + Expects(index >= 0 && index < _entries.size()); + + auto &entry = _entries[index]; + const auto &st = st::topPeers; + const auto size = st.photo; + const auto rect = QRect(x + st.photoLeft, st.photoTop, size, size); + + const auto online = entry.onlineShown.value(entry.online ? 1. : 0.); + const auto useFrame = !entry.userpicFrame.isNull() + && !entry.userpicFrameDirty + && (entry.userpicFrameOnline == online); + if (useFrame) { + p.drawImage(rect, entry.userpicFrame); + return; + } + const auto simple = entry.userpic->image(size); + const auto ratio = style::DevicePixelRatio(); + const auto renderFrame = (online > 0) || entry.badge; + if (!renderFrame) { + entry.userpicFrame = QImage(); + p.drawImage(rect, simple); + return; + } else if (entry.userpicFrame.size() != QSize(size, size) * ratio) { + entry.userpicFrame = QImage( + QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + entry.userpicFrame.setDevicePixelRatio(ratio); + } + entry.userpicFrame.fill(Qt::transparent); + entry.userpicFrameDirty = 0; + entry.userpicFrameOnline = online; + + auto q = QPainter(&entry.userpicFrame); + const auto inner = QRect(0, 0, size, size); + q.drawImage(inner, simple); + + auto hq = PainterHighQualityEnabler(p); + + if (online > 0) { + q.setCompositionMode(QPainter::CompositionMode_Source); + const auto onlineSize = st::dialogsOnlineBadgeSize; + const auto stroke = st::dialogsOnlineBadgeStroke; + const auto skip = st::dialogsOnlineBadgeSkip; + const auto shrink = (onlineSize / 2) * (1. - online); + + auto pen = QPen(Qt::transparent); + pen.setWidthF(stroke * online); + q.setPen(pen); + q.setBrush(st::dialogsOnlineBadgeFg); + q.drawEllipse(QRectF( + size - skip.x() - onlineSize, + size - skip.y() - onlineSize, + onlineSize, + onlineSize + ).marginsRemoved({ shrink, shrink, shrink, shrink })); + q.setCompositionMode(QPainter::CompositionMode_SourceOver); + } + + if (entry.badge) { + if (entry.badgeString.isEmpty()) { + entry.badgeString = (entry.badge < 1000) + ? QString::number(entry.badge) + : (QString::number(entry.badge / 1000) + 'K'); + } + auto st = Ui::UnreadBadgeStyle(); + st.selected = (_selected == index); + st.muted = entry.muted; + const auto &counter = entry.badgeString; + const auto badge = PaintUnreadBadge(q, counter, size, 0, st); + } + + q.end(); + + p.drawImage(rect, entry.userpicFrame); +} + void TopPeersStrip::contextMenuEvent(QContextMenuEvent *e) { _menu = nullptr; diff --git a/Telegram/SourceFiles/dialogs/ui/top_peers_strip.h b/Telegram/SourceFiles/dialogs/ui/top_peers_strip.h index 551b82480..9875fc4b2 100644 --- a/Telegram/SourceFiles/dialogs/ui/top_peers_strip.h +++ b/Telegram/SourceFiles/dialogs/ui/top_peers_strip.h @@ -21,7 +21,7 @@ struct TopPeersEntry { uint64 id = 0; QString name; std::shared_ptr userpic; - uint32 badge : 30 = 0; + uint32 badge : 28 = 0; uint32 muted : 1 = 0; uint32 online : 1 = 0; }; @@ -63,6 +63,9 @@ private: void updateSelected(); void checkDragging(); bool finishDragging(); + void subscribeUserpic(Entry &entry); + void unsubscribeUserpics(bool all = false); + void paintUserpic(Painter &p, int index, int x); void apply(const TopPeersList &list); void apply(Entry &entry, const TopPeersEntry &data);