From 1d27c8c940795af5a46aa883b9593626a8fca68c Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 22 May 2023 19:59:16 +0400 Subject: [PATCH] Paint nice stories userpics in chats list. --- Telegram/CMakeLists.txt | 2 + Telegram/SourceFiles/data/data_stories.cpp | 8 + Telegram/SourceFiles/data/data_stories.h | 10 +- Telegram/SourceFiles/dialogs/dialogs.style | 41 +++ .../dialogs/dialogs_inner_widget.cpp | 42 ++- .../dialogs/dialogs_inner_widget.h | 9 + .../dialogs/ui/dialogs_stories_content.cpp | 187 ++++++++++++ .../dialogs/ui/dialogs_stories_content.h | 21 ++ .../dialogs/ui/dialogs_stories_list.cpp | 283 ++++++++++++++++++ .../dialogs/ui/dialogs_stories_list.h | 76 +++++ Telegram/cmake/td_ui.cmake | 3 + 11 files changed, 675 insertions(+), 7 deletions(-) create mode 100644 Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp create mode 100644 Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h create mode 100644 Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp create mode 100644 Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 3ed0b31c8..a32950631 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -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 diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 827ec2e18..2fb2f8c28 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.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 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) { diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 373f90b95..ce8ca4d60 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -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 user; std::vector 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 owner); ~Stories(); + [[nodiscard]] Session &owner() const; + void loadMore(); void apply(const MTPDupdateStories &data); [[nodiscard]] const std::vector &all(); [[nodiscard]] bool allLoaded() const; + [[nodiscard]] rpl::producer<> allChanged() const; // #TODO stories testing [[nodiscard]] StoryId generate( @@ -76,7 +83,7 @@ public: not_null> media); private: - [[nodiscard]] StoriesList parse(const MTPUserStories &data); + [[nodiscard]] StoriesList parse(const MTPUserStories &stories); [[nodiscard]] std::optional parse(const MTPDstoryItem &data); void pushToBack(StoriesList &&list); @@ -85,6 +92,7 @@ private: const not_null _owner; std::vector _all; + rpl::event_stream<> _allChanged; QString _state; bool _allLoaded = false; diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index f1ae4dbaa..c520e9de3 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -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); + } +} diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index b763ca574..5c29a8a36 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -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) : RpWidget(parent) , _controller(controller) +, _stories(std::make_unique( + 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) { diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 6b01e940f..56e71ec33 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -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 folder); void refreshWithCollapsedRows(bool toTop = false); @@ -309,7 +314,9 @@ private: void fillArchiveSearchMenu(not_null 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 _controller; + const std::unique_ptr _stories; + not_null _shownList; FilterId _filterId = 0; bool _mouseSelection = false; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp new file mode 100644 index 000000000..d4844a582 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp @@ -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 peer); + + QImage image(int size) override; + void subscribeToUpdates(Fn callback) override; + +private: + struct Subscribed { + explicit Subscribed(Fn callback) + : callback(std::move(callback)) { + } + + Ui::PeerUserpicView view; + Fn callback; + InMemoryKey key; + rpl::lifetime photoLifetime; + rpl::lifetime downloadLifetime; + }; + + [[nodiscard]] bool waitingUserpicLoad() const; + void processNewPhoto(); + + const not_null _peer; + QImage _frame; + std::unique_ptr _subscribed; + +}; + +class State final { +public: + explicit State(not_null data); + + [[nodiscard]] Content next(); + +private: + const not_null _data; + base::flat_map, std::shared_ptr> _userpics; + +}; + +PeerUserpic::PeerUserpic(not_null 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 callback) { + if (!callback) { + _subscribed = nullptr; + return; + } + _subscribed = std::make_unique(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) +: _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(); + 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(); + if (const auto i = _userpics.find(user); i != end(_userpics)) { + userpic = i->second; + } else { + userpic = std::make_shared(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 ContentForSession(not_null session) { + return [=](auto consumer) { + auto result = rpl::lifetime(); + const auto stories = &session->data().stories(); + const auto state = result.make_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 diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h new file mode 100644 index 000000000..dc81f2528 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h @@ -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 ContentForSession( + not_null session); + +} // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp new file mode 100644 index 000000000..b87d4b5de --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -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 parent, + rpl::producer content, + Fn 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 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) { + 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 diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h new file mode 100644 index 000000000..c0b80501e --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -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 callback) = 0; +}; + +struct User { + uint64 id = 0; + QString name; + std::shared_ptr userpic; + bool unread = false; + + friend inline bool operator==(const User &a, const User &b) = default; +}; + +struct Content { + std::vector users; + + friend inline bool operator==( + const Content &a, + const Content &b) = default; +}; + +class List final : public Ui::RpWidget { +public: + List( + not_null parent, + rpl::producer content, + Fn shownHeight); + + [[nodiscard]] rpl::producer 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 _items; + Fn _shownHeight = 0; + rpl::event_stream _clicks; + rpl::event_stream<> _expandRequests; + int _scrollLeft = 0; + +}; + +} // namespace Dialogs::Stories diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 404f72080..08b9990c4 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -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