From 14314df26ad87e0386927bef47a9fac56b526a52 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 9 Sep 2021 00:10:49 +0300 Subject: [PATCH] Show userpics in who read context item. --- Telegram/SourceFiles/api/api_who_read.cpp | 306 +++++++++++++----- Telegram/SourceFiles/api/api_who_read.h | 7 +- .../group/calls_group_viewport_opengl.cpp | 18 +- .../group/calls_group_viewport_raster.cpp | 19 +- Telegram/SourceFiles/data/data_peer.cpp | 68 ++-- Telegram/SourceFiles/data/data_peer.h | 18 +- .../history/history_inner_widget.cpp | 3 +- Telegram/SourceFiles/ui/chat/chat.style | 19 +- .../ui/controls/who_read_context_action.cpp | 20 +- .../ui/controls/who_read_context_action.h | 5 +- 10 files changed, 302 insertions(+), 181 deletions(-) diff --git a/Telegram/SourceFiles/api/api_who_read.cpp b/Telegram/SourceFiles/api/api_who_read.cpp index 6886751e4..c7d420633 100644 --- a/Telegram/SourceFiles/api/api_who_read.cpp +++ b/Telegram/SourceFiles/api/api_who_read.cpp @@ -21,17 +21,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "main/main_account.h" #include "base/unixtime.h" +#include "base/weak_ptr.h" #include "ui/controls/who_read_context_action.h" #include "apiwrap.h" +#include "styles/style_chat.h" namespace Api { namespace { struct Cached { - explicit Cached(UserId unknownFlag) - : list(std::vector{ unknownFlag }) { + explicit Cached(PeerId unknownFlag) + : list(std::vector{ unknownFlag }) { } - rpl::variable> list; + rpl::variable> list; mtpRequestId requestId = 0; }; @@ -46,11 +48,25 @@ struct Context { } return cached.emplace( item, - Cached(item->history()->session().userId()) + Cached(item->history()->session().userPeerId()) ).first->second; } }; +struct Userpic { + not_null peer; + mutable std::shared_ptr view; + mutable InMemoryKey uniqueKey; +}; + +struct State { + std::vector userpics; + Ui::WhoReadContent current; + base::has_weak_ptr guard; + bool someUserpicsNotLoaded = false; + bool scheduled = false; +}; + [[nodiscard]] auto Contexts() -> base::flat_map, std::unique_ptr> & { static auto result = base::flat_map< @@ -82,10 +98,10 @@ struct Context { } [[nodiscard]] bool ListUnknown( - const std::vector &list, + const std::vector &list, not_null item) { return (list.size() == 1) - && (list.front() == item->history()->session().userId()); + && (list.front() == item->history()->session().userPeerId()); } [[nodiscard]] Ui::WhoReadType DetectType(not_null item) { @@ -103,6 +119,161 @@ struct Context { return Ui::WhoReadType::Seen; } +[[nodiscard]] rpl::producer> WhoReadIds( + not_null item, + not_null context) { + auto weak = QPointer(context.get()); + const auto fullId = item->fullId(); + const auto session = &item->history()->session(); + return [=](auto consumer) { + if (!weak) { + return rpl::lifetime(); + } + const auto context = ContextAt(weak.data()); + if (!context->subscriptions.contains(session)) { + session->changes().messageUpdates( + Data::MessageUpdate::Flag::Destroyed + ) | rpl::start_with_next([=](const Data::MessageUpdate &update) { + const auto i = context->cached.find(update.item); + if (i == end(context->cached)) { + return; + } + session->api().request(i->second.requestId).cancel(); + context->cached.erase(i); + }, context->subscriptions[session]); + } + auto &entry = context->cache(item); + if (!entry.requestId) { + entry.requestId = session->api().request( + MTPmessages_GetMessageReadParticipants( + item->history()->peer->input, + MTP_int(item->id) + ) + ).done([=](const MTPVector &result) { + auto &entry = context->cache(item); + entry.requestId = 0; + auto peers = std::vector(); + peers.reserve(std::max(result.v.size(), 1)); + for (const auto &id : result.v) { + peers.push_back(UserId(id)); + } + entry.list = std::move(peers); + }).fail([=](const MTP::Error &error) { + auto &entry = context->cache(item); + entry.requestId = 0; + if (ListUnknown(entry.list.current(), item)) { + entry.list = std::vector(); + } + }).send(); + } + return entry.list.value().start_existing(consumer); + }; +} + +bool UpdateUserpics( + not_null state, + not_null item, + const std::vector &ids) { + auto &owner = item->history()->owner(); + + const auto peers = ranges::views::all( + ids + ) | ranges::views::transform([&](PeerId id) { + return owner.peerLoaded(id); + }) | ranges::views::filter([](PeerData *peer) { + return peer != nullptr; + }) | ranges::views::transform([](PeerData *peer) { + return not_null(peer); + }) | ranges::to_vector; + + const auto same = ranges::equal( + state->userpics, + peers, + ranges::less(), + &Userpic::peer); + if (same) { + return false; + } + auto &was = state->userpics; + auto now = std::vector(); + for (const auto peer : peers) { + if (ranges::contains(now, peer, &Userpic::peer)) { + continue; + } + const auto i = ranges::find(was, peer, &Userpic::peer); + if (i != end(was)) { + now.push_back(std::move(*i)); + continue; + } + now.push_back(Userpic{ + .peer = peer, + }); + auto &userpic = now.back(); + userpic.uniqueKey = peer->userpicUniqueKey(userpic.view); + peer->loadUserpic(); + } + was = std::move(now); + return true; +} + +void RegenerateUserpics(not_null state, int small, int large) { + Expects(state->userpics.size() == state->current.participants.size()); + + state->someUserpicsNotLoaded = false; + const auto count = int(state->userpics.size()); + for (auto i = 0; i != count; ++i) { + auto &userpic = state->userpics[i]; + auto &participant = state->current.participants[i]; + const auto peer = userpic.peer; + const auto key = peer->userpicUniqueKey(userpic.view); + if (peer->hasUserpic() && peer->useEmptyUserpic(userpic.view)) { + state->someUserpicsNotLoaded = true; + } + if (userpic.uniqueKey == key) { + continue; + } + participant.userpicKey = userpic.uniqueKey = key; + participant.userpicLarge = peer->generateUserpicImage( + userpic.view, + large); + if (i < Ui::WhoReadParticipant::kMaxSmallUserpics) { + participant.userpicSmall = peer->generateUserpicImage( + userpic.view, + small); + } + } +} + +void RegenerateParticipants(not_null state, int small, int large) { + auto old = base::take(state->current.participants); + auto &now = state->current.participants; + now.reserve(state->userpics.size()); + for (auto &userpic : state->userpics) { + const auto peer = userpic.peer; + const auto id = peer->id.value; + const auto was = ranges::find(old, id, &Ui::WhoReadParticipant::id); + if (was != end(old)) { + was->name = peer->name; + now.push_back(std::move(*was)); + continue; + } + now.push_back({ + .name = peer->name, + .userpicLarge = peer->generateUserpicImage( + userpic.view, + large), + .userpicKey = userpic.uniqueKey, + .id = id, + }); + if (now.size() <= Ui::WhoReadParticipant::kMaxSmallUserpics) { + now.back().userpicSmall = peer->generateUserpicImage( + userpic.view, + small); + } + } + RegenerateUserpics(state, small, large); +} + } // namespace bool WhoReadExists(not_null item) { @@ -144,91 +315,54 @@ bool WhoReadExists(not_null item) { return true; } -rpl::producer> WhoReadIds( +rpl::producer WhoRead( not_null item, - not_null context) { - auto weak = QPointer(context.get()); - const auto fullId = item->fullId(); - const auto session = &item->history()->session(); + not_null context, + const style::WhoRead &st) { + const auto small = st.userpics.size; + const auto large = st.photoSize; return [=](auto consumer) { - if (!weak) { - return rpl::lifetime(); - } - const auto context = ContextAt(weak.data()); - if (!context->subscriptions.contains(session)) { - session->changes().messageUpdates( - Data::MessageUpdate::Flag::Destroyed - ) | rpl::start_with_next([=](const Data::MessageUpdate &update) { - const auto i = context->cached.find(update.item); - if (i == end(context->cached)) { + auto lifetime = rpl::lifetime(); + + const auto state = lifetime.make_state(); + const auto pushNext = [=] { + consumer.put_next_copy(state->current); + }; + + WhoReadIds( + item, + context + ) | rpl::start_with_next([=](const std::vector &peers) { + if (ListUnknown(peers, item)) { + state->userpics.clear(); + consumer.put_next(Ui::WhoReadContent{ .unknown = true }); + return; + } else if (UpdateUserpics(state, item, peers)) { + RegenerateParticipants(state, small, large); + pushNext(); + } + }, lifetime); + + item->history()->session().downloaderTaskFinished( + ) | rpl::filter([=] { + return state->someUserpicsNotLoaded && !state->scheduled; + }) | rpl::start_with_next([=] { + for (const auto &userpic : state->userpics) { + if (userpic.peer->userpicUniqueKey(userpic.view) + != userpic.uniqueKey) { + state->scheduled = true; + crl::on_main(&state->guard, [=] { + state->scheduled = false; + RegenerateUserpics(state, small, large); + pushNext(); + }); return; } - session->api().request(i->second.requestId).cancel(); - context->cached.erase(i); - }, context->subscriptions[session]); - } - auto &entry = context->cache(item); - if (!entry.requestId) { - const auto makeEmpty = [=] { - // Special value that marks a validated empty list. - return std::vector{ - item->history()->session().userId() - }; - }; - entry.requestId = session->api().request( - MTPmessages_GetMessageReadParticipants( - item->history()->peer->input, - MTP_int(item->id) - ) - ).done([=](const MTPVector &result) { - auto &entry = context->cache(item); - entry.requestId = 0; - auto users = std::vector(); - users.reserve(std::max(result.v.size(), 1)); - for (const auto &id : result.v) { - users.push_back(UserId(id)); - } - entry.list = std::move(users); - }).fail([=](const MTP::Error &error) { - auto &entry = context->cache(item); - entry.requestId = 0; - if (ListUnknown(entry.list.current(), item)) { - entry.list = std::vector(); - } - }).send(); - } - return entry.list.value().start_existing(consumer); + } + }, lifetime); + + return lifetime; }; } -rpl::producer WhoRead( - not_null item, - not_null context) { - return WhoReadIds( - item, - context - ) | rpl::map([=](const std::vector &users) { - const auto owner = &item->history()->owner(); - if (ListUnknown(users, item)) { - return Ui::WhoReadContent{ .unknown = true }; - } - auto participants = ranges::views::all( - users - ) | ranges::views::transform([&](UserId id) { - return owner->userLoaded(id); - }) | ranges::views::filter([](UserData *user) { - return user != nullptr; - }) | ranges::views::transform([](UserData *user) { - return Ui::WhoReadParticipant{ - .name = user->name, - .id = user->id.value, - }; - }) | ranges::to_vector; - return Ui::WhoReadContent{ - .participants = std::move(participants), - .type = DetectType(item), - }; - }); -} - } // namespace Api diff --git a/Telegram/SourceFiles/api/api_who_read.h b/Telegram/SourceFiles/api/api_who_read.h index 4e7fe7ed1..9ea7f3051 100644 --- a/Telegram/SourceFiles/api/api_who_read.h +++ b/Telegram/SourceFiles/api/api_who_read.h @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class HistoryItem; +namespace style { +struct WhoRead; +} // namespace style + namespace Ui { struct WhoReadContent; } // namespace Ui @@ -20,6 +24,7 @@ namespace Api { // The context must be destroyed before the session holding this item. [[nodiscard]] rpl::producer WhoRead( not_null item, - not_null context); // Cache results for this lifetime. + not_null context, + const style::WhoRead &st); // Cache results for this lifetime. } // namespace Api diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp index 27e2bff25..54024aa44 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp @@ -462,19 +462,11 @@ void Viewport::RendererGL::validateUserpicFrame( } else if (!tileData.userpicFrame.isNull()) { return; } - tileData.userpicFrame = QImage( - tile->trackOrUserpicSize(), - QImage::Format_ARGB32_Premultiplied); - tileData.userpicFrame.fill(Qt::black); - { - auto p = Painter(&tileData.userpicFrame); - tile->row()->peer()->paintUserpicSquare( - p, - tile->row()->ensureUserpicView(), - 0, - 0, - tileData.userpicFrame.width()); - } + const auto size = tile->trackOrUserpicSize(); + tileData.userpicFrame = tile->row()->peer()->generateUserpicImage( + tile->row()->ensureUserpicView(), + size.width(), + ImageRoundRadius::None); } void Viewport::RendererGL::paintTile( diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp index b76bf0bcf..354e84c29 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp @@ -71,21 +71,12 @@ void Viewport::RendererSW::validateUserpicFrame( } else if (!data.userpicFrame.isNull()) { return; } - auto userpic = QImage( - tile->trackOrUserpicSize(), - QImage::Format_ARGB32_Premultiplied); - userpic.fill(Qt::black); - { - auto p = Painter(&userpic); - tile->row()->peer()->paintUserpicSquare( - p, - tile->row()->ensureUserpicView(), - 0, - 0, - userpic.width()); - } + const auto size = tile->trackOrUserpicSize(); data.userpicFrame = Images::BlurLargeImage( - std::move(userpic), + tile->row()->peer()->generateUserpicImage( + tile->row()->ensureUserpicView(), + size.width(), + ImageRoundRadius::None), kBlurRadius); } diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 07ec4c5f6..7d9f31ea1 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -335,32 +335,6 @@ void PeerData::paintUserpic( } } -void PeerData::paintUserpicRounded( - Painter &p, - std::shared_ptr &view, - int x, - int y, - int size) const { - if (const auto userpic = currentUserpic(view)) { - p.drawPixmap(x, y, userpic->pixRounded(size, size, ImageRoundRadius::Small)); - } else { - ensureEmptyUserpic()->paintRounded(p, x, y, x + size + x, size); - } -} - -void PeerData::paintUserpicSquare( - Painter &p, - std::shared_ptr &view, - int x, - int y, - int size) const { - if (const auto userpic = currentUserpic(view)) { - p.drawPixmap(x, y, userpic->pix(size, size)); - } else { - ensureEmptyUserpic()->paintSquare(p, x, y, x + size + x, size); - } -} - void PeerData::loadUserpic() { _userpic.load(&session(), userpicOrigin()); } @@ -398,14 +372,17 @@ void PeerData::saveUserpic( std::shared_ptr &view, const QString &path, int size) const { - genUserpic(view, size).save(path, "PNG"); + generateUserpicImage(view, size * cIntRetinaFactor()).save(path, "PNG"); } void PeerData::saveUserpicRounded( std::shared_ptr &view, const QString &path, int size) const { - genUserpicRounded(view, size).save(path, "PNG"); + generateUserpicImage( + view, + size * cIntRetinaFactor(), + ImageRoundRadius::Small).save(path, "PNG"); } QPixmap PeerData::genUserpic( @@ -424,20 +401,43 @@ QPixmap PeerData::genUserpic( return Ui::PixmapFromImage(std::move(result)); } -QPixmap PeerData::genUserpicRounded( +QImage PeerData::generateUserpicImage( std::shared_ptr &view, int size) const { + return generateUserpicImage(view, size, ImageRoundRadius::Ellipse); +} + +QImage PeerData::generateUserpicImage( + std::shared_ptr &view, + int size, + ImageRoundRadius radius) const { if (const auto userpic = currentUserpic(view)) { - return userpic->pixRounded(size, size, ImageRoundRadius::Small); + const auto options = (radius == ImageRoundRadius::Ellipse) + ? (Images::Option::RoundedAll | Images::Option::Circled) + : (radius == ImageRoundRadius::None) + ? Images::Options() + : (Images::Option::RoundedAll | Images::Option::RoundedSmall); + return userpic->pixNoCache( + size, + size, + Images::Option::Smooth | options + ).toImage(); } - auto result = QImage(QSize(size, size) * cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied); - result.setDevicePixelRatio(cRetinaFactor()); + auto result = QImage( + QSize(size, size), + QImage::Format_ARGB32_Premultiplied); result.fill(Qt::transparent); { Painter p(&result); - paintUserpicRounded(p, view, 0, 0, size); + if (radius == ImageRoundRadius::Ellipse) { + ensureEmptyUserpic()->paint(p, 0, 0, size, size); + } else if (radius == ImageRoundRadius::None) { + ensureEmptyUserpic()->paintSquare(p, 0, 0, size, size); + } else { + ensureEmptyUserpic()->paintRounded(p, 0, 0, size, size); + } } - return Ui::PixmapFromImage(std::move(result)); + return result; } Data::FileOrigin PeerData::userpicOrigin() const { diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 59fd57420..a4dd2b0f1 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -340,18 +340,6 @@ public: int size) const { paintUserpic(p, view, rtl() ? (w - x - size) : x, y, size); } - void paintUserpicRounded( - Painter &p, - std::shared_ptr &view, - int x, - int y, - int size) const; - void paintUserpicSquare( - Painter &p, - std::shared_ptr &view, - int x, - int y, - int size) const; void loadUserpic(); [[nodiscard]] bool hasUserpic() const; [[nodiscard]] std::shared_ptr activeUserpicView(); @@ -371,9 +359,13 @@ public: [[nodiscard]] QPixmap genUserpic( std::shared_ptr &view, int size) const; - [[nodiscard]] QPixmap genUserpicRounded( + [[nodiscard]] QImage generateUserpicImage( std::shared_ptr &view, int size) const; + [[nodiscard]] QImage generateUserpicImage( + std::shared_ptr &view, + int size, + ImageRoundRadius radius) const; [[nodiscard]] ImageLocation userpicLocation() const { return _userpic.location(); } diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index d74c757d6..5880b567e 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -1591,8 +1591,9 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { }; _menu->addAction(Ui::WhoReadContextAction( _menu.get(), - Api::WhoRead(item, this), + Api::WhoRead(item, this, st::defaultWhoRead), participantChosen)); + _menu->addSeparator(); } if (canSendMessages) { _menu->addAction(tr::lng_context_reply_msg(tr::now), [=] { diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 3a5e0c060..b111ffb66 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -863,9 +863,18 @@ ttlDividerLabelPadding: margins(22px, 10px, 22px, 19px); ttlItemPadding: margins(0px, 4px, 0px, 4px); ttlItemTimerFont: font(12px); -seenItemUserpics: GroupCallUserpics { - size: 32px; - shift: 12px; - stroke: 4px; - align: align(right); +WhoRead { + userpics: GroupCallUserpics; + photoSize: pixels; + itemPadding: margins; +} +defaultWhoRead: WhoRead { + userpics: GroupCallUserpics { + size: 22px; + shift: 8px; + stroke: 4px; + align: align(right); + } + photoSize: 30px; + itemPadding: margins(17px, 8px, 17px, 6px); } diff --git a/Telegram/SourceFiles/ui/controls/who_read_context_action.cpp b/Telegram/SourceFiles/ui/controls/who_read_context_action.cpp index 3e97339e6..8187a7db1 100644 --- a/Telegram/SourceFiles/ui/controls/who_read_context_action.cpp +++ b/Telegram/SourceFiles/ui/controls/who_read_context_action.cpp @@ -17,8 +17,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { namespace { -constexpr auto kMaxUserpics = 3; - class Action final : public Menu::ItemBase { public: Action( @@ -77,23 +75,19 @@ Action::Action( , _dummyAction(new QAction(parentMenu->menu())) , _participantChosen(std::move(participantChosen)) , _userpics(std::make_unique( - st::historyGroupCallUserpics, + st::defaultWhoRead.userpics, rpl::never(), [=] { update(); })) , _st(parentMenu->menu()->st()) -, _height(_st.itemPadding.top() +, _height(st::defaultWhoRead.itemPadding.top() + _st.itemStyle.font->height - + _st.itemPadding.bottom()) { + + st::defaultWhoRead.itemPadding.bottom()) { const auto parent = parentMenu->menu(); setAcceptBoth(true); initResizeHook(parent->sizeValue()); resolveMinWidth(); - auto copy = std::move( - content - ) | rpl::start_spawning(lifetime()); - _userpics->widthValue( ) | rpl::start_with_next([=](int width) { _userpicsWidth = width; @@ -160,12 +154,12 @@ void Action::updateUserpicsFromContent() { if (!_content.participants.empty()) { const auto count = std::min( int(_content.participants.size()), - kMaxUserpics); + WhoReadParticipant::kMaxSmallUserpics); users.reserve(count); for (auto i = 0; i != count; ++i) { const auto &participant = _content.participants[i]; users.push_back({ - .userpic = participant.userpic, + .userpic = participant.userpicSmall, .userpicKey = participant.userpicKey, .id = participant.id, }); @@ -217,8 +211,8 @@ void Action::paint(Painter &p) { _userpics->paint( p, width() - _st.itemPadding.right(), - _st.itemPadding.top(), - st::historyGroupCallUserpics.size); + (height() - st::defaultWhoRead.userpics.size) / 2, + st::defaultWhoRead.userpics.size); } void Action::refreshText() { diff --git a/Telegram/SourceFiles/ui/controls/who_read_context_action.h b/Telegram/SourceFiles/ui/controls/who_read_context_action.h index d577940d4..deb7b9736 100644 --- a/Telegram/SourceFiles/ui/controls/who_read_context_action.h +++ b/Telegram/SourceFiles/ui/controls/who_read_context_action.h @@ -18,9 +18,12 @@ class PopupMenu; struct WhoReadParticipant { QString name; - QImage userpic; + QImage userpicSmall; + QImage userpicLarge; std::pair userpicKey = {}; uint64 id = 0; + + static constexpr auto kMaxSmallUserpics = 3; }; bool operator==(const WhoReadParticipant &a, const WhoReadParticipant &b);