/* This file is part of Telegram Desktop, 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 "info/profile/info_profile_inner_widget.h" #include "info/info_controller.h" #include "info/info_memento.h" #include "info/info_wrap_widget.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_values.h" #include "info/saved/info_saved_music_widget.h" #include "data/data_changes.h" #include "data/data_channel.h" #include "data/data_document.h" #include "data/data_forum_topic.h" #include "data/data_peer.h" #include "data/data_photo.h" #include "data/data_file_origin.h" #include "data/data_user.h" #include "data/data_saved_music.h" #include "data/data_saved_sublist.h" #include "info/saved/info_saved_music_common.h" #include "info_profile_actions.h" #include "main/main_session.h" #include "apiwrap.h" #include "api/api_peer_photo.h" #include "lang/lang_keys.h" #include "ui/text/custom_emoji_helper.h" #include "ui/text/format_song_document_name.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/labels.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/shadow.h" #include "ui/wrap/fade_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" #include "ui/painter.h" #include "ui/vertical_list.h" #include "ui/ui_utility.h" #include "styles/style_info.h" namespace Info { namespace Profile { namespace { constexpr auto kMembersInlineMax = 5; [[nodiscard]] rpl::producer MembersInTabValue(not_null peer) { const auto channel = peer->asChannel(); return MembersCountValue( peer ) | rpl::filter([=](int count) { return (count > 0) && (!channel || channel->membersCountKnown()); }) | rpl::map([](int count) { return (count > kMembersInlineMax); }) | rpl::take(1); } void AddSavedMusic( not_null layout, not_null controller, not_null peer, rpl::producer> topBarColor) { const auto wrap = layout->add( object_ptr>( layout, object_ptr(layout))); Info::Saved::SetupSavedMusic( wrap->entity(), controller, peer, std::move(topBarColor)); using namespace rpl::mappers; wrap->toggleOn( wrap->entity()->heightValue() | rpl::map(_1 > 0), anim::type::instant); } void AddUnofficialSecurityRiskWarning( not_null layout, not_null user) { const auto wrap = layout->add( object_ptr>( layout, object_ptr(layout))); const auto content = wrap->entity(); user->session().changes().peerFlagsValue( user, Data::PeerUpdate::Flag::FullInfo ) | rpl::on_next([=] { while (content->count()) { delete content->widgetAt(0); } if (user->unofficialSecurityRisk()) { auto helper = Ui::Text::CustomEmojiHelper(); auto icon = helper.paletteDependent({ .factory = [] { const auto s = st::infoSecurityRiskIconSize; const auto ratio = style::DevicePixelRatio(); const auto rect = QRect(0, 0, s, s); auto result = QImage( rect.size() * ratio, QImage::Format_ARGB32_Premultiplied); result.setDevicePixelRatio(ratio); result.fill(Qt::transparent); auto p = QPainter(&result); auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(st::attentionButtonFg); p.drawEllipse(rect); p.setPen(st::windowFgActive); p.setFont(st::semiboldFont); p.drawText(rect, u"!"_q, style::al_center); p.end(); return result; }, .margin = st::infoSecurityRiskIconMargin, }); auto label = object_ptr( content, tr::lng_profile_unofficial_warning( lt_icon, rpl::single(std::move(icon)), lt_name, rpl::single(TextWithEntities{ user->firstName }), tr::marked), st::defaultDividerLabel.label, st::defaultPopupMenu, helper.context([=] { content->update(); })); content->add(object_ptr( content, std::move(label), st::defaultBoxDividerLabelPadding, st::defaultDividerLabel.bar, RectParts())); } content->resizeToWidth(content->width()); }, content->lifetime()); using namespace rpl::mappers; wrap->toggleOn( content->heightValue() | rpl::map(_1 > 0), anim::type::instant); } } // namespace InnerWidget::InnerWidget( QWidget *parent, not_null controller, Origin origin) : RpWidget(parent) , _controller(controller) , _peer(_controller->key().peer()) , _migrated(_controller->migrated()) , _topic(_controller->key().topic()) , _sublist(_controller->key().sublist()) , _content(setupContent(this, origin)) { _content->heightValue( ) | rpl::on_next([this](int height) { if (!_inResize) { resizeToWidth(width()); updateDesiredHeight(); } }, lifetime()); } rpl::producer<> InnerWidget::backRequest() const { return _backClicks.events(); } object_ptr InnerWidget::setupContent( not_null parent, Origin origin) { if (const auto user = _peer->asUser()) { user->session().changes().peerFlagsValue( user, Data::PeerUpdate::Flag::FullInfo ) | rpl::on_next([=] { auto &photos = user->session().api().peerPhoto(); if (const auto original = photos.nonPersonalPhoto(user)) { // Preload it for the edit contact box. _nonPersonalView = original->createMediaView(); const auto id = peerToUser(user->id); original->load(Data::FileOriginFullUser{ id }); } }, lifetime()); } auto result = object_ptr(parent); const auto musicPeer = _sublist ? _sublist->sublistPeer().get() : _peer.get(); AddSavedMusic( result.data(), _controller, musicPeer, _topBarColor.value()); if (const auto user = _peer->asUser()) { AddUnofficialSecurityRiskWarning(result.data(), user); } auto stack = SectionStack(result.data()); if (_topic && _topic->creating()) { stack.finalize(); return result; } BuildProfileDetailsSections( stack, _controller, _peer, _topic, _sublist, origin); const auto thirdColumn = (_controller->wrap() == Wrap::Side); const auto tabs = UseProfileMediaTabs() && !thirdColumn; auto sharedTracker = Ui::MultiSlideTracker(); if (!tabs) { auto sharedMediaWidget = SetupSharedMediaClassic( result.data(), _controller, _peer, _topic, _sublist, _migrated, sharedTracker); const auto raw = sharedMediaWidget.data(); _sharedMediaWrap = raw; stack.addPlainSeparator(); stack.add(Section{ .widget = std::move(sharedMediaWidget), .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 countValue = [&](Storage::SharedMediaType type) { return SharedMediaCountValue( tabsPeer, topicRootId, monoforumPeerId, _migrated, type); }; const auto addTab = [&](Storage::SharedMediaType type) { tabs.push_back(MakeMediaTabDescriptor( type, countValue(type) | rpl::map(_1 > 0))); }; const auto addMediaTabs = [&] { using Type = Storage::SharedMediaType; tabs.push_back(MakeMediaTabDescriptor( Type::PhotoVideo, rpl::combine( MediaTabsExpandedValue(), countValue(Type::Photo), countValue(Type::Video) ) | rpl::map([](bool expanded, int photos, int videos) { return !expanded && (photos + videos > 0); }))); const auto addSplit = [&](Type type) { tabs.push_back(MakeMediaTabDescriptor( type, rpl::combine( MediaTabsExpandedValue(), countValue(type) ) | rpl::map([](bool expanded, int count) { return expanded && (count > 0); }))); }; addSplit(Type::Photo); addSplit(Type::Video); }; if (!_topic) { tabs.push_back(MakeStoriesTabDescriptor(tabsPeer)); if (!_sublist) { tabs.push_back(MakeGiftsTabDescriptor(_peer)); } } if ((_peer->isChat() || _peer->isMegagroup()) && !_peer->isMonoforum() && !_topic && !_sublist) { tabs.push_back(MakeMembersTabDescriptor( _peer, MembersInTabValue(_peer))); } addMediaTabs(); if (!_topic) { tabs.push_back(MakeSavedTabDescriptor(tabsPeer)); } addTab(Storage::SharedMediaType::File); addTab(Storage::SharedMediaType::Link); addTab(Storage::SharedMediaType::MusicFile); 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; } if (auto manage = SetupChannelMembersAndManage( _controller, result.data(), _peer)) { const auto raw = static_cast*>( manage.data()); stack.addPlainSeparator(); stack.add(Section{ .widget = std::move(manage), .shown = raw->toggledValue(), }); } if (auto actions = SetupActions(_controller, result.data(), _peer)) { stack.addPlainSeparator(); stack.add(Section{ .widget = std::move(actions), .shown = rpl::single(true), }); } if ((_peer->isChat() || _peer->isMegagroup()) && !_peer->isMonoforum()) { auto shown = [&]() -> rpl::producer { if (!tabs) { return rpl::single(true); } return MembersInTabValue(_peer) | rpl::map([](bool inTab) { return !inTab; }); }(); stack.addPlainSeparator(); stack.add(makeMembersSection(result.data(), std::move(shown))); } if (tabs) { addTabsHost(); } stack.finalize(); return result; } Section InnerWidget::makeMembersSection( not_null parent, rpl::producer shown) { auto wrap = object_ptr>( parent, object_ptr(parent)); const auto raw = wrap.data(); const auto inner = raw->entity(); const auto toggled = raw->lifetime().make_state>(); raw->toggleOn(toggled->value(), anim::type::instant); using namespace rpl::mappers; std::move( shown ) | rpl::filter(_1) | rpl::take(1) | rpl::on_next([=] { _members = inner->add(object_ptr(inner, _controller)); _members->scrollToRequests( ) | rpl::on_next([this](Ui::ScrollToRequest request) { auto min = (request.ymin < 0) ? request.ymin : MapFrom(this, _members, QPoint(0, request.ymin)).y(); auto max = (request.ymin < 0) ? MapFrom(this, _members, QPoint()).y() : (request.ymax < 0) ? request.ymax : MapFrom(this, _members, QPoint(0, request.ymax)).y(); _scrollToRequests.fire({ min, max }); }, _members->lifetime()); _members->onlineCountValue( ) | rpl::on_next([=](int count) { _onlineCount.fire_copy(count); }, _members->lifetime()); _members->fullCountValue( ) | rpl::on_next([=](int count) { *toggled = (count > 0); }, _members->lifetime()); }, raw->lifetime()); return Section{ .widget = std::move(wrap), .shown = raw->toggledValue(), }; } int InnerWidget::countDesiredHeight() const { return _content->height() + (_members ? (_members->desiredHeight() - _members->height()) : 0); } 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 = (top > 0) && (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) { if (_members) { _members->restoreState(memento->membersState()); } if (_sharedMediaWrap) { _sharedMediaWrap->finishAnimating(); } if (_tabsHost) { if (const auto active = memento->activeTab(); !active.isEmpty()) { _tabsHost->restoreActiveTab(active); } } } rpl::producer InnerWidget::scrollToRequests() const { return _scrollToRequests.events(); } rpl::producer InnerWidget::desiredHeightValue() const { return _desiredHeight.events_starting_with(countDesiredHeight()); } int InnerWidget::resizeGetHeight(int newWidth) { _inResize = true; auto guard = gsl::finally([&] { _inResize = false; }); _content->resizeToWidth(newWidth); _content->moveToLeft(0, 0); updateDesiredHeight(); return _content->heightNoMargins(); } void InnerWidget::enableBackButton() { _backToggles.force_assign(true); } void InnerWidget::showFinished() { _showFinished.fire({}); } bool InnerWidget::hasFlexibleTopBar() const { return true; } base::weak_qptr InnerWidget::createPinnedToTop( not_null parent) { const auto content = Ui::CreateChild( parent, TopBar::Descriptor{ .controller = _controller->parentController(), .key = _controller->key(), .wrap = _controller->wrapValue(), .peer = _sublist ? _sublist->sublistPeer().get() : nullptr, .backToggles = _backToggles.value(), .showFinished = _showFinished.events(), }); 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 }); } base::weak_qptr InnerWidget::createPinnedToBottom( not_null parent) { return nullptr; } } // namespace Profile } // namespace Info