2
0
mirror of https://github.com/telegramdesktop/tdesktop synced 2025-08-31 14:38:15 +00:00

Paint nice stories userpics in chats list.

This commit is contained in:
John Preston
2023-05-22 19:59:16 +04:00
parent 2c5d990e1c
commit 1d27c8c940
11 changed files with 675 additions and 7 deletions

View File

@@ -0,0 +1,187 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "dialogs/ui/dialogs_stories_content.h"
#include "data/data_changes.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "data/data_user.h"
#include "dialogs/ui/dialogs_stories_list.h"
#include "main/main_session.h"
#include "ui/painter.h"
#include "history/history.h" // #TODO stories testing
namespace Dialogs::Stories {
namespace {
class PeerUserpic final : public Userpic {
public:
explicit PeerUserpic(not_null<PeerData*> peer);
QImage image(int size) override;
void subscribeToUpdates(Fn<void()> callback) override;
private:
struct Subscribed {
explicit Subscribed(Fn<void()> callback)
: callback(std::move(callback)) {
}
Ui::PeerUserpicView view;
Fn<void()> callback;
InMemoryKey key;
rpl::lifetime photoLifetime;
rpl::lifetime downloadLifetime;
};
[[nodiscard]] bool waitingUserpicLoad() const;
void processNewPhoto();
const not_null<PeerData*> _peer;
QImage _frame;
std::unique_ptr<Subscribed> _subscribed;
};
class State final {
public:
explicit State(not_null<Data::Stories*> data);
[[nodiscard]] Content next();
private:
const not_null<Data::Stories*> _data;
base::flat_map<not_null<UserData*>, std::shared_ptr<Userpic>> _userpics;
};
PeerUserpic::PeerUserpic(not_null<PeerData*> peer)
: _peer(peer) {
}
QImage PeerUserpic::image(int size) {
Expects(_subscribed != nullptr);
const auto good = (_frame.width() == size * _frame.devicePixelRatio());
const auto key = _peer->userpicUniqueKey(_subscribed->view);
if (!good || (_subscribed->key != key && !waitingUserpicLoad())) {
_subscribed->key = key;
_frame = QImage(
QSize(size, size) * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
_frame.setDevicePixelRatio(style::DevicePixelRatio());
_frame.fill(Qt::transparent);
auto p = Painter(&_frame);
_peer->paintUserpic(p, _subscribed->view, 0, 0, size);
}
return _frame;
}
bool PeerUserpic::waitingUserpicLoad() const {
return _peer->hasUserpic() && _peer->useEmptyUserpic(_subscribed->view);
}
void PeerUserpic::subscribeToUpdates(Fn<void()> callback) {
if (!callback) {
_subscribed = nullptr;
return;
}
_subscribed = std::make_unique<Subscribed>(std::move(callback));
_peer->session().changes().peerUpdates(
_peer,
Data::PeerUpdate::Flag::Photo
) | rpl::start_with_next([=] {
_subscribed->callback();
processNewPhoto();
}, _subscribed->photoLifetime);
processNewPhoto();
}
void PeerUserpic::processNewPhoto() {
Expects(_subscribed != nullptr);
if (!waitingUserpicLoad()) {
_subscribed->downloadLifetime.destroy();
return;
}
_peer->session().downloaderTaskFinished(
) | rpl::filter([=] {
return !waitingUserpicLoad();
}) | rpl::start_with_next([=] {
_subscribed->callback();
_subscribed->downloadLifetime.destroy();
}, _subscribed->downloadLifetime);
}
State::State(not_null<Data::Stories*> data)
: _data(data) {
}
Content State::next() {
auto result = Content();
#if 0 // #TODO stories testing
const auto &all = _data->all();
result.users.reserve(all.size());
for (const auto &list : all) {
auto userpic = std::shared_ptr<Userpic>();
const auto user = list.user;
#endif
const auto list = _data->owner().chatsList();
const auto &all = list->indexed()->all();
result.users.reserve(all.size());
for (const auto &entry : all) {
if (const auto history = entry->history()) {
if (const auto user = history->peer->asUser(); user && !user->isBot()) {
auto userpic = std::shared_ptr<Userpic>();
if (const auto i = _userpics.find(user); i != end(_userpics)) {
userpic = i->second;
} else {
userpic = std::make_shared<PeerUserpic>(user);
_userpics.emplace(user, userpic);
}
result.users.push_back({
.id = uint64(user->id.value),
.name = user->shortName(),
.userpic = std::move(userpic),
.unread = history->chatListBadgesState().unread// list.unread(),
});
}
}
}
return result;
}
} // namespace
rpl::producer<Content> ContentForSession(not_null<Main::Session*> session) {
return [=](auto consumer) {
auto result = rpl::lifetime();
const auto stories = &session->data().stories();
const auto state = result.make_state<State>(stories);
rpl::single(
rpl::empty
) | rpl::then(
#if 0 // #TODO stories testing
stories->allChanged()
#endif
session->data().chatsListChanges(
) | rpl::filter(
rpl::mappers::_1 == nullptr
) | rpl::to_empty
) | rpl::start_with_next([=] {
consumer.put_next(state->next());
}, result);
return result;
};
}
} // namespace Dialogs::Stories

View File

@@ -0,0 +1,21 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Main {
class Session;
} // namespace Main
namespace Dialogs::Stories {
struct Content;
[[nodiscard]] rpl::producer<Content> ContentForSession(
not_null<Main::Session*> session);
} // namespace Dialogs::Stories

View File

@@ -0,0 +1,283 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "dialogs/ui/dialogs_stories_list.h"
#include "ui/painter.h"
#include "styles/style_dialogs.h"
namespace Dialogs::Stories {
namespace {
constexpr auto kSmallUserpicsShown = 3;
constexpr auto kSmallReadOpacity = 0.6;
} // namespace
List::List(
not_null<QWidget*> parent,
rpl::producer<Content> content,
Fn<int()> shownHeight)
: RpWidget(parent)
, _shownHeight(shownHeight) {
resize(0, st::dialogsStoriesFull.height);
std::move(content) | rpl::start_with_next([=](Content &&content) {
showContent(std::move(content));
}, lifetime());
}
void List::showContent(Content &&content) {
if (_content == content) {
return;
}
_content = std::move(content);
auto items = base::take(_items);
_items.reserve(_content.users.size());
for (const auto &user : _content.users) {
const auto i = ranges::find(items, user.id, [](const Item &item) {
return item.user.id;
});
if (i != end(items)) {
_items.push_back(std::move(*i));
auto &item = _items.back();
if (item.user.userpic != user.userpic) {
item.user.userpic = user.userpic;
item.subscribed = false;
}
if (item.user.name != user.name) {
item.user.name = user.name;
item.nameCache = QImage();
}
} else {
_items.emplace_back(Item{ .user = user });
}
}
update();
}
rpl::producer<uint64> List::clicks() const {
return _clicks.events();
}
rpl::producer<> List::expandRequests() const {
return _expandRequests.events();
}
void List::paintEvent(QPaintEvent *e) {
const auto &st = st::dialogsStories;
const auto &full = st::dialogsStoriesFull;
const auto shownHeight = std::max(_shownHeight(), st.height);
const auto ratio = float64(shownHeight - st.height)
/ (full.height - st.height);
const auto lerp = [=](float64 a, float64 b) {
return a + (b - a) * ratio;
};
const auto photo = lerp(st.photo, full.photo);
const auto photoTopSmall = (st.height - st.photo) / 2.;
const auto photoTop = lerp(photoTopSmall, full.photoTop);
const auto line = lerp(st.lineTwice, full.lineTwice) / 2.;
const auto lineRead = lerp(st.lineReadTwice, full.lineReadTwice) / 2.;
const auto nameTop = (photoTop + photo)
* (full.nameTop / float64(full.photoTop + full.photo));
const auto infoTop = st.nameTop
- (st.photoTop + (st.photo / 2.))
+ (photoTop + (photo / 2.));
const auto singleSmall = st.shift;
const auto singleFull = full.photoLeft * 2 + full.photo;
const auto single = lerp(singleSmall, singleFull);
const auto itemsCount = int(_items.size());
const auto leftSmall = st.left;
const auto leftFull = full.left - _scrollLeft;
const auto startIndexFull = std::max(-leftFull, 0) / singleFull;
const auto cellLeftFull = leftFull + (startIndexFull * singleFull);
const auto endIndexFull = std::min(
(width() - cellLeftFull + singleFull - 1) / singleFull,
itemsCount);
const auto startIndexSmall = 0;
const auto endIndexSmall = std::min(kSmallUserpicsShown, itemsCount);
const auto cellLeftSmall = leftSmall;
const auto userpicLeftFull = cellLeftFull + full.photoLeft;
const auto userpicLeftSmall = cellLeftSmall + st.photoLeft;
const auto userpicLeft = lerp(userpicLeftSmall, userpicLeftFull);
const auto photoLeft = lerp(st.photoLeft, full.photoLeft);
const auto left = userpicLeft - photoLeft;
const auto readUserpicOpacity = lerp(kSmallReadOpacity, 1.);
const auto readUserpicAppearingOpacity = lerp(kSmallReadOpacity, 0.);
auto p = QPainter(this);
p.fillRect(e->rect(), st::dialogsBg);
p.translate(0, height() - shownHeight);
const auto drawSmall = (ratio < 1.);
const auto drawFull = (ratio > 0.);
auto hq = PainterHighQualityEnabler(p);
const auto subscribe = [&](not_null<Item*> item) {
if (!item->subscribed) {
item->subscribed = true;
//const auto id = item.user.id;
item->user.userpic->subscribeToUpdates([=] {
update();
});
}
};
const auto count = std::max(
endIndexFull - startIndexFull,
endIndexSmall - startIndexSmall);
struct Single {
float64 x = 0.;
int indexSmall = 0;
Item *itemSmall = nullptr;
int indexFull = 0;
Item *itemFull = nullptr;
explicit operator bool() const {
return itemSmall || itemFull;
}
};
const auto lookup = [&](int index) {
const auto indexSmall = startIndexSmall + index;
const auto indexFull = startIndexFull + index;
const auto small = (drawSmall && indexSmall < endIndexSmall)
? &_items[indexSmall]
: nullptr;
const auto full = (drawFull && indexFull < endIndexFull)
? &_items[indexFull]
: nullptr;
const auto x = left + single * index;
return Single{ x, indexSmall, small, indexFull, full };
};
const auto hasUnread = [&](const Single &single) {
return (single.itemSmall && single.itemSmall->user.unread)
|| (single.itemFull && single.itemFull->user.unread);
};
const auto enumerate = [&](auto &&paintGradient, auto &&paintOther) {
auto nextGradientPainted = false;
for (auto i = count; i != 0;) {
--i;
const auto gradientPainted = nextGradientPainted;
nextGradientPainted = false;
if (const auto current = lookup(i)) {
if (!gradientPainted) {
paintGradient(current);
}
if (i > 0 && hasUnread(current)) {
if (const auto next = lookup(i - 1)) {
if (current.itemSmall || !next.itemSmall) {
nextGradientPainted = true;
paintGradient(next);
}
}
}
paintOther(current);
}
}
};
enumerate([&](Single single) {
// Unread gradient.
const auto x = single.x;
const auto userpic = QRectF(x + photoLeft, photoTop, photo, photo);
const auto small = single.itemSmall;
const auto itemFull = single.itemFull;
const auto smallUnread = small && small->user.unread;
const auto fullUnread = itemFull && itemFull->user.unread;
const auto unreadOpacity = (smallUnread && fullUnread)
? 1.
: smallUnread
? (1. - ratio)
: fullUnread
? ratio
: 0.;
if (unreadOpacity > 0.) {
p.setOpacity(unreadOpacity);
const auto outerAdd = 2 * line;
const auto outer = userpic.marginsAdded(
{ outerAdd, outerAdd, outerAdd, outerAdd });
p.setPen(Qt::NoPen);
auto gradient = QLinearGradient(
userpic.topRight(),
userpic.bottomLeft());
gradient.setStops({
{ 0., st::groupCallLive1->c },
{ 1., st::groupCallMuted1->c },
});
p.setBrush(gradient);
p.drawEllipse(outer);
p.setOpacity(1.);
}
}, [&](Single single) {
Expects(single.itemSmall || single.itemFull);
const auto x = single.x;
const auto userpic = QRectF(x + photoLeft, photoTop, photo, photo);
const auto small = single.itemSmall;
const auto itemFull = single.itemFull;
const auto smallUnread = small && small->user.unread;
const auto fullUnread = itemFull && itemFull->user.unread;
// White circle with possible read gray line.
if (itemFull && !fullUnread) {
auto color = st::dialogsUnreadBgMuted->c;
color.setAlphaF(color.alphaF() * ratio);
auto pen = QPen(color);
pen.setWidthF(lineRead);
p.setPen(pen);
} else {
p.setPen(Qt::NoPen);
}
const auto add = line + (itemFull ? (lineRead / 2.) : 0.);
const auto rect = userpic.marginsAdded({ add, add, add, add });
p.setBrush(st::dialogsBg);
p.drawEllipse(rect);
// Userpic.
if (itemFull == small) {
p.setOpacity(smallUnread ? 1. : readUserpicOpacity);
subscribe(itemFull);
const auto size = full.photo;
p.drawImage(userpic, itemFull->user.userpic->image(size));
} else {
if (small) {
p.setOpacity(smallUnread
? (itemFull ? 1. : (1. - ratio))
: (itemFull
? kSmallReadOpacity
: readUserpicAppearingOpacity));
subscribe(small);
const auto size = (ratio > 0.) ? full.photo : st.photo;
p.drawImage(userpic, small->user.userpic->image(size));
}
if (itemFull) {
p.setOpacity(ratio);
subscribe(itemFull);
const auto size = full.photo;
p.drawImage(userpic, itemFull->user.userpic->image(size));
}
}
p.setOpacity(1.);
});
}
void List::wheelEvent(QWheelEvent *e) {
}
void List::mouseMoveEvent(QMouseEvent *e) {
}
void List::mousePressEvent(QMouseEvent *e) {
}
void List::mouseReleaseEvent(QMouseEvent *e) {
}
} // namespace Dialogs::Stories

View File

@@ -0,0 +1,76 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/qt/qt_compare.h"
#include "ui/rp_widget.h"
class QPainter;
namespace Dialogs::Stories {
class Userpic {
public:
[[nodiscard]] virtual QImage image(int size) = 0;
virtual void subscribeToUpdates(Fn<void()> callback) = 0;
};
struct User {
uint64 id = 0;
QString name;
std::shared_ptr<Userpic> userpic;
bool unread = false;
friend inline bool operator==(const User &a, const User &b) = default;
};
struct Content {
std::vector<User> users;
friend inline bool operator==(
const Content &a,
const Content &b) = default;
};
class List final : public Ui::RpWidget {
public:
List(
not_null<QWidget*> parent,
rpl::producer<Content> content,
Fn<int()> shownHeight);
[[nodiscard]] rpl::producer<uint64> clicks() const;
[[nodiscard]] rpl::producer<> expandRequests() const;
private:
struct Item {
User user;
QImage frameSmall;
QImage frameFull;
QImage nameCache;
QColor nameCacheColor;
bool subscribed = false;
};
void showContent(Content &&content);
void paintEvent(QPaintEvent *e) override;
void wheelEvent(QWheelEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
Content _content;
std::vector<Item> _items;
Fn<int()> _shownHeight = 0;
rpl::event_stream<uint64> _clicks;
rpl::event_stream<> _expandRequests;
int _scrollLeft = 0;
};
} // namespace Dialogs::Stories