Added profile media tabs behind experimental option.

This commit is contained in:
23rd
2026-07-11 18:31:12 +03:00
parent 580dd4e4de
commit 76e26ebbb1
8 changed files with 280 additions and 9 deletions

View File

@@ -210,6 +210,9 @@ protected:
void scrollTopRestore(int scrollTop);
void scrollTo(const Ui::ScrollToRequest &request);
[[nodiscard]] rpl::producer<int> scrollTopValue() const;
[[nodiscard]] int innerTopReserve() const {
return _innerTopReserve;
}
void setPaintPadding(const style::margins &padding);

View File

@@ -10,12 +10,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "info/profile/info_profile_widget.h"
#include "info/profile/tabs/adapters/info_profile_tab_media.h"
#include "info/profile/tabs/adapters/info_profile_tab_members.h"
#include "info/profile/tabs/adapters/info_profile_tab_peer_lists.h"
#include "info/profile/tabs/adapters/info_profile_tab_polls.h"
#include "info/profile/tabs/adapters/info_profile_tab_saved.h"
#include "info/profile/tabs/adapters/info_profile_tab_stories.h"
#include "info/profile/tabs/info_profile_tabs_host.h"
#include "info/profile/info_profile_icon.h"
#include "info/profile/info_profile_members.h"
#include "info/profile/info_profile_music_button.h"
#include "info/profile/info_profile_shared_media_classic.h"
#include "info/profile/info_profile_top_bar.h"
#include "info/profile/info_profile_actions.h"
#include "info/profile/info_profile_shared_media_classic.h"
#include "info/profile/info_profile_values.h"
#include "info/saved/info_saved_music_widget.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
@@ -214,8 +222,9 @@ object_ptr<Ui::RpWidget> InnerWidget::setupContent(
_sublist,
origin);
const auto tabs = UseProfileMediaTabs();
auto sharedTracker = Ui::MultiSlideTracker();
{
if (!tabs) {
auto sharedMediaWidget = SetupSharedMediaClassic(
result.data(),
_controller,
@@ -232,7 +241,94 @@ object_ptr<Ui::RpWidget> InnerWidget::setupContent(
.shown = raw->toggledValue(),
});
}
const auto addTabsHost = [&] {
using namespace rpl::mappers;
const auto tabsPeer = _sublist ? _sublist->sublistPeer() : _peer;
const auto topicRootId = _topic ? _topic->rootId() : MsgId();
const auto monoforumPeerId = _sublist
? _sublist->sublistPeer()->id
: PeerId();
auto tabs = std::vector<MediaTabDescriptor>();
const auto addTab = [&](Storage::SharedMediaType type) {
tabs.push_back(MakeMediaTabDescriptor(
type,
SharedMediaCountValue(
tabsPeer,
topicRootId,
monoforumPeerId,
_migrated,
type) | rpl::map(_1 > 0)));
};
if ((_peer->isChat() || _peer->isMegagroup())
&& !_peer->isMonoforum()
&& !_topic
&& !_sublist) {
tabs.push_back(MakeMembersTabDescriptor(_peer));
}
if (!_topic) {
tabs.push_back(MakeStoriesTabDescriptor(tabsPeer));
if (!_sublist) {
tabs.push_back(MakeGiftsTabDescriptor(_peer));
}
tabs.push_back(MakeSavedTabDescriptor(tabsPeer));
}
addTab(Storage::SharedMediaType::Photo);
addTab(Storage::SharedMediaType::Video);
addTab(Storage::SharedMediaType::File);
addTab(Storage::SharedMediaType::MusicFile);
addTab(Storage::SharedMediaType::Link);
tabs.push_back(MakePollsTabDescriptor(SharedMediaCountValue(
tabsPeer,
topicRootId,
monoforumPeerId,
_migrated,
Storage::SharedMediaType::Poll) | rpl::map(_1 > 0)));
addTab(Storage::SharedMediaType::RoundVoiceFile);
addTab(Storage::SharedMediaType::GIF);
if (!_topic && !_sublist) {
if (const auto user = _peer->asUser()) {
tabs.push_back(MakeCommonGroupsTabDescriptor(user));
}
if (_peer->asBot() || _peer->asBroadcast()) {
tabs.push_back(MakeSimilarPeersTabDescriptor(_peer));
}
}
auto tabsHost = object_ptr<TabsHost>(
result.data(),
TabsHost::Descriptor{
.context = MediaTabContext{
.controller = _controller,
.peer = _peer,
.topic = _topic,
.sublist = _sublist,
.migrated = _migrated,
.onlineCountChanged = [this](int count) {
_onlineCount.fire_copy(count);
},
},
.tabs = std::move(tabs),
});
const auto raw = tabsHost.data();
_tabsHost = raw;
raw->scrollToRequests(
) | rpl::on_next([this, raw](Ui::ScrollToRequest request) {
const auto shift = MapFrom(this, raw, QPoint()).y();
_scrollToRequests.fire({
request.ymin + shift,
(request.ymax < 0) ? -1 : (request.ymax + shift),
});
}, raw->lifetime());
stack.addPlainSeparator();
stack.add(Section{
.widget = std::move(tabsHost),
.shown = raw->heightValue() | rpl::map(_1 > 0),
});
};
if (_topic || _sublist) {
if (tabs) {
addTabsHost();
}
stack.finalize();
return result;
}
@@ -255,7 +351,9 @@ object_ptr<Ui::RpWidget> InnerWidget::setupContent(
.shown = rpl::single(true),
});
}
if ((_peer->isChat() || _peer->isMegagroup())
if (tabs) {
addTabsHost();
} else if ((_peer->isChat() || _peer->isMegagroup())
&& !_peer->isMonoforum()) {
stack.addPlainSeparator();
stack.add(makeMembersSection(result.data()));
@@ -308,12 +406,20 @@ void InnerWidget::visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) {
setChildVisibleTopBottom(_content, visibleTop, visibleBottom);
if (_tabsHost) {
const auto top = MapFrom(this, _tabsHost, QPoint()).y();
_tabsHost->setVisibleRegion(visibleTop - top, visibleBottom - top);
_tabsDocked = (visibleTop >= top);
}
}
void InnerWidget::saveState(not_null<Memento*> memento) {
if (_members) {
memento->setMembersState(_members->saveState());
}
if (_tabsHost) {
memento->setActiveTab(_tabsHost->activeId());
}
}
void InnerWidget::restoreState(not_null<Memento*> memento) {
@@ -323,6 +429,11 @@ void InnerWidget::restoreState(not_null<Memento*> memento) {
if (_sharedMediaWrap) {
_sharedMediaWrap->finishAnimating();
}
if (_tabsHost) {
if (const auto active = memento->activeTab(); !active.isEmpty()) {
_tabsHost->restoreActiveTab(active);
}
}
}
rpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {
@@ -370,6 +481,11 @@ base::weak_qptr<Ui::RpWidget> InnerWidget::createPinnedToTop(
content->backRequest(
) | rpl::start_to_stream(_backClicks, content->lifetime());
content->setOnlineCount(_onlineCount.events());
if (_tabsHost) {
content->bindActiveTab(
_tabsHost->activeTabBindings(),
_tabsDocked.value());
}
_topBarColor = content->edgeColor();
return base::make_weak(not_null<Ui::RpWidget*>{ content });
}

View File

@@ -38,6 +38,7 @@ namespace Profile {
class Memento;
class Members;
class TabsHost;
struct Origin;
class InnerWidget final : public Ui::RpWidget {
@@ -64,6 +65,13 @@ public:
void enableBackButton();
void showFinished();
[[nodiscard]] TabsHost *tabsHost() const {
return _tabsHost;
}
[[nodiscard]] rpl::producer<bool> tabsDockedValue() const {
return _tabsDocked.value();
}
protected:
int resizeGetHeight(int newWidth) override;
void visibleTopBottomUpdated(
@@ -78,7 +86,11 @@ private:
int countDesiredHeight() const;
void updateDesiredHeight() {
_desiredHeight.fire(countDesiredHeight());
const auto value = countDesiredHeight();
if (_lastDesiredHeight != value) {
_lastDesiredHeight = value;
_desiredHeight.fire_copy(value);
}
}
const not_null<Controller*> _controller;
@@ -88,6 +100,7 @@ private:
Data::SavedSublist * const _sublist = nullptr;
bool _inResize = false;
int _lastDesiredHeight = -1;
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
rpl::event_stream<int> _desiredHeight;
@@ -102,6 +115,8 @@ private:
Members *_members = nullptr;
Ui::SlideWrap<RpWidget> *_sharedMediaWrap = nullptr;
TabsHost *_tabsHost = nullptr;
rpl::variable<bool> _tabsDocked = false;
object_ptr<RpWidget> _content;
};

View File

@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "info/profile/info_profile_inner_widget.h"
#include "info/profile/info_profile_members.h"
#include "info/profile/tabs/info_profile_tabs_host.h"
#include "info/settings/info_settings_widget.h"
#include "ui/widgets/scroll_area.h"
#include "ui/ui_utility.h"
@@ -107,6 +108,7 @@ Widget::Widget(
controller->setSearchEnabledByContent(false);
const auto classic = UseClassicProfileScroll();
const auto tabs = (_inner->tabsHost() != nullptr);
const auto flexible = _pinnedToTop
&& _pinnedToTop->minimumHeight()
&& _inner->hasFlexibleTopBar();
@@ -115,12 +117,19 @@ Widget::Widget(
}
_inner->scrollToRequests(
) | rpl::on_next([this](Ui::ScrollToRequest request) {
) | rpl::on_next([this, tabs](Ui::ScrollToRequest request) {
if (request.ymin < 0) {
scrollTopRestore(
qMin(scrollTopSave(), request.ymax));
} else {
} else if (!tabs) {
scrollTo(request);
} else {
// Inner coordinates miss the flexible cover top padding.
const auto reserve = innerTopReserve();
scrollTo({
request.ymin + reserve,
(request.ymax < 0) ? -1 : (request.ymax + reserve),
});
}
}, lifetime());
@@ -128,8 +137,12 @@ Widget::Widget(
checkBeforeClose([=] { controller->showBackFromStack(); });
}, _inner->lifetime());
setSwipeInterceptor([this](Ui::Controls::SwipeHandlerInitData data) {
return swipeTabsFinishData(data);
});
if (!classic && flexible) {
setupFlexibleRegularScroll(_inner, _pinnedToTop.get());
setupFlexibleRegularScroll(_inner, _pinnedToTop.get(), tabs);
} else if (_pinnedToTop) {
_inner->widthValue(
) | rpl::on_next([=](int w) {
@@ -154,7 +167,8 @@ Widget::Widget(
[=](rpl::producer<not_null<QEvent*>> &&events) {
ContentWidget::setViewport(std::move(events));
},
_flexibleScroll);
_flexibleScroll,
tabs);
}
if (_pinnedToBottom) {
@@ -175,6 +189,89 @@ Widget::Widget(
heightValue()
) | rpl::on_next(processHeight, _pinnedToBottom->lifetime());
}
setupTabsStripFloat();
}
void Widget::setupTabsStripFloat() {
const auto host = _inner->tabsHost();
if (!host || !_pinnedToTop) {
return;
}
_tabsHost = base::make_weak(host);
_tabsStrip = base::make_weak(host->stripWidget().get());
_inner->tabsDockedValue(
) | rpl::distinct_until_changed(
) | rpl::on_next([=](bool docked) {
const auto host = _tabsHost.get();
const auto strip = _tabsStrip.get();
if (!host || !strip) {
return;
}
if (docked) {
if (!_tabsStripFloat) {
_tabsStripFloat = base::make_unique_q<Ui::RpWidget>(this);
ContentWidget::setViewport(_tabsStripFloat->events(
) | rpl::filter([](not_null<QEvent*> e) {
return (e->type() == QEvent::Wheel);
}));
}
strip->setParent(_tabsStripFloat.get());
strip->moveToLeft(0, 0);
strip->show();
_tabsStripFloat->show();
_tabsStripFloat->raise();
updateTabsStripFloatGeometry();
} else if (_tabsStripFloat) {
_tabsStripFloat->hide();
host->returnStrip();
}
}, lifetime());
rpl::combine(
widthValue(),
host->stripWidget()->heightValue(),
host->stripWidget()->naturalWidthValue()
) | rpl::on_next([=] {
updateTabsStripFloatGeometry();
}, lifetime());
}
void Widget::updateTabsStripFloatGeometry() {
const auto strip = _tabsStrip.get();
if (!strip
|| !_tabsStripFloat
|| _tabsStripFloat->isHidden()
|| !_pinnedToTop) {
return;
}
const auto stripWidth = std::min(strip->naturalWidth(), width());
strip->resizeToWidth(stripWidth);
strip->moveToLeft(0, 0);
_tabsStripFloat->setGeometry(
(width() - stripWidth) / 2,
_pinnedToTop->minimumHeight(),
stripWidth,
strip->height());
}
auto Widget::swipeTabsFinishData(Ui::Controls::SwipeHandlerInitData data)
-> Ui::Controls::SwipeHandlerFinishData {
const auto host = _inner->tabsHost();
if (!host) {
return {};
}
const auto shift = Ui::MapFrom(_inner, host, QPoint());
const auto body = host->bodyGeometry().translated(shift);
if (!body.contains(data.cursorPosition)) {
return {};
}
const auto toNextTab = (data.direction == Qt::LeftToRight);
auto callback = host->prepareSwitch(toNextTab);
return callback
? Ui::Controls::DefaultSwipeBackHandlerFinishData(
std::move(callback))
: Ui::Controls::SwipeHandlerFinishData();
}
void Widget::setInnerFocus() {

View File

@@ -17,6 +17,7 @@ class ForumTopic;
namespace Info::Profile {
class InnerWidget;
class TabsHost;
struct MembersState;
struct GroupReactionOrigin {
@@ -50,7 +51,14 @@ public:
}
void setMembersState(std::unique_ptr<MembersState> state);
std::unique_ptr<MembersState> membersState();
[[nodiscard]] std::unique_ptr<MembersState> membersState();
void setActiveTab(const QString &id) {
_activeTab = id;
}
[[nodiscard]] QString activeTab() const {
return _activeTab;
}
~Memento();
@@ -64,6 +72,7 @@ private:
std::unique_ptr<MembersState> _membersState;
Origin _origin;
QString _activeTab;
};
@@ -88,6 +97,11 @@ public:
private:
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
void setupTabsStripFloat();
void updateTabsStripFloatGeometry();
[[nodiscard]] auto swipeTabsFinishData(
Ui::Controls::SwipeHandlerInitData data)
-> Ui::Controls::SwipeHandlerFinishData;
std::shared_ptr<ContentMemento> doCreateMemento() override;
@@ -96,6 +110,9 @@ private:
base::weak_qptr<Ui::RpWidget> _pinnedToTop;
base::weak_qptr<Ui::RpWidget> _pinnedToBottom;
std::unique_ptr<FlexibleScrollHelper> _flexibleScrollHelper;
base::unique_qptr<Ui::RpWidget> _tabsStripFloat;
base::weak_qptr<TabsHost> _tabsHost;
base::weak_qptr<Ui::RpWidget> _tabsStrip;
};

View File

@@ -8,6 +8,7 @@ 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 "base/options.h"
#include "ui/widgets/scroll_area.h"
#include "ui/effects/slide_animation.h"
#include "ui/painter.h"
@@ -16,6 +17,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_info.h"
namespace Info::Profile {
namespace {
base::options::toggle ProfileMediaTabs({
.id = kOptionProfileMediaTabs,
.name = "Show shared media as tabs in the profile.",
.description = "Replace the shared media buttons in profiles with "
"a strip of tabs holding the media lists inline. Work in progress.",
});
} // namespace
const char kOptionProfileMediaTabs[] = "profile-media-tabs";
bool UseProfileMediaTabs() {
return ProfileMediaTabs.value();
}
TabsHost::TabsHost(not_null<QWidget*> parent, Descriptor descriptor)
: RpWidget(parent)

View File

@@ -20,6 +20,10 @@ struct ScrollToRequest;
namespace Info::Profile {
extern const char kOptionProfileMediaTabs[];
[[nodiscard]] bool UseProfileMediaTabs();
class TabsStrip;
class TabsHost final : public Ui::RpWidget {

View File

@@ -39,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/controls/compose_controls_common.h"
#include "history/view/history_view_message.h"
#include "info/profile/info_profile_actions.h"
#include "info/profile/tabs/info_profile_tabs_host.h"
#include "lang/lang_keys.h"
#include "mainwindow.h"
#include "mainwidget.h"
@@ -286,6 +287,7 @@ void SetupExperimental(
addToggle(Window::kOptionViewProfileInChatsListContextMenu);
addToggle(Info::Profile::kOptionShowPeerIdBelowAbout);
addToggle(Info::Profile::kOptionShowChannelJoinedBelowAbout);
addToggle(Info::Profile::kOptionProfileMediaTabs);
addToggle(Ui::kOptionUseSmallMsgBubbleRadius);
addToggle(Media::Player::kOptionDisableAutoplayNext);
addToggle(Webview::kOptionWebviewDebugEnabled);