Improve message context menu in feed.

This commit is contained in:
John Preston 2018-01-28 18:08:34 +03:00
parent 7435bd7fb0
commit 533fba8c70
7 changed files with 365 additions and 89 deletions

View File

@ -1552,33 +1552,6 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
saveDocumentToFile(document); saveDocumentToFile(document);
})); }));
} }
} else if (media->type() == MediaTypeGif && !link) {
if (auto document = media->getDocument()) {
if (document->loading()) {
_menu->addAction(lang(lng_context_cancel_download), [=] {
cancelContextDownload(document);
});
} else {
if (document->isGifv()) {
if (!cAutoPlayGif()) {
_menu->addAction(lang(lng_context_open_gif), [=] {
openContextGif(itemId);
});
}
_menu->addAction(lang(lng_context_save_gif), [=] {
saveContextGif(itemId);
});
}
if (!document->filepath(DocumentData::FilePathResolveChecked).isEmpty()) {
_menu->addAction(lang((cPlatform() == dbipMac || cPlatform() == dbipMacOld) ? lng_context_show_in_finder : lng_context_show_in_folder), [=] {
showContextInFolder(document);
});
}
_menu->addAction(lang(lng_context_save_file), App::LambdaDelayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, document] {
saveDocumentToFile(document);
}));
}
}
} }
} }
if (msg && view && !link && (view->hasVisibleText() || mediaHasTextForCopy)) { if (msg && view && !link && (view->hasVisibleText() || mediaHasTextForCopy)) {

View File

@ -377,7 +377,7 @@ bool HistoryItem::allowsEdit(const QDateTime &now) const {
} }
bool HistoryItem::canDelete() const { bool HistoryItem::canDelete() const {
if (isLogEntry()) { if (isLogEntry() || (!IsServerMsgId(id) && serviceMsg())) {
return false; return false;
} }
auto channel = _history->peer->asChannel(); auto channel = _history->peer->asChannel();
@ -450,7 +450,7 @@ bool HistoryItem::suggestDeleteAllReport() const {
} }
bool HistoryItem::hasDirectLink() const { bool HistoryItem::hasDirectLink() const {
if (id <= 0) { if (!IsServerMsgId(id)) {
return false; return false;
} }
if (auto channel = _history->peer->asChannel()) { if (auto channel = _history->peer->asChannel()) {

View File

@ -8,14 +8,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_context_menu.h" #include "history/view/history_view_context_menu.h"
#include "history/view/history_view_list_widget.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_item.h"
#include "history/history_message.h"
#include "history/history_item_text.h" #include "history/history_item_text.h"
#include "history/history_media_types.h" #include "history/history_media_types.h"
#include "ui/widgets/popup_menu.h" #include "ui/widgets/popup_menu.h"
#include "chat_helpers/message_field.h" #include "chat_helpers/message_field.h"
#include "boxes/confirm_box.h"
#include "data/data_photo.h" #include "data/data_photo.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_media_types.h" #include "data/data_media_types.h"
#include "data/data_session.h"
#include "data/data_groups.h"
#include "core/file_utilities.h" #include "core/file_utilities.h"
#include "window/window_peer_menu.h" #include "window/window_peer_menu.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
@ -61,6 +67,25 @@ void CopyImage(not_null<PhotoData*> photo) {
QApplication::clipboard()->setPixmap(photo->full->pix()); QApplication::clipboard()->setPixmap(photo->full->pix());
} }
void ShowStickerPackInfo(not_null<DocumentData*> document) {
if (const auto sticker = document->sticker()) {
if (sticker->set.type() != mtpc_inputStickerSetEmpty) {
App::main()->stickersBox(sticker->set);
}
}
}
void ToggleFavedSticker(not_null<DocumentData*> document) {
const auto unfave = Stickers::IsFaved(document);
MTP::send(
MTPmessages_FaveSticker(
document->mtpInput(),
MTP_bool(unfave)),
rpcDone([=](const MTPBool &result) {
Stickers::SetFaved(document, !unfave);
}));
}
void AddPhotoActions( void AddPhotoActions(
not_null<Ui::PopupMenu*> menu, not_null<Ui::PopupMenu*> menu,
not_null<PhotoData*> photo) { not_null<PhotoData*> photo) {
@ -129,6 +154,19 @@ void AddDocumentActions(
}); });
} }
} }
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); });
}
if (!document->filepath( if (!document->filepath(
DocumentData::FilePathResolveChecked).isEmpty()) { DocumentData::FilePathResolveChecked).isEmpty()) {
menu->addAction( menu->addAction(
@ -140,14 +178,256 @@ void AddDocumentActions(
AddSaveDocumentAction(menu, document); AddSaveDocumentAction(menu, document);
} }
void ShowStickerPackInfo(not_null<DocumentData*> document) { void CopyPostLink(FullMsgId itemId) {
if (const auto sticker = document->sticker()) { if (const auto item = App::histItemById(itemId)) {
if (sticker->set.type() != mtpc_inputStickerSetEmpty) { if (item->hasDirectLink()) {
App::main()->stickersBox(sticker->set); QApplication::clipboard()->setText(item->directLink());
} }
} }
} }
void AddPostLinkAction(
not_null<Ui::PopupMenu*> 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<Ui::PopupMenu*> menu,
const ContextMenuRequest &request,
not_null<ListWidget*> 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<Ui::PopupMenu*> menu,
const ContextMenuRequest &request,
not_null<ListWidget*> 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<Ui::PopupMenu*> menu,
const ContextMenuRequest &request,
not_null<ListWidget*> list) {
AddForwardSelectedAction(menu, request, list);
AddForwardMessageAction(menu, request, list);
}
bool AddDeleteSelectedAction(
not_null<Ui::PopupMenu*> menu,
const ContextMenuRequest &request,
not_null<ListWidget*> 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<DeleteMessagesBox>(std::move(items)));
box->setDeleteConfirmedCallback([=] {
if (const auto strong = weak.data()) {
strong->cancelSelection();
}
});
});
return true;
}
bool AddDeleteMessageAction(
not_null<Ui::PopupMenu*> menu,
const ContextMenuRequest &request,
not_null<ListWidget*> 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<DeleteMessagesBox>(
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<DeleteMessagesBox>(item, suggestModerateActions));
}
});
return true;
}
void AddDeleteAction(
not_null<Ui::PopupMenu*> menu,
const ContextMenuRequest &request,
not_null<ListWidget*> list) {
if (!AddDeleteSelectedAction(menu, request, list)) {
AddDeleteMessageAction(menu, request, list);
}
}
bool AddClearSelectionAction(
not_null<Ui::PopupMenu*> menu,
const ContextMenuRequest &request,
not_null<ListWidget*> list) {
if (!request.overSelection || request.selectedItems.empty()) {
return false;
}
menu->addAction(lang(lng_context_clear_selection), [=] {
list->cancelSelection();
});
return true;
}
bool AddSelectMessageAction(
not_null<Ui::PopupMenu*> menu,
const ContextMenuRequest &request,
not_null<ListWidget*> 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<Ui::PopupMenu*> menu,
const ContextMenuRequest &request,
not_null<ListWidget*> list) {
if (!AddClearSelectionAction(menu, request, list)) {
AddSelectMessageAction(menu, request, list);
}
}
void AddMessageActions(
not_null<Ui::PopupMenu*> menu,
const ContextMenuRequest &request,
not_null<ListWidget*> list) {
AddPostLinkAction(menu, request);
AddForwardAction(menu, request, list);
AddDeleteAction(menu, request, list);
AddSelectionAction(menu, request, list);
}
void AddCopyLinkAction(
not_null<Ui::PopupMenu*> 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 } // namespace
base::unique_qptr<Ui::PopupMenu> FillContextMenu( base::unique_qptr<Ui::PopupMenu> FillContextMenu(
@ -157,7 +437,7 @@ base::unique_qptr<Ui::PopupMenu> FillContextMenu(
const auto link = request.link; const auto link = request.link;
const auto view = request.view; const auto view = request.view;
const auto item = view ? view->data().get() : nullptr; const auto item = request.item;
const auto itemId = item ? item->fullId() : FullMsgId(); const auto itemId = item ? item->fullId() : FullMsgId();
const auto rawLink = link.get(); const auto rawLink = link.get();
const auto linkPhoto = dynamic_cast<PhotoClickHandler*>(rawLink); const auto linkPhoto = dynamic_cast<PhotoClickHandler*>(rawLink);
@ -174,7 +454,10 @@ base::unique_qptr<Ui::PopupMenu> FillContextMenu(
|| !request.selectedText.text.isEmpty(); || !request.selectedText.text.isEmpty();
if (request.overSelection) { if (request.overSelection) {
result->addAction(lang(lng_context_copy_selected), [=] { const auto text = lang(request.selectedItems.empty()
? lng_context_copy_selected
: lng_context_copy_selected_items);
result->addAction(text, [=] {
SetClipboardWithEntities(list->getSelectedText()); SetClipboardWithEntities(list->getSelectedText());
}); });
} }
@ -187,57 +470,38 @@ base::unique_qptr<Ui::PopupMenu> FillContextMenu(
if (list->delegate()->listContext() == Context::Feed) { if (list->delegate()->listContext() == Context::Feed) {
AddToggleGroupingAction(result, linkPeer->peer()); AddToggleGroupingAction(result, linkPeer->peer());
} }
} else { // maybe cursor on some text history item? } else if (!request.overSelection && view && !hasSelection) {
bool canDelete = item && item->canDelete() && (item->id > 0 || !item->serviceMsg()); auto media = view->media();
bool canForward = item && item->allowsForward(); const auto mediaHasTextForCopy = media && media->hasTextForCopy();
if (media) {
const auto msg = item->toHistoryMessage(); if (media->type() == MediaTypeWebPage
if (!request.overSelection) { && static_cast<HistoryWebPage*>(media)->attach()) {
if (item && !hasSelection) { media = static_cast<HistoryWebPage*>(media)->attach();
auto mediaHasTextForCopy = false; }
if (auto media = view->media()) { if (media->type() == MediaTypeSticker) {
mediaHasTextForCopy = media->hasTextForCopy(); if (const auto document = media->getDocument()) {
if (media->type() == MediaTypeWebPage AddDocumentActions(result, document, view->data()->fullId());
&& static_cast<HistoryWebPage*>(media)->attach()) {
media = static_cast<HistoryWebPage*>(media)->attach();
}
if (media->type() == MediaTypeSticker) {
if (auto document = media->getDocument()) {
if (document->sticker() && document->sticker()->set.type() != mtpc_inputStickerSetEmpty) {
result->addAction(lang(document->isStickerSetInstalled() ? lng_context_pack_info : lng_context_pack_add), [=] {
ShowStickerPackInfo(document);
});
}
result->addAction(
lang(lng_context_save_image),
App::LambdaDelayed(
st::defaultDropdownMenu.menu.ripple.hideDuration,
list,
[=] { DocumentSaveClickHandler::doSave(document, true); }));
}
}
}
if (!link && (view->hasVisibleText() || mediaHasTextForCopy)) {
result->addAction(lang(lng_context_copy_text), [=] {
if (const auto item = App::histItemById(itemId)) {
SetClipboardWithEntities(HistoryItemText(item));
}
});
} }
} }
} }
if (!link && (view->hasVisibleText() || mediaHasTextForCopy)) {
const auto actionText = link const auto asGroup = (request.pointState != PointState::GroupPart);
? link->copyToClipboardContextItemText() result->addAction(lang(lng_context_copy_text), [=] {
: QString(); if (const auto item = App::histItemById(itemId)) {
if (!actionText.isEmpty()) { if (asGroup) {
result->addAction( if (const auto group = Auth().data().groups().find(item)) {
actionText, SetClipboardWithEntities(HistoryGroupText(group));
[text = link->copyToClipboardText()] { return;
QApplication::clipboard()->setText(text); }
}); }
SetClipboardWithEntities(HistoryItemText(item));
}
});
} }
} }
AddCopyLinkAction(result, link);
AddMessageActions(result, request, list);
return result; return result;
} }

View File

@ -15,16 +15,20 @@ class PopupMenu;
namespace HistoryView { namespace HistoryView {
enum class PointState : char;
class ListWidget; class ListWidget;
class Element; class Element;
struct SelectedItem;
using SelectedItems = std::vector<SelectedItem>;
struct ContextMenuRequest { struct ContextMenuRequest {
ClickHandlerPtr link; ClickHandlerPtr link;
Element *view = nullptr; Element *view = nullptr;
MessageIdsList selectedItems; HistoryItem *item = nullptr;
SelectedItems selectedItems;
TextWithEntities selectedText; TextWithEntities selectedText;
bool overView = false;
bool overSelection = false; bool overSelection = false;
PointState pointState = PointState();
}; };
base::unique_qptr<Ui::PopupMenu> FillContextMenu( base::unique_qptr<Ui::PopupMenu> FillContextMenu(

View File

@ -715,6 +715,28 @@ void ListWidget::cancelSelection() {
clearTextSelection(); clearTextSelection();
} }
void ListWidget::selectItem(not_null<HistoryItem*> item) {
if (const auto view = viewForItem(item)) {
clearTextSelection();
changeSelection(
_selected,
item,
SelectAction::Select);
pushSelectedItems();
}
}
void ListWidget::selectItemAsGroup(not_null<HistoryItem*> item) {
if (const auto view = viewForItem(item)) {
clearTextSelection();
changeSelectionAsGroup(
_selected,
item,
SelectAction::Select);
pushSelectedItems();
}
}
void ListWidget::clearSelected() { void ListWidget::clearSelected() {
if (_selected.empty()) { if (_selected.empty()) {
return; return;
@ -1325,13 +1347,14 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
ContextMenuRequest request; ContextMenuRequest request;
request.link = ClickHandler::getActive(); request.link = ClickHandler::getActive();
request.view = _overElement; request.view = _overElement;
// #TODO group part context menu using _overItemExact request.item = _overItemExact
request.overView = _overElement ? _overItemExact
&& (_overState.pointState != PointState::Outside); : _overElement
? _overElement->data().get()
: nullptr;
request.pointState = _overState.pointState;
request.selectedText = _selectedText; request.selectedText = _selectedText;
if (!_selected.empty()) { request.selectedItems = collectSelectedItems();
request.selectedItems = collectSelectedIds();
}
request.overSelection = showFromTouch request.overSelection = showFromTouch
|| (_overElement && isInsideSelection( || (_overElement && isInsideSelection(
_overElement, _overElement,

View File

@ -138,6 +138,8 @@ public:
TextWithEntities getSelectedText() const; TextWithEntities getSelectedText() const;
MessageIdsList getSelectedItems() const; MessageIdsList getSelectedItems() const;
void cancelSelection(); void cancelSelection();
void selectItem(not_null<HistoryItem*> item);
void selectItemAsGroup(not_null<HistoryItem*> item);
// AbstractTooltipShower interface // AbstractTooltipShower interface
QString tooltipText() const override; QString tooltipText() const override;

View File

@ -442,6 +442,16 @@ QPointer<const Widget> make_weak(const Widget *object) {
return QPointer<const Widget>(object); return QPointer<const Widget>(object);
} }
template <typename Widget>
QPointer<Widget> make_weak(not_null<Widget*> object) {
return QPointer<Widget>(object.get());
}
template <typename Widget>
QPointer<const Widget> make_weak(not_null<const Widget*> object) {
return QPointer<const Widget>(object.get());
}
class SingleQueuedInvokation : public QObject { class SingleQueuedInvokation : public QObject {
public: public:
SingleQueuedInvokation(base::lambda<void()> callback) : _callback(callback) { SingleQueuedInvokation(base::lambda<void()> callback) : _callback(callback) {