diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index bbfb2acb1..6f27553d0 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -889,9 +889,11 @@ void PeerListRow::createCheckbox( } void PeerListRow::setCheckedInternal(bool checked, anim::type animated) { - Expects(_checkbox != nullptr); + Expects(!checked || _checkbox != nullptr); - _checkbox->setChecked(checked, animated); + if (_checkbox) { + _checkbox->setChecked(checked, animated); + } } void PeerListRow::setCustomizedCheckSegments( diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index 88f0fac9a..5831ec0b5 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -54,40 +54,6 @@ namespace { constexpr auto kSortByOnlineThrottle = 3 * crl::time(1000); constexpr auto kSearchPerPage = 50; -[[nodiscard]] std::vector PrepareSegments( - int count, - int unread, - const QBrush &unreadBrush) { - Expects(unread <= count); - Expects(count > 0); - - auto result = std::vector(); - const auto add = [&](bool unread) { - result.push_back({ - .brush = unread ? unreadBrush : st::dialogsUnreadBgMuted->b, - .width = (unread - ? st::dialogsStoriesFull.lineTwice / 2. - : st::dialogsStoriesFull.lineReadTwice / 2.), - }); - }; - result.reserve(count); - for (auto i = 0, till = count - unread; i != till; ++i) { - add(false); - } - for (auto i = 0; i != unread; ++i) { - add(true); - } - return result; -} - -[[nodiscard]] QBrush CreateStoriesGradient() { - const auto &st = st::contactsWithStories.item; - const auto left = st.photoPosition.x(); - const auto top = st.photoPosition.y(); - const auto size = st.photoSize; - return Ui::UnreadStoryOutlineGradient(QRectF(left, top, size, size)); -} - } // namespace object_ptr PrepareContactsBox( @@ -124,6 +90,39 @@ object_ptr PrepareContactsBox( return Box(std::move(controller), std::move(init)); } +QBrush PeerListStoriesGradient(const style::PeerList &st) { + const auto left = st.item.photoPosition.x(); + const auto top = st.item.photoPosition.y(); + const auto size = st.item.photoSize; + return Ui::UnreadStoryOutlineGradient(QRectF(left, top, size, size)); +} + +std::vector PeerListStoriesSegments( + int count, + int unread, + const QBrush &unreadBrush) { + Expects(unread <= count); + Expects(count > 0); + + auto result = std::vector(); + const auto add = [&](bool unread) { + result.push_back({ + .brush = unread ? unreadBrush : st::dialogsUnreadBgMuted->b, + .width = (unread + ? st::dialogsStoriesFull.lineTwice / 2. + : st::dialogsStoriesFull.lineReadTwice / 2.), + }); + }; + result.reserve(count); + for (auto i = 0, till = count - unread; i != till; ++i) { + add(false); + } + for (auto i = 0; i != unread; ++i) { + add(true); + } + return result; +} + void PeerListRowWithLink::setActionLink(const QString &action) { _action = action; refreshActionLink(); @@ -359,6 +358,115 @@ bool ChatsListBoxController::appendRow(not_null history) { return false; } +PeerListStories::PeerListStories( + not_null controller, + not_null session) +: _controller(controller) +, _session(session) { +} + +void PeerListStories::updateColors() { + for (auto i = begin(_counts); i != end(_counts); ++i) { + if (const auto row = _delegate->peerListFindRow(i->first)) { + if (i->second.count >= 0 && i->second.unread >= 0) { + applyForRow(row, i->second.count, i->second.unread, true); + } + } + } +} + +void PeerListStories::updateFor( + uint64 id, + int count, + int unread) { + if (const auto row = _delegate->peerListFindRow(id)) { + applyForRow(row, count, unread); + _delegate->peerListUpdateRow(row); + } +} + +void PeerListStories::process(not_null row) { + const auto user = row->peer()->asUser(); + if (!user) { + return; + } + const auto stories = &_session->data().stories(); + const auto source = stories->source(user->id); + const auto count = source + ? int(source->ids.size()) + : user->hasActiveStories() + ? 1 + : 0; + const auto unread = source + ? source->info().unreadCount + : user->hasUnreadStories() + ? 1 + : 0; + applyForRow(row, count, unread, true); +} + +bool PeerListStories::handleClick(not_null peer) { + const auto point = _delegate->peerListLastRowMousePosition(); + const auto &st = _controller->listSt()->item; + if (point && point->x() < st.photoPosition.x() + st.photoSize) { + if (const auto window = peer->session().tryResolveWindow()) { + if (const auto user = peer->asUser()) { + if (user->hasActiveStories()) { + window->openPeerStories(peer->id); + return true; + } + } + } + } + return false; +} + +void PeerListStories::prepare(not_null delegate) { + _delegate = delegate; + + _unreadBrush = PeerListStoriesGradient(*_controller->listSt()); + style::PaletteChanged() | rpl::start_with_next([=] { + _unreadBrush = PeerListStoriesGradient(*_controller->listSt()); + updateColors(); + }, _lifetime); + + _session->changes().peerUpdates( + Data::PeerUpdate::Flag::StoriesState + ) | rpl::start_with_next([=](const Data::PeerUpdate &update) { + const auto id = update.peer->id.value; + if (const auto row = _delegate->peerListFindRow(id)) { + process(row); + } + }, _lifetime); + + const auto stories = &_session->data().stories(); + stories->sourceChanged() | rpl::start_with_next([=](PeerId id) { + const auto source = stories->source(id); + const auto info = source + ? source->info() + : Data::StoriesSourceInfo(); + updateFor(id.value, info.count, info.unreadCount); + }, _lifetime); +} + +void PeerListStories::applyForRow( + not_null row, + int count, + int unread, + bool force) { + auto &counts = _counts[row->id()]; + if (!force && counts.count == count && counts.unread == unread) { + return; + } + counts.count = count; + counts.unread = unread; + _delegate->peerListSetRowChecked(row, count > 0); + if (count > 0) { + row->setCustomizedCheckSegments( + PeerListStoriesSegments(count, unread, _unreadBrush)); + } +} + ContactsBoxController::ContactsBoxController( not_null session) : ContactsBoxController( @@ -385,31 +493,8 @@ void ContactsBoxController::prepare() { prepareViewHook(); - if (_storiesShown) { - _storiesUnread = CreateStoriesGradient(); - style::PaletteChanged() | rpl::start_with_next([=] { - _storiesUnread = CreateStoriesGradient(); - for (auto &entry : _storiesCounts) { - entry.second.count = entry.second.unread = -1; - } - updateStories(); - }, lifetime()); - - const auto stories = &session().data().stories(); - rpl::merge( - rpl::single(rpl::empty), - stories->sourcesChanged(Data::StorySourcesList::NotHidden), - stories->sourcesChanged(Data::StorySourcesList::Hidden) - ) | rpl::start_with_next([=] { - updateStories(); - }, lifetime()); - stories->sourceChanged() | rpl::start_with_next([=](PeerId id) { - const auto source = stories->source(id); - const auto info = source - ? source->info() - : Data::StoriesSourceInfo(); - updateStoriesFor(id.value, info.count, info.unreadCount); - }, lifetime()); + if (_stories) { + _stories->prepare(delegate()); } session().data().contactsLoaded().value( @@ -456,16 +541,10 @@ std::unique_ptr ContactsBoxController::createSearchRow( void ContactsBoxController::rowClicked(not_null row) { const auto peer = row->peer(); - if (const auto window = peer->session().tryResolveWindow()) { - if (_storiesShown) { - const auto point = delegate()->peerListLastRowMousePosition(); - const auto &st = st::contactsWithStories.item; - if (point && point->x() < st.photoPosition.x() + st.photoSize) { - window->openPeerStories(peer->id); - return; - } - } - window->showPeerHistory(row->peer()); + if (_stories && _stories->handleClick(peer)) { + return; + } else if (const auto window = peer->session().tryResolveWindow()) { + window->showPeerHistory(peer); } } @@ -491,52 +570,7 @@ void ContactsBoxController::setSortMode(SortMode mode) { } void ContactsBoxController::setStoriesShown(bool shown) { - _storiesShown = shown; -} - -void ContactsBoxController::updateStories() { - const auto stories = &_session->data().stories(); - const auto &a = stories->sources(Data::StorySourcesList::NotHidden); - const auto &b = stories->sources(Data::StorySourcesList::Hidden); - auto checked = base::flat_set(); - for (const auto &info : ranges::views::concat(a, b)) { - const auto id = info.id.value; - checked.emplace(id); - updateStoriesFor(id, info.count, info.unreadCount); - } - for (auto i = begin(_storiesCounts); i != end(_storiesCounts); ++i) { - if (i->second.count && !checked.contains(i->first)) { - updateStoriesFor(i->first, 0, 0); - } - } -} - -void ContactsBoxController::updateStoriesFor( - uint64 id, - int count, - int unread) { - if (const auto row = delegate()->peerListFindRow(id)) { - applyRowStories(row, count, unread); - delegate()->peerListUpdateRow(row); - } -} - -void ContactsBoxController::applyRowStories( - not_null row, - int count, - int unread, - bool force) { - auto &counts = _storiesCounts[row->id()]; - if (!force && counts.count == count && counts.unread == unread) { - return; - } - counts.count = count; - counts.unread = unread; - delegate()->peerListSetRowChecked(row, count > 0); - if (count > 0) { - row->setCustomizedCheckSegments( - PrepareSegments(count, unread, _storiesUnread)); - } + _stories = std::make_unique(this, _session); } void ContactsBoxController::sort() { @@ -586,12 +620,8 @@ bool ContactsBoxController::appendRow(not_null user) { if (auto row = createRow(user)) { const auto raw = row.get(); delegate()->peerListAppendRow(std::move(row)); - if (_storiesShown) { - const auto stories = &session().data().stories(); - if (const auto source = stories->source(user->id)) { - const auto info = source->info(); - applyRowStories(raw, info.count, info.unreadCount, true); - } + if (_stories) { + _stories->process(raw); } return true; } diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.h b/Telegram/SourceFiles/boxes/peer_list_controllers.h index 46d1a184c..0ba108f01 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.h +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.h @@ -20,12 +20,21 @@ class Forum; class ForumTopic; } // namespace Data +namespace Ui { +struct OutlineSegment; +} // namespace Ui + namespace Window { class SessionController; } // namespace Window [[nodiscard]] object_ptr PrepareContactsBox( not_null sessionController); +[[nodiscard]] QBrush PeerListStoriesGradient(const style::PeerList &st); +[[nodiscard]] std::vector PeerListStoriesSegments( + int count, + int unread, + const QBrush &unreadBrush); class PeerListRowWithLink : public PeerListRow { public: @@ -116,6 +125,41 @@ private: }; +class PeerListStories final { +public: + PeerListStories( + not_null controller, + not_null session); + + void prepare(not_null delegate); + + void process(not_null row); + bool handleClick(not_null peer); + +private: + struct Counts { + int count = 0; + int unread = 0; + }; + + void updateColors(); + void updateFor(uint64 id, int count, int unread); + void applyForRow( + not_null row, + int count, + int unread, + bool force = false); + + const not_null _controller; + const not_null _session; + PeerListDelegate *_delegate = nullptr; + + QBrush _unreadBrush; + base::flat_map _counts; + rpl::lifetime _lifetime; + +}; + class ContactsBoxController : public PeerListController { public: explicit ContactsBoxController(not_null session); @@ -129,7 +173,7 @@ public: not_null peer) override final; void rowClicked(not_null row) override; bool trackSelectedList() override { - return !_storiesShown; + return !_stories; } enum class SortMode { @@ -147,32 +191,19 @@ protected: } private: - struct StoriesCount { - int count = 0; - int unread = 0; - }; void sort(); void sortByName(); void sortByOnline(); void rebuildRows(); - void updateStories(); void checkForEmptyRows(); bool appendRow(not_null user); - void updateStoriesFor(uint64 id, int count, int unread); - void applyRowStories( - not_null row, - int count, - int unread, - bool force = false); const not_null _session; SortMode _sortMode = SortMode::Alphabet; base::Timer _sortByOnlineTimer; rpl::lifetime _sortByOnlineLifetime; - QBrush _storiesUnread; - base::flat_map _storiesCounts; - bool _storiesShown = false; + std::unique_ptr _stories; }; diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index 94caec150..c35fc9fbb 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -25,11 +25,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_indexed_list.h" #include "data/data_peer_values.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_user.h" #include "data/data_changes.h" #include "base/unixtime.h" +#include "ui/effects/outline_segments.h" #include "ui/widgets/popup_menu.h" #include "ui/ui_utility.h" #include "info/profile/info_profile_values.h" @@ -900,7 +902,11 @@ void ParticipantsBoxController::setupListChangeViewers() { return (row.peer() == user); }); } else if (auto row = createRow(user)) { + const auto raw = row.get(); delegate()->peerListPrependRow(std::move(row)); + if (_stories) { + _stories->process(raw); + } refreshRows(); if (_onlineSorter) { _onlineSorter->sort(); @@ -1160,8 +1166,14 @@ void ParticipantsBoxController::restoreState( loadMoreRows(); } PeerListController::restoreState(std::move(state)); - if (delegate()->peerListFullRowsCount() > 0 || _allLoaded) { + const auto count = delegate()->peerListFullRowsCount(); + if (count > 0 || _allLoaded) { refreshDescription(); + if (_stories) { + for (auto i = 0; i != count; ++i) { + _stories->process(delegate()->peerListRowAt(i)); + } + } } if (_onlineSorter) { _onlineSorter->sort(); @@ -1177,14 +1189,21 @@ rpl::producer ParticipantsBoxController::fullCountValue() const { return _fullCountValue.value(); } +void ParticipantsBoxController::setStoriesShown(bool shown) { + _stories = std::make_unique( + this, + &_navigation->session()); +} + void ParticipantsBoxController::prepare() { auto title = [&] { switch (_role) { case Role::Admins: return tr::lng_channel_admins(); case Role::Profile: - case Role::Members: return (_peer->isChannel() && !_peer->isMegagroup() - ? tr::lng_profile_subscribers_section() - : tr::lng_profile_participants_section()); + case Role::Members: + return ((_peer->isChannel() && !_peer->isMegagroup()) + ? tr::lng_profile_subscribers_section() + : tr::lng_profile_participants_section()); case Role::Restricted: return tr::lng_exceptions_list_title(); case Role::Kicked: return tr::lng_removed_list_title(); } @@ -1207,6 +1226,10 @@ void ParticipantsBoxController::prepare() { setDescriptionText(tr::lng_contacts_loading(tr::now)); setSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now)); + if (_stories) { + _stories->prepare(delegate()); + } + if (_role == Role::Profile) { auto visible = _peer->isMegagroup() ? Info::Profile::CanViewParticipantsValue(_peer->asMegagroup()) @@ -1319,7 +1342,11 @@ void ParticipantsBoxController::rebuildChatParticipants( } for (const auto &user : participants) { if (auto row = createRow(user)) { + const auto raw = row.get(); delegate()->peerListAppendRow(std::move(row)); + if (_stories) { + _stories->process(raw); + } } } _onlineSorter->sort(); @@ -1373,7 +1400,11 @@ void ParticipantsBoxController::rebuildChatAdmins( } for (const auto user : list) { if (auto row = createRow(user)) { + const auto raw = row.get(); delegate()->peerListAppendRow(std::move(row)); + if (_stories) { + _stories->process(raw); + } } } @@ -1543,6 +1574,11 @@ bool ParticipantsBoxController::feedMegagroupLastParticipants() { void ParticipantsBoxController::rowClicked(not_null row) { const auto participant = row->peer(); const auto user = participant->asUser(); + + if (_stories && _stories->handleClick(participant)) { + return; + } + if (_role == Role::Admins) { Assert(user != nullptr); showAdmin(user); @@ -1890,7 +1926,11 @@ bool ParticipantsBoxController::appendRow(not_null participant) { recomputeTypeFor(participant); return false; } else if (auto row = createRow(participant)) { + const auto raw = row.get(); delegate()->peerListAppendRow(std::move(row)); + if (_stories) { + _stories->process(raw); + } if (_role != Role::Kicked) { setDescriptionText(QString()); } @@ -1906,10 +1946,17 @@ bool ParticipantsBoxController::prependRow(not_null participant) { if (_role == Role::Admins) { // Perhaps we've added a new admin from search. delegate()->peerListPrependRowFromSearchResult(row); + if (_stories) { + _stories->process(row); + } } return false; } else if (auto row = createRow(participant)) { + const auto raw = row.get(); delegate()->peerListPrependRow(std::move(row)); + if (_stories) { + _stories->process(raw); + } if (_role != Role::Kicked) { setDescriptionText(QString()); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h index 30b445813..0882f214a 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/weak_ptr.h" #include "info/profile/info_profile_members_controllers.h" +class PeerListStories; struct ChatAdminRightsInfo; struct ChatRestrictionsInfo; @@ -174,6 +175,9 @@ public: QWidget *parent, not_null row) override; void loadMoreRows() override; + bool trackSelectedList() override { + return !_stories; + } void peerListSearchAddRow(not_null peer) override; std::unique_ptr createSearchRow( @@ -187,6 +191,8 @@ public: [[nodiscard]] rpl::producer onlineCountValue() const; [[nodiscard]] rpl::producer fullCountValue() const; + void setStoriesShown(bool shown); + protected: // Allow child controllers not providing navigation. // This is their responsibility to override all methods that use it. @@ -288,6 +294,8 @@ private: Ui::BoxPointer _addBox; QPointer _editParticipantBox; + std::unique_ptr _stories; + }; // Members, banned and restricted users server side search. diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index 3f0e587dd..d4fee22c8 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -85,26 +85,27 @@ struct PeerUpdate { SupportInfo = (1ULL << 23), IsBot = (1ULL << 24), EmojiStatus = (1ULL << 25), + StoriesState = (1ULL << 26), // For chats and channels - InviteLinks = (1ULL << 26), - Members = (1ULL << 27), - Admins = (1ULL << 28), - BannedUsers = (1ULL << 29), - Rights = (1ULL << 30), - PendingRequests = (1ULL << 31), - Reactions = (1ULL << 32), + InviteLinks = (1ULL << 27), + Members = (1ULL << 28), + Admins = (1ULL << 29), + BannedUsers = (1ULL << 30), + Rights = (1ULL << 31), + PendingRequests = (1ULL << 32), + Reactions = (1ULL << 33), // For channels - ChannelAmIn = (1ULL << 33), - StickersSet = (1ULL << 34), - ChannelLinkedChat = (1ULL << 35), - ChannelLocation = (1ULL << 36), - Slowmode = (1ULL << 37), - GroupCall = (1ULL << 38), + ChannelAmIn = (1ULL << 34), + StickersSet = (1ULL << 35), + ChannelLinkedChat = (1ULL << 36), + ChannelLocation = (1ULL << 37), + Slowmode = (1ULL << 38), + GroupCall = (1ULL << 39), // For iteration - LastUsedBit = (1ULL << 38), + LastUsedBit = (1ULL << 39), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index d25f37780..09c66a922 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -155,6 +155,7 @@ void UserData::setStoriesState(StoriesState state) { if (const auto history = owner().historyLoaded(this)) { history->updateChatListEntryPostponed(); } + session().changes().peerUpdated(this, UpdateFlag::StoriesState); } } diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index eb746c3c5..c5567dd79 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -488,6 +488,15 @@ infoMembersList: PeerList(defaultPeerList) { photoPosition: point(18px, 6px); namePosition: point(79px, 11px); statusPosition: point(79px, 31px); + checkbox: RoundImageCheckbox(defaultPeerListCheckbox) { + selectExtendTwice: 1px; + imageRadius: 21px; + imageSmallRadius: 19px; + check: RoundCheckbox(defaultPeerListCheck) { + size: 0px; + } + } + nameFgChecked: contactsNameFg; } } infoMembersButtonPosition: point(12px, 0px); diff --git a/Telegram/SourceFiles/info/profile/info_profile_members.cpp b/Telegram/SourceFiles/info/profile/info_profile_members.cpp index 605ebbad5..b226935c3 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_members.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_members.cpp @@ -50,6 +50,7 @@ Members::Members( , _controller(controller) , _peer(_controller->key().peer()) , _listController(CreateMembersController(controller, _peer)) { + _listController->setStoriesShown(true); setupHeader(); setupList(); setContent(_list.data()); @@ -232,6 +233,7 @@ void Members::setupButtons() { void Members::setupList() { auto topSkip = _header ? _header->height() : 0; _listController->setStyleOverrides(&st::infoMembersList); + _listController->setStoriesShown(true); _list = object_ptr( this, _listController.get());