/* 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 "history/view/history_view_context_menu.h" #include "history/view/history_view_list_widget.h" #include "history/view/history_view_cursor_state.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_message.h" #include "history/history_item_text.h" #include "history/media/history_media.h" #include "history/media/history_media_web_page.h" #include "ui/widgets/popup_menu.h" #include "ui/image/image.h" #include "ui/toast/toast.h" #include "chat_helpers/message_field.h" #include "boxes/confirm_box.h" #include "boxes/sticker_set_box.h" #include "data/data_photo.h" #include "data/data_document.h" #include "data/data_media_types.h" #include "data/data_session.h" #include "data/data_groups.h" #include "data/data_channel.h" #include "core/file_utilities.h" #include "window/window_peer_menu.h" #include "lang/lang_keys.h" #include "core/application.h" #include "mainwidget.h" #include "auth_session.h" #include "apiwrap.h" namespace HistoryView { namespace { // If we can't cloud-export link for such time we export it locally. constexpr auto kExportLocalTimeout = crl::time(1000); //void AddToggleGroupingAction( // #feed // not_null menu, // not_null peer) { // if (const auto channel = peer->asChannel()) { // const auto grouped = (channel->feed() != nullptr); // menu->addAction( // #feed // lang(grouped ? lng_feed_ungroup : lng_feed_group), // [=] { Window::ToggleChannelGrouping(channel, !grouped); }); // } //} void SavePhotoToFile(not_null photo) { if (photo->isNull() || !photo->loaded()) { return; } FileDialog::GetWritePath( Core::App().getFileDialogParent(), lang(lng_save_photo), qsl("JPEG Image (*.jpg);;") + FileDialog::AllFilesFilter(), filedialogDefaultName(qsl("photo"), qsl(".jpg")), crl::guard(&Auth(), [=](const QString &result) { if (!result.isEmpty()) { photo->large()->original().save(result, "JPG"); } })); } void CopyImage(not_null photo) { if (photo->isNull() || !photo->loaded()) { return; } QApplication::clipboard()->setImage(photo->large()->original()); } void ShowStickerPackInfo(not_null document) { StickerSetBox::Show(document); } void ToggleFavedSticker( not_null document, FullMsgId contextId) { Auth().api().toggleFavedSticker( document, contextId, !Stickers::IsFaved(document)); } void AddPhotoActions( not_null menu, not_null photo) { menu->addAction( lang(lng_context_save_image), App::LambdaDelayed( st::defaultDropdownMenu.menu.ripple.hideDuration, &Auth(), [=] { SavePhotoToFile(photo); })); menu->addAction(lang(lng_context_copy_image), [=] { CopyImage(photo); }); } void OpenGif(FullMsgId itemId) { if (const auto item = App::histItemById(itemId)) { if (const auto media = item->media()) { if (const auto document = media->document()) { Core::App().showDocument(document, item); } } } } void ShowInFolder(not_null document) { const auto filepath = document->filepath( DocumentData::FilePathResolve::Checked); if (!filepath.isEmpty()) { File::ShowInFolder(filepath); } } void AddSaveDocumentAction( not_null menu, Data::FileOrigin origin, not_null document) { const auto save = [=] { DocumentSaveClickHandler::Save( origin, document, DocumentSaveClickHandler::Mode::ToNewFile); }; menu->addAction( lang(document->isVideoFile() ? lng_context_save_video : (document->isVoiceMessage() ? lng_context_save_audio : (document->isAudioFile() ? lng_context_save_audio_file : (document->sticker() ? lng_context_save_image : lng_context_save_file)))), App::LambdaDelayed( st::defaultDropdownMenu.menu.ripple.hideDuration, &Auth(), save)); } void AddDocumentActions( not_null menu, not_null document, FullMsgId contextId) { if (document->loading()) { menu->addAction(lang(lng_context_cancel_download), [=] { document->cancel(); }); return; } if (document->loaded() && document->isGifv()) { if (!cAutoPlayGif()) { menu->addAction(lang(lng_context_open_gif), [=] { OpenGif(contextId); }); } } if (document->sticker() && document->sticker()->set.type() != mtpc_inputStickerSetEmpty) { menu->addAction( lang(document->isStickerSetInstalled() ? lng_context_pack_info : lng_context_pack_add), [=] { ShowStickerPackInfo(document); }); menu->addAction( lang(Stickers::IsFaved(document) ? lng_faved_stickers_remove : lng_faved_stickers_add), [=] { ToggleFavedSticker(document, contextId); }); } if (!document->filepath( DocumentData::FilePathResolve::Checked).isEmpty()) { menu->addAction( lang((cPlatform() == dbipMac || cPlatform() == dbipMacOld) ? lng_context_show_in_finder : lng_context_show_in_folder), [=] { ShowInFolder(document); }); } AddSaveDocumentAction(menu, contextId, document); } void AddPostLinkAction( not_null menu, const ContextMenuRequest &request) { const auto item = request.item; if (!item || !item->hasDirectLink() || request.pointState == PointState::Outside) { return; } else if (request.link && !request.link->copyToClipboardContextItemText().isEmpty()) { return; } const auto itemId = item->fullId(); menu->addAction( lang(item->history()->peer->isMegagroup() ? lng_context_copy_link : lng_context_copy_post_link), [=] { CopyPostLink(itemId); }); } MessageIdsList ExtractIdsList(const SelectedItems &items) { return ranges::view::all( items ) | ranges::view::transform([](const auto &item) { return item.msgId; }) | ranges::to_vector; } bool AddForwardSelectedAction( not_null menu, const ContextMenuRequest &request, not_null list) { if (!request.overSelection || request.selectedItems.empty()) { return false; } if (ranges::find_if(request.selectedItems, [](const auto &item) { return !item.canForward; }) != end(request.selectedItems)) { return false; } menu->addAction(lang(lng_context_forward_selected), [=] { const auto weak = make_weak(list); auto items = ExtractIdsList(request.selectedItems); Window::ShowForwardMessagesBox(std::move(items), [=] { if (const auto strong = weak.data()) { strong->cancelSelection(); } }); }); return true; } bool AddForwardMessageAction( not_null menu, const ContextMenuRequest &request, not_null list) { const auto item = request.item; if (!request.selectedItems.empty()) { return false; } else if (!item || !item->allowsForward()) { return false; } const auto asGroup = (request.pointState != PointState::GroupPart); if (asGroup) { if (const auto group = Auth().data().groups().find(item)) { if (ranges::find_if(group->items, [](auto item) { return !item->allowsForward(); }) != end(group->items)) { return false; } } } const auto itemId = item->fullId(); menu->addAction(lang(lng_context_forward_msg), [=] { if (const auto item = App::histItemById(itemId)) { Window::ShowForwardMessagesBox(asGroup ? Auth().data().itemOrItsGroup(item) : MessageIdsList{ 1, itemId }); } }); return true; } void AddForwardAction( not_null menu, const ContextMenuRequest &request, not_null list) { AddForwardSelectedAction(menu, request, list); AddForwardMessageAction(menu, request, list); } bool AddDeleteSelectedAction( not_null menu, const ContextMenuRequest &request, not_null list) { if (!request.overSelection || request.selectedItems.empty()) { return false; } if (ranges::find_if(request.selectedItems, [](const auto &item) { return !item.canDelete; }) != end(request.selectedItems)) { return false; } menu->addAction(lang(lng_context_delete_selected), [=] { const auto weak = make_weak(list); auto items = ExtractIdsList(request.selectedItems); const auto box = Ui::show(Box(std::move(items))); box->setDeleteConfirmedCallback([=] { if (const auto strong = weak.data()) { strong->cancelSelection(); } }); }); return true; } bool AddDeleteMessageAction( not_null menu, const ContextMenuRequest &request, not_null list) { const auto item = request.item; if (!request.selectedItems.empty()) { return false; } else if (!item || !item->canDelete()) { return false; } const auto asGroup = (request.pointState != PointState::GroupPart); if (asGroup) { if (const auto group = Auth().data().groups().find(item)) { if (ranges::find_if(group->items, [](auto item) { return !IsServerMsgId(item->id) || !item->canDelete(); }) != end(group->items)) { return false; } } } const auto itemId = item->fullId(); menu->addAction(lang(lng_context_delete_msg), [=] { if (const auto item = App::histItemById(itemId)) { if (asGroup) { if (const auto group = Auth().data().groups().find(item)) { Ui::show(Box( Auth().data().itemsToIds(group->items))); return; } } if (const auto message = item->toHistoryMessage()) { if (message->uploading()) { App::main()->cancelUploadLayer(item); return; } } const auto suggestModerateActions = true; Ui::show(Box(item, suggestModerateActions)); } }); return true; } void AddDeleteAction( not_null menu, const ContextMenuRequest &request, not_null list) { if (!AddDeleteSelectedAction(menu, request, list)) { AddDeleteMessageAction(menu, request, list); } } bool AddClearSelectionAction( not_null menu, const ContextMenuRequest &request, not_null list) { if (!request.overSelection || request.selectedItems.empty()) { return false; } menu->addAction(lang(lng_context_clear_selection), [=] { list->cancelSelection(); }); return true; } bool AddSelectMessageAction( not_null menu, const ContextMenuRequest &request, not_null list) { const auto item = request.item; if (request.overSelection && !request.selectedItems.empty()) { return false; } else if (!item || !IsServerMsgId(item->id) || item->serviceMsg()) { return false; } const auto itemId = item->fullId(); const auto asGroup = (request.pointState != PointState::GroupPart); menu->addAction(lang(lng_context_select_msg), [=] { if (const auto item = App::histItemById(itemId)) { if (asGroup) { list->selectItemAsGroup(item); } else { list->selectItem(item); } } }); return true; } void AddSelectionAction( not_null menu, const ContextMenuRequest &request, not_null list) { if (!AddClearSelectionAction(menu, request, list)) { AddSelectMessageAction(menu, request, list); } } void AddMessageActions( not_null menu, const ContextMenuRequest &request, not_null list) { AddPostLinkAction(menu, request); AddForwardAction(menu, request, list); AddDeleteAction(menu, request, list); AddSelectionAction(menu, request, list); } void AddCopyLinkAction( not_null menu, const ClickHandlerPtr &link) { if (!link) { return; } const auto action = link->copyToClipboardContextItemText(); if (action.isEmpty()) { return; } const auto text = link->copyToClipboardText(); menu->addAction( action, [=] { QApplication::clipboard()->setText(text); }); } } // namespace base::unique_qptr FillContextMenu( not_null list, const ContextMenuRequest &request) { auto result = base::make_unique_q(list); const auto link = request.link; const auto view = request.view; const auto item = request.item; const auto itemId = item ? item->fullId() : FullMsgId(); const auto rawLink = link.get(); const auto linkPhoto = dynamic_cast(rawLink); const auto linkDocument = dynamic_cast(rawLink); const auto linkPeer = dynamic_cast(rawLink); const auto photo = linkPhoto ? linkPhoto->photo().get() : nullptr; const auto document = linkDocument ? linkDocument->document().get() : nullptr; const auto isVideoLink = document ? document->isVideoFile() : false; const auto isVoiceLink = document ? document->isVoiceMessage() : false; const auto isAudioLink = document ? document->isAudioFile() : false; const auto hasSelection = !request.selectedItems.empty() || !request.selectedText.empty(); if (request.overSelection) { const auto text = lang(request.selectedItems.empty() ? lng_context_copy_selected : lng_context_copy_selected_items); result->addAction(text, [=] { SetClipboardText(list->getSelectedText()); }); } if (linkPhoto) { AddPhotoActions(result, photo); } else if (linkDocument) { AddDocumentActions(result, document, itemId); //} else if (linkPeer) { // #feed // const auto peer = linkPeer->peer(); // if (peer->isChannel() // && peer->asChannel()->feed() != nullptr // && (list->delegate()->listContext() == Context::Feed)) { // Window::PeerMenuAddMuteAction(peer, [&]( // const QString &text, // Fn handler) { // return result->addAction(text, handler); // }); // AddToggleGroupingAction(result, linkPeer->peer()); // } } else if (!request.overSelection && view && !hasSelection) { const auto media = view->media(); const auto mediaHasTextForCopy = media && media->hasTextForCopy(); if (const auto document = media ? media->getDocument() : nullptr) { AddDocumentActions(result, document, view->data()->fullId()); } if (!link && (view->hasVisibleText() || mediaHasTextForCopy)) { const auto asGroup = (request.pointState != PointState::GroupPart); result->addAction(lang(lng_context_copy_text), [=] { if (const auto item = App::histItemById(itemId)) { if (asGroup) { if (const auto group = Auth().data().groups().find(item)) { SetClipboardText(HistoryGroupText(group)); return; } } SetClipboardText(HistoryItemText(item)); } }); } } AddCopyLinkAction(result, link); AddMessageActions(result, request, list); return result; } void CopyPostLink(FullMsgId itemId) { const auto item = App::histItemById(itemId); if (!item || !item->hasDirectLink()) { return; } QApplication::clipboard()->setText( item->history()->session().api().exportDirectMessageLink(item)); const auto channel = item->history()->peer->asChannel(); Assert(channel != nullptr); Ui::Toast::Show(lang(channel->isPublic() ? lng_channel_public_link_copied : lng_context_about_private_link)); } void StopPoll(FullMsgId itemId) { const auto stop = [=] { Ui::hideLayer(); if (const auto item = App::histItemById(itemId)) { item->history()->session().api().closePoll(item); } }; Ui::show(Box( lang(lng_polls_stop_warning), lang(lng_polls_stop_sure), lang(lng_cancel), stop)); } } // namespace HistoryView