From b2d8e2a1e66316e91b4133a047f61fa5e1979df4 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 19 Apr 2024 14:55:39 +0400 Subject: [PATCH] Initial version of channels/recommendations. --- Telegram/Resources/animations/noresults.tgs | Bin 0 -> 8590 bytes Telegram/Resources/langs/lang.strings | 8 + .../Resources/qrc/telegram/animations.qrc | 1 + .../SourceFiles/api/api_chat_participants.cpp | 40 +- .../SourceFiles/api/api_chat_participants.h | 13 + Telegram/SourceFiles/dialogs/dialogs.style | 20 + .../SourceFiles/dialogs/dialogs_widget.cpp | 4 +- .../dialogs/ui/dialogs_suggestions.cpp | 710 ++++++++++++++++-- .../dialogs/ui/dialogs_suggestions.h | 74 +- .../dialogs/ui/top_peers_strip.cpp | 1 + .../ui/widgets/discrete_sliders.cpp | 8 +- 11 files changed, 807 insertions(+), 72 deletions(-) create mode 100644 Telegram/Resources/animations/noresults.tgs diff --git a/Telegram/Resources/animations/noresults.tgs b/Telegram/Resources/animations/noresults.tgs new file mode 100644 index 0000000000000000000000000000000000000000..62a21279fb766aa9aa25ddc077936f2f51f34031 GIT binary patch literal 8590 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 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index f8d2b1175..0f4535b16 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -5123,6 +5123,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_recent_hide_sure" = "Are you sure you want to clear and disable frequent contacts list?\n\nYou can always turn this feature back on in Settings > Privacy > Suggest Frequent Contacts."; "lng_recent_hide_button" = "Hide"; "lng_recent_none" = "Recent search results\nwill appear here."; +"lng_recent_chats" = "Chats"; +"lng_recent_channels" = "Channels"; +"lng_channels_none_title" = "No channels yet..."; +"lng_channels_none_about" = "You are not currently subscribed to any channels."; +"lng_channels_your_title" = "Channels you joined"; +"lng_channels_your_more" = "Show more"; +"lng_channels_your_less" = "Show less"; +"lng_channels_recommended" = "Recommended channels"; // Wnd specific diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index 3322b1387..16920ae4d 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -25,5 +25,6 @@ ../../animations/collectible_username.tgs ../../animations/collectible_phone.tgs ../../animations/search.tgs + ../../animations/noresults.tgs diff --git a/Telegram/SourceFiles/api/api_chat_participants.cpp b/Telegram/SourceFiles/api/api_chat_participants.cpp index a7d3d9f64..d1c8650d3 100644 --- a/Telegram/SourceFiles/api/api_chat_participants.cpp +++ b/Telegram/SourceFiles/api/api_chat_participants.cpp @@ -212,7 +212,7 @@ void ApplyBotsList( } [[nodiscard]] ChatParticipants::Channels ParseSimilar( - not_null channel, + not_null session, const MTPmessages_Chats &chats) { auto result = ChatParticipants::Channels(); std::vector>(); @@ -220,13 +220,13 @@ void ApplyBotsList( const auto &list = data.vchats().v; result.list.reserve(list.size()); for (const auto &chat : list) { - const auto peer = channel->owner().processChat(chat); + const auto peer = session->data().processChat(chat); if (const auto channel = peer->asChannel()) { result.list.push_back(channel); } } if constexpr (MTPDmessages_chatsSlice::Is()) { - if (channel->session().premiumPossible()) { + if (session->premiumPossible()) { result.more = data.vcount().v - data.vchats().v.size(); } } @@ -234,6 +234,12 @@ void ApplyBotsList( return result; } +[[nodiscard]] ChatParticipants::Channels ParseSimilar( + not_null channel, + const MTPmessages_Chats &chats) { + return ParseSimilar(&channel->session(), chats); +} + } // namespace ChatParticipant::ChatParticipant( @@ -351,7 +357,8 @@ QString ChatParticipant::rank() const { } ChatParticipants::ChatParticipants(not_null api) -: _api(&api->instance()) { +: _session(&api->session()) +, _api(&api->instance()) { } void ChatParticipants::requestForAdd( @@ -769,4 +776,29 @@ auto ChatParticipants::similarLoaded() const return _similarLoaded.events(); } +void ChatParticipants::loadRecommendations() { + if (_recommendationsLoaded.current() || _recommendations.requestId) { + return; + } + _recommendations.requestId = _api.request( + MTPchannels_GetChannelRecommendations( + MTP_flags(0), + MTP_inputChannelEmpty()) + ).done([=](const MTPmessages_Chats &result) { + _recommendations.requestId = 0; + auto parsed = ParseSimilar(_session, result); + _recommendations.channels = std::move(parsed); + _recommendations.channels.more = 0; + _recommendationsLoaded = true; + }).send(); +} + +const ChatParticipants::Channels &ChatParticipants::recommendations() const { + return _recommendations.channels; +} + +rpl::producer<> ChatParticipants::recommendationsLoaded() const { + return _recommendationsLoaded.changes() | rpl::to_empty; +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_chat_participants.h b/Telegram/SourceFiles/api/api_chat_participants.h index b8fc9fc67..fbab3f425 100644 --- a/Telegram/SourceFiles/api/api_chat_participants.h +++ b/Telegram/SourceFiles/api/api_chat_participants.h @@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class ApiWrap; class ChannelData; +namespace Main { +class Session; +} // namespace Main + namespace Ui { class Show; } // namespace Ui @@ -134,12 +138,18 @@ public: [[nodiscard]] auto similarLoaded() const -> rpl::producer>; + void loadRecommendations(); + [[nodiscard]] const Channels &recommendations() const; + [[nodiscard]] rpl::producer<> recommendationsLoaded() const; + private: struct SimilarChannels { Channels channels; mtpRequestId requestId = 0; }; + const not_null _session; + MTP::Sender _api; using PeerRequests = base::flat_map; @@ -165,6 +175,9 @@ private: base::flat_map, SimilarChannels> _similar; rpl::event_stream> _similarLoaded; + SimilarChannels _recommendations; + rpl::variable _recommendationsLoaded = false; + }; } // namespace Api diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index 7796afb4e..8298341c5 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -624,6 +624,26 @@ recentPeersList: PeerList(defaultPeerList) { } recentPeersSpecialNamePosition: point(64px, 19px); +dialogsSearchTabs: SettingsSlider(defaultSettingsSlider) { + height: 33px; + barTop: 30px; + barSkip: 0px; + barStroke: 6px; + barRadius: 2px; + barFg: transparent; + barSnapToLabel: true; + strictSkip: 34px; + labelTop: 7px; + labelStyle: semiboldTextStyle; + labelFg: windowSubTextFg; + labelFgActive: lightButtonFg; + rippleBottomSkip: 1px; + rippleBg: windowBgOver; + rippleBgActive: lightButtonBgOver; + ripple: defaultRippleAnimation; +} + + dialogsStoriesList: DialogsStoriesList { small: dialogsStories; full: dialogsStoriesFull; diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index e5676f3bc..00cd49329 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -1151,7 +1151,9 @@ void Widget::updateSuggestions(anim::type animated) { rpl::merge( _suggestions->topPeerChosen(), - _suggestions->recentPeerChosen() + _suggestions->recentPeerChosen(), + _suggestions->myChannelChosen(), + _suggestions->recommendationChosen() ) | rpl::start_with_next([=](not_null peer) { chosenRow({ .key = peer->owner().history(peer), diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp index 033cdc7c9..17d761fe0 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp @@ -7,11 +7,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "dialogs/ui/dialogs_suggestions.h" +#include "api/api_chat_participants.h" +#include "apiwrap.h" #include "base/unixtime.h" #include "boxes/peer_list_box.h" #include "data/components/recent_peers.h" #include "data/components/top_peers.h" #include "data/data_changes.h" +#include "data/data_channel.h" +#include "data/data_chat.h" +#include "data/data_folder.h" #include "data/data_peer_values.h" #include "data/data_session.h" #include "data/data_user.h" @@ -23,9 +28,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/confirm_box.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/discrete_sliders.h" #include "ui/widgets/elastic_scroll.h" #include "ui/widgets/labels.h" #include "ui/widgets/popup_menu.h" +#include "ui/widgets/shadow.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" #include "ui/delayed_activation.h" @@ -43,6 +50,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Dialogs { namespace { +constexpr auto kCollapsedChannelsCount = 5; +constexpr auto kProbablyMaxChannels = 1000; +constexpr auto kProbablyMaxRecommendations = 100; + class RecentRow final : public PeerListRow { public: explicit RecentRow(not_null peer); @@ -110,6 +121,76 @@ private: }; +class MyChannelsController final + : public PeerListController + , public base::has_weak_ptr { +public: + explicit MyChannelsController( + not_null window); + + [[nodiscard]] rpl::producer count() const { + return _count.value(); + } + [[nodiscard]] rpl::producer> chosen() const { + return _chosen.events(); + } + + void prepare() override; + void rowClicked(not_null row) override; + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override; + Main::Session &session() const override; + +private: + void setupDivider(); + void appendRow(not_null channel); + void fill(); + + const not_null _window; + std::vector> _channels; + rpl::variable _toggleExpanded = nullptr; + rpl::variable _count = 0; + rpl::variable _expanded = false; + base::unique_qptr _menu; + rpl::event_stream> _chosen; + rpl::lifetime _lifetime; + +}; + +class RecommendationsController final + : public PeerListController + , public base::has_weak_ptr { +public: + explicit RecommendationsController( + not_null window); + + [[nodiscard]] rpl::producer count() const { + return _count.value(); + } + [[nodiscard]] rpl::producer> chosen() const { + return _chosen.events(); + } + + void prepare() override; + void rowClicked(not_null row) override; + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override; + Main::Session &session() const override; + +private: + void setupDivider(); + void appendRow(not_null channel); + + const not_null _window; + rpl::variable _count; + base::unique_qptr _menu; + rpl::event_stream> _chosen; + rpl::lifetime _lifetime; + +}; + struct EntryMenuDescriptor { not_null controller; not_null peer; @@ -189,6 +270,20 @@ RecentRow::RecentRow(not_null peer) , _history(peer->owner().history(peer)) { if (peer->isSelf() || peer->isRepliesChat()) { setCustomStatus(u" "_q); + } else if (const auto chat = peer->asChat()) { + if (chat->count > 0) { + setCustomStatus( + tr::lng_chat_status_members(tr::now, lt_count, chat->count)); + } + } else if (const auto channel = peer->asChannel()) { + if (channel->membersCountKnown()) { + setCustomStatus((channel->isBroadcast() + ? tr::lng_chat_status_subscribers + : tr::lng_chat_status_members)( + tr::now, + lt_count, + channel->membersCount())); + } } refreshBadge(); } @@ -426,6 +521,259 @@ void RecentsController::subscribeToEvents() { }, _lifetime); } +MyChannelsController::MyChannelsController( + not_null window) +: _window(window) { +} + +void MyChannelsController::prepare() { + setupDivider(); + + _channels.reserve(kProbablyMaxChannels); + const auto owner = &session().data(); + const auto add = [&](not_null list) { + for (const auto &row : list->indexed()->all()) { + if (const auto history = row->history()) { + if (const auto channel = history->peer->asBroadcast()) { + _channels.push_back(history); + } + } + } + }; + add(owner->chatsList()); + if (const auto folder = owner->folderLoaded(Data::Folder::kId)) { + add(owner->chatsList(folder)); + } + + ranges::sort(_channels, ranges::greater(), &History::chatListTimeId); + _count = int(_channels.size()); + + _expanded.value() | rpl::start_with_next([=] { + fill(); + }, _lifetime); + + auto loading = owner->chatsListChanges( + ) | rpl::take_while([=](Data::Folder *folder) { + return !owner->chatsListLoaded(folder); + }); + rpl::merge( + std::move(loading), + owner->chatsListLoadedEvents() + ) | rpl::start_with_next([=](Data::Folder *folder) { + const auto list = owner->chatsList(folder); + for (const auto &row : list->indexed()->all()) { + if (const auto history = row->history()) { + if (const auto channel = history->peer->asBroadcast()) { + if (ranges::contains(_channels, not_null(history))) { + _channels.push_back(history); + } + } + } + } + const auto was = _count.current(); + const auto now = int(_channels.size()); + if (was != now) { + _count = now; + fill(); + } + }, _lifetime); +} + +void MyChannelsController::fill() { + const auto count = _count.current(); + const auto limit = _expanded.current() + ? count + : std::min(count, kCollapsedChannelsCount); + const auto already = delegate()->peerListFullRowsCount(); + const auto delta = limit - already; + if (!delta) { + return; + } else if (delta > 0) { + for (auto i = already; i != limit; ++i) { + appendRow(_channels[i]->peer->asBroadcast()); + } + } else { + for (auto i = already; i != limit;) { + delegate()->peerListRemoveRow(delegate()->peerListRowAt(--i)); + } + } + delegate()->peerListRefreshRows(); +} + +void MyChannelsController::appendRow(not_null channel) { + auto row = std::make_unique(channel); + if (channel->membersCountKnown()) { + row->setCustomStatus((channel->isBroadcast() + ? tr::lng_chat_status_subscribers + : tr::lng_chat_status_members)( + tr::now, + lt_count, + channel->membersCount())); + } + delegate()->peerListAppendRow(std::move(row)); +} + +void MyChannelsController::rowClicked(not_null row) { + _chosen.fire(row->peer()); +} + +base::unique_qptr MyChannelsController::rowContextMenu( + QWidget *parent, + not_null row) { + return nullptr; +} + +Main::Session &MyChannelsController::session() const { + return _window->session(); +} + +void MyChannelsController::setupDivider() { + auto result = object_ptr( + (QWidget*)nullptr, + st::searchedBarHeight); + const auto raw = result.data(); + const auto label = Ui::CreateChild( + raw, + tr::lng_channels_your_title(), + st::searchedBarLabel); + _count.value( + ) | rpl::map( + rpl::mappers::_1 > kCollapsedChannelsCount + ) | rpl::distinct_until_changed() | rpl::start_with_next([=](bool more) { + _expanded = false; + if (!more) { + const auto toggle = _toggleExpanded.current(); + _toggleExpanded = nullptr; + delete toggle; + return; + } else if (_toggleExpanded.current()) { + return; + } + const auto toggle = Ui::CreateChild( + raw, + tr::lng_channels_your_more(tr::now), + st::searchedBarLink); + toggle->show(); + toggle->setClickedCallback([=] { + const auto expand = !_expanded.current(); + toggle->setText(expand + ? tr::lng_channels_your_less(tr::now) + : tr::lng_channels_your_more(tr::now)); + _expanded = expand; + }); + rpl::combine( + raw->sizeValue(), + toggle->widthValue() + ) | rpl::start_with_next([=](QSize size, int width) { + const auto x = st::searchedBarPosition.x(); + const auto y = st::searchedBarPosition.y(); + toggle->moveToRight(0, 0, size.width()); + label->resizeToWidth(size.width() - x - width); + label->moveToLeft(x, y, size.width()); + }, toggle->lifetime()); + _toggleExpanded = toggle; + }, raw->lifetime()); + + rpl::combine( + raw->sizeValue(), + _toggleExpanded.value() + ) | rpl::filter( + rpl::mappers::_2 == nullptr + ) | rpl::start_with_next([=](QSize size, const auto) { + const auto x = st::searchedBarPosition.x(); + const auto y = st::searchedBarPosition.y(); + label->resizeToWidth(size.width() - x * 2); + label->moveToLeft(x, y, size.width()); + }, raw->lifetime()); + + raw->paintRequest() | rpl::start_with_next([=](QRect clip) { + QPainter(raw).fillRect(clip, st::searchedBarBg); + }, raw->lifetime()); + + delegate()->peerListSetAboveWidget(std::move(result)); +} + +RecommendationsController::RecommendationsController( + not_null window) +: _window(window) { +} + +void RecommendationsController::prepare() { + setupDivider(); + + const auto participants = &session().api().chatParticipants(); + const auto fill = [=] { + const auto &list = participants->recommendations().list; + if (list.empty()) { + return; + } + for (const auto &peer : list) { + if (const auto channel = peer->asBroadcast()) { + appendRow(channel); + } + } + delegate()->peerListRefreshRows(); + _count = delegate()->peerListFullRowsCount(); + }; + + fill(); + if (!_count.current()) { + participants->loadRecommendations(); + participants->recommendationsLoaded( + ) | rpl::take(1) | rpl::start_with_next(fill, _lifetime); + } +} + +void RecommendationsController::appendRow(not_null channel) { + auto row = std::make_unique(channel); + if (channel->membersCountKnown()) { + row->setCustomStatus((channel->isBroadcast() + ? tr::lng_chat_status_subscribers + : tr::lng_chat_status_members)( + tr::now, + lt_count, + channel->membersCount())); + } + delegate()->peerListAppendRow(std::move(row)); +} + +void RecommendationsController::rowClicked(not_null row) { + _chosen.fire(row->peer()); +} + +base::unique_qptr RecommendationsController::rowContextMenu( + QWidget *parent, + not_null row) { + return nullptr; +} + +Main::Session &RecommendationsController::session() const { + return _window->session(); +} + +void RecommendationsController::setupDivider() { + auto result = object_ptr( + (QWidget*)nullptr, + st::searchedBarHeight); + const auto raw = result.data(); + const auto label = Ui::CreateChild( + raw, + tr::lng_channels_recommended(), + st::searchedBarLabel); + raw->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + const auto x = st::searchedBarPosition.x(); + const auto y = st::searchedBarPosition.y(); + label->resizeToWidth(size.width() - x * 2); + label->moveToLeft(x, y, size.width()); + }, raw->lifetime()); + raw->paintRequest() | rpl::start_with_next([=](QRect clip) { + QPainter(raw).fillRect(clip, st::searchedBarBg); + }, raw->lifetime()); + + delegate()->peerListSetAboveWidget(std::move(result)); +} + } // namespace Suggestions::Suggestions( @@ -434,16 +782,54 @@ Suggestions::Suggestions( rpl::producer topPeers, RecentPeersList recentPeers) : RpWidget(parent) -, _scroll(std::make_unique(this)) -, _content(_scroll->setOwnedWidget(object_ptr(this))) -, _topPeersWrap(_content->add(object_ptr>( - this, - object_ptr(this, std::move(topPeers))))) +, _controller(controller) +, _tabs(std::make_unique(this, st::dialogsSearchTabs)) +, _chatsScroll(std::make_unique(this)) +, _chatsContent( + _chatsScroll->setOwnedWidget(object_ptr(this))) +, _topPeersWrap( + _chatsContent->add(object_ptr>( + this, + object_ptr(this, std::move(topPeers))))) , _topPeers(_topPeersWrap->entity()) -, _recentPeers( - _content->add( - setupRecentPeers(controller, std::move(recentPeers)))) -, _emptyRecent(_content->add(setupEmptyRecent(controller))) { +, _recentPeers(_chatsContent->add(setupRecentPeers(std::move(recentPeers)))) +, _emptyRecent(_chatsContent->add(setupEmptyRecent())) +, _channelsScroll(std::make_unique(this)) +, _channelsContent( + _channelsScroll->setOwnedWidget(object_ptr(this))) +, _myChannels(_channelsContent->add(setupMyChannels())) +, _recommendations(_channelsContent->add(setupRecommendations())) +, _emptyChannels(_channelsContent->add(setupEmptyChannels())) { + + setupTabs(); + setupChats(); + setupChannels(); +} + +Suggestions::~Suggestions() = default; + +void Suggestions::setupTabs() { + const auto shadow = Ui::CreateChild(this); + shadow->lower(); + + _tabs->sizeValue() | rpl::start_with_next([=](QSize size) { + const auto line = st::lineWidth; + shadow->setGeometry(0, size.height() - line, width(), line); + }, shadow->lifetime()); + + shadow->showOn(_tabs->shownValue()); + + _tabs->setSections({ + tr::lng_recent_chats(tr::now), + tr::lng_recent_channels(tr::now), + }); + _tabs->sectionActivated( + ) | rpl::start_with_next([=](int section) { + switchTab(section ? Tab::Channels : Tab::Chats); + }, _tabs->lifetime()); +} + +void Suggestions::setupChats() { _recentCount.value() | rpl::start_with_next([=](int count) { _recentPeers->toggle(count > 0, anim::type::instant); _emptyRecent->toggle(count == 0, anim::type::instant); @@ -455,13 +841,13 @@ Suggestions::Suggestions( _topPeers->clicks() | rpl::start_with_next([=](uint64 peerIdRaw) { const auto peerId = PeerId(peerIdRaw); - _topPeerChosen.fire(controller->session().data().peer(peerId)); + _topPeerChosen.fire(_controller->session().data().peer(peerId)); }, _topPeers->lifetime()); _topPeers->showMenuRequests( ) | rpl::start_with_next([=](const ShowTopPeerMenuRequest &request) { const auto weak = Ui::MakeWeak(this); - const auto owner = &controller->session().data(); + const auto owner = &_controller->session().data(); const auto peer = owner->peer(PeerId(request.id)); const auto removeOne = [=] { peer->session().topPeers().remove(peer); @@ -469,7 +855,7 @@ Suggestions::Suggestions( _topPeers->removeLocally(peer->id.value); } }; - const auto session = &controller->session(); + const auto session = &_controller->session(); const auto removeAll = crl::guard(session, [=] { session->topPeers().toggleDisabled(true); if (weak) { @@ -477,7 +863,7 @@ Suggestions::Suggestions( } }); FillEntryMenu(request.callback, { - .controller = controller, + .controller = _controller, .peer = peer, .removeOneText = tr::lng_recent_remove(tr::now), .removeOne = removeOne, @@ -487,9 +873,28 @@ Suggestions::Suggestions( .removeAll = removeAll, }); }, _topPeers->lifetime()); + + _chatsScroll->setVisible(_tab == Tab::Chats); } -Suggestions::~Suggestions() = default; +void Suggestions::setupChannels() { + _myChannelsCount.value() | rpl::start_with_next([=](int count) { + _myChannels->toggle(count > 0, anim::type::instant); + }, _myChannels->lifetime()); + + _recommendationsCount.value() | rpl::start_with_next([=](int count) { + _recommendations->toggle(count > 0, anim::type::instant); + }, _recommendations->lifetime()); + + _emptyChannels->toggleOn( + rpl::combine( + _myChannelsCount.value(), + _recommendationsCount.value(), + rpl::mappers::_1 + rpl::mappers::_2 == 0), + anim::type::instant); + + _channelsScroll->setVisible(_tab == Tab::Channels); +} void Suggestions::selectJump(Qt::Key direction, int pageSize) { const auto recentHasSelection = [=] { @@ -507,7 +912,7 @@ void Suggestions::selectJump(Qt::Key direction, int pageSize) { } if (_recentSelectJump(direction, pageSize) == JumpResult::AppliedAndOut) { if (direction == Qt::Key_Up) { - _scroll->scrollTo(0); + _chatsScroll->scrollTo(0); } } } @@ -515,7 +920,7 @@ void Suggestions::selectJump(Qt::Key direction, int pageSize) { if (_recentSelectJump(direction, pageSize) == JumpResult::AppliedAndOut) { _topPeers->selectByKeyboard(Qt::Key()); - _scroll->scrollTo(0); + _chatsScroll->scrollTo(0); } else { _topPeers->deselectByKeyboard(); } @@ -529,12 +934,12 @@ void Suggestions::selectJump(Qt::Key direction, int pageSize) { _recentSelectJump(direction, pageSize); } else { _topPeers->selectByKeyboard(Qt::Key()); - _scroll->scrollTo(0); + _chatsScroll->scrollTo(0); } } else if (direction == Qt::Key_Left || direction == Qt::Key_Right) { if (!recentHasSelection()) { _topPeers->selectByKeyboard(direction); - _scroll->scrollTo(0); + _chatsScroll->scrollTo(0); } } } @@ -550,19 +955,9 @@ void Suggestions::show(anim::type animated, Fn finish) { _hidden = false; if (animated == anim::type::instant) { - _shownAnimation.stop(); - _scroll->show(); + finishShow(); } else { - _shownAnimation.start([=] { - update(); - if (!_shownAnimation.animating() && finish) { - finish(); - _cache = QPixmap(); - _scroll->show(); - } - }, 0., 1., st::slideDuration, anim::easeOutQuint); - _cache = Ui::GrabWidget(_scroll.get()); - _scroll->hide(); + startShownAnimation(true, std::move(finish)); } } @@ -573,17 +968,78 @@ void Suggestions::hide(anim::type animated, Fn finish) { } else if (animated == anim::type::instant) { RpWidget::hide(); } else { - _shownAnimation.start([=] { - update(); - if (!_shownAnimation.animating() && finish) { - finish(); - } - }, 1., 0., st::slideDuration, anim::easeOutQuint); - _cache = Ui::GrabWidget(_scroll.get()); - _scroll->hide(); + startShownAnimation(false, std::move(finish)); } } +void Suggestions::switchTab(Tab tab) { + if (_tab == tab) { + return; + } + _tab = tab; + if (_tabs->isHidden()) { + return; + } + startSlideAnimation(); +} + +void Suggestions::startSlideAnimation() { + if (!_slideAnimation.animating()) { + _slideLeft = Ui::GrabWidget(_chatsScroll.get()); + _slideRight = Ui::GrabWidget(_channelsScroll.get()); + _chatsScroll->hide(); + _channelsScroll->hide(); + } + const auto from = (_tab == Tab::Channels) ? 0. : 1.; + const auto to = (_tab == Tab::Channels) ? 1. : 0.; + _slideAnimation.start([=] { + update(); + if (!_slideAnimation.animating() && !_shownAnimation.animating()) { + finishShow(); + } + }, from, to, st::slideDuration, anim::sineInOut); +} + +void Suggestions::startShownAnimation(bool shown, Fn finish) { + const auto from = shown ? 0. : 1.; + const auto to = shown ? 1. : 0.; + _shownAnimation.start([=] { + update(); + if (!_shownAnimation.animating() && finish) { + finish(); + if (shown) { + finishShow(); + } + } + }, from, to, st::slideDuration, anim::easeOutQuint); + if (_cache.isNull()) { + const auto now = width(); + if (now < st::columnMinimalWidthLeft) { + resize(st::columnMinimalWidthLeft, height()); + } + _cache = Ui::GrabWidget(this); + if (now < st::columnMinimalWidthLeft) { + resize(now, height()); + } + } + _tabs->hide(); + _chatsScroll->hide(); + _channelsScroll->hide(); + _slideAnimation.stop(); +} + +void Suggestions::finishShow() { + _slideAnimation.stop(); + _slideLeft = _slideRight = QPixmap(); + + _shownAnimation.stop(); + _cache = QPixmap(); + + _tabs->show(); + _chatsScroll->setVisible(_tab == Tab::Chats); + _channelsScroll->setVisible(_tab == Tab::Channels); +} + float64 Suggestions::shownOpacity() const { return _shownAnimation.value(_hidden ? 0. : 1.); } @@ -595,28 +1051,48 @@ void Suggestions::paintEvent(QPaintEvent *e) { auto p = QPainter(this); p.fillRect(e->rect(), color); - if (_scroll->isHidden()) { + if (!_cache.isNull()) { const auto slide = st::topPeers.height + st::searchedBarHeight; p.setOpacity(opacity); p.drawPixmap(0, (opacity - 1.) * slide, _cache); + } else if (!_slideLeft.isNull()) { + const auto slide = st::topPeers.height + st::searchedBarHeight; + const auto right = (_tab == Tab::Channels); + const auto progress = _slideAnimation.value(right ? 1. : 0.); + const auto shift = st::topPeers.height + st::searchedBarHeight; + p.setOpacity(1. - progress); + p.drawPixmap( + anim::interpolate(0, -slide, progress), + _chatsScroll->y(), + _slideLeft); + p.setOpacity(progress); + p.drawPixmap( + anim::interpolate(slide, 0, progress), + _channelsScroll->y(), + _slideRight); } } void Suggestions::resizeEvent(QResizeEvent *e) { const auto w = std::max(width(), st::columnMinimalWidthLeft); - _scroll->setGeometry(0, 0, w, height()); - _content->resizeToWidth(w); + _tabs->resizeToWidth(w); + const auto tabs = _tabs->height(); + + _chatsScroll->setGeometry(0, tabs, w, height() - tabs); + _chatsContent->resizeToWidth(w); + + _channelsScroll->setGeometry(0, tabs, w, height() - tabs); + _channelsContent->resizeToWidth(w); } object_ptr> Suggestions::setupRecentPeers( - not_null window, RecentPeersList recentPeers) { - auto &lifetime = _content->lifetime(); + auto &lifetime = _chatsContent->lifetime(); const auto delegate = lifetime.make_state< PeerListContentDelegateSimple >(); const auto controller = lifetime.make_state( - window, + _controller, std::move(recentPeers)); controller->setStyleOverrides(&st::recentPeersList); @@ -624,11 +1100,11 @@ object_ptr> Suggestions::setupRecentPeers( controller->chosen( ) | rpl::start_with_next([=](not_null peer) { - window->session().recentPeers().bump(peer); + _controller->session().recentPeers().bump(peer); _recentPeerChosen.fire_copy(peer); }, lifetime); - auto content = object_ptr(_content, controller); + auto content = object_ptr(_chatsContent, controller); const auto raw = content.data(); _recentPeersChoose = [=] { @@ -658,7 +1134,7 @@ object_ptr> Suggestions::setupRecentPeers( raw->scrollToRequests( ) | rpl::start_with_next([this](Ui::ScrollToRequest request) { const auto add = _topPeersWrap->toggled() ? _topPeers->height() : 0; - _scroll->scrollToY(request.ymin + add, request.ymax + add); + _chatsScroll->scrollToY(request.ymin + add, request.ymax + add); }, lifetime); delegate->setContent(raw); @@ -667,26 +1143,39 @@ object_ptr> Suggestions::setupRecentPeers( return object_ptr>(this, std::move(content)); } -object_ptr> Suggestions::setupEmptyRecent( - not_null window) { - auto content = object_ptr(_content); +object_ptr> Suggestions::setupEmptyRecent() { + return setupEmpty(_chatsContent, "search", tr::lng_recent_none()); +} + +object_ptr> Suggestions::setupEmptyChannels() { + return setupEmpty( + _channelsContent, + "noresults", + tr::lng_channels_none_about()); +} + +object_ptr> Suggestions::setupEmpty( + not_null parent, + const QString &animation, + rpl::producer text) { + auto content = object_ptr(parent); const auto raw = content.data(); const auto label = Ui::CreateChild( raw, - tr::lng_recent_none(), + std::move(text), st::defaultPeerListAbout); const auto size = st::recentPeersEmptySize; const auto [widget, animate] = Settings::CreateLottieIcon( raw, { - .name = u"search"_q, + .name = animation, .sizeOverride = { size, size }, }, st::recentPeersEmptyMargin); const auto icon = widget.data(); - _scroll->heightValue() | rpl::start_with_next([=](int height) { + _chatsScroll->heightValue() | rpl::start_with_next([=](int height) { raw->resize(raw->width(), height - st::topPeers.height); }, raw->lifetime()); @@ -699,11 +1188,13 @@ object_ptr> Suggestions::setupEmptyRecent( y + icon->height() + st::recentPeersEmptySkip); }, raw->lifetime()); - auto result = object_ptr>(_content, std::move(content)); + auto result = object_ptr>( + parent, + std::move(content)); result->toggle(false, anim::type::instant); result->toggledValue() | rpl::filter([=](bool shown) { - return shown && window->session().data().chatsListLoaded(); + return shown && _controller->session().data().chatsListLoaded(); }) | rpl::start_with_next([=] { animate(anim::repeat::once); }, raw->lifetime()); @@ -711,6 +1202,115 @@ object_ptr> Suggestions::setupEmptyRecent( return result; } +object_ptr> Suggestions::setupMyChannels() { + auto &lifetime = _channelsContent->lifetime(); + const auto delegate = lifetime.make_state< + PeerListContentDelegateSimple + >(); + const auto controller = lifetime.make_state( + _controller); + controller->setStyleOverrides(&st::recentPeersList); + + _myChannelsCount = controller->count(); + + controller->chosen( + ) | rpl::start_with_next([=](not_null peer) { + _myChannelChosen.fire_copy(peer); + }, lifetime); + + auto content = object_ptr(_channelsContent, controller); + + const auto raw = content.data(); + _myChannelsChoose = [=] { + return raw->submitted(); + }; + _myChannelsSelectJump = [raw](Qt::Key direction, int pageSize) { + const auto had = raw->hasSelection(); + if (direction == Qt::Key()) { + return had ? JumpResult::Applied : JumpResult::NotApplied; + } else if (direction == Qt::Key_Up && !had) { + return JumpResult::NotApplied; + } else if (direction == Qt::Key_Down || direction == Qt::Key_Up) { + const auto delta = (direction == Qt::Key_Down) ? 1 : -1; + if (pageSize > 0) { + raw->selectSkipPage(pageSize, delta); + } else { + raw->selectSkip(delta); + } + return raw->hasSelection() + ? JumpResult::Applied + : had + ? JumpResult::AppliedAndOut + : JumpResult::NotApplied; + } + return JumpResult::NotApplied; + }; + raw->scrollToRequests( + ) | rpl::start_with_next([this](Ui::ScrollToRequest request) { + _channelsScroll->scrollToY(request.ymin, request.ymax); + }, lifetime); + + delegate->setContent(raw); + controller->setDelegate(delegate); + + return object_ptr>(this, std::move(content)); +} + +object_ptr> Suggestions::setupRecommendations() { + auto &lifetime = _channelsContent->lifetime(); + const auto delegate = lifetime.make_state< + PeerListContentDelegateSimple + >(); + const auto controller = lifetime.make_state( + _controller); + controller->setStyleOverrides(&st::recentPeersList); + + _recommendationsCount = controller->count(); + + controller->chosen( + ) | rpl::start_with_next([=](not_null peer) { + _recommendationChosen.fire_copy(peer); + }, lifetime); + + auto content = object_ptr(_channelsContent, controller); + + const auto raw = content.data(); + _recommendationsChoose = [=] { + return raw->submitted(); + }; + _recommendationsSelectJump = [raw](Qt::Key direction, int pageSize) { + const auto had = raw->hasSelection(); + if (direction == Qt::Key()) { + return had ? JumpResult::Applied : JumpResult::NotApplied; + } else if (direction == Qt::Key_Up && !had) { + return JumpResult::NotApplied; + } else if (direction == Qt::Key_Down || direction == Qt::Key_Up) { + const auto delta = (direction == Qt::Key_Down) ? 1 : -1; + if (pageSize > 0) { + raw->selectSkipPage(pageSize, delta); + } else { + raw->selectSkip(delta); + } + return raw->hasSelection() + ? JumpResult::Applied + : had + ? JumpResult::AppliedAndOut + : JumpResult::NotApplied; + } + return JumpResult::NotApplied; + }; + raw->scrollToRequests( + ) | rpl::start_with_next([this](Ui::ScrollToRequest request) { + const auto add = _myChannels->toggled() ? _myChannels->height() : 0; + _channelsScroll->scrollToY(request.ymin + add, request.ymax + add); + }, lifetime); + + delegate->setContent(raw); + controller->setDelegate(delegate); + + return object_ptr>(this, std::move(content)); +} + rpl::producer TopPeersContent( not_null session) { return [=](auto consumer) { diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h index b6be97e26..5749d7af7 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h @@ -18,6 +18,7 @@ class Session; namespace Ui { class ElasticScroll; +class SettingsSlider; class VerticalLayout; template class SlideWrap; @@ -52,12 +53,25 @@ public: [[nodiscard]] rpl::producer> topPeerChosen() const { return _topPeerChosen.events(); } - [[nodiscard]] rpl::producer> recentPeerChosen() const { + [[nodiscard]] auto recentPeerChosen() const + -> rpl::producer> { return _recentPeerChosen.events(); } + [[nodiscard]] auto myChannelChosen() const + -> rpl::producer> { + return _myChannelChosen.events(); + } + [[nodiscard]] auto recommendationChosen() const + -> rpl::producer> { + return _recommendationChosen.events(); + } private: - enum class JumpResult { + enum class Tab : uchar { + Chats, + Channels, + }; + enum class JumpResult : uchar { NotApplied, Applied, AppliedAndOut, @@ -66,14 +80,34 @@ private: void paintEvent(QPaintEvent *e) override; void resizeEvent(QResizeEvent *e) override; - [[nodiscard]] object_ptr> setupRecentPeers( - not_null window, - RecentPeersList recentPeers); - [[nodiscard]] object_ptr> setupEmptyRecent( - not_null window); + void setupTabs(); + void setupChats(); + void setupChannels(); - const std::unique_ptr _scroll; - const not_null _content; + [[nodiscard]] object_ptr> setupRecentPeers( + RecentPeersList recentPeers); + [[nodiscard]] object_ptr> setupEmptyRecent(); + [[nodiscard]] object_ptr> setupMyChannels(); + [[nodiscard]] auto setupRecommendations() + -> object_ptr>; + [[nodiscard]] auto setupEmptyChannels() + -> object_ptr>; + [[nodiscard]] object_ptr> setupEmpty( + not_null parent, + const QString &animation, + rpl::producer text); + + void switchTab(Tab tab); + void startShownAnimation(bool shown, Fn finish); + void startSlideAnimation(); + void finishShow(); + + const not_null _controller; + + const std::unique_ptr _tabs; + + const std::unique_ptr _chatsScroll; + const not_null _chatsContent; const not_null*> _topPeersWrap; const not_null _topPeers; @@ -83,14 +117,36 @@ private: const not_null*> _recentPeers; const not_null*> _emptyRecent; + const std::unique_ptr _channelsScroll; + const not_null _channelsContent; + + rpl::variable _myChannelsCount; + Fn _myChannelsChoose; + Fn _myChannelsSelectJump; + const not_null*> _myChannels; + + rpl::variable _recommendationsCount; + Fn _recommendationsChoose; + Fn _recommendationsSelectJump; + const not_null*> _recommendations; + + const not_null*> _emptyChannels; + rpl::event_stream> _topPeerChosen; rpl::event_stream> _recentPeerChosen; + rpl::event_stream> _myChannelChosen; + rpl::event_stream> _recommendationChosen; Ui::Animations::Simple _shownAnimation; Fn _showFinished; + Tab _tab = Tab::Chats; bool _hidden = false; QPixmap _cache; + Ui::Animations::Simple _slideAnimation; + QPixmap _slideLeft; + QPixmap _slideRight; + }; [[nodiscard]] rpl::producer TopPeersContent( diff --git a/Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp b/Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp index 4797b7abb..52efe6f4d 100644 --- a/Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp +++ b/Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp @@ -467,6 +467,7 @@ void TopPeersStrip::paintEvent(QPaintEvent *e) { const auto nameLeft = x + st.nameLeft; const auto nameWidth = single - 2 * st.nameLeft; + p.setPen(st::dialogsNameFg); entry.name.drawElided( p, nameLeft, diff --git a/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp b/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp index 3dda10b06..c2ff31487 100644 --- a/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp +++ b/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp @@ -243,8 +243,10 @@ std::vector SettingsSlider::countSectionsWidths( return true; }); // If labelsWidth > sectionsWidth we're screwed anyway. - if (!commonWidth && labelsWidth <= sectionsWidth) { - auto padding = (sectionsWidth - labelsWidth) / (2. * count); + if (_st.strictSkip || (!commonWidth && labelsWidth <= sectionsWidth)) { + auto padding = _st.strictSkip + ? (_st.strictSkip / 2.) + : (sectionsWidth - labelsWidth) / (2. * count); auto currentWidth = result.begin(); enumerateSections([&](const Section §ion) { Expects(currentWidth != result.end()); @@ -334,7 +336,7 @@ void SettingsSlider::paintEvent(QPaintEvent *e) { + (section.width - activeWidth) / 2; auto active = 1. - std::clamp( - qAbs(range.left - activeLeft) / float64(section.width), + qAbs(range.left - activeLeft) / float64(range.width), 0., 1.); if (section.ripple) {