diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 2bf9d92438..d76ed8016e 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -791,10 +791,10 @@ PRIVATE dialogs/ui/dialogs_topics_view.h dialogs/ui/dialogs_video_userpic.cpp dialogs/ui/dialogs_video_userpic.h - dialogs/dialogs_community_chats.cpp - dialogs/dialogs_community_chats.h dialogs/dialogs_community_chats_list.cpp dialogs/dialogs_community_chats_list.h + dialogs/dialogs_community_rows_view.cpp + dialogs/dialogs_community_rows_view.h dialogs/dialogs_entry.cpp dialogs/dialogs_entry.h dialogs/dialogs_indexed_list.cpp diff --git a/Telegram/SourceFiles/api/api_chat_filters.cpp b/Telegram/SourceFiles/api/api_chat_filters.cpp index cbfacbc0a0..2c720f0629 100644 --- a/Telegram/SourceFiles/api/api_chat_filters.cpp +++ b/Telegram/SourceFiles/api/api_chat_filters.cpp @@ -386,7 +386,8 @@ void ToggleChatsController::prepare() { return peer->isChat() ? peer->asChat()->isForbidden() : peer->isChannel() - ? peer->asChannel()->isForbidden() + ? (peer->asChannel()->isForbidden() + || peer->asChannel()->isCommunity()) : false; }; const auto add = [&](not_null peer, bool additional = false) { diff --git a/Telegram/SourceFiles/api/api_communities.cpp b/Telegram/SourceFiles/api/api_communities.cpp index 0c4b21c314..4293f46918 100644 --- a/Telegram/SourceFiles/api/api_communities.cpp +++ b/Telegram/SourceFiles/api/api_communities.cpp @@ -92,8 +92,7 @@ void Communities::addPeerLink( togglePeerLink( community, peer, - visible, - false, + visible ? PeerLinkAction::Visible : PeerLinkAction::Hidden, std::move(done), std::move(fail)); } @@ -106,8 +105,7 @@ void Communities::removePeerLink( togglePeerLink( community, peer, - std::nullopt, - true, + PeerLinkAction::Deleted, std::move(done), std::move(fail)); } @@ -115,14 +113,15 @@ void Communities::removePeerLink( void Communities::togglePeerLink( not_null community, not_null peer, - std::optional visible, - bool remove, + PeerLinkAction action, Fn done, Fn fail) { using Flag = MTPcommunities_TogglePeerLink::Flag; - const auto flags = (remove ? Flag::f_deleted : Flag()) - | (visible.value_or(false) ? Flag::f_visible : Flag()) - | ((visible && !*visible) ? Flag::f_hidden : Flag()); + const auto flags = (action == PeerLinkAction::Deleted) + ? Flag::f_deleted + : (action == PeerLinkAction::Hidden) + ? Flag::f_hidden + : Flag::f_visible; _api.request(MTPcommunities_TogglePeerLink( MTP_flags(flags), community->inputChannel(), @@ -180,8 +179,11 @@ void Communities::toggleCollapsedInDialogs( if (!_collapseRequests.emplace(community).second) { return; } + const auto history = community->owner().history(community); const auto was = (community->flags() & ChannelDataFlag::CommunityCollapsed) != 0; + const auto wasPinned = history->folderKnown() + && history->isPinnedDialog(FilterId()); const auto apply = [=](bool value) { if (value) { community->addFlags(ChannelDataFlag::CommunityCollapsed); @@ -189,13 +191,10 @@ void Communities::toggleCollapsedInDialogs( community->removeFlags(ChannelDataFlag::CommunityCollapsed); } }; + + // Clearing the flag drops the grouped row from the chat list, which + // self-unpins it; the server's updateDialogPinned confirms it on done. apply(collapsed); - if (!collapsed) { - const auto history = community->owner().history(community); - if (history->folderKnown() && history->isPinnedDialog(FilterId())) { - community->owner().setChatPinned(history, FilterId(), false); - } - } using Flag = MTPcommunities_ToggleCommunityCollapsedInDialogs::Flag; _api.request(MTPcommunities_ToggleCommunityCollapsedInDialogs( MTP_flags(collapsed ? Flag::f_collapsed : Flag()), @@ -206,6 +205,9 @@ void Communities::toggleCollapsedInDialogs( }).fail([=] { _collapseRequests.remove(community); apply(was); + if (wasPinned) { + community->owner().setChatPinned(history, FilterId(), true); + } }).send(); } @@ -214,11 +216,17 @@ void Communities::requestPeerLinkRequests( const QString &offset, int limit, Fn done) { - _api.request(MTPcommunities_GetPeerLinkRequests( + const auto i = _peerLinkRequestsRequests.find(community); + if (i != end(_peerLinkRequestsRequests)) { + _api.request(i->second).cancel(); + _peerLinkRequestsRequests.erase(i); + } + const auto requestId = _api.request(MTPcommunities_GetPeerLinkRequests( community->inputChannel(), MTP_string(offset), MTP_int(limit) )).done([=](const MTPcommunities_PeerLinkRequests &result) { + _peerLinkRequestsRequests.remove(community); const auto &data = result.data(); auto &owner = _session->data(); owner.processUsers(data.vusers()); @@ -244,10 +252,12 @@ void Communities::requestPeerLinkRequests( done(std::move(slice)); } }).fail([=] { + _peerLinkRequestsRequests.remove(community); if (done) { done({}); } }).send(); + _peerLinkRequestsRequests[community] = requestId; } void Communities::togglePeerLinkRequestApproval( @@ -262,10 +272,16 @@ void Communities::togglePeerLinkRequestApproval( community->inputChannel(), peer->input() )).done([=] { + // Optimistic decrement for instant feedback, then a forced fresh full + // fetch for the authoritative count and the newly-added member. A plain + // requestFullPeer() would be deduped against an in-flight full fetch (the + // surface requests it lazily) and the older stale response would clobber + // the count back to the pre-approval value; reloadFullPeer cancels that + // stale request and refetches. community->setPendingRequestsCount( std::max(community->pendingRequestsCount() - 1, 0), QVector()); - _session->api().requestFullPeer(community); + _session->api().reloadFullPeer(community); if (done) { done(); } @@ -286,8 +302,11 @@ void Communities::toggleAllPeerLinkRequestApproval( MTP_flags(reject ? Flag::f_reject : Flag()), community->inputChannel() )).done([=] { + // Optimistic reset, then a forced fresh full fetch for the authoritative + // state; see togglePeerLinkRequestApproval for why reloadFullPeer (not a + // dedupable requestFullPeer) is used here. community->setPendingRequestsCount(0, QVector()); - _session->api().requestFullPeer(community); + _session->api().reloadFullPeer(community); if (done) { done(); } diff --git a/Telegram/SourceFiles/api/api_communities.h b/Telegram/SourceFiles/api/api_communities.h index c8a142f75b..b38edee500 100644 --- a/Telegram/SourceFiles/api/api_communities.h +++ b/Telegram/SourceFiles/api/api_communities.h @@ -41,6 +41,12 @@ struct CommunityParticipantJoinedChats { std::vector> joinedChats; }; +enum class PeerLinkAction { + Visible, + Hidden, + Deleted, +}; + class Communities final { public: explicit Communities(not_null api); @@ -105,8 +111,7 @@ private: void togglePeerLink( not_null community, not_null peer, - std::optional visible, - bool remove, + PeerLinkAction action, Fn done, Fn fail); @@ -114,6 +119,9 @@ private: MTP::Sender _api; base::flat_set> _collapseRequests; + base::flat_map< + not_null, + mtpRequestId> _peerLinkRequestsRequests; mtpRequestId _joinedRequestId = 0; }; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index e4c5c202ea..b7c62a27bf 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -1227,6 +1227,18 @@ void ApiWrap::requestFullPeer(not_null peer) { _fullPeerRequests.emplace(peer, requestId); } +void ApiWrap::reloadFullPeer(not_null peer) { + // Force a fresh full-peer fetch even if one is already in flight, so the + // result reflects the latest server state instead of a possibly stale + // in-flight response (used after a mutation like approving a join request). + if (const auto i = _fullPeerRequests.find(peer) + ; i != end(_fullPeerRequests)) { + request(i->second).cancel(); + _fullPeerRequests.erase(i); + } + requestFullPeer(peer); +} + void ApiWrap::processFullPeer( not_null peer, const MTPmessages_ChatFull &result) { diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index ff87850843..cbc9a1328d 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -202,6 +202,7 @@ public: Fn fail); void requestFullPeer(not_null peer); + void reloadFullPeer(not_null peer); void requestPeerSettings(not_null peer); using UpdatedFileReferences = Data::UpdatedFileReferences; diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.cpp b/Telegram/SourceFiles/boxes/choose_filter_box.cpp index ae2479bb99..0c4a5a9558 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/choose_filter_box.cpp @@ -209,11 +209,15 @@ ChooseFilterValidator::ChooseFilterValidator(not_null history) : _history(history) { } -bool ChooseFilterValidator::canAdd() const { - if (const auto channel = _history->peer->asChannel() - ; channel +bool ChooseFilterValidator::communityAddBlocked() const { + const auto channel = _history->peer->asChannel(); + return channel && channel->isCommunity() - && !channel->collapsedInDialogs()) { + && !channel->collapsedInDialogs(); +} + +bool ChooseFilterValidator::canAdd() const { + if (communityAddBlocked()) { return false; } for (const auto &filter : _history->owner().chatsFilters().list()) { @@ -227,10 +231,7 @@ bool ChooseFilterValidator::canAdd() const { bool ChooseFilterValidator::canAdd(FilterId filterId) const { Expects(filterId != 0); - if (const auto channel = _history->peer->asChannel() - ; channel - && channel->isCommunity() - && !channel->collapsedInDialogs()) { + if (communityAddBlocked()) { return false; } const auto list = _history->owner().chatsFilters().list(); diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.h b/Telegram/SourceFiles/boxes/choose_filter_box.h index 59cff96333..505f171c06 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.h +++ b/Telegram/SourceFiles/boxes/choose_filter_box.h @@ -41,6 +41,8 @@ public: void remove(FilterId filterId) const; private: + [[nodiscard]] bool communityAddBlocked() const; + const not_null _history; }; diff --git a/Telegram/SourceFiles/boxes/peers/community_box.cpp b/Telegram/SourceFiles/boxes/peers/community_box.cpp index a1cc7447c5..8d51d0c1b8 100644 --- a/Telegram/SourceFiles/boxes/peers/community_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/community_box.cpp @@ -43,6 +43,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { +[[nodiscard]] std::unique_ptr MakeCommunityChatRow( + not_null peer) { + auto row = std::make_unique(peer); + const auto channel = peer->asChannel(); + if (channel && channel->membersCountKnown()) { + row->setCustomStatus(tr::lng_chat_status_members( + tr::now, + lt_count_decimal, + channel->membersCount())); + } + return row; +} + class ChatsController final : public PeerListController { public: ChatsController( @@ -89,15 +102,7 @@ void ChatsController::prepare() { delegate()->peerListFullRowsCount() - 1)); } for (const auto &peer : list) { - auto row = std::make_unique(peer); - const auto channel = peer->asChannel(); - if (channel && channel->membersCountKnown()) { - row->setCustomStatus(tr::lng_chat_status_members( - tr::now, - lt_count_decimal, - channel->membersCount())); - } - delegate()->peerListAppendRow(std::move(row)); + delegate()->peerListAppendRow(MakeCommunityChatRow(peer)); } delegate()->peerListRefreshRows(); _count = int(list.size()); @@ -339,14 +344,7 @@ void ChooseChatController::prepare() { if (delegate()->peerListFindRow(channel->id.value)) { continue; } - auto row = std::make_unique(channel); - if (channel->membersCountKnown()) { - row->setCustomStatus(tr::lng_chat_status_members( - tr::now, - lt_count_decimal, - channel->membersCount())); - } - delegate()->peerListAppendRow(std::move(row)); + delegate()->peerListAppendRow(MakeCommunityChatRow(channel)); } delegate()->peerListRefreshRows(); } diff --git a/Telegram/SourceFiles/boxes/peers/community_pending_requests_box.cpp b/Telegram/SourceFiles/boxes/peers/community_pending_requests_box.cpp index 6f9d7780fb..9fc707661f 100644 --- a/Telegram/SourceFiles/boxes/peers/community_pending_requests_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/community_pending_requests_box.cpp @@ -48,17 +48,7 @@ constexpr auto kPerPage = 100; constexpr auto kAcceptButton = 1; constexpr auto kRejectButton = 2; constexpr auto kUndoToastDuration = crl::time(3000); - -struct PendingAction { - enum class Stage { - Pending, - Performed, - Undone, - }; - not_null peer; - bool reject = false; - Stage stage = Stage::Pending; -}; +constexpr auto kPendingRowOpacity = 0.4; [[nodiscard]] object_ptr MakeUserpicToastIcon( not_null peer, @@ -183,10 +173,13 @@ struct PendingAction { return result; } +class Row; + class RowDelegate { public: [[nodiscard]] virtual QSize rowAcceptButtonSize() = 0; [[nodiscard]] virtual QSize rowRejectButtonSize() = 0; + virtual void rowUpdateRow(not_null row) = 0; virtual void rowPaintAccept( Painter &p, QRect geometry, @@ -207,6 +200,13 @@ public: not_null delegate, const Api::CommunityPeerRequest &request); + [[nodiscard]] bool pending() const { + return _pending; + } + void setPending(bool pending); + + float64 opacity() override; + int elementsCount() const override; QRect elementGeometry(int element, int outerWidth) const override; bool elementDisabled(int element) const override; @@ -226,6 +226,7 @@ private: const not_null _delegate; std::unique_ptr _acceptRipple; std::unique_ptr _rejectRipple; + bool _pending = false; }; @@ -253,6 +254,21 @@ Row::Row( setCustomStatus(status); } +void Row::setPending(bool pending) { + if (_pending == pending) { + return; + } + _pending = pending; + if (_pending) { + elementsStopLastRipple(); + } + _delegate->rowUpdateRow(this); +} + +float64 Row::opacity() { + return _pending ? kPendingRowOpacity : 1.; +} + int Row::elementsCount() const { return 2; } @@ -276,7 +292,7 @@ QRect Row::elementGeometry(int element, int outerWidth) const { } bool Row::elementDisabled(int element) const { - return false; + return _pending; } bool Row::elementOnlySelect(int element) const { @@ -326,6 +342,9 @@ void Row::elementsPaint( int outerWidth, bool selected, int selectedElement) { + if (_pending) { + return; + } const auto accept = elementGeometry(kAcceptButton, outerWidth); const auto reject = elementGeometry(kRejectButton, outerWidth); @@ -346,6 +365,18 @@ void Row::elementsPaint( over(kRejectButton)); } +struct PendingAction { + enum class Stage { + Pending, + Performed, + Undone, + }; + not_null row; + not_null peer; + bool reject = false; + Stage stage = Stage::Pending; +}; + class Controller final : public PeerListController , public RowDelegate @@ -370,6 +401,7 @@ public: QSize rowAcceptButtonSize() override; QSize rowRejectButtonSize() override; + void rowUpdateRow(not_null row) override; void rowPaintAccept( Painter &p, QRect geometry, @@ -403,7 +435,8 @@ private: const not_null _community; QPointer _toastParent; base::weak_ptr _toast; - std::shared_ptr _pending; + std::vector> _pending; + std::shared_ptr _current; QString _offset; bool _allLoaded = false; @@ -470,7 +503,14 @@ void Controller::loadMoreRows() { })); } +void Controller::rowUpdateRow(not_null row) { + delegate()->peerListUpdateRow(row); +} + void Controller::rowClicked(not_null row) { + if (static_cast(row.get())->pending()) { + return; + } const auto peer = row->peer(); if (const auto window = _navigation->parentController()) { window->showPeer(peer); @@ -490,16 +530,17 @@ void Controller::rowElementClicked( void Controller::startUndoable(not_null row, bool reject) { hideCurrentToast(); + const auto raw = static_cast(row.get()); const auto peer = row->peer(); - const auto id = peer->id.value; - delegate()->peerListSetRowHidden(row, true); - delegate()->peerListRefreshRows(); + raw->setPending(true); const auto action = std::make_shared(PendingAction{ + .row = raw, .peer = peer, .reject = reject, }); - _pending = action; + _pending.push_back(action); + _current = action; if (!_toastParent) { performNow(action); @@ -551,17 +592,20 @@ void Controller::startUndoable(not_null row, bool reject) { } }; const auto undo = [=] { - action->stage = PendingAction::Stage::Undone; - if (const auto restore = delegate()->peerListFindRow(id)) { - delegate()->peerListSetRowHidden(restore, false); - delegate()->peerListRefreshRows(); + if (action->stage != PendingAction::Stage::Pending) { + return; } + action->stage = PendingAction::Stage::Undone; + action->row->setPending(false); if (const auto strong = _toast.get()) { strong->hideAnimated(); } - if (_pending == action) { - _pending = nullptr; + if (_current == action) { + _current = nullptr; } + _pending.erase( + ranges::remove(_pending, action), + end(_pending)); }; const auto button = MakeUndoButton( widget.get(), @@ -587,9 +631,10 @@ void Controller::performNow(const std::shared_ptr &action) { return; } action->stage = PendingAction::Stage::Performed; - if (_pending == action) { - _pending = nullptr; + if (_current == action) { + _current = nullptr; } + _pending.erase(ranges::remove(_pending, action), end(_pending)); const auto peer = action->peer; const auto id = peer->id.value; const auto reject = action->reject; @@ -609,13 +654,20 @@ void Controller::performNow(const std::shared_ptr &action) { } void Controller::hideCurrentToast() { + // Perform the outgoing action explicitly instead of relying on the + // fading toast's destruction callback, which may never fire if the + // box is closed before the fade finishes. + if (const auto action = base::take(_current)) { + performNow(action); + } if (const auto strong = base::take(_toast).get()) { strong->hideAnimated(); } } void Controller::flushPendingOnClose() { - if (const auto action = base::take(_pending)) { + _current = nullptr; + for (const auto &action : base::take(_pending)) { performNow(action); } if (const auto strong = base::take(_toast).get()) { diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 0e069aa351..3ab9d85e50 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -225,6 +225,18 @@ void ChannelData::setFlags(ChannelDataFlags which) { } }); } + + // A membership change in a community member chat moves its history + // between the community's joined and other-linked lists. + if (const auto communityId = linkedCommunityId()) { + if (const auto community = owner().channelLoaded(communityId)) { + if (const auto info = community->communityInfo()) { + if (const auto history = owner().historyLoaded(this)) { + info->refreshOneMembership(history); + } + } + } + } } if (diff & (Flag::Forum | Flag::MonoforumAdmin diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 2a54f725c2..a72bd288ed 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -257,7 +257,7 @@ public: return flags() & Flag::Left; } [[nodiscard]] bool amIn() const { - return !isForbidden() && !haveLeft(); + return !isForbidden() && !haveLeft() && !isCommunity(); } [[nodiscard]] bool addsSignature() const { return flags() & Flag::Signatures; @@ -266,7 +266,7 @@ public: return flags() & Flag::SignatureProfiles; } [[nodiscard]] bool isForbidden() const { - return flags() & (Flag::Forbidden | Flag::Community); + return flags() & Flag::Forbidden; } [[nodiscard]] bool isVerified() const { return flags() & Flag::Verified; diff --git a/Telegram/SourceFiles/data/data_community.cpp b/Telegram/SourceFiles/data/data_community.cpp index d166b9b7f1..295e4ed893 100644 --- a/Telegram/SourceFiles/data/data_community.cpp +++ b/Telegram/SourceFiles/data/data_community.cpp @@ -155,14 +155,20 @@ void CommunityInfo::applyLinkedPeers(const QVector &list) { history->peer, &CommunityLinkedPeer::peer); }; - for (const auto &history : base::duplicate(_histories)) { - if (!stillLinked(history)) { - const auto channel = history->peer->asChannel(); - if (channel && channel->linkedCommunityId() == communityId) { - channel->setLinkedCommunityId(ChannelId()); + const auto unlinkStale = [&]( + const base::flat_set> &set) { + for (const auto &history : base::duplicate(set)) { + if (!stillLinked(history)) { + const auto channel = history->peer->asChannel(); + if (channel + && channel->linkedCommunityId() == communityId) { + channel->setLinkedCommunityId(ChannelId()); + } } } - } + }; + unlinkStale(_histories); + unlinkStale(_otherHistories); ++_chatListViewVersion; repaintRow(); _linkedPeersChanges.fire({}); @@ -183,7 +189,7 @@ bool CommunityInfo::isHidden(not_null peer) const { } bool CommunityInfo::collapsedInDialogs() const { - return _channel->flags() & ChannelDataFlag::CommunityCollapsed; + return _channel->collapsedInDialogs(); } void CommunityInfo::moveHistory( @@ -222,9 +228,58 @@ void CommunityInfo::ensureRowInChatList() { } void CommunityInfo::registerOne(not_null history) { - if (!_histories.emplace(history).second) { + const auto channel = history->peer->asChannel(); + if (channel && channel->amIn()) { + if (!_histories.emplace(history).second) { + return; + } + memberAdded(history); + } else if (!_otherHistories.emplace(history).second) { return; } + ensureRowInChatList(); + updateRowSortPosition(); +} + +void CommunityInfo::unregisterOne(not_null history) { + if (!_histories.remove(history)) { + _otherHistories.remove(history); + return; + } + if (history->chatListTimeId() >= _chatsListDate) { + recountChatsListDate(); + } + reorderLastHistories(); + updateRowSortPosition(); +} + +void CommunityInfo::refreshOneMembership(not_null history) { + const auto channel = history->peer->asChannel(); + if (channel && channel->amIn()) { + // A non-member linked chat the user just joined moves into the + // member aggregate. + if (_histories.contains(history) + || !_otherHistories.remove(history)) { + return; + } + _histories.emplace(history); + memberAdded(history); + updateRowSortPosition(); + } else { + // A member chat the user just left moves out of the aggregate. + if (!_histories.remove(history)) { + return; + } + _otherHistories.emplace(history); + if (history->chatListTimeId() >= _chatsListDate) { + recountChatsListDate(); + } + reorderLastHistories(); + updateRowSortPosition(); + } +} + +void CommunityInfo::memberAdded(not_null history) { const auto date = history->chatListTimeId(); if (date > _chatsListDate) { _chatsListDate = date; @@ -233,19 +288,6 @@ void CommunityInfo::registerOne(not_null history) { if (collapsedInDialogs()) { history->updateChatListExistence(); } - ensureRowInChatList(); - updateRowSortPosition(); -} - -void CommunityInfo::unregisterOne(not_null history) { - if (!_histories.remove(history)) { - return; - } - if (history->chatListTimeId() >= _chatsListDate) { - recountChatsListDate(); - } - reorderLastHistories(); - updateRowSortPosition(); } void CommunityInfo::oneChatsListDateChanged(TimeId was, TimeId now) { @@ -270,10 +312,6 @@ void CommunityInfo::oneUnreadStateChanged() { void CommunityInfo::recountChatsListDate() { auto result = TimeId(0); for (const auto &history : _histories) { - const auto channel = history->peer->asChannel(); - if (channel && !channel->amIn()) { - continue; - } result = std::max(result, history->chatListTimeId()); } _chatsListDate = result; @@ -291,10 +329,6 @@ void CommunityInfo::reorderLastHistories() { _lastHistories.reserve( std::min(int(_histories.size()), kShowChatNamesCount)); for (const auto &history : _histories) { - const auto channel = history->peer->asChannel(); - if (channel && !channel->amIn()) { - continue; - } const auto i = ranges::upper_bound(_lastHistories, history, pred); if (int(_lastHistories.size()) < kShowChatNamesCount || i != end(_lastHistories)) { diff --git a/Telegram/SourceFiles/data/data_community.h b/Telegram/SourceFiles/data/data_community.h index 060573368c..623ea7cbac 100644 --- a/Telegram/SourceFiles/data/data_community.h +++ b/Telegram/SourceFiles/data/data_community.h @@ -50,6 +50,7 @@ public: void registerOne(not_null history); void unregisterOne(not_null history); + void refreshOneMembership(not_null history); [[nodiscard]] auto histories() const -> const base::flat_set> & { return _histories; @@ -74,6 +75,7 @@ public: } private: + void memberAdded(not_null history); void recountChatsListDate(); void reorderLastHistories(); void updateRowSortPosition(); @@ -86,7 +88,12 @@ private: rpl::event_stream<> _linkedPeersChanges; rpl::event_stream<> _refreshed; + // Member chats (amIn()) the user is joined to; the source of the + // grouped row's aggregated badge / date / preview. Non-member linked + // chats whose History is loaded are tracked separately in + // _otherHistories so they never leak into those aggregates. base::flat_set> _histories; + base::flat_set> _otherHistories; std::vector> _lastHistories; Ui::Text::String _listEntryCache; int _listEntryCacheVersion = 0; diff --git a/Telegram/SourceFiles/data/notify/data_notify_settings.cpp b/Telegram/SourceFiles/data/notify/data_notify_settings.cpp index 027898fdaa..b6ca89de47 100644 --- a/Telegram/SourceFiles/data/notify/data_notify_settings.cpp +++ b/Telegram/SourceFiles/data/notify/data_notify_settings.cpp @@ -48,7 +48,7 @@ constexpr auto kMaxNotifyCheckDelay = 24 * 3600 * crl::time(1000); } else if (const auto chat = peer->asChat()) { return chat->isDeactivated() || chat->isForbidden(); } else if (const auto channel = peer->asChannel()) { - return channel->isForbidden(); + return channel->isForbidden() || channel->isCommunity(); } return false; } diff --git a/Telegram/SourceFiles/dialogs/dialogs_community_chats_list.cpp b/Telegram/SourceFiles/dialogs/dialogs_community_chats_list.cpp index 4f5eb4d9c3..3f9fe2b800 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_community_chats_list.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_community_chats_list.cpp @@ -36,13 +36,16 @@ CommunityChatsList::CommunityChatsList( , _st(&st::defaultDialogRow) { setMouseTracking(true); _view.setRepaint([=] { update(); }); - rebuild(); + // linkedPeersValue() fires immediately on subscription, which performs + // the first rebuild below. _community->linkedPeersValue( ) | rpl::on_next([=] { rebuild(); }, lifetime()); + // Member chats are not MainList rows here, so they repaint via the + // community's refreshed() signal (fired on aggregate/unread changes). _community->refreshed( ) | rpl::on_next([=] { update(); diff --git a/Telegram/SourceFiles/dialogs/dialogs_community_chats_list.h b/Telegram/SourceFiles/dialogs/dialogs_community_chats_list.h index a91777a941..5848bc4e1f 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_community_chats_list.h +++ b/Telegram/SourceFiles/dialogs/dialogs_community_chats_list.h @@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "dialogs/dialogs_community_chats.h" +#include "dialogs/dialogs_community_rows_view.h" #include "ui/rp_widget.h" class History; diff --git a/Telegram/SourceFiles/dialogs/dialogs_community_chats.cpp b/Telegram/SourceFiles/dialogs/dialogs_community_rows_view.cpp similarity index 98% rename from Telegram/SourceFiles/dialogs/dialogs_community_chats.cpp rename to Telegram/SourceFiles/dialogs/dialogs_community_rows_view.cpp index 37bcb593a0..6c455ade7f 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_community_chats.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_community_rows_view.cpp @@ -5,7 +5,7 @@ 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/dialogs_community_chats.h" +#include "dialogs/dialogs_community_rows_view.h" #include "data/data_forum.h" #include "dialogs/dialogs_row.h" diff --git a/Telegram/SourceFiles/dialogs/dialogs_community_chats.h b/Telegram/SourceFiles/dialogs/dialogs_community_rows_view.h similarity index 100% rename from Telegram/SourceFiles/dialogs/dialogs_community_chats.h rename to Telegram/SourceFiles/dialogs/dialogs_community_rows_view.h diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index e5e3378569..4f9b624710 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -611,6 +611,18 @@ bool InnerWidget::updateEntryHeight(not_null entry) { top += result.row->height(); } } + if (_openedCommunity) { + if (const auto history = entry->asHistory()) { + const auto recount = [&](CommunityRowsView &view) { + if (view.contains(history)) { + view.recountHeights(_narrowRatio); + changing = true; + } + }; + recount(_communityViewable); + recount(_communityRequestable); + } + } return _shownList->updateHeight(entry, _narrowRatio) || changing; } @@ -925,9 +937,10 @@ void InnerWidget::changeOpenedCommunity(Data::CommunityInfo *community) { clearSelection(); _openedCommunity = community; refreshShownList(); - rebuildCommunitySections(); _openedCommunityLifetime.destroy(); if (community) { + // linkedPeersValue() fires immediately on subscription, which + // performs the first rebuild + refresh below. community->linkedPeersValue( ) | rpl::on_next([=] { rebuildCommunitySections(); @@ -938,6 +951,8 @@ void InnerWidget::changeOpenedCommunity(Data::CommunityInfo *community) { ) | rpl::on_next([=] { update(); }, _openedCommunityLifetime); + } else { + rebuildCommunitySections(); } refreshWithCollapsedRows(true); if (_loadMoreCallback) { diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 1dcd839429..dca40e7ed9 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -12,7 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "data/data_messages.h" #include "dialogs/ui/dialogs_quick_action_context.h" -#include "dialogs/dialogs_community_chats.h" +#include "dialogs/dialogs_community_rows_view.h" #include "dialogs/dialogs_inner_widget_accessibility.h" #include "dialogs/dialogs_key.h" #include "lang/lang_keys.h" diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index f779461474..bc977ac569 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -1676,7 +1676,7 @@ void Widget::updateControlsVisibility(bool fast) { _frozenAccountBar->show(); } if (_chatFilters) { - _chatFilters->setVisible(!_openedForum); + _chatFilters->setVisible(!_openedForum && !_openedCommunity); } if (_openedFolder || _openedForum || _openedCommunity) { _subsectionTopBar->show(); @@ -1753,7 +1753,8 @@ void Widget::toggleFiltersMenu(bool enabled) { if (_layout == Layout::Child) { enabled = false; } - if (const auto id = controller()->windowId(); id.forum() || id.folder()) { + if (const auto id = controller()->windowId() + ; id.forum() || id.folder() || id.community()) { enabled = false; } if (!enabled == !_chatFilters) { @@ -3754,6 +3755,7 @@ bool Widget::applySearchState(SearchState state) { if (_chatFilters && (queryEmptyChanged || inChatChanged)) { _chatFilters->setVisible(_searchState.query.isEmpty() && !_openedForum + && !_openedCommunity && !searchInPeer()); updateControlsGeometry(); } diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index 66303e3391..4c2eaad2f5 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -620,13 +620,18 @@ void PaintRow( st::dialogsTextFont->height); PaintFolderEntryText(p, folder, context, rect); } else if (const auto info = CommunityListInfo(history)) { + // Unlike the Archive folder (fixed on top), a collapsed community is + // a movable pinned entry, so it shows the pinned icon when pinned and + // without an unread counter, exactly like an ordinary chat. + const auto displayPinnedIcon = entry->isPinnedDialog(context.filter) + && (context.filter || !entry->fixedOnTopIndex()); const auto availableWidth = PaintWideCounter( p, context, badgesState, texttop, namewidth, - false); + displayPinnedIcon); const auto rect = QRect( nameleft, texttop, diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index fc387f1f43..aba3010c5c 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -2303,7 +2303,7 @@ void History::setUnreadCount(int newUnreadCount) { } else if (!_firstUnreadView && !_unreadBarView && loadedAtBottom()) { calculateFirstUnreadMessage(); } - if (_communityInfo) { + if (isLinkedCommunityMember()) { _communityInfo->oneUnreadStateChanged(); } } @@ -2318,7 +2318,7 @@ void History::setUnreadMark(bool unread) { const auto notifier = unreadStateChangeNotifier( useMyUnreadInParent() && !unreadCount()); Thread::setUnreadMarkFlag(unread); - if (_communityInfo) { + if (isLinkedCommunityMember()) { _communityInfo->oneUnreadStateChanged(); } } @@ -2480,11 +2480,19 @@ void History::updateCommunityRegistration() { } void History::communityChatsListDateChanged(TimeId wasDate) { - if (_communityInfo) { + if (isLinkedCommunityMember()) { _communityInfo->oneChatsListDateChanged(wasDate, chatListTimeId()); } } +bool History::isLinkedCommunityMember() const { + if (!_communityInfo) { + return false; + } + const auto channel = peer->asChannel(); + return channel && channel->amIn(); +} + int History::chatListNameVersion() const { return peer->nameVersion(); } @@ -3078,7 +3086,7 @@ void History::setChatListMessage(HistoryItem *item) { if (const auto folder = this->folder()) { folder->oneListMessageChanged(was, item); } - if (_communityInfo) { + if (isLinkedCommunityMember()) { _communityInfo->oneListMessageChanged(); } if (const auto to = peer->migrateTo()) { diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index 4573913b2e..7910bb3623 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -455,6 +455,7 @@ public: } void updateCommunityRegistration(); void communityChatsListDateChanged(TimeId wasDate); + [[nodiscard]] bool isLinkedCommunityMember() const; // Interface for Data::Histories. void setInboxReadTill(MsgId upTo); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 6d33873d52..7c3a00350b 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -5501,13 +5501,32 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { } } else if (type == mtpc_messageActionChangeCommunity) { const auto &data = action.c_messageActionChangeCommunity(); - const auto communityId = data.vcommunity_id().value_or_empty(); - if (communityId) { - const auto community = _history->owner().channelLoaded( - ChannelId(communityId)); - if (community) { - UpdateComponents(HistoryServiceCommunityAdded::Bit()); - Get()->community = community; + if (data.vcommunity_id().has_value()) { + const auto communityId = ChannelId(data.vcommunity_id()->v); + const auto owner = &_history->owner(); + UpdateComponents(HistoryServiceCommunityAdded::Bit()); + const auto added = Get(); + added->communityId = communityId; + added->community = owner->channelLoaded(communityId); + added->lifetime.destroy(); + if (!added->community) { + // The community channel isn't loaded yet, re-resolve once it + // materializes so that both the card and the text appear. + using Flag = Data::PeerUpdate::Flag; + owner->session().changes().peerUpdates( + owner->channel(communityId), + Flag::Name | Flag::Photo | Flag::Username | Flag::FullInfo + ) | rpl::filter([=] { + return (owner->channelLoaded(communityId) != nullptr); + }) | rpl::take(1) | rpl::on_next([=] { + const auto added = Get(); + if (!added) { + return; + } + added->community = owner->channelLoaded(communityId); + setServiceMessageByAction(action); + owner->requestItemViewRefresh(this); + }, added->lifetime); } } } @@ -7304,10 +7323,19 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { auto prepareChangeCommunity = [this](const MTPDmessageActionChangeCommunity &action) { auto result = PreparedServiceText(); result.links.push_back(fromLink()); - const auto communityId = action.vcommunity_id().value_or_empty(); - const auto community = communityId - ? _history->owner().channelLoaded(ChannelId(communityId)) - : nullptr; + const auto present = action.vcommunity_id().has_value(); + // Resolve against the same cached community as the card uses, so the + // text and the card never disagree. + const auto community = [&]() -> ChannelData* { + if (!present) { + return nullptr; + } else if (const auto added = Get() + ; added && added->community) { + return added->community; + } + return _history->owner().channelLoaded( + ChannelId(action.vcommunity_id()->v)); + }(); if (community && !community->name().isEmpty()) { result.links.push_back(community->createOpenLink()); result.text = tr::lng_action_community_added( @@ -7317,7 +7345,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { lt_community, tr::link(community->name(), 2), tr::marked); - } else if (communityId) { + } else if (present) { result.text = tr::lng_action_community_added_unknown( tr::now, lt_from, diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index e8c3630f51..0e33bca3ca 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -826,7 +826,9 @@ struct HistoryServiceNoForwardsToggle struct HistoryServiceCommunityAdded : RuntimeComponent { + ChannelId communityId = 0; ChannelData *community = nullptr; + rpl::lifetime lifetime; }; struct HistoryServiceGameScore diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 02cd836183..b19edaff30 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -1619,16 +1619,24 @@ void Element::refreshMedia(Element *replacing) { .service = true, .hideServiceText = true, }); - } else if (const auto added = item->Get() - ; added && added->community) { - _media = std::make_unique( - this, - GenerateCommunityAddedMedia(this, added->community), - MediaGenericDescriptor{ - .maxWidth = st::msgServiceGiftBoxSize.width(), - .service = true, - .hideServiceText = true, - }); + } else if (const auto added = item->Get()) { + if (!added->community && added->communityId) { + // Resolve lazily in case the channel loaded after parse time. + added->community = history()->owner().channelLoaded( + added->communityId); + } + if (added->community) { + _media = std::make_unique( + this, + GenerateCommunityAddedMedia(this, added->community), + MediaGenericDescriptor{ + .maxWidth = st::msgServiceGiftBoxSize.width(), + .service = true, + .hideServiceText = true, + }); + } else { + _media = nullptr; + } } else { _media = nullptr; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_community_added.cpp b/Telegram/SourceFiles/history/view/media/history_view_community_added.cpp index 78824df2f4..c6b4f34eb7 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_community_added.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_community_added.cpp @@ -8,20 +8,169 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_community_added.h" #include "core/click_handler_types.h" // ClickHandlerContext +#include "data/data_changes.h" #include "data/data_channel.h" #include "history/history_item.h" #include "history/view/history_view_element.h" #include "history/view/media/history_view_media_generic.h" #include "history/view/media/history_view_unique_gift.h" // MakeGenericButtonPart #include "lang/lang_keys.h" -#include "ui/dynamic_thumbnails.h" +#include "main/main_session.h" +#include "ui/dynamic_image.h" +#include "ui/painter.h" #include "ui/text/text_utilities.h" +#include "ui/userpic_view.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" #include "styles/style_menu_icons.h" #include "styles/style_premium.h" namespace HistoryView { +namespace { + +// The single place where the empty-state community icon is chosen. +// The feature owner will swap the final SVG here later. +[[nodiscard]] const style::icon &CommunityServiceEmptyIcon() { + return st::menuIconCommunity; +} + +// A rounded-square service userpic for the community-added card. When the +// community has a real photo it paints (and auto-updates) the userpic; when it +// has none it fills the rounded-square with the service-bubble background and +// paints a centered group-style icon in the service foreground color. +class CommunityServiceUserpic final : public Ui::DynamicImage { +public: + explicit CommunityServiceUserpic(not_null community); + + std::shared_ptr clone() override; + + 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; + int paletteVersion = 0; + bool hadUserpic = false; + rpl::lifetime photoLifetime; + rpl::lifetime downloadLifetime; + }; + + [[nodiscard]] bool waitingUserpicLoad() const; + void processNewPhoto(); + + const not_null _community; + QImage _frame; + std::unique_ptr _subscribed; + +}; + +CommunityServiceUserpic::CommunityServiceUserpic( + not_null community) +: _community(community) { +} + +std::shared_ptr CommunityServiceUserpic::clone() { + return std::make_shared(_community); +} + +QImage CommunityServiceUserpic::image(int size) { + Expects(_subscribed != nullptr); + + const auto hasUserpic = _community->hasUserpic(); + const auto good = (_frame.width() == size * _frame.devicePixelRatio()); + const auto key = _community->userpicUniqueKey(_subscribed->view); + const auto paletteVersion = style::PaletteVersion(); + if (!good + || _subscribed->hadUserpic != hasUserpic + || (_subscribed->paletteVersion != paletteVersion + && (!hasUserpic + || _community->useEmptyUserpic(_subscribed->view))) + || (_subscribed->key != key && !waitingUserpicLoad())) { + _subscribed->key = key; + _subscribed->paletteVersion = paletteVersion; + _subscribed->hadUserpic = hasUserpic; + + const auto ratio = style::DevicePixelRatio(); + if (!good) { + _frame = QImage( + QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + _frame.setDevicePixelRatio(ratio); + } + _frame.fill(Qt::transparent); + + if (hasUserpic) { + auto p = Painter(&_frame); + _community->paintUserpic(p, _subscribed->view, { + .position = QPoint(), + .size = size, + .shape = Ui::PeerUserpicShape::Forum, + }); + } else { + auto p = Painter(&_frame); + auto hq = PainterHighQualityEnabler(p); + const auto radius = size * Ui::ForumUserpicRadiusMultiplier(); + p.setPen(Qt::NoPen); + p.setBrush(st::msgServiceBg); + p.drawRoundedRect(QRect(0, 0, size, size), radius, radius); + CommunityServiceEmptyIcon().paintInCenter( + p, + QRect(0, 0, size, size), + st::msgServiceFg->c); + } + } + return _frame; +} + +bool CommunityServiceUserpic::waitingUserpicLoad() const { + return _community->hasUserpic() + && _community->useEmptyUserpic(_subscribed->view); +} + +void CommunityServiceUserpic::subscribeToUpdates(Fn callback) { + if (!callback) { + _subscribed = nullptr; + return; + } + const auto old = std::exchange( + _subscribed, + std::make_unique(std::move(callback))); + + _community->session().changes().peerUpdates( + _community, + Data::PeerUpdate::Flag::Photo + ) | rpl::on_next([=] { + _subscribed->callback(); + processNewPhoto(); + }, _subscribed->photoLifetime); + + processNewPhoto(); +} + +void CommunityServiceUserpic::processNewPhoto() { + Expects(_subscribed != nullptr); + + if (!waitingUserpicLoad()) { + _subscribed->downloadLifetime.destroy(); + return; + } + _community->session().downloaderTaskFinished( + ) | rpl::filter([=] { + return !waitingUserpicLoad(); + }) | rpl::on_next([=] { + _subscribed->callback(); + _subscribed->downloadLifetime.destroy(); + }, _subscribed->downloadLifetime); +} + +} // namespace auto GenerateCommunityAddedMedia( not_null parent, @@ -42,18 +191,15 @@ auto GenerateCommunityAddedMedia( } }); - auto image = community->hasUserpic() - ? Ui::MakeUserpicThumbnail(community) - : Ui::MakeIconThumbnail(st::menuIconGroups); push(std::make_unique( parent, - std::move(image), + std::make_shared(community), st::msgServiceCommunityAddedPhoto, QMargins( 0, - st::msgServiceGiftBoxButtonMargins.top(), + st::msgServiceGiftBoxButtonMargins.top() * 2, 0, - st::msgServiceGiftBoxTitlePadding.top()), + st::msgServiceGiftBoxButtonMargins.bottom()), open, true)); // Paint the community stacked-cards effect behind it. diff --git a/Telegram/SourceFiles/info/community/info_community_widget.cpp b/Telegram/SourceFiles/info/community/info_community_widget.cpp index 386848b1b6..19e1095d34 100644 --- a/Telegram/SourceFiles/info/community/info_community_widget.cpp +++ b/Telegram/SourceFiles/info/community/info_community_widget.cpp @@ -42,8 +42,6 @@ public: [[nodiscard]] bool hasFlexibleTopBar() const; base::weak_qptr createPinnedToTop( not_null parent); - base::weak_qptr createPinnedToBottom( - not_null parent); private: [[nodiscard]] rpl::producer chatsStatusValue() const; @@ -135,11 +133,6 @@ base::weak_qptr InnerWidget::createPinnedToTop( return base::make_weak(not_null{ content }); } -base::weak_qptr InnerWidget::createPinnedToBottom( - not_null parent) { - return nullptr; -} - Memento::Memento(not_null peer) : ContentMemento(peer, nullptr, nullptr, PeerId()) { } @@ -168,8 +161,7 @@ Widget::Widget( setupFlexibleInnerWidget( object_ptr(this, controller, peer), _flexibleScroll)) -, _pinnedToTop(_inner->createPinnedToTop(this)) -, _pinnedToBottom(_inner->createPinnedToBottom(this)) { +, _pinnedToTop(_inner->createPinnedToTop(this)) { _inner->move(0, 0); _inner->backRequest() | rpl::on_next([=] { @@ -227,10 +219,8 @@ bool Widget::showInternal(not_null memento) { return false; } if (auto communityMemento = dynamic_cast(memento.get())) { - if (communityMemento->peer() == peer()) { - restoreState(communityMemento); - return true; - } + restoreState(communityMemento); + return true; } return false; } diff --git a/Telegram/SourceFiles/info/community/info_community_widget.h b/Telegram/SourceFiles/info/community/info_community_widget.h index 34a1108fcd..495ec7d706 100644 --- a/Telegram/SourceFiles/info/community/info_community_widget.h +++ b/Telegram/SourceFiles/info/community/info_community_widget.h @@ -61,7 +61,6 @@ private: FlexibleScrollData _flexibleScroll; InnerWidget *_inner = nullptr; base::weak_qptr _pinnedToTop; - base::weak_qptr _pinnedToBottom; std::unique_ptr _flexibleScrollHelper; }; diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 9c9332b456..b4bc735525 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -2611,23 +2611,19 @@ Section DetailsFiller::makeCommunityLink(not_null channel) { delegate->setContent(content); controller->setDelegate(delegate); - auto hidden = rpl::single(rpl::empty) | rpl::then( - channel->session().changes().peerUpdates( - community, - Data::PeerUpdate::Flag::FullInfo - ) | rpl::to_empty + auto hidden = channel->session().changes().peerFlagsValue( + community, + Data::PeerUpdate::Flag::FullInfo ) | rpl::map([=] { - const auto info = community->communityInfo(); + return community->communityInfo(); + }) | rpl::map([=](Data::CommunityInfo *info) -> rpl::producer { if (!info) { - return false; + return rpl::single(false); } - for (const auto &linked : info->linkedPeers()) { - if (linked.peer == channel) { - return linked.visible.has_value() && !*linked.visible; - } - } - return false; - }) | rpl::start_spawning(container->lifetime()); + return info->linkedPeersValue() | rpl::map([=] { + return info->isHidden(channel); + }); + }) | rpl::flatten_latest() | rpl::start_spawning(container->lifetime()); const auto hiddenWrap = container->add( object_ptr>( diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 134a4238f8..2683c8a814 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -194,6 +194,14 @@ namespace { constexpr auto kArchivedToastDuration = crl::time(5000); constexpr auto kMaxUnreadWithoutConfirmation = 1000; +[[nodiscard]] bool InsideCollapsedCommunity(History *history) { + // A member chat hidden inside a collapsed community lives in that + // community's own list, not the main chats list, so the top-level + // placement actions (archive / pin / add-to-folder) don't apply to it. + const auto info = history ? history->communityListInfo() : nullptr; + return info && info->collapsedInDialogs(); +} + [[nodiscard]] QString LookupMemberRank( not_null peer, not_null user) { @@ -554,6 +562,8 @@ void Filler::addTogglePin() { && community->isCommunity() && !community->collapsedInDialogs()) { return; + } else if (InsideCollapsedCommunity(_request.key.history())) { + return; } const auto pinText = [=] { return entry->isPinnedDialog(filterId) @@ -677,6 +687,8 @@ void Filler::addToggleFolder() { && channel->isCommunity() && !channel->collapsedInDialogs()) { return; + } else if (InsideCollapsedCommunity(history)) { + return; } _addAction(PeerMenuCallback::Args{ .text = tr::lng_filters_menu_add(tr::now), @@ -4337,6 +4349,8 @@ bool CanArchive(History *history, PeerData *peer) { } else if (const auto channel = peer ? peer->asChannel() : nullptr ; channel && channel->isCommunity()) { return false; + } else if (InsideCollapsedCommunity(history)) { + return false; } else if (peer && (peer->isNotificationsUser() || peer->isSelf())) { if (!history || !history->folder()) { return false;