diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index 27da683b66..1330a69718 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -210,6 +210,9 @@ protected: void scrollTopRestore(int scrollTop); void scrollTo(const Ui::ScrollToRequest &request); [[nodiscard]] rpl::producer scrollTopValue() const; + [[nodiscard]] int innerTopReserve() const { + return _innerTopReserve; + } void setPaintPadding(const style::margins &padding); diff --git a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp index 3b45dac5aa..a7547f571c 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp @@ -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 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 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(); + 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( + 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 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) { if (_members) { memento->setMembersState(_members->saveState()); } + if (_tabsHost) { + memento->setActiveTab(_tabsHost->activeId()); + } } void InnerWidget::restoreState(not_null memento) { @@ -323,6 +429,11 @@ void InnerWidget::restoreState(not_null memento) { if (_sharedMediaWrap) { _sharedMediaWrap->finishAnimating(); } + if (_tabsHost) { + if (const auto active = memento->activeTab(); !active.isEmpty()) { + _tabsHost->restoreActiveTab(active); + } + } } rpl::producer InnerWidget::scrollToRequests() const { @@ -370,6 +481,11 @@ base::weak_qptr 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{ content }); } diff --git a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.h b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.h index 4db223f100..7a602c473b 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.h +++ b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.h @@ -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 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; @@ -88,6 +100,7 @@ private: Data::SavedSublist * const _sublist = nullptr; bool _inResize = false; + int _lastDesiredHeight = -1; rpl::event_stream _scrollToRequests; rpl::event_stream _desiredHeight; @@ -102,6 +115,8 @@ private: Members *_members = nullptr; Ui::SlideWrap *_sharedMediaWrap = nullptr; + TabsHost *_tabsHost = nullptr; + rpl::variable _tabsDocked = false; object_ptr _content; }; diff --git a/Telegram/SourceFiles/info/profile/info_profile_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_widget.cpp index bff8b4fffd..7007bf8845 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_widget.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_widget.cpp @@ -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> &&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(this); + ContentWidget::setViewport(_tabsStripFloat->events( + ) | rpl::filter([](not_null 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() { diff --git a/Telegram/SourceFiles/info/profile/info_profile_widget.h b/Telegram/SourceFiles/info/profile/info_profile_widget.h index 0665c5d625..e22e8cdb2e 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_widget.h +++ b/Telegram/SourceFiles/info/profile/info_profile_widget.h @@ -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 state); - std::unique_ptr membersState(); + [[nodiscard]] std::unique_ptr membersState(); + + void setActiveTab(const QString &id) { + _activeTab = id; + } + [[nodiscard]] QString activeTab() const { + return _activeTab; + } ~Memento(); @@ -64,6 +72,7 @@ private: std::unique_ptr _membersState; Origin _origin; + QString _activeTab; }; @@ -88,6 +97,11 @@ public: private: void saveState(not_null memento); void restoreState(not_null memento); + void setupTabsStripFloat(); + void updateTabsStripFloatGeometry(); + [[nodiscard]] auto swipeTabsFinishData( + Ui::Controls::SwipeHandlerInitData data) + -> Ui::Controls::SwipeHandlerFinishData; std::shared_ptr doCreateMemento() override; @@ -96,6 +110,9 @@ private: base::weak_qptr _pinnedToTop; base::weak_qptr _pinnedToBottom; std::unique_ptr _flexibleScrollHelper; + base::unique_qptr _tabsStripFloat; + base::weak_qptr _tabsHost; + base::weak_qptr _tabsStrip; }; 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 cf3018ff8b..7dcf133671 100644 --- a/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_host.cpp +++ b/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_host.cpp @@ -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 parent, Descriptor descriptor) : RpWidget(parent) 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 13c237e581..800f2856a3 100644 --- a/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_host.h +++ b/Telegram/SourceFiles/info/profile/tabs/info_profile_tabs_host.h @@ -20,6 +20,10 @@ struct ScrollToRequest; namespace Info::Profile { +extern const char kOptionProfileMediaTabs[]; + +[[nodiscard]] bool UseProfileMediaTabs(); + class TabsStrip; class TabsHost final : public Ui::RpWidget { diff --git a/Telegram/SourceFiles/settings/settings_experimental.cpp b/Telegram/SourceFiles/settings/settings_experimental.cpp index 13ecca0b42..d36f814e55 100644 --- a/Telegram/SourceFiles/settings/settings_experimental.cpp +++ b/Telegram/SourceFiles/settings/settings_experimental.cpp @@ -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);