mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-16 14:17:12 +02:00
Paint nice stories userpics in chats list.
This commit is contained in:
parent
2c5d990e1c
commit
1d27c8c940
11 changed files with 675 additions and 7 deletions
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
187
Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
Normal file
187
Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
Normal 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
|
21
Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h
Normal file
21
Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h
Normal 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
|
283
Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
Normal file
283
Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
Normal 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
|
76
Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
Normal file
76
Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue