Show userpics in who read context item.

This commit is contained in:
John Preston 2021-09-09 00:10:49 +03:00
parent 8f480b52e7
commit 14314df26a
10 changed files with 302 additions and 181 deletions

View File

@ -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<UserId>{ unknownFlag }) {
explicit Cached(PeerId unknownFlag)
: list(std::vector<PeerId>{ unknownFlag }) {
}
rpl::variable<std::vector<UserId>> list;
rpl::variable<std::vector<PeerId>> 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<PeerData*> peer;
mutable std::shared_ptr<Data::CloudImageView> view;
mutable InMemoryKey uniqueKey;
};
struct State {
std::vector<Userpic> userpics;
Ui::WhoReadContent current;
base::has_weak_ptr guard;
bool someUserpicsNotLoaded = false;
bool scheduled = false;
};
[[nodiscard]] auto Contexts()
-> base::flat_map<not_null<QWidget*>, std::unique_ptr<Context>> & {
static auto result = base::flat_map<
@ -82,10 +98,10 @@ struct Context {
}
[[nodiscard]] bool ListUnknown(
const std::vector<UserId> &list,
const std::vector<PeerId> &list,
not_null<HistoryItem*> item) {
return (list.size() == 1)
&& (list.front() == item->history()->session().userId());
&& (list.front() == item->history()->session().userPeerId());
}
[[nodiscard]] Ui::WhoReadType DetectType(not_null<HistoryItem*> item) {
@ -103,6 +119,161 @@ struct Context {
return Ui::WhoReadType::Seen;
}
[[nodiscard]] rpl::producer<std::vector<PeerId>> WhoReadIds(
not_null<HistoryItem*> item,
not_null<QWidget*> context) {
auto weak = QPointer<QWidget>(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<MTPlong> &result) {
auto &entry = context->cache(item);
entry.requestId = 0;
auto peers = std::vector<PeerId>();
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<PeerId>();
}
}).send();
}
return entry.list.value().start_existing(consumer);
};
}
bool UpdateUserpics(
not_null<State*> state,
not_null<HistoryItem*> item,
const std::vector<PeerId> &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<Userpic>();
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*> 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*> 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<HistoryItem*> item) {
@ -144,91 +315,54 @@ bool WhoReadExists(not_null<HistoryItem*> item) {
return true;
}
rpl::producer<std::vector<UserId>> WhoReadIds(
rpl::producer<Ui::WhoReadContent> WhoRead(
not_null<HistoryItem*> item,
not_null<QWidget*> context) {
auto weak = QPointer<QWidget>(context.get());
const auto fullId = item->fullId();
const auto session = &item->history()->session();
not_null<QWidget*> 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<State>();
const auto pushNext = [=] {
consumer.put_next_copy(state->current);
};
WhoReadIds(
item,
context
) | rpl::start_with_next([=](const std::vector<PeerId> &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<UserId>{
item->history()->session().userId()
};
};
entry.requestId = session->api().request(
MTPmessages_GetMessageReadParticipants(
item->history()->peer->input,
MTP_int(item->id)
)
).done([=](const MTPVector<MTPlong> &result) {
auto &entry = context->cache(item);
entry.requestId = 0;
auto users = std::vector<UserId>();
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<UserId>();
}
}).send();
}
return entry.list.value().start_existing(consumer);
}
}, lifetime);
return lifetime;
};
}
rpl::producer<Ui::WhoReadContent> WhoRead(
not_null<HistoryItem*> item,
not_null<QWidget*> context) {
return WhoReadIds(
item,
context
) | rpl::map([=](const std::vector<UserId> &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

View File

@ -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<Ui::WhoReadContent> WhoRead(
not_null<HistoryItem*> item,
not_null<QWidget*> context); // Cache results for this lifetime.
not_null<QWidget*> context,
const style::WhoRead &st); // Cache results for this lifetime.
} // namespace Api

View File

@ -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(

View File

@ -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);
}

View File

@ -335,32 +335,6 @@ void PeerData::paintUserpic(
}
}
void PeerData::paintUserpicRounded(
Painter &p,
std::shared_ptr<Data::CloudImageView> &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<Data::CloudImageView> &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<Data::CloudImageView> &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<Data::CloudImageView> &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<Data::CloudImageView> &view,
int size) const {
return generateUserpicImage(view, size, ImageRoundRadius::Ellipse);
}
QImage PeerData::generateUserpicImage(
std::shared_ptr<Data::CloudImageView> &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 {

View File

@ -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<Data::CloudImageView> &view,
int x,
int y,
int size) const;
void paintUserpicSquare(
Painter &p,
std::shared_ptr<Data::CloudImageView> &view,
int x,
int y,
int size) const;
void loadUserpic();
[[nodiscard]] bool hasUserpic() const;
[[nodiscard]] std::shared_ptr<Data::CloudImageView> activeUserpicView();
@ -371,9 +359,13 @@ public:
[[nodiscard]] QPixmap genUserpic(
std::shared_ptr<Data::CloudImageView> &view,
int size) const;
[[nodiscard]] QPixmap genUserpicRounded(
[[nodiscard]] QImage generateUserpicImage(
std::shared_ptr<Data::CloudImageView> &view,
int size) const;
[[nodiscard]] QImage generateUserpicImage(
std::shared_ptr<Data::CloudImageView> &view,
int size,
ImageRoundRadius radius) const;
[[nodiscard]] ImageLocation userpicLocation() const {
return _userpic.location();
}

View File

@ -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), [=] {

View File

@ -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);
}

View File

@ -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<GroupCallUserpics>(
st::historyGroupCallUserpics,
st::defaultWhoRead.userpics,
rpl::never<bool>(),
[=] { 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() {

View File

@ -18,9 +18,12 @@ class PopupMenu;
struct WhoReadParticipant {
QString name;
QImage userpic;
QImage userpicSmall;
QImage userpicLarge;
std::pair<uint64, uint64> userpicKey = {};
uint64 id = 0;
static constexpr auto kMaxSmallUserpics = 3;
};
bool operator==(const WhoReadParticipant &a, const WhoReadParticipant &b);