From f586e9dcd66bd6e2c5e71eb80b9cb009a0a1bb07 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sun, 12 Jul 2026 09:35:53 +0300 Subject: [PATCH] Added ordering and setting of main profile media tab. --- Telegram/Resources/langs/lang.strings | 1 + .../tabs/adapters/info_profile_tab_media.cpp | 13 ++ .../adapters/info_profile_tab_peer_lists.cpp | 1 + .../adapters/info_profile_tab_stories.cpp | 1 + .../profile/tabs/info_profile_tab_content.h | 2 + .../profile/tabs/info_profile_tabs_host.cpp | 140 ++++++++++++++++-- .../profile/tabs/info_profile_tabs_host.h | 12 ++ .../profile/tabs/info_profile_tabs_strip.cpp | 11 ++ .../profile/tabs/info_profile_tabs_strip.h | 3 + 9 files changed, 171 insertions(+), 13 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 2e3120b33f..be19fa2318 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1869,6 +1869,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_media_type_similar" = "Similar"; "lng_media_type_gifts" = "Gifts"; "lng_media_type_saved" = "Saved"; +"lng_profile_tab_set_as_main" = "Set as Main Tab"; "lng_polls_search_none" = "No polls found"; "lng_media_type_rounds" = "Video messages"; "lng_media_zoom_in" = "Zoom In"; diff --git a/Telegram/SourceFiles/info/profile/tabs/adapters/info_profile_tab_media.cpp b/Telegram/SourceFiles/info/profile/tabs/adapters/info_profile_tab_media.cpp index ce8f7510f0..b990529515 100644 --- a/Telegram/SourceFiles/info/profile/tabs/adapters/info_profile_tab_media.cpp +++ b/Telegram/SourceFiles/info/profile/tabs/adapters/info_profile_tab_media.cpp @@ -59,6 +59,18 @@ using SharedMediaType = Storage::SharedMediaType; return u"media:"_q + QString::number(int(type)); } +[[nodiscard]] Data::ProfileTab MediaProfileTab(SharedMediaType type) { + switch (type) { + case SharedMediaType::Photo: return Data::ProfileTab::Media; + case SharedMediaType::File: return Data::ProfileTab::Files; + case SharedMediaType::MusicFile: return Data::ProfileTab::Music; + case SharedMediaType::RoundVoiceFile: return Data::ProfileTab::Voice; + case SharedMediaType::Link: return Data::ProfileTab::Links; + case SharedMediaType::GIF: return Data::ProfileTab::Gifs; + default: return Data::ProfileTab::None; + } +} + class MediaTabAdapter final : public MediaTabContent { public: MediaTabAdapter(MediaTabContext context, SharedMediaType type) @@ -253,6 +265,7 @@ MediaTabDescriptor MakeMediaTabDescriptor( std::move(context), type); }, + .profileTab = MediaProfileTab(type), }; } diff --git a/Telegram/SourceFiles/info/profile/tabs/adapters/info_profile_tab_peer_lists.cpp b/Telegram/SourceFiles/info/profile/tabs/adapters/info_profile_tab_peer_lists.cpp index f1df739040..22b8defd92 100644 --- a/Telegram/SourceFiles/info/profile/tabs/adapters/info_profile_tab_peer_lists.cpp +++ b/Telegram/SourceFiles/info/profile/tabs/adapters/info_profile_tab_peer_lists.cpp @@ -190,6 +190,7 @@ MediaTabDescriptor MakeGiftsTabDescriptor(not_null peer) { std::move(context), peer); }, + .profileTab = Data::ProfileTab::Gifts, }; } diff --git a/Telegram/SourceFiles/info/profile/tabs/adapters/info_profile_tab_stories.cpp b/Telegram/SourceFiles/info/profile/tabs/adapters/info_profile_tab_stories.cpp index a5dfecf75f..3a151d628e 100644 --- a/Telegram/SourceFiles/info/profile/tabs/adapters/info_profile_tab_stories.cpp +++ b/Telegram/SourceFiles/info/profile/tabs/adapters/info_profile_tab_stories.cpp @@ -154,6 +154,7 @@ MediaTabDescriptor MakeStoriesTabDescriptor(not_null peer) { .factory = [](MediaTabContext context) { return std::make_unique(std::move(context)); }, + .profileTab = Data::ProfileTab::Posts, }; } diff --git a/Telegram/SourceFiles/info/profile/tabs/info_profile_tab_content.h b/Telegram/SourceFiles/info/profile/tabs/info_profile_tab_content.h index 7a56084809..eefdf9486a 100644 --- a/Telegram/SourceFiles/info/profile/tabs/info_profile_tab_content.h +++ b/Telegram/SourceFiles/info/profile/tabs/info_profile_tab_content.h @@ -14,6 +14,7 @@ class PeerData; namespace Data { class ForumTopic; class SavedSublist; +enum class ProfileTab : uchar; } // namespace Data namespace Ui { @@ -64,6 +65,7 @@ struct MediaTabDescriptor { rpl::producer title; rpl::producer shown; Fn(MediaTabContext)> factory; + Data::ProfileTab profileTab = {}; }; } // namespace Info::Profile diff --git a/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_host.cpp b/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_host.cpp index 7dcf133671..b4c75aae66 100644 --- a/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_host.cpp +++ b/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_host.cpp @@ -8,13 +8,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/tabs/info_profile_tabs_host.h" #include "info/profile/tabs/info_profile_tabs_strip.h" +#include "apiwrap.h" #include "base/options.h" +#include "data/data_changes.h" +#include "data/data_channel.h" +#include "data/data_peer.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/widgets/menu/menu_add_action_callback.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" +#include "ui/widgets/popup_menu.h" #include "ui/widgets/scroll_area.h" #include "ui/effects/slide_animation.h" #include "ui/painter.h" #include "ui/ui_utility.h" #include "styles/style_basic.h" #include "styles/style_info.h" +#include "styles/style_menu_icons.h" namespace Info::Profile { namespace { @@ -48,8 +58,14 @@ TabsHost::TabsHost(not_null parent, Descriptor descriptor) } _stripTitles.assign(_tabs.size(), QString()); _tabsShown.assign(_tabs.size(), false); + refreshOrder(); wireStripTitles(); wireTabsVisibility(); + wireMainTab(); + _strip->contextMenuRequests( + ) | rpl::on_next([this](const QString &id) { + showTabMenu(id); + }, lifetime()); _strip->activated( ) | rpl::on_next([this](const QString &id) { _userChosenTab = true; @@ -159,10 +175,109 @@ void TabsHost::wireTabsVisibility() { } } +void TabsHost::wireMainTab() { + const auto peer = _context.peer; + peer->session().changes().peerFlagsValue( + peer, + Data::PeerUpdate::Flag::MainProfileTab + ) | rpl::on_next([this] { + if (refreshOrder()) { + syncStripTitles(); + ensureActiveVisible(); + } + }, lifetime()); +} + +bool TabsHost::refreshOrder() { + const auto main = _context.peer->mainProfileTab(); + const auto i = (main == Data::ProfileTab::None) + ? end(_tabs) + : ranges::find(_tabs, main, &MediaTabDescriptor::profileTab); + const auto mainTabIndex = (i != end(_tabs)) + ? int(i - begin(_tabs)) + : -1; + if (!_order.empty() && _mainTabIndex == mainTabIndex) { + return false; + } + _mainTabIndex = mainTabIndex; + _order.resize(_tabs.size()); + for (auto index = 0; index != int(_order.size()); ++index) { + _order[index] = index; + } + if (mainTabIndex > 0) { + std::rotate( + begin(_order), + begin(_order) + mainTabIndex, + begin(_order) + mainTabIndex + 1); + } + return true; +} + +int TabsHost::displayPosition(int index) const { + return int(ranges::find(_order, index) - begin(_order)); +} + +int TabsHost::firstVisibleIndex() const { + for (const auto i : _order) { + if (_tabsShown[i]) { + return i; + } + } + return -1; +} + +bool TabsHost::canSetMainTab(Data::ProfileTab tab) const { + if (tab == Data::ProfileTab::None || _context.topic || _context.sublist) { + return false; + } else if (const auto channel = _context.peer->asBroadcast()) { + return channel->canEditInformation(); + } + return _context.peer->isSelf() + && ((tab == Data::ProfileTab::Posts) + || (tab == Data::ProfileTab::Gifts)); +} + +void TabsHost::showTabMenu(const QString &id) { + const auto strip = _stripWeak.get(); + const auto i = ranges::find(_tabs, id, &MediaTabDescriptor::id); + if (!strip || i == end(_tabs)) { + return; + } + const auto tab = i->profileTab; + const auto index = int(i - begin(_tabs)); + if (!canSetMainTab(tab) || index == firstVisibleIndex()) { + return; + } + _menu = base::make_unique_q( + strip, + st::popupMenuWithIcons); + Ui::Menu::CreateAddActionCallback(_menu)( + tr::lng_profile_tab_set_as_main(tr::now), + crl::guard(this, [=] { setMainTab(tab); }), + &st::menuIconReorder); + _menu->popup(QCursor::pos()); +} + +void TabsHost::setMainTab(Data::ProfileTab tab) { + const auto peer = _context.peer; + const auto sending = Data::ProfileTabToMTP(tab); + if (const auto channel = peer->asChannel()) { + peer->session().api().request(MTPchannels_SetMainProfileTab( + channel->inputChannel(), + sending + )).send(); + } else { + peer->session().api().request( + MTPaccount_SetMainProfileTab(sending) + ).send(); + } + peer->setMainProfileTab(tab); +} + void TabsHost::syncStripTitles() { auto stripTabs = std::vector(); stripTabs.reserve(_tabs.size()); - for (auto i = 0; i != int(_tabs.size()); ++i) { + for (const auto i : _order) { if (!_tabsShown[i]) { continue; } @@ -184,14 +299,7 @@ void TabsHost::syncStripTitles() { } void TabsHost::ensureActiveVisible() { - const auto firstVisible = [&] { - for (auto i = 0; i != int(_tabs.size()); ++i) { - if (_tabsShown[i]) { - return i; - } - } - return -1; - }(); + const auto firstVisible = firstVisibleIndex(); const auto activeIndex = _activeId.isEmpty() ? -1 : int(ranges::find(_tabs, _activeId, &MediaTabDescriptor::id) @@ -246,10 +354,15 @@ Fn TabsHost::prepareSwitch(bool toNextTab) { if (active >= int(_tabs.size())) { return nullptr; } + const auto position = displayPosition(active); + if (position >= int(_order.size())) { + return nullptr; + } const auto delta = toNextTab ? 1 : -1; - for (auto i = active + delta - ; i >= 0 && i < int(_tabs.size()) - ; i += delta) { + for (auto p = position + delta + ; p >= 0 && p < int(_order.size()) + ; p += delta) { + const auto i = _order[p]; if (!_tabsShown[i]) { continue; } @@ -380,7 +493,8 @@ void TabsHost::activateTab(const QString &id, bool animated) { startSlideAnimation( std::move(wasCache), active, - previousIndex > int(it - begin(_tabs))); + displayPosition(previousIndex) + > displayPosition(int(it - begin(_tabs)))); } previous->widget()->hide(); } diff --git a/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_host.h b/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_host.h index 800f2856a3..f6f8d57dea 100644 --- a/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_host.h +++ b/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_host.h @@ -9,10 +9,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/object_ptr.h" #include "base/flat_map.h" +#include "base/unique_qptr.h" #include "info/profile/tabs/info_profile_tab_content.h" #include "ui/rp_widget.h" namespace Ui { +class PopupMenu; class RpWidget; class SlideAnimation; struct ScrollToRequest; @@ -60,6 +62,13 @@ protected: private: void wireStripTitles(); void wireTabsVisibility(); + void wireMainTab(); + bool refreshOrder(); + [[nodiscard]] int displayPosition(int index) const; + [[nodiscard]] int firstVisibleIndex() const; + [[nodiscard]] bool canSetMainTab(Data::ProfileTab tab) const; + void showTabMenu(const QString &id); + void setMainTab(Data::ProfileTab tab); void syncStripTitles(); void ensureActiveVisible(); void pushViewportToActive(); @@ -77,9 +86,12 @@ private: std::vector _tabs; std::vector _stripTitles; std::vector _tabsShown; + std::vector _order; + int _mainTabIndex = -1; TabsStrip *_strip = nullptr; base::weak_qptr _stripWeak; + base::unique_qptr _menu; Ui::RpWidget *_body = nullptr; int _stripHeight = 0; int _visibleTop = 0; diff --git a/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_strip.cpp b/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_strip.cpp index 86144eef2a..c09479c406 100644 --- a/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_strip.cpp +++ b/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_strip.cpp @@ -120,6 +120,10 @@ rpl::producer TabsStrip::activated() const { return _activated.events(); } +rpl::producer TabsStrip::contextMenuRequests() const { + return _contextMenuRequests.events(); +} + QRect TabsStrip::islandRect() const { return rect() - _st.margin; } @@ -333,6 +337,13 @@ void TabsStrip::mousePressEvent(QMouseEvent *e) { } } +void TabsStrip::contextMenuEvent(QContextMenuEvent *e) { + const auto index = indexAt(e->pos()); + if (index >= 0) { + _contextMenuRequests.fire_copy(_buttons[index].tab.id); + } +} + void TabsStrip::mouseReleaseEvent(QMouseEvent *e) { if (e->button() != Qt::LeftButton) { return; diff --git a/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_strip.h b/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_strip.h index f6f07060c0..4b08e826bb 100644 --- a/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_strip.h +++ b/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_strip.h @@ -35,6 +35,7 @@ public: void setActiveTab(const QString &id); [[nodiscard]] rpl::producer activated() const; + [[nodiscard]] rpl::producer contextMenuRequests() const; private: struct Button { @@ -49,6 +50,7 @@ private: void mouseMoveEvent(QMouseEvent *e) override; void mousePressEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; + void contextMenuEvent(QContextMenuEvent *e) override; void paintEvent(QPaintEvent *e) override; bool eventHook(QEvent *e) override; @@ -69,6 +71,7 @@ private: Ui::BoxShadow _shadow; std::vector