/* 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_actions.h" #include "data/data_peer_values.h" #include "data/data_session.h" #include "data/data_folder.h" #include "data/data_channel.h" #include "data/data_changes.h" #include "data/data_user.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/padding_wrap.h" #include "ui/wrap/slide_wrap.h" #include "ui/widgets/shadow.h" #include "ui/widgets/labels.h" #include "ui/widgets/buttons.h" #include "ui/widgets/box_content_divider.h" #include "ui/boxes/report_box.h" #include "ui/layers/generic_box.h" #include "ui/toast/toast.h" #include "ui/text/text_utilities.h" // Ui::Text::ToUpper #include "history/history_location_manager.h" // LocationClickHandler. #include "history/view/history_view_context_menu.h" // HistoryView::ShowReportPeerBox #include "boxes/abstract_box.h" #include "ui/boxes/confirm_box.h" #include "boxes/peer_list_box.h" #include "boxes/peer_list_controllers.h" #include "boxes/add_contact_box.h" #include "boxes/peers/edit_contact_box.h" #include "lang/lang_keys.h" #include "info/info_controller.h" #include "info/info_memento.h" #include "info/profile/info_profile_icon.h" #include "info/profile/info_profile_values.h" #include "info/profile/info_profile_text.h" #include "support/support_helper.h" #include "window/window_session_controller.h" #include "window/window_controller.h" // Window::Controller::show. #include "window/window_peer_menu.h" #include "mainwidget.h" #include "mainwindow.h" // MainWindow::controller. #include "main/main_session.h" #include "core/application.h" #include "core/click_handler_types.h" #include "apiwrap.h" #include "api/api_blocked_peers.h" #include "facades.h" #include "styles/style_info.h" #include "styles/style_boxes.h" #include #include namespace Info { namespace Profile { namespace { object_ptr CreateSkipWidget( not_null parent) { return Ui::CreateSkipWidget(parent, st::infoProfileSkip); } object_ptr> CreateSlideSkipWidget( not_null parent) { auto result = Ui::CreateSlideSkipWidget( parent, st::infoProfileSkip); result->setDuration(st::infoSlideDuration); return result; } template auto AddActionButton( not_null parent, Text &&text, ToggleOn &&toggleOn, Callback &&callback, const style::SettingsButton &st = st::infoSharedMediaButton) { auto result = parent->add(object_ptr>( parent, object_ptr( parent, std::move(text), st)) ); result->setDuration( st::infoSlideDuration )->toggleOn( std::move(toggleOn) )->entity()->addClickHandler(std::move(callback)); result->finishAnimating(); return result; }; template auto AddMainButton( not_null parent, Text &&text, ToggleOn &&toggleOn, Callback &&callback, Ui::MultiSlideTracker &tracker) { tracker.track(AddActionButton( parent, std::move(text) | Ui::Text::ToUpper(), std::move(toggleOn), std::move(callback), st::infoMainButton)); } class DetailsFiller { public: DetailsFiller( not_null controller, not_null parent, not_null peer); object_ptr fill(); private: object_ptr setupInfo(); object_ptr setupMuteToggle(); void setupMainButtons(); Ui::MultiSlideTracker fillUserButtons( not_null user); Ui::MultiSlideTracker fillChannelButtons( not_null channel); template < typename Widget, typename = std::enable_if_t< std::is_base_of_v>> Widget *add( object_ptr &&child, const style::margins &margin = style::margins()) { return _wrap->add( std::move(child), margin); } not_null _controller; not_null _parent; not_null _peer; object_ptr _wrap; }; class ActionsFiller { public: ActionsFiller( not_null controller, not_null parent, not_null peer); object_ptr fill(); private: void addInviteToGroupAction(not_null user); void addShareContactAction(not_null user); void addEditContactAction(not_null user); void addDeleteContactAction(not_null user); void addClearHistoryAction(not_null user); void addDeleteConversationAction(not_null user); void addBotCommandActions(not_null user); void addReportAction(); void addBlockAction(not_null user); void addLeaveChannelAction(not_null channel); void addJoinChannelAction(not_null channel); void fillUserActions(not_null user); void fillChannelActions(not_null channel); not_null _controller; not_null _parent; not_null _peer; object_ptr _wrap = { nullptr }; }; DetailsFiller::DetailsFiller( not_null controller, not_null parent, not_null peer) : _controller(controller) , _parent(parent) , _peer(peer) , _wrap(_parent) { } template bool SetClickContext( const ClickHandlerPtr &handler, const ClickContext &context) { if (const auto casted = std::dynamic_pointer_cast(handler)) { casted->T::onClick(context); return true; } return false; } object_ptr DetailsFiller::setupInfo() { auto result = object_ptr(_wrap); auto tracker = Ui::MultiSlideTracker(); // Fill context for a mention / hashtag / bot command link. const auto infoClickFilter = [=, peer = _peer.get(), window = _controller->parentController()]( const ClickHandlerPtr &handler, Qt::MouseButton button) { const auto context = ClickContext{ button, QVariant::fromValue(ClickHandlerContext{ .sessionWindow = base::make_weak(window.get()), .peer = peer, }) }; if (SetClickContext(handler, context)) { return false; } else if (SetClickContext(handler, context)) { return false; } else if (SetClickContext(handler, context)) { return false; } else if (SetClickContext(handler, context)) { return false; } return true; }; auto addInfoLineGeneric = [&]( rpl::producer &&label, rpl::producer &&text, const style::FlatLabel &textSt = st::infoLabeled) { auto line = CreateTextWithLabel( result, std::move(label) | Ui::Text::ToWithEntities(), std::move(text), textSt, st::infoProfileLabeledPadding); tracker.track(result->add(std::move(line.wrap))); line.text->setClickHandlerFilter(infoClickFilter); return line.text; }; auto addInfoLine = [&]( rpl::producer &&label, rpl::producer &&text, const style::FlatLabel &textSt = st::infoLabeled) { return addInfoLineGeneric( std::move(label), std::move(text), textSt); }; auto addInfoOneLine = [&]( rpl::producer &&label, rpl::producer &&text, const QString &contextCopyText) { auto result = addInfoLine( std::move(label), std::move(text), st::infoLabeledOneLine); result->setDoubleClickSelectsParagraph(true); result->setContextCopyText(contextCopyText); return result; }; if (const auto user = _peer->asUser()) { if (user->session().supportMode()) { addInfoLineGeneric( user->session().supportHelper().infoLabelValue(user), user->session().supportHelper().infoTextValue(user)); } addInfoOneLine( tr::lng_info_mobile_label(), PhoneOrHiddenValue(user), tr::lng_profile_copy_phone(tr::now)); auto label = user->isBot() ? tr::lng_info_about_label() : tr::lng_info_bio_label(); addInfoLine(std::move(label), AboutValue(user)); addInfoOneLine( tr::lng_info_username_label(), UsernameValue(user), tr::lng_context_copy_mention(tr::now)); const auto controller = _controller->parentController(); AddMainButton( result, tr::lng_info_add_as_contact(), CanAddContactValue(user), [=] { controller->window().show(Box(EditContactBox, controller, user)); }, tracker); } else { auto linkText = LinkValue( _peer ) | rpl::map([](const QString &link) { return link.isEmpty() ? TextWithEntities() : Ui::Text::Link( (link.startsWith(qstr("https://")) ? link.mid(qstr("https://").size()) : link), link); }); auto link = addInfoOneLine( tr::lng_info_link_label(), std::move(linkText), QString()); link->setClickHandlerFilter([peer = _peer](auto&&...) { const auto link = peer->session().createInternalLinkFull( peer->userName()); if (!link.isEmpty()) { QGuiApplication::clipboard()->setText(link); Ui::Toast::Show(tr::lng_username_copied(tr::now)); } return false; }); if (const auto channel = _peer->asChannel()) { auto locationText = LocationValue( channel ) | rpl::map([](const ChannelLocation *location) { return location ? Ui::Text::Link( TextUtilities::SingleLine(location->address), LocationClickHandler::Url(location->point)) : TextWithEntities(); }); addInfoOneLine( tr::lng_info_location_label(), std::move(locationText), QString() )->setLinksTrusted(); } addInfoLine(tr::lng_info_about_label(), AboutValue(_peer)); } if (!_peer->isSelf()) { // No notifications toggle for Self => no separator. result->add(object_ptr>( result, object_ptr(result), st::infoProfileSeparatorPadding) )->setDuration( st::infoSlideDuration )->toggleOn( std::move(tracker).atLeastOneShownValue() ); } object_ptr( result, st::infoIconInformation, st::infoInformationIconPosition); return result; } object_ptr DetailsFiller::setupMuteToggle() { const auto peer = _peer; auto result = object_ptr( _wrap, tr::lng_profile_enable_notifications(), st::infoNotificationsButton); result->toggleOn( NotificationsEnabledValue(peer) )->addClickHandler([=] { const auto muteForSeconds = peer->owner().notifyIsMuted(peer) ? 0 : Data::NotifySettings::kDefaultMutePeriod; peer->owner().updateNotifySettings(peer, muteForSeconds); }); object_ptr( result, st::infoIconNotifications, st::infoNotificationsIconPosition); return result; } void DetailsFiller::setupMainButtons() { auto wrapButtons = [=](auto &&callback) { auto topSkip = _wrap->add(CreateSlideSkipWidget(_wrap)); auto tracker = callback(); topSkip->toggleOn(std::move(tracker).atLeastOneShownValue()); }; if (auto user = _peer->asUser()) { wrapButtons([=] { return fillUserButtons(user); }); } else if (auto channel = _peer->asChannel()) { if (!channel->isMegagroup()) { wrapButtons([=] { return fillChannelButtons(channel); }); } } } Ui::MultiSlideTracker DetailsFiller::fillUserButtons( not_null user) { using namespace rpl::mappers; Ui::MultiSlideTracker tracker; auto window = _controller->parentController(); auto addSendMessageButton = [&] { auto activePeerValue = window->activeChatValue( ) | rpl::map([](Dialogs::Key key) { return key.peer(); }); auto sendMessageVisible = rpl::combine( _controller->wrapValue(), std::move(activePeerValue), (_1 != Wrap::Side) || (_2 != user)); auto sendMessage = [window, user] { window->showPeerHistory( user, Window::SectionShow::Way::Forward); }; AddMainButton( _wrap, tr::lng_profile_send_message(), std::move(sendMessageVisible), std::move(sendMessage), tracker); }; if (user->isSelf()) { auto separator = _wrap->add(object_ptr>( _wrap, object_ptr(_wrap), st::infoProfileSeparatorPadding) )->setDuration( st::infoSlideDuration ); addSendMessageButton(); separator->toggleOn( std::move(tracker).atLeastOneShownValue() ); } else { addSendMessageButton(); } return tracker; } Ui::MultiSlideTracker DetailsFiller::fillChannelButtons( not_null channel) { using namespace rpl::mappers; Ui::MultiSlideTracker tracker; auto window = _controller->parentController(); auto activePeerValue = window->activeChatValue( ) | rpl::map([](Dialogs::Key key) { return key.peer(); }); auto viewChannelVisible = rpl::combine( _controller->wrapValue(), std::move(activePeerValue), (_1 != Wrap::Side) || (_2 != channel)); auto viewChannel = [=] { window->showPeerHistory( channel, Window::SectionShow::Way::Forward); }; AddMainButton( _wrap, tr::lng_profile_view_channel(), std::move(viewChannelVisible), std::move(viewChannel), tracker); return tracker; } object_ptr DetailsFiller::fill() { add(object_ptr(_wrap)); add(CreateSkipWidget(_wrap)); add(setupInfo()); if (!_peer->isSelf()) { add(setupMuteToggle()); } setupMainButtons(); add(CreateSkipWidget(_wrap)); return std::move(_wrap); } ActionsFiller::ActionsFiller( not_null controller, not_null parent, not_null peer) : _controller(controller) , _parent(parent) , _peer(peer) { } void ActionsFiller::addInviteToGroupAction( not_null user) { AddActionButton( _wrap, tr::lng_profile_invite_to_group(), CanInviteBotToGroupValue(user), [=] { AddBotToGroupBoxController::Start(user); }); } void ActionsFiller::addShareContactAction(not_null user) { const auto controller = _controller; AddActionButton( _wrap, tr::lng_info_share_contact(), CanShareContactValue(user), [=] { Window::PeerMenuShareContactBox(controller, user); }); } void ActionsFiller::addEditContactAction(not_null user) { const auto controller = _controller->parentController(); AddActionButton( _wrap, tr::lng_info_edit_contact(), IsContactValue(user), [=] { controller->window().show(Box(EditContactBox, controller, user)); }); } void ActionsFiller::addDeleteContactAction( not_null user) { AddActionButton( _wrap, tr::lng_info_delete_contact(), IsContactValue(user), [user] { Window::PeerMenuDeleteContact(user); }); } void ActionsFiller::addClearHistoryAction(not_null user) { AddActionButton( _wrap, tr::lng_profile_clear_history(), rpl::single(true), Window::ClearHistoryHandler(user)); } void ActionsFiller::addDeleteConversationAction( not_null user) { AddActionButton( _wrap, tr::lng_profile_delete_conversation(), rpl::single(true), Window::DeleteAndLeaveHandler(user)); } void ActionsFiller::addBotCommandActions(not_null user) { auto findBotCommand = [user](const QString &command) { if (!user->isBot()) { return QString(); } for (const auto &data : user->botInfo->commands) { const auto isSame = !data.command.compare( command, Qt::CaseInsensitive); if (isSame) { return data.command; } } return QString(); }; auto hasBotCommandValue = [=](const QString &command) { return user->session().changes().peerFlagsValue( user, Data::PeerUpdate::Flag::BotCommands ) | rpl::map([=] { return !findBotCommand(command).isEmpty(); }); }; auto sendBotCommand = [=, window = _controller->parentController()]( const QString &command) { const auto original = findBotCommand(command); if (original.isEmpty()) { return; } BotCommandClickHandler('/' + original).onClick(ClickContext{ Qt::LeftButton, QVariant::fromValue(ClickHandlerContext{ .sessionWindow = base::make_weak(window.get()), .peer = user, }) }); }; auto addBotCommand = [=]( rpl::producer text, const QString &command) { AddActionButton( _wrap, std::move(text), hasBotCommandValue(command), [=] { sendBotCommand(command); }); }; addBotCommand(tr::lng_profile_bot_help(), qsl("help")); addBotCommand(tr::lng_profile_bot_settings(), qsl("settings")); addBotCommand(tr::lng_profile_bot_privacy(), qsl("privacy")); } void ActionsFiller::addReportAction() { const auto peer = _peer; const auto controller = _controller->parentController(); const auto report = [=] { HistoryView::ShowReportPeerBox(controller, peer); }; AddActionButton( _wrap, tr::lng_profile_report(), rpl::single(true), report, st::infoBlockButton); } void ActionsFiller::addBlockAction(not_null user) { const auto window = &_controller->parentController()->window(); auto text = user->session().changes().peerFlagsValue( user, Data::PeerUpdate::Flag::IsBlocked ) | rpl::map([=] { switch (user->blockStatus()) { case UserData::BlockStatus::Blocked: return ((user->isBot() && !user->isSupport()) ? tr::lng_profile_restart_bot : tr::lng_profile_unblock_user)(); case UserData::BlockStatus::NotBlocked: default: return ((user->isBot() && !user->isSupport()) ? tr::lng_profile_block_bot : tr::lng_profile_block_user)(); } }) | rpl::flatten_latest( ) | rpl::start_spawning(_wrap->lifetime()); auto toggleOn = rpl::duplicate( text ) | rpl::map([](const QString &text) { return !text.isEmpty(); }); auto callback = [=] { if (user->isBlocked()) { Window::PeerMenuUnblockUserWithBotRestart(user); if (user->isBot()) { Ui::showPeerHistory(user, ShowAtUnreadMsgId); } } else if (user->isBot()) { user->session().api().blockedPeers().block(user); } else { window->show(Box( Window::PeerMenuBlockUserBox, window, user, v::null, v::null)); } }; AddActionButton( _wrap, rpl::duplicate(text), std::move(toggleOn), std::move(callback), st::infoBlockButton); } void ActionsFiller::addLeaveChannelAction( not_null channel) { AddActionButton( _wrap, tr::lng_profile_leave_channel(), AmInChannelValue(channel), Window::DeleteAndLeaveHandler(channel)); } void ActionsFiller::addJoinChannelAction( not_null channel) { using namespace rpl::mappers; auto joinVisible = AmInChannelValue(channel) | rpl::map(!_1) | rpl::start_spawning(_wrap->lifetime()); AddActionButton( _wrap, tr::lng_profile_join_channel(), rpl::duplicate(joinVisible), [=] { channel->session().api().joinChannel(channel); }); _wrap->add(object_ptr>( _wrap, CreateSkipWidget( _wrap, st::infoBlockButtonSkip)) )->setDuration( st::infoSlideDuration )->toggleOn( rpl::duplicate(joinVisible) ); } void ActionsFiller::fillUserActions(not_null user) { if (user->isBot()) { addInviteToGroupAction(user); } addShareContactAction(user); if (!user->isSelf()) { addEditContactAction(user); addDeleteContactAction(user); } addClearHistoryAction(user); addDeleteConversationAction(user); if (!user->isSelf() && !user->isSupport()) { if (user->isBot()) { addBotCommandActions(user); } _wrap->add(CreateSkipWidget( _wrap, st::infoBlockButtonSkip)); if (user->isBot()) { addReportAction(); } addBlockAction(user); } } void ActionsFiller::fillChannelActions( not_null channel) { using namespace rpl::mappers; addJoinChannelAction(channel); addLeaveChannelAction(channel); if (!channel->amCreator()) { addReportAction(); } } object_ptr ActionsFiller::fill() { auto wrapResult = [=](auto &&callback) { _wrap = object_ptr(_parent); _wrap->add(CreateSkipWidget(_wrap)); callback(); _wrap->add(CreateSkipWidget(_wrap)); object_ptr( _wrap, st::infoIconActions, st::infoIconPosition); return std::move(_wrap); }; if (auto user = _peer->asUser()) { return wrapResult([=] { fillUserActions(user); }); } else if (auto channel = _peer->asChannel()) { if (channel->isMegagroup()) { return { nullptr }; } return wrapResult([=] { fillChannelActions(channel); }); } return { nullptr }; } } // namespace object_ptr SetupDetails( not_null controller, not_null parent, not_null peer) { DetailsFiller filler(controller, parent, peer); return filler.fill(); } object_ptr SetupActions( not_null controller, not_null parent, not_null peer) { ActionsFiller filler(controller, parent, peer); return filler.fill(); } void SetupAddChannelMember( not_null navigation, not_null parent, not_null channel) { auto add = Ui::CreateChild( parent.get(), st::infoMembersAddMember); add->showOn(CanAddMemberValue(channel)); add->addClickHandler([=] { Window::PeerMenuAddChannelMembers(navigation, channel); }); parent->widthValue( ) | rpl::start_with_next([add](int newWidth) { auto availableWidth = newWidth - st::infoMembersButtonPosition.x(); add->moveToLeft( availableWidth - add->width(), st::infoMembersButtonPosition.y(), newWidth); }, add->lifetime()); } object_ptr SetupChannelMembers( not_null controller, not_null parent, not_null peer) { using namespace rpl::mappers; auto channel = peer->asChannel(); if (!channel || channel->isMegagroup()) { return { nullptr }; } auto membersShown = rpl::combine( MembersCountValue(channel), Data::PeerFlagValue( channel, ChannelDataFlag::CanViewParticipants), (_1 > 0) && _2); auto membersText = tr::lng_chat_status_subscribers( lt_count_decimal, MembersCountValue(channel) | tr::to_count()); auto membersCallback = [=] { controller->showSection(std::make_shared( channel, Section::Type::Members)); }; auto result = object_ptr>( parent, object_ptr(parent)); result->setDuration( st::infoSlideDuration )->toggleOn( std::move(membersShown) ); auto members = result->entity(); members->add(object_ptr(members)); members->add(CreateSkipWidget(members)); auto button = AddActionButton( members, std::move(membersText), rpl::single(true), std::move(membersCallback))->entity(); SetupAddChannelMember(controller, button, channel); object_ptr( members, st::infoIconMembers, st::infoChannelMembersIconPosition); members->add(CreateSkipWidget(members)); return result; } } // namespace Profile } // namespace Info