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

@ -591,6 +591,8 @@ PRIVATE
dialogs/ui/dialogs_layout.h
dialogs/ui/dialogs_message_view.cpp
dialogs/ui/dialogs_message_view.h
dialogs/ui/dialogs_stories_content.cpp
dialogs/ui/dialogs_stories_content.h
dialogs/ui/dialogs_topics_view.cpp
dialogs/ui/dialogs_topics_view.h
dialogs/ui/dialogs_video_userpic.cpp

View File

@ -149,6 +149,10 @@ bool Stories::allLoaded() const {
return _allLoaded;
}
rpl::producer<> Stories::allChanged() const {
return _allChanged.events();
}
// #TODO stories testing
StoryId Stories::generate(
not_null<HistoryItem*> item,
@ -244,10 +248,14 @@ StoryId Stories::generate(
void Stories::pushToBack(StoriesList &&list) {
const auto i = ranges::find(_all, list.user, &StoriesList::user);
if (i != end(_all)) {
if (*i == list) {
return;
}
*i = std::move(list);
} else {
_all.push_back(std::move(list));
}
_allChanged.fire({});
}
void Stories::pushToFront(StoriesList &&list) {

View File

@ -30,6 +30,7 @@ struct StoryItem {
TextWithEntities caption;
TimeId date = 0;
StoryPrivacy privacy;
bool pinned = false;
friend inline bool operator==(StoryItem, StoryItem) = default;
};
@ -37,8 +38,11 @@ struct StoryItem {
struct StoriesList {
not_null<UserData*> user;
std::vector<StoryItem> items;
StoryId readTill = 0;
int total = 0;
[[nodiscard]] bool unread() const;
friend inline bool operator==(StoriesList, StoriesList) = default;
};
@ -61,11 +65,14 @@ public:
explicit Stories(not_null<Session*> owner);
~Stories();
[[nodiscard]] Session &owner() const;
void loadMore();
void apply(const MTPDupdateStories &data);
[[nodiscard]] const std::vector<StoriesList> &all();
[[nodiscard]] bool allLoaded() const;
[[nodiscard]] rpl::producer<> allChanged() const;
// #TODO stories testing
[[nodiscard]] StoryId generate(
@ -76,7 +83,7 @@ public:
not_null<DocumentData*>> media);
private:
[[nodiscard]] StoriesList parse(const MTPUserStories &data);
[[nodiscard]] StoriesList parse(const MTPUserStories &stories);
[[nodiscard]] std::optional<StoryItem> parse(const MTPDstoryItem &data);
void pushToBack(StoriesList &&list);
@ -85,6 +92,7 @@ private:
const not_null<Session*> _owner;
std::vector<StoriesList> _all;
rpl::event_stream<> _allChanged;
QString _state;
bool _allLoaded = false;

View File

@ -485,3 +485,44 @@ chooseTopicListItem: PeerListItem(defaultPeerListItem) {
chooseTopicList: PeerList(defaultPeerList) {
item: chooseTopicListItem;
}
DialogsStories {
left: pixels;
height: pixels;
photo: pixels;
photoLeft: pixels;
photoTop: pixels;
shift: pixels;
lineTwice: pixels;
lineReadTwice: pixels;
nameTop: pixels;
nameStyle: TextStyle;
}
dialogsStories: DialogsStories {
left: 4px;
height: 35px;
photo: 24px;
photoLeft: 10px;
shift: 16px;
lineTwice: 3px;
lineReadTwice: 0px;
nameTop: 9px;
nameStyle: semiboldTextStyle;
}
dialogsStoriesFull: DialogsStories {
left: 4px;
height: 77px;
photo: 42px;
photoLeft: 10px;
photoTop: 9px;
lineTwice: 4px;
lineReadTwice: 2px;
nameTop: 58px;
nameStyle: TextStyle(defaultTextStyle) {
font: font(12px);
linkFont: font(12px);
linkFontOver: font(12px);
}
}

View File

@ -7,9 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "dialogs/dialogs_inner_widget.h"
#include "dialogs/dialogs_indexed_list.h"
#include "dialogs/ui/dialogs_layout.h"
#include "dialogs/ui/dialogs_stories_content.h"
#include "dialogs/ui/dialogs_stories_list.h"
#include "dialogs/ui/dialogs_video_userpic.h"
#include "dialogs/dialogs_indexed_list.h"
#include "dialogs/dialogs_widget.h"
#include "dialogs/dialogs_search_from_controllers.h"
#include "history/history.h"
@ -137,6 +139,10 @@ InnerWidget::InnerWidget(
rpl::producer<ChildListShown> childListShown)
: RpWidget(parent)
, _controller(controller)
, _stories(std::make_unique<Stories::List>(
this,
Stories::ContentForSession(&controller->session()),
[=] { return st::dialogsStoriesFull.height - _visibleTop; }))
, _shownList(controller->session().data().chatsList()->indexed())
, _st(&st::defaultDialogRow)
, _pinnedShiftAnimation([=](crl::time now) {
@ -406,8 +412,19 @@ int InnerWidget::skipTopHeight() const {
: 0;
}
bool InnerWidget::storiesShown() const {
return (_state == WidgetState::Default)
&& !_openedFolder
&& !_openedForum;
}
int InnerWidget::collapsedRowsOffset() const {
return storiesShown() ? _stories->height() : 0;
}
int InnerWidget::dialogsOffset() const {
return _collapsedRows.size() * st::dialogsImportantBarHeight
return collapsedRowsOffset()
+ (_collapsedRows.size() * st::dialogsImportantBarHeight)
- skipTopHeight();
}
@ -493,6 +510,7 @@ void InnerWidget::changeOpenedFolder(Data::Folder *folder) {
stopReorderPinned();
clearSelection();
_openedFolder = folder;
_stories->setVisible(storiesShown());
refreshShownList();
refreshWithCollapsedRows(true);
if (_loadMoreCallback) {
@ -519,6 +537,7 @@ void InnerWidget::changeOpenedForum(Data::Forum *forum) {
}
_openedForum = forum;
_st = forum ? &st::forumTopicRow : &st::defaultDialogRow;
_stories->setVisible(storiesShown());
refreshShownList();
_openedForumLifetime.destroy();
@ -596,7 +615,9 @@ void InnerWidget::paintEvent(QPaintEvent *e) {
Ui::RowPainter::Paint(p, row, validateVideoUserpic(row), context);
};
if (_state == WidgetState::Default) {
paintCollapsedRows(p, r);
const auto collapsedSkip = collapsedRowsOffset();
p.translate(0, collapsedSkip);
paintCollapsedRows(p, r.translated(0, -collapsedSkip));
const auto &list = _shownList->all();
const auto shownBottom = _shownList->height() - skipTopHeight();
@ -1748,6 +1769,7 @@ void InnerWidget::setSearchedPressed(int pressed) {
}
void InnerWidget::resizeEvent(QResizeEvent *e) {
_stories->resizeToWidth(width());
resizeEmptyLabel();
moveCancelSearchButtons();
}
@ -2255,7 +2277,7 @@ void InnerWidget::applyFilterUpdate(QString newFilter, bool force) {
if (_filter.isEmpty() && !_searchFromPeer) {
clearFilter();
} else {
_state = WidgetState::Filtered;
setState(WidgetState::Filtered);
_waitingForSearch = true;
_filterResults.clear();
_filterResultsGlobal.clear();
@ -2506,6 +2528,7 @@ void InnerWidget::visibleTopBottomUpdated(
int visibleBottom) {
_visibleTop = visibleTop;
_visibleBottom = visibleBottom;
_stories->update();
preloadRowsData();
const auto loadTill = _visibleTop
+ PreloadHeightsCount * (_visibleBottom - _visibleTop);
@ -2915,10 +2938,10 @@ void InnerWidget::repaintSearchResult(int index) {
void InnerWidget::clearFilter() {
if (_state == WidgetState::Filtered || _searchInChat) {
if (_searchInChat) {
_state = WidgetState::Filtered;
setState(WidgetState::Filtered);
_waitingForSearch = true;
} else {
_state = WidgetState::Default;
setState(WidgetState::Default);
}
_hashtagResults.clear();
_filterResults.clear();
@ -2930,6 +2953,13 @@ void InnerWidget::clearFilter() {
}
}
void InnerWidget::setState(WidgetState state) {
if (_state != state) {
_state = state;
_stories->setVisible(storiesShown());
}
}
void InnerWidget::selectSkip(int32 direction) {
clearMouseSelection();
if (_state == WidgetState::Default) {

View File

@ -52,6 +52,10 @@ struct PaintContext;
struct TopicJumpCache;
} // namespace Dialogs::Ui
namespace Dialogs::Stories {
class List;
} // namespace Dialogs::Stories
namespace Dialogs {
class Row;
@ -219,6 +223,7 @@ private:
void dialogRowReplaced(Row *oldRow, Row *newRow);
void setState(WidgetState state);
void editOpenedFilter();
void repaintCollapsedFolderRow(not_null<Data::Folder*> folder);
void refreshWithCollapsedRows(bool toTop = false);
@ -309,7 +314,9 @@ private:
void fillArchiveSearchMenu(not_null<Ui::PopupMenu*> menu);
void refreshShownList();
[[nodiscard]] bool storiesShown() const;
[[nodiscard]] int skipTopHeight() const;
[[nodiscard]] int collapsedRowsOffset() const;
[[nodiscard]] int dialogsOffset() const;
[[nodiscard]] int shownHeight(int till = -1) const;
[[nodiscard]] int fixedOnTopCount() const;
@ -394,6 +401,8 @@ private:
const not_null<Window::SessionController*> _controller;
const std::unique_ptr<Stories::List> _stories;
not_null<IndexedList*> _shownList;
FilterId _filterId = 0;
bool _mouseSelection = false;

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

View File

@ -70,6 +70,9 @@ PRIVATE
data/data_subscription_option.h
dialogs/ui/dialogs_stories_list.cpp
dialogs/ui/dialogs_stories_list.h
editor/controllers/undo_controller.cpp
editor/controllers/undo_controller.h
editor/editor_crop.cpp