/* 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 "window/window_peer_menu.h" #include "api/api_chat_participants.h" #include "lang/lang_keys.h" #include "ui/boxes/confirm_box.h" #include "base/options.h" #include "base/unixtime.h" #include "boxes/delete_messages_box.h" #include "boxes/max_invite_box.h" #include "boxes/mute_settings_box.h" #include "boxes/add_contact_box.h" #include "boxes/choose_filter_box.h" #include "boxes/create_poll_box.h" #include "boxes/pin_messages_box.h" #include "boxes/premium_limits_box.h" #include "boxes/report_messages_box.h" #include "boxes/peers/add_bot_to_chat_box.h" #include "boxes/peers/add_participants_box.h" #include "boxes/peers/edit_contact_box.h" #include "ui/boxes/report_box.h" #include "ui/toast/toast.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/widgets/labels.h" #include "ui/widgets/checkbox.h" #include "ui/layers/generic_box.h" #include "ui/toasts/common_toasts.h" #include "main/main_session.h" #include "main/main_session_settings.h" #include "menu/menu_mute.h" #include "menu/menu_ttl_validator.h" #include "apiwrap.h" #include "mainwidget.h" #include "mainwindow.h" #include "api/api_blocked_peers.h" #include "api/api_chat_filters.h" #include "api/api_polls.h" #include "api/api_updates.h" #include "mtproto/mtproto_config.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_message.h" // GetErrorTextForSending. #include "history/view/history_view_context_menu.h" #include "window/window_adaptive.h" // Adaptive::isThreeColumn #include "window/window_session_controller.h" #include "window/window_controller.h" #include "support/support_helper.h" #include "info/info_memento.h" #include "info/info_controller.h" #include "info/profile/info_profile_values.h" #include "data/notify/data_notify_settings.h" #include "data/data_changes.h" #include "data/data_session.h" #include "data/data_folder.h" #include "data/data_poll.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_drafts.h" #include "data/data_user.h" #include "data/data_scheduled_messages.h" #include "data/data_histories.h" #include "data/data_chat_filters.h" #include "dialogs/dialogs_key.h" #include "core/application.h" #include "export/export_manager.h" #include "boxes/peers/edit_peer_info_box.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_window.h" // st::windowMinWidth #include "styles/style_menu_icons.h" #include namespace Window { const char kOptionViewProfileInChatsListContextMenu[] = "view-profile-in-chats-list-context-menu"; namespace { constexpr auto kArchivedToastDuration = crl::time(5000); constexpr auto kMaxUnreadWithoutConfirmation = 1000; base::options::toggle ViewProfileInChatsListContextMenu({ .id = kOptionViewProfileInChatsListContextMenu, .name = "Add \"View Profile\"", .description = "Add \"View Profile\" to context menu in chats list", }); void SetActionText(not_null action, rpl::producer &&text) { const auto lifetime = Ui::CreateChild(action.get()); std::move( text ) | rpl::start_with_next([=](const QString &actionText) { action->setText(actionText); }, *lifetime); } [[nodiscard]] bool IsUnreadHistory(not_null history) { return (history->chatListUnreadCount() > 0) || (history->chatListUnreadMark()); } void MarkAsReadHistory(not_null history) { const auto read = [&](not_null history) { if (IsUnreadHistory(history)) { history->peer->owner().histories().readInbox(history); } }; read(history); if (const auto migrated = history->migrateSibling()) { read(migrated); } } void MarkAsReadChatList(not_null list) { auto mark = std::vector>(); for (const auto &row : list->indexed()->all()) { if (const auto history = row->history()) { mark.push_back(history); } } ranges::for_each(mark, MarkAsReadHistory); } void PeerMenuAddMuteSubmenuAction( not_null controller, not_null peer, const PeerMenuCallback &addAction) { peer->owner().notifySettings().request(peer); const auto isMuted = peer->owner().notifySettings().isMuted(peer); if (isMuted) { const auto text = tr::lng_context_unmute(tr::now) + '\t' + Ui::FormatMuteForTiny(peer->notifyMuteUntil().value_or(0) - base::unixtime::now()); addAction(text, [=] { peer->owner().notifySettings().update(peer, 0); }, &st::menuIconUnmute); } else { const auto show = std::make_shared(controller); addAction(PeerMenuCallback::Args{ .text = tr::lng_context_mute(tr::now), .handler = nullptr, .icon = peer->owner().notifySettings().sound(peer).none ? &st::menuIconSilent : &st::menuIconMute, .fillSubmenu = [=](not_null menu) { MuteMenu::FillMuteMenu(menu, { peer, show }); }, }); } } class Filler { public: Filler( not_null controller, Dialogs::EntryState request, const PeerMenuCallback &addAction); void fill(); private: using Section = Dialogs::EntryState::Section; void fillChatsListActions(); void fillHistoryActions(); void fillProfileActions(); void fillRepliesActions(); void fillScheduledActions(); void fillArchiveActions(); void addHidePromotion(); void addTogglePin(); void addToggleMuteSubmenu(bool addSeparator); void addSupportInfo(); void addInfo(); void addToggleFolder(); void addToggleUnreadMark(); void addToggleArchive(); void addClearHistory(); void addDeleteChat(); void addLeaveChat(); void addManageChat(); void addCreatePoll(); void addThemeEdit(); void addBlockUser(); void addViewDiscussion(); void addExportChat(); void addReport(); void addNewContact(); void addShareContact(); void addEditContact(); void addBotToGroup(); void addNewMembers(); void addDeleteContact(); void addTTLSubmenu(bool addSeparator); void addGiftPremium(); not_null _controller; Dialogs::EntryState _request; PeerData *_peer = nullptr; Data::Folder *_folder = nullptr; const PeerMenuCallback &_addAction; }; History *FindWastedPin(not_null data, Data::Folder *folder) { const auto &order = data->pinnedChatsOrder(folder, FilterId()); for (const auto &pinned : order) { if (const auto history = pinned.history()) { if (history->peer->isChat() && history->peer->asChat()->isDeactivated() && !history->inChatList()) { return history; } } } return nullptr; } void AddChatMembers( not_null navigation, not_null chat) { AddParticipantsBoxController::Start(navigation, chat); } bool PinnedLimitReached( not_null controller, not_null history, FilterId filterId) { Expects(filterId != 0 || history->folderKnown()); const auto owner = &history->owner(); const auto folder = history->folder(); if (owner->pinnedCanPin(folder, filterId, history)) { return false; } // Some old chat, that was converted, maybe is still pinned. const auto wasted = filterId ? nullptr : FindWastedPin(owner, folder); if (wasted) { owner->setChatPinned(wasted, FilterId(), false); owner->setChatPinned(history, FilterId(), true); history->session().api().savePinnedOrder(folder); } else if (filterId) { controller->show( Box(FilterPinsLimitBox, &history->session(), filterId)); } else if (folder) { controller->show(Box(FolderPinsLimitBox, &history->session())); } else { controller->show(Box(PinsLimitBox, &history->session())); } return true; } void TogglePinnedDialog( not_null controller, not_null history) { if (!history->folderKnown()) { return; } const auto owner = &history->owner(); const auto isPinned = !history->isPinnedDialog(0); if (isPinned && PinnedLimitReached(controller, history, 0)) { return; } owner->setChatPinned(history, FilterId(), isPinned); const auto flags = isPinned ? MTPmessages_ToggleDialogPin::Flag::f_pinned : MTPmessages_ToggleDialogPin::Flag(0); history->session().api().request(MTPmessages_ToggleDialogPin( MTP_flags(flags), MTP_inputDialogPeer(history->peer->input) )).done([=] { owner->notifyPinnedDialogsOrderUpdated(); }).send(); if (isPinned) { controller->content()->dialogsToUp(); } } void TogglePinnedDialog( not_null controller, not_null history, FilterId filterId) { if (!filterId) { return TogglePinnedDialog(controller, history); } const auto owner = &history->owner(); // This can happen when you remove this filter from another client. if (!ranges::contains( (&owner->session())->data().chatsFilters().list(), filterId, &Data::ChatFilter::id)) { Ui::Toast::Show( Window::Show(controller).toastParent(), tr::lng_cant_do_this(tr::now)); return; } const auto isPinned = !history->isPinnedDialog(filterId); if (isPinned && PinnedLimitReached(controller, history, filterId)) { return; } owner->setChatPinned(history, filterId, isPinned); Api::SaveNewFilterPinned(&owner->session(), filterId); if (isPinned) { controller->content()->dialogsToUp(); } } Filler::Filler( not_null controller, Dialogs::EntryState request, const PeerMenuCallback &addAction) : _controller(controller) , _request(request) , _peer(request.key.peer()) , _folder(request.key.folder()) , _addAction(addAction) { } void Filler::addHidePromotion() { const auto history = _peer->owner().historyLoaded(_peer); if (!history || !history->useTopPromotion() || history->topPromotionType().isEmpty()) { return; } _addAction(tr::lng_context_hide_psa(tr::now), [=] { history->cacheTopPromotion(false, QString(), QString()); history->session().api().request(MTPhelp_HidePromoData( history->peer->input )).send(); }, &st::menuIconRemove); } void Filler::addTogglePin() { const auto controller = _controller; const auto filterId = _request.filterId; const auto peer = _peer; const auto history = peer->owner().historyLoaded(peer); if (!history || history->fixedOnTopIndex()) { return; } const auto pinText = [=] { return history->isPinnedDialog(filterId) ? tr::lng_context_unpin_from_top(tr::now) : tr::lng_context_pin_to_top(tr::now); }; const auto pinToggle = [=] { TogglePinnedDialog(controller, history, filterId); }; const auto pinAction = _addAction( pinText(), pinToggle, (history->isPinnedDialog(filterId) ? &st::menuIconUnpin : &st::menuIconPin)); auto actionText = history->session().changes().historyUpdates( history, Data::HistoryUpdate::Flag::IsPinned ) | rpl::map(pinText); SetActionText(pinAction, std::move(actionText)); } void Filler::addToggleMuteSubmenu(bool addSeparator) { if (_peer->isSelf()) { return; } PeerMenuAddMuteSubmenuAction(_controller, _peer, _addAction); if (addSeparator) { _addAction(PeerMenuCallback::Args{ .isSeparator = true }); } } void Filler::addSupportInfo() { if (!_peer->session().supportMode()) { return; } const auto user = _peer->asUser(); if (!user) { return; } const auto controller = _controller; _addAction("Edit support info", [=] { user->session().supportHelper().editInfo(controller, user); }, &st::menuIconEdit); } void Filler::addInfo() { if (_peer->isSelf() || _peer->isRepliesChat()) { return; } else if (_controller->adaptive().isThreeColumn()) { if (Core::App().settings().thirdSectionInfoEnabled() || Core::App().settings().tabbedReplacedWithInfo()) { return; } } const auto controller = _controller; const auto peer = _peer; const auto text = (peer->isChat() || peer->isMegagroup()) ? tr::lng_context_view_group(tr::now) : (peer->isUser() ? tr::lng_context_view_profile(tr::now) : tr::lng_context_view_channel(tr::now)); _addAction(text, [=] { controller->showPeerInfo(peer); }, peer->isUser() ? &st::menuIconProfile : &st::menuIconInfo); } void Filler::addToggleFolder() { const auto controller = _controller; const auto history = _request.key.history(); if (!history || !history->owner().chatsFilters().has()) { return; } _addAction(PeerMenuCallback::Args{ .text = tr::lng_filters_menu_add(tr::now), .handler = nullptr, .icon = &st::menuIconAddToFolder, .fillSubmenu = [=](not_null menu) { FillChooseFilterMenu(controller, menu, history); }, }); } void Filler::addToggleUnreadMark() { const auto peer = _peer; const auto history = peer->owner().history(peer); const auto label = [=] { return IsUnreadHistory(history) ? tr::lng_context_mark_read(tr::now) : tr::lng_context_mark_unread(tr::now); }; auto action = _addAction(label(), [=] { const auto markAsRead = IsUnreadHistory(history); if (markAsRead) { MarkAsReadHistory(history); } else { peer->owner().histories().changeDialogUnreadMark( history, !markAsRead); } }, (IsUnreadHistory(history) ? &st::menuIconMarkRead : &st::menuIconMarkUnread)); auto actionText = history->session().changes().historyUpdates( history, Data::HistoryUpdate::Flag::UnreadView ) | rpl::map(label); SetActionText(action, std::move(actionText)); } void Filler::addToggleArchive() { const auto peer = _peer; const auto history = peer->owner().historyLoaded(peer); if (history && history->useTopPromotion()) { return; } else if (peer->isNotificationsUser() || peer->isSelf()) { if (!history || !history->folder()) { return; } } const auto isArchived = [=] { return (history->folder() != nullptr); }; const auto label = [=] { return isArchived() ? tr::lng_archived_remove(tr::now) : tr::lng_archived_add(tr::now); }; const auto toggle = [=] { ToggleHistoryArchived(history, !isArchived()); }; const auto archiveAction = _addAction( label(), toggle, isArchived() ? &st::menuIconUnarchive : &st::menuIconArchive); auto actionText = history->session().changes().historyUpdates( history, Data::HistoryUpdate::Flag::Folder ) | rpl::map(label); SetActionText(archiveAction, std::move(actionText)); } void Filler::addClearHistory() { const auto channel = _peer->asChannel(); const auto isGroup = _peer->isChat() || _peer->isMegagroup(); if (channel) { if (!channel->amIn()) { return; } else if (!channel->canDeleteMessages() && (!isGroup || channel->isPublic())) { return; } } _addAction( tr::lng_profile_clear_history(tr::now), ClearHistoryHandler(_controller, _peer), &st::menuIconClear); } void Filler::addDeleteChat() { if (_peer->isChannel()) { return; } _addAction({ .text = (_peer->isUser() ? tr::lng_profile_delete_conversation(tr::now) : tr::lng_profile_clear_and_exit(tr::now)), .handler = DeleteAndLeaveHandler(_controller, _peer), .icon = &st::menuIconDeleteAttention, .isAttention = true, }); } void Filler::addLeaveChat() { const auto channel = _peer->asChannel(); if (!channel || !channel->amIn()) { return; } _addAction({ .text = (_peer->isMegagroup() ? tr::lng_profile_leave_group(tr::now) : tr::lng_profile_leave_channel(tr::now)), .handler = DeleteAndLeaveHandler(_controller, _peer), .icon = &st::menuIconLeaveAttention, .isAttention = true, }); } void Filler::addBlockUser() { const auto user = _peer->asUser(); if (!user || user->isInaccessible() || user->isSelf() || user->isRepliesChat()) { return; } const auto window = &_controller->window(); const auto blockText = [](not_null user) { return user->isBlocked() ? ((user->isBot() && !user->isSupport()) ? tr::lng_profile_restart_bot(tr::now) : tr::lng_profile_unblock_user(tr::now)) : ((user->isBot() && !user->isSupport()) ? tr::lng_profile_block_bot(tr::now) : tr::lng_profile_block_user(tr::now)); }; const auto blockAction = _addAction(blockText(user), [=] { if (user->isBlocked()) { PeerMenuUnblockUserWithBotRestart(user); } else if (user->isBot()) { user->session().api().blockedPeers().block(user); } else { window->show(Box( PeerMenuBlockUserBox, window, user, v::null, v::null)); } }, (!user->isBlocked() ? &st::menuIconBlock : user->isBot() ? &st::menuIconRestartBot : &st::menuIconUnblock)); auto actionText = _peer->session().changes().peerUpdates( _peer, Data::PeerUpdate::Flag::IsBlocked ) | rpl::map([=] { return blockText(user); }); SetActionText(blockAction, std::move(actionText)); if (user->blockStatus() == UserData::BlockStatus::Unknown) { user->session().api().requestFullPeer(user); } } void Filler::addViewDiscussion() { const auto channel = _peer->asBroadcast(); if (!channel) { return; } const auto chat = channel->linkedChat(); if (!chat) { return; } const auto navigation = _controller; _addAction(tr::lng_profile_view_discussion(tr::now), [=] { if (channel->invitePeekExpires()) { Ui::Toast::Show( Window::Show(navigation).toastParent(), tr::lng_channel_invite_private(tr::now)); return; } navigation->showPeerHistory( chat, Window::SectionShow::Way::Forward); }, &st::menuIconDiscussion); } void Filler::addExportChat() { if (!_peer->canExportChatHistory()) { return; } const auto peer = _peer; _addAction( tr::lng_profile_export_chat(tr::now), [=] { PeerMenuExportChat(peer); }, &st::menuIconExport); } void Filler::addReport() { const auto chat = _peer->asChat(); const auto channel = _peer->asChannel(); if ((!chat || chat->amCreator()) && (!channel || channel->amCreator())) { return; } const auto peer = _peer; const auto navigation = _controller; _addAction(tr::lng_profile_report(tr::now), [=] { ShowReportPeerBox(navigation, peer); }, &st::menuIconReport); } void Filler::addNewContact() { const auto user = _peer->asUser(); if (!user || user->isContact() || user->isSelf() || user->isInaccessible() || user->isBot()) { return; } const auto controller = _controller; _addAction( tr::lng_info_add_as_contact(tr::now), [=] { controller->show(Box(EditContactBox, controller, user)); }, &st::menuIconInvite); } void Filler::addShareContact() { const auto user = _peer->asUser(); if (!user || !user->canShareThisContact()) { return; } const auto controller = _controller; _addAction( tr::lng_info_share_contact(tr::now), [=] { PeerMenuShareContactBox(controller, user); }, &st::menuIconShare); } void Filler::addEditContact() { const auto user = _peer->asUser(); if (!user || !user->isContact() || user->isSelf()) { return; } const auto controller = _controller; _addAction( tr::lng_info_edit_contact(tr::now), [=] { controller->show(Box(EditContactBox, controller, user)); }, &st::menuIconEdit); } void Filler::addBotToGroup() { const auto user = _peer->asUser(); if (!user) { return; } [[maybe_unused]] const auto lifetime = Info::Profile::InviteToChatButton( user ) | rpl::take(1) | rpl::start_with_next([=](QString label) { if (!label.isEmpty()) { const auto controller = _controller; _addAction( label, [=] { AddBotToGroupBoxController::Start(controller, user); }, &st::menuIconInvite); } }); } void Filler::addNewMembers() { const auto chat = _peer->asChat(); const auto channel = _peer->asChannel(); if ((!chat || !chat->canAddMembers()) && (!channel || !channel->canAddMembers())) { return; } const auto navigation = _controller; const auto callback = chat ? Fn([=] { AddChatMembers(navigation, chat); }) : [=] { PeerMenuAddChannelMembers(navigation, channel); }; _addAction( ((chat || channel->isMegagroup()) ? tr::lng_channel_add_members(tr::now) : tr::lng_channel_add_users(tr::now)), callback, &st::menuIconInvite); } void Filler::addDeleteContact() { const auto user = _peer->asUser(); if (!user || !user->isContact() || user->isSelf()) { return; } const auto controller = _controller; _addAction({ .text = tr::lng_info_delete_contact(tr::now), .handler = [=] { PeerMenuDeleteContact(controller, user); }, .icon = &st::menuIconDeleteAttention, .isAttention = true, }); } void Filler::addManageChat() { if (!EditPeerInfoBox::Available(_peer)) { return; } const auto peer = _peer; const auto navigation = _controller; const auto text = (peer->isChat() || peer->isMegagroup()) ? tr::lng_manage_group_title(tr::now) : tr::lng_manage_channel_title(tr::now); _addAction(text, [=] { navigation->showEditPeerBox(peer); }, &st::menuIconManage); } void Filler::addCreatePoll() { if (!_peer->canSendPolls()) { return; } const auto peer = _peer; const auto controller = _controller; const auto source = (_request.section == Section::Scheduled) ? Api::SendType::Scheduled : Api::SendType::Normal; const auto sendMenuType = (_request.section == Section::Scheduled) ? SendMenu::Type::Disabled : (_request.section == Section::Replies) ? SendMenu::Type::SilentOnly : SendMenu::Type::Scheduled; const auto flag = PollData::Flags(); const auto replyToId = _request.currentReplyToId ? _request.currentReplyToId : _request.rootId; auto callback = [=] { PeerMenuCreatePoll( controller, peer, replyToId, flag, flag, source, sendMenuType); }; _addAction( tr::lng_polls_create(tr::now), std::move(callback), &st::menuIconCreatePoll); } void Filler::addThemeEdit() { const auto user = _peer->asUser(); if (!user || user->isBot()) { return; } const auto controller = _controller; _addAction( tr::lng_chat_theme_change(tr::now), [=] { controller->toggleChooseChatTheme(user); }, &st::menuIconChangeColors); } void Filler::addTTLSubmenu(bool addSeparator) { const auto validator = TTLMenu::TTLValidator( std::make_shared(_controller), _peer); if (!validator.can()) { return; } const auto text = tr::lng_manage_messages_ttl_menu(tr::now) + (_peer->messagesTTL() ? ('\t' + Ui::FormatTTLTiny(_peer->messagesTTL())) : QString()); _addAction(text, [=] { validator.showBox(); }, validator.icon()); if (addSeparator) { _addAction(PeerMenuCallback::Args{ .isSeparator = true }); } } void Filler::addGiftPremium() { const auto user = _peer->asUser(); if (!user || user->isInaccessible() || user->isSelf() || user->isBot() || user->isNotificationsUser() || !user->canReceiveGifts() || user->isRepliesChat()) { return; } const auto navigation = _controller; _addAction(tr::lng_profile_gift_premium(tr::now), [=] { navigation->showGiftPremiumBox(user); }, &st::menuIconGiftPremium); } void Filler::fill() { if (_folder) { fillArchiveActions(); return; } switch (_request.section) { case Section::ChatsList: fillChatsListActions(); break; case Section::History: fillHistoryActions(); break; case Section::Profile: fillProfileActions(); break; case Section::Replies: fillRepliesActions(); break; case Section::Scheduled: fillScheduledActions(); break; default: Unexpected("_request.section in Filler::fill."); } } void Filler::fillChatsListActions() { addHidePromotion(); addToggleArchive(); addTogglePin(); if (ViewProfileInChatsListContextMenu.value()) { addInfo(); } addToggleMuteSubmenu(false); addToggleUnreadMark(); addToggleFolder(); if (const auto user = _peer->asUser()) { if (!user->isContact()) { addBlockUser(); } } addClearHistory(); addDeleteChat(); addLeaveChat(); } void Filler::fillHistoryActions() { addToggleMuteSubmenu(true); addInfo(); addSupportInfo(); addManageChat(); addCreatePoll(); addThemeEdit(); addViewDiscussion(); addExportChat(); addReport(); addClearHistory(); addDeleteChat(); addLeaveChat(); } void Filler::fillProfileActions() { addTTLSubmenu(true); addSupportInfo(); addNewContact(); addShareContact(); addEditContact(); addGiftPremium(); addBotToGroup(); addNewMembers(); addManageChat(); addViewDiscussion(); addExportChat(); addBlockUser(); addReport(); addLeaveChat(); addDeleteContact(); } void Filler::fillRepliesActions() { addCreatePoll(); } void Filler::fillScheduledActions() { addCreatePoll(); } void Filler::fillArchiveActions() { Expects(_folder != nullptr); if (_folder->id() != Data::Folder::kId) { return; } const auto controller = _controller; const auto hidden = controller->session().settings().archiveCollapsed(); const auto text = hidden ? tr::lng_context_archive_expand(tr::now) : tr::lng_context_archive_collapse(tr::now); _addAction(text, [=] { controller->session().settings().setArchiveCollapsed(!hidden); controller->session().saveSettingsDelayed(); }, hidden ? &st::menuIconExpand : &st::menuIconCollapse); _addAction(tr::lng_context_archive_to_menu(tr::now), [=] { Ui::Toast::Show( Window::Show(controller).toastParent(), Ui::Toast::Config{ .text = { tr::lng_context_archive_to_menu_info(tr::now) }, .st = &st::windowArchiveToast, .durationMs = kArchivedToastDuration, .multiline = true, }); controller->session().settings().setArchiveInMainMenu( !controller->session().settings().archiveInMainMenu()); controller->session().saveSettingsDelayed(); }, &st::menuIconToMainMenu); MenuAddMarkAsReadChatListAction( controller, [folder = _folder] { return folder->chatsList(); }, _addAction); } } // namespace void PeerMenuExportChat(not_null peer) { Core::App().exportManager().start(peer); } void PeerMenuDeleteContact( not_null controller, not_null user) { const auto text = tr::lng_sure_delete_contact( tr::now, lt_contact, user->name); const auto deleteSure = [=](Fn &&close) { close(); user->session().api().request(MTPcontacts_DeleteContacts( MTP_vector(1, user->inputUser) )).done([=](const MTPUpdates &result) { user->session().api().applyUpdates(result); }).send(); }; controller->show( Ui::MakeConfirmBox({ .text = text, .confirmed = deleteSure, .confirmText = tr::lng_box_delete(), }), Ui::LayerOption::CloseOther); } void PeerMenuShareContactBox( not_null navigation, not_null user) { // There is no async to make weak from controller. const auto weak = std::make_shared>(); auto callback = [=](not_null peer) { if (!peer->canWrite()) { navigation->parentController()->show( Ui::MakeInformBox(tr::lng_forward_share_cant()), Ui::LayerOption::KeepOther); return; } else if (peer->isSelf()) { auto action = Api::SendAction(peer->owner().history(peer)); action.clearDraft = false; user->session().api().shareContact(user, action); Ui::Toast::Show( Window::Show(navigation).toastParent(), tr::lng_share_done(tr::now)); if (auto strong = *weak) { strong->closeBox(); } return; } auto recipient = peer->isUser() ? peer->name : '\xAB' + peer->name + '\xBB'; navigation->parentController()->show( Ui::MakeConfirmBox({ .text = tr::lng_forward_share_contact( tr::now, lt_recipient, recipient), .confirmed = [peer, user, navigation](Fn &&close) { const auto history = peer->owner().history(peer); navigation->showPeerHistory( history, Window::SectionShow::Way::ClearStack, ShowAtTheEndMsgId); auto action = Api::SendAction(history); action.clearDraft = false; user->session().api().shareContact(user, action); close(); }, .confirmText = tr::lng_forward_send(), }), Ui::LayerOption::KeepOther); }; *weak = navigation->parentController()->show( Box( std::make_unique( &navigation->session(), std::move(callback)), [](not_null box) { box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); }), Ui::LayerOption::CloseOther); } void PeerMenuCreatePoll( not_null controller, not_null peer, MsgId replyToId, PollData::Flags chosen, PollData::Flags disabled, Api::SendType sendType, SendMenu::Type sendMenuType) { if (peer->isChannel() && !peer->isMegagroup()) { chosen &= ~PollData::Flag::PublicVotes; disabled |= PollData::Flag::PublicVotes; } auto box = Box( controller, chosen, disabled, sendType, sendMenuType); const auto weak = Ui::MakeWeak(box.data()); const auto lock = box->lifetime().make_state(false); box->submitRequests( ) | rpl::start_with_next([=](const CreatePollBox::Result &result) { if (std::exchange(*lock, true)) { return; } auto action = Api::SendAction( peer->owner().history(peer), result.options); action.clearDraft = false; action.replyTo = replyToId; if (const auto localDraft = action.history->localDraft()) { action.clearDraft = localDraft->textWithTags.text.isEmpty(); } const auto api = &peer->session().api(); api->polls().create(result.poll, action, crl::guard(weak, [=] { weak->closeBox(); }), crl::guard(weak, [=](const MTP::Error &error) { *lock = false; weak->submitFailed(tr::lng_attach_failed(tr::now)); })); }, box->lifetime()); controller->show(std::move(box), Ui::LayerOption::CloseOther); } void PeerMenuBlockUserBox( not_null box, not_null window, not_null peer, std::variant suggestReport, std::variant suggestClear) { const auto settings = peer->settings().value_or(PeerSettings(0)); const auto reportNeeded = v::is_null(suggestReport) ? ((settings & PeerSetting::ReportSpam) != 0) : v::get(suggestReport); const auto user = peer->asUser(); const auto name = user ? user->shortName() : peer->name; if (user) { box->addRow(object_ptr( box, tr::lng_blocked_list_confirm_text( lt_name, rpl::single(Ui::Text::Bold(name)), Ui::Text::WithEntities), st::blockUserConfirmation)); box->addSkip(st::boxMediumSkip); } const auto report = reportNeeded ? box->addRow(object_ptr( box, tr::lng_report_spam(tr::now), true, st::defaultBoxCheckbox)) : nullptr; if (report) { box->addSkip(st::boxMediumSkip); } const auto clear = v::is(suggestClear) ? box->addRow(object_ptr( box, tr::lng_blocked_list_confirm_clear(tr::now), true, st::defaultBoxCheckbox)) : v::is(suggestClear) ? box->addRow(object_ptr( box, tr::lng_context_delete_msg(tr::now), true, st::defaultBoxCheckbox)) : nullptr; if (clear) { box->addSkip(st::boxMediumSkip); } const auto allFromUser = v::is(suggestClear) ? box->addRow(object_ptr( box, tr::lng_delete_all_from_user( tr::now, lt_user, Ui::Text::Bold(peer->name), Ui::Text::WithEntities), true, st::defaultBoxCheckbox)) : nullptr; if (allFromUser) { box->addSkip(st::boxLittleSkip); } box->setTitle(tr::lng_blocked_list_confirm_title( lt_name, rpl::single(name))); box->addButton(tr::lng_blocked_list_confirm_ok(), [=] { const auto reportChecked = report && report->checked(); const auto clearChecked = clear && clear->checked(); const auto fromUserChecked = allFromUser && allFromUser->checked(); box->closeBox(); if (const auto clearReply = std::get_if(&suggestClear)) { using Flag = MTPcontacts_BlockFromReplies::Flag; peer->session().api().request(MTPcontacts_BlockFromReplies( MTP_flags((clearChecked ? Flag::f_delete_message : Flag(0)) | (fromUserChecked ? Flag::f_delete_history : Flag(0)) | (reportChecked ? Flag::f_report_spam : Flag(0))), MTP_int(clearReply->replyId.msg) )).done([=](const MTPUpdates &result) { peer->session().updates().applyUpdates(result); }).send(); } else { peer->session().api().blockedPeers().block(peer); if (reportChecked) { peer->session().api().request(MTPmessages_ReportSpam( peer->input )).send(); } if (clearChecked) { crl::on_main(&peer->session(), [=] { peer->session().api().deleteConversation(peer, false); }); window->sessionController()->showBackFromStack(); } } Ui::Toast::Show( Window::Show(window).toastParent(), tr::lng_new_contact_block_done(tr::now, lt_user, name)); }, st::attentionBoxButton); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } void PeerMenuUnblockUserWithBotRestart(not_null user) { user->session().api().blockedPeers().unblock(user, [=] { if (user->isBot() && !user->isSupport()) { user->session().api().sendBotStart(user); } }); } void BlockSenderFromRepliesBox( not_null box, not_null controller, FullMsgId id) { const auto item = controller->session().data().message(id); Assert(item != nullptr); PeerMenuBlockUserBox( box, &controller->window(), item->senderOriginal(), true, Window::ClearReply{ id }); } QPointer ShowForwardMessagesBox( not_null navigation, Data::ForwardDraft &&draft, FnMut &&successCallback) { const auto weak = std::make_shared>(); auto callback = [ draft = std::move(draft), callback = std::move(successCallback), weak, navigation ](not_null peer) mutable { const auto content = navigation->parentController()->content(); if (peer->isSelf() && !draft.ids.empty() && draft.ids.front().peer != peer->id) { const auto history = peer->owner().history(peer); auto resolved = history->resolveForwardDraft(draft); if (!resolved.items.empty()) { const auto api = &peer->session().api(); auto action = Api::SendAction(peer->owner().history(peer)); action.clearDraft = false; action.generateLocal = false; const auto weakContent = Ui::MakeWeak(content); api->forwardMessages( std::move(resolved), action, crl::guard(weakContent, [w = weakContent] { Ui::Toast::Show(w, tr::lng_share_done(tr::now)); })); } } else if (!content->setForwardDraft(peer->id, std::move(draft))) { return; } if (const auto strong = *weak) { strong->closeBox(); } if (callback) { callback(); } }; auto initBox = [](not_null box) { box->addButton(tr::lng_cancel(), [box] { box->closeBox(); }); }; *weak = navigation->parentController()->show(Box( std::make_unique( &navigation->session(), std::move(callback)), std::move(initBox)), Ui::LayerOption::KeepOther); return weak->data(); } QPointer ShowForwardMessagesBox( not_null navigation, MessageIdsList &&items, FnMut &&successCallback) { return ShowForwardMessagesBox( navigation, Data::ForwardDraft{ .ids = std::move(items) }, std::move(successCallback)); } QPointer ShowSendNowMessagesBox( not_null navigation, not_null history, MessageIdsList &&items, Fn &&successCallback) { const auto session = &navigation->session(); const auto text = (items.size() > 1) ? tr::lng_scheduled_send_now_many(tr::now, lt_count, items.size()) : tr::lng_scheduled_send_now(tr::now); const auto error = GetErrorTextForSending( history->peer, session->data().idsToItems(items), TextWithTags()); if (!error.isEmpty()) { Ui::ShowMultilineToast({ .parentOverride = Window::Show(navigation).toastParent(), .text = { error }, }); return { nullptr }; } auto done = [ =, list = std::move(items), callback = std::move(successCallback) ](Fn &&close) { close(); auto ids = QVector(); for (const auto item : session->data().idsToItems(list)) { if (item->allowsSendNow()) { ids.push_back(MTP_int( session->data().scheduledMessages().lookupId(item))); } } session->api().request(MTPmessages_SendScheduledMessages( history->peer->input, MTP_vector(ids) )).done([=](const MTPUpdates &result) { session->api().applyUpdates(result); }).fail([=](const MTP::Error &error) { session->api().sendMessageFail(error, history->peer); }).send(); if (callback) { callback(); } }; return navigation->parentController()->show( Ui::MakeConfirmBox({ .text = text, .confirmed = std::move(done), .confirmText = tr::lng_send_button(), }), Ui::LayerOption::KeepOther).data(); } void PeerMenuAddChannelMembers( not_null navigation, not_null channel) { if (!channel->isMegagroup() && (channel->membersCount() >= channel->session().serverConfig().chatSizeMax)) { navigation->parentController()->show( Box(channel), Ui::LayerOption::KeepOther); return; } const auto api = &channel->session().api(); api->chatParticipants().requestForAdd(channel, crl::guard(navigation, [=]( const Api::ChatParticipants::TLMembers &data) { const auto &[availableCount, list] = Api::ChatParticipants::Parse( channel, data); const auto already = ( list ) | ranges::views::transform([&](const Api::ChatParticipant &p) { return p.isUser() ? channel->owner().userLoaded(p.userId()) : nullptr; }) | ranges::views::filter([](UserData *user) { return (user != nullptr); }) | ranges::to_vector; AddParticipantsBoxController::Start( navigation, channel, { already.begin(), already.end() }); })); } void ToggleMessagePinned( not_null navigation, FullMsgId itemId, bool pin) { const auto item = navigation->session().data().message(itemId); if (!item || !item->canPin()) { return; } if (pin) { navigation->parentController()->show( Box(PinMessageBox, item->history()->peer, item->id), Ui::LayerOption::CloseOther); } else { const auto peer = item->history()->peer; const auto session = &peer->session(); const auto callback = crl::guard(session, [=](Fn &&close) { close(); session->api().request(MTPmessages_UpdatePinnedMessage( MTP_flags(MTPmessages_UpdatePinnedMessage::Flag::f_unpin), peer->input, MTP_int(itemId.msg) )).done([=](const MTPUpdates &result) { session->api().applyUpdates(result); }).send(); }); navigation->parentController()->show( Ui::MakeConfirmBox({ .text = tr::lng_pinned_unpin_sure(), .confirmed = callback, .confirmText = tr::lng_pinned_unpin(), }), Ui::LayerOption::CloseOther); } } void HidePinnedBar( not_null navigation, not_null peer, Fn onHidden) { const auto callback = crl::guard(navigation, [=](Fn &&close) { close(); auto &session = peer->session(); const auto migrated = peer->migrateFrom(); const auto top = Data::ResolveTopPinnedId(peer, migrated); const auto universal = !top ? MsgId(0) : (migrated && !peerIsChannel(top.peer)) ? (top.msg - ServerMaxMsgId) : top.msg; if (universal) { session.settings().setHiddenPinnedMessageId(peer->id, universal); session.saveSettingsDelayed(); if (onHidden) { onHidden(); } } else { session.api().requestFullPeer(peer); } }); navigation->parentController()->show( Ui::MakeConfirmBox({ .text = tr::lng_pinned_hide_all_sure(), .confirmed = callback, .confirmText = tr::lng_pinned_hide_all_hide(), }), Ui::LayerOption::CloseOther); } void UnpinAllMessages( not_null navigation, not_null history) { const auto callback = crl::guard(navigation, [=](Fn &&close) { close(); const auto api = &history->session().api(); const auto sendRequest = [=](auto self) -> void { api->request(MTPmessages_UnpinAllMessages( history->peer->input )).done([=](const MTPmessages_AffectedHistory &result) { const auto peer = history->peer; const auto offset = api->applyAffectedHistory(peer, result); if (offset > 0) { self(self); } else { history->unpinAllMessages(); } }).send(); }; sendRequest(sendRequest); }); navigation->parentController()->show( Ui::MakeConfirmBox({ .text = tr::lng_pinned_unpin_all_sure(), .confirmed = callback, .confirmText = tr::lng_pinned_unpin(), }), Ui::LayerOption::CloseOther); } void PeerMenuAddMuteAction( not_null controller, not_null peer, const PeerMenuCallback &addAction) { // There is no async to make weak from controller. peer->owner().notifySettings().request(peer); const auto muteText = [](bool isUnmuted) { return isUnmuted ? tr::lng_context_mute(tr::now) : tr::lng_context_unmute(tr::now); }; const auto muteAction = addAction(QString("-"), [=] { if (!peer->owner().notifySettings().isMuted(peer)) { controller->show( Box(peer), Ui::LayerOption::CloseOther); } else { peer->owner().notifySettings().update(peer, 0); } }, (peer->owner().notifySettings().isMuted(peer) ? &st::menuIconUnmute : &st::menuIconMute)); auto actionText = Info::Profile::NotificationsEnabledValue( peer ) | rpl::map(muteText); SetActionText(muteAction, std::move(actionText)); } void MenuAddMarkAsReadAllChatsAction( not_null controller, const PeerMenuCallback &addAction) { // There is no async to make weak from controller. auto callback = [=, owner = &controller->session().data()] { auto boxCallback = [=](Fn &&close) { close(); MarkAsReadChatList(owner->chatsList()); if (const auto folder = owner->folderLoaded(Data::Folder::kId)) { MarkAsReadChatList(folder->chatsList()); } }; controller->show( Ui::MakeConfirmBox({ tr::lng_context_mark_read_all_sure(), std::move(boxCallback) }), Ui::LayerOption::CloseOther); }; addAction( tr::lng_context_mark_read_all(tr::now), std::move(callback), &st::menuIconMarkRead); } void MenuAddMarkAsReadChatListAction( not_null controller, Fn()> &&list, const PeerMenuCallback &addAction) { // There is no async to make weak from controller. const auto unreadState = list()->unreadState(); if (unreadState.empty()) { return; } auto callback = [=] { if (unreadState.messages > kMaxUnreadWithoutConfirmation) { auto boxCallback = [=](Fn &&close) { MarkAsReadChatList(list()); close(); }; controller->show( Ui::MakeConfirmBox({ tr::lng_context_mark_read_sure(), std::move(boxCallback) }), Ui::LayerOption::CloseOther); } else { MarkAsReadChatList(list()); } }; addAction( tr::lng_context_mark_read(tr::now), std::move(callback), &st::menuIconMarkRead); } void ToggleHistoryArchived(not_null history, bool archived) { const auto callback = [=] { Ui::Toast::Show(Ui::Toast::Config{ .text = { (archived ? tr::lng_archived_added(tr::now) : tr::lng_archived_removed(tr::now)) }, .st = &st::windowArchiveToast, .durationMs = (archived ? kArchivedToastDuration : Ui::Toast::kDefaultDuration), .multiline = true, }); }; history->session().api().toggleHistoryArchived( history, archived, callback); } Fn ClearHistoryHandler( not_null controller, not_null peer) { return [=] { controller->show( Box(peer, true), Ui::LayerOption::KeepOther); }; } Fn DeleteAndLeaveHandler( not_null controller, not_null peer) { return [=] { controller->show( Box(peer, false), Ui::LayerOption::KeepOther); }; } void FillDialogsEntryMenu( not_null controller, Dialogs::EntryState request, const PeerMenuCallback &callback) { Filler(controller, request, callback).fill(); } } // namespace Window