Allow multiple items selection in HistoryView.

This commit is contained in:
John Preston 2018-01-26 18:40:11 +03:00
parent 2aa477176c
commit 63c1212ef1
42 changed files with 1402 additions and 537 deletions

View File

@ -574,20 +574,20 @@ void DeleteMessagesBox::deleteAndClear() {
}
}
if (!_singleItem) {
App::main()->clearSelectedItems();
if (_deleteConfirmedCallback) {
_deleteConfirmedCallback();
}
QMap<PeerData*, QVector<MTPint>> idsByPeer;
for_const (auto fullId, _ids) {
if (auto item = App::histItemById(fullId)) {
for (const auto itemId : _ids) {
if (auto item = App::histItemById(itemId)) {
auto history = item->history();
auto wasOnServer = (item->id > 0);
auto wasLast = (history->lastMsg == item);
item->destroy();
if (wasOnServer) {
idsByPeer[history->peer].push_back(MTP_int(fullId.msg));
idsByPeer[history->peer].push_back(MTP_int(itemId.msg));
} else if (wasLast) {
App::main()->checkPeerHistory(history->peer);
}

View File

@ -166,6 +166,10 @@ public:
bool suggestModerateActions);
DeleteMessagesBox(QWidget*, MessageIdsList &&selected);
void setDeleteConfirmedCallback(base::lambda<void()> callback) {
_deleteConfirmedCallback = std::move(callback);
}
protected:
void prepare() override;
@ -188,6 +192,8 @@ private:
object_ptr<Ui::Checkbox> _reportSpam = { nullptr };
object_ptr<Ui::Checkbox> _deleteAll = { nullptr };
base::lambda<void()> _deleteConfirmedCallback;
};
class ConfirmInviteBox : public BoxContent, public RPCSender {

View File

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_item.h"
#include "history/history_location_manager.h"
#include "history/view/history_view_element.h"
#include "ui/text_options.h"
#include "storage/storage_shared_media.h"
#include "storage/localstorage.h"
#include "data/data_session.h"
@ -129,6 +130,19 @@ QString WithCaptionNotificationText(
caption);
}
TextWithEntities WithCaptionClipboardText(
const QString &attachType,
TextWithEntities &&caption) {
TextWithEntities result;
result.text.reserve(5 + attachType.size() + caption.text.size());
result.text.append(qstr("[ ")).append(attachType).append(qstr(" ]"));
if (!caption.text.isEmpty()) {
result.text.append(qstr("\n"));
TextUtilities::Append(result, std::move(caption));
}
return result;
}
} // namespace
Media::Media(not_null<HistoryItem*> parent) : _parent(parent) {
@ -298,6 +312,12 @@ QString MediaPhoto::pinnedTextSubstring() const {
return lang(lng_action_pinned_media_photo);
}
TextWithEntities MediaPhoto::clipboardText() const {
return WithCaptionClipboardText(
lang(lng_in_dlg_photo),
parent()->clipboardText());
}
bool MediaPhoto::allowsEditCaption() const {
return true;
}
@ -536,6 +556,36 @@ QString MediaFile::pinnedTextSubstring() const {
return lang(lng_action_pinned_media_file);
}
TextWithEntities MediaFile::clipboardText() const {
const auto attachType = [&] {
const auto name = _document->composeNameString();
const auto addName = !name.isEmpty()
? qstr(" : ") + name
: QString();
if (const auto sticker = _document->sticker()) {
if (!_emoji.isEmpty()) {
return lng_in_dlg_sticker_emoji(lt_emoji, _emoji);
}
return lang(lng_in_dlg_sticker);
} else if (_document->isAnimation()) {
if (_document->isVideoMessage()) {
return lang(lng_in_dlg_video_message);
}
return qsl("GIF");
} else if (_document->isVideoFile()) {
return lang(lng_in_dlg_video);
} else if (_document->isVoiceMessage()) {
return lang(lng_in_dlg_audio) + addName;
} else if (_document->isSong()) {
return lang(lng_in_dlg_audio_file) + addName;
}
return lang(lng_in_dlg_file) + addName;
}();
return WithCaptionClipboardText(
attachType,
parent()->clipboardText());
}
bool MediaFile::allowsEditCaption() const {
return !_document->isVideoMessage() && !_document->sticker();
}
@ -666,6 +716,18 @@ QString MediaContact::pinnedTextSubstring() const {
return lang(lng_action_pinned_media_contact);
}
TextWithEntities MediaContact::clipboardText() const {
const auto text = qsl("[ ") + lang(lng_in_dlg_contact) + qsl(" ]\n")
+ lng_full_name(
lt_first_name,
_contact.firstName,
lt_last_name,
_contact.lastName).trimmed()
+ '\n'
+ _contact.phoneNumber;
return { text, EntitiesInText() };
}
bool MediaContact::updateInlineResultMedia(const MTPMessageMedia &media) {
return false;
}
@ -734,6 +796,29 @@ QString MediaLocation::pinnedTextSubstring() const {
return lang(lng_action_pinned_media_location);
}
TextWithEntities MediaLocation::clipboardText() const {
TextWithEntities result = {
qsl("[ ") + lang(lng_maps_point) + qsl(" ]\n"),
EntitiesInText()
};
auto titleResult = TextUtilities::ParseEntities(
TextUtilities::Clean(_title),
Ui::WebpageTextTitleOptions().flags);
auto descriptionResult = TextUtilities::ParseEntities(
TextUtilities::Clean(_description),
TextParseLinks | TextParseMultiline | TextParseRichText);
if (!titleResult.text.isEmpty()) {
TextUtilities::Append(result, std::move(titleResult));
result.text.append('\n');
}
if (!descriptionResult.text.isEmpty()) {
TextUtilities::Append(result, std::move(descriptionResult));
result.text.append('\n');
}
result.text += LocationClickHandler(_location->coords).dragText();
return result;
}
bool MediaLocation::updateInlineResultMedia(const MTPMessageMedia &media) {
return false;
}
@ -783,6 +868,10 @@ QString MediaCall::pinnedTextSubstring() const {
return QString();
}
TextWithEntities MediaCall::clipboardText() const {
return { qsl("[ ") + notificationText() + qsl(" ]"), EntitiesInText() };
}
bool MediaCall::allowsForward() const {
return false;
}
@ -852,6 +941,10 @@ QString MediaWebPage::pinnedTextSubstring() const {
return QString();
}
TextWithEntities MediaWebPage::clipboardText() const {
return TextWithEntities();
}
bool MediaWebPage::allowsEdit() const {
return true;
}
@ -904,6 +997,10 @@ QString MediaGame::pinnedTextSubstring() const {
return lng_action_pinned_media_game(lt_game, title);
}
TextWithEntities MediaGame::clipboardText() const {
return TextWithEntities();
}
QString MediaGame::errorTextForForward(
not_null<ChannelData*> channel) const {
if (channel->restricted(ChannelRestriction::f_send_games)) {
@ -965,6 +1062,10 @@ QString MediaInvoice::pinnedTextSubstring() const {
return QString();
}
TextWithEntities MediaInvoice::clipboardText() const {
return TextWithEntities();
}
bool MediaInvoice::updateInlineResultMedia(const MTPMessageMedia &media) {
return true;
}

View File

@ -89,6 +89,7 @@ public:
virtual QString chatsListText() const;
virtual QString notificationText() const = 0;
virtual QString pinnedTextSubstring() const = 0;
virtual TextWithEntities clipboardText() const = 0;
virtual bool allowsForward() const;
virtual bool allowsEdit() const;
virtual bool allowsEditCaption() const;
@ -136,6 +137,7 @@ public:
QString chatsListText() const override;
QString notificationText() const override;
QString pinnedTextSubstring() const override;
TextWithEntities clipboardText() const override;
bool allowsEditCaption() const override;
QString errorTextForForward(
not_null<ChannelData*> channel) const override;
@ -169,6 +171,7 @@ public:
QString chatsListText() const override;
QString notificationText() const override;
QString pinnedTextSubstring() const override;
TextWithEntities clipboardText() const override;
bool allowsEditCaption() const override;
bool forwardedBecomesUnread() const override;
QString errorTextForForward(
@ -201,6 +204,7 @@ public:
const SharedContact *sharedContact() const override;
QString notificationText() const override;
QString pinnedTextSubstring() const override;
TextWithEntities clipboardText() const override;
bool updateInlineResultMedia(const MTPMessageMedia &media) override;
bool updateSentMedia(const MTPMessageMedia &media) override;
@ -230,6 +234,7 @@ public:
QString chatsListText() const override;
QString notificationText() const override;
QString pinnedTextSubstring() const override;
TextWithEntities clipboardText() const override;
bool updateInlineResultMedia(const MTPMessageMedia &media) override;
bool updateSentMedia(const MTPMessageMedia &media) override;
@ -255,6 +260,7 @@ public:
const Call *call() const override;
QString notificationText() const override;
QString pinnedTextSubstring() const override;
TextWithEntities clipboardText() const override;
bool allowsForward() const override;
bool allowsRevoke() const override;
@ -286,6 +292,7 @@ public:
QString chatsListText() const override;
QString notificationText() const override;
QString pinnedTextSubstring() const override;
TextWithEntities clipboardText() const override;
bool allowsEdit() const override;
bool updateInlineResultMedia(const MTPMessageMedia &media) override;
@ -311,6 +318,7 @@ public:
QString notificationText() const override;
QString pinnedTextSubstring() const override;
TextWithEntities clipboardText() const override;
QString errorTextForForward(
not_null<ChannelData*> channel) const override;
@ -343,6 +351,7 @@ public:
QString notificationText() const override;
QString pinnedTextSubstring() const override;
TextWithEntities clipboardText() const override;
bool updateInlineResultMedia(const MTPMessageMedia &media) override;
bool updateSentMedia(const MTPMessageMedia &media) override;

View File

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_media_types.h"
#include "history/history_message.h"
#include "history/history_item_components.h"
#include "history/history_item_text.h"
#include "history/admin_log/history_admin_log_section.h"
#include "history/admin_log/history_admin_log_filter.h"
#include "history/view/history_view_message.h"
@ -486,6 +487,11 @@ std::unique_ptr<HistoryView::Element> InnerWidget::elementCreate(
return std::make_unique<HistoryView::Service>(this, message);
}
bool InnerWidget::elementUnderCursor(
not_null<const HistoryView::Element*> view) {
return (App::hoveredItem() == view);
}
void InnerWidget::elementAnimationAutoplayAsync(
not_null<const HistoryView::Element*> view) {
crl::on_main(this, [this, msgId = view->data()->fullId()] {
@ -1121,9 +1127,7 @@ void InnerWidget::openContextGif(FullMsgId itemId) {
void InnerWidget::copyContextText(FullMsgId itemId) {
if (const auto item = App::histItemById(itemId)) {
if (const auto view = viewForItem(item)) {
SetClipboardWithEntities(view->selectedText(FullSelection));
}
SetClipboardWithEntities(HistoryItemText(item));
}
}

View File

@ -74,8 +74,10 @@ public:
not_null<HistoryMessage*> message) override;
std::unique_ptr<HistoryView::Element> elementCreate(
not_null<HistoryService*> message) override;
bool elementUnderCursor(
not_null<const HistoryView::Element*> view) override;
void elementAnimationAutoplayAsync(
not_null<const HistoryView::Element*> element) override;
not_null<const HistoryView::Element*> view) override;
~InnerWidget();

View File

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_element.h"
#include "history/view/history_view_message.h"
#include "history/view/history_view_service_message.h"
#include "history/history_item.h"
#include "mainwidget.h"
#include "lang/lang_keys.h"
#include "ui/widgets/buttons.h"
@ -20,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/popup_menu.h"
#include "boxes/confirm_box.h"
#include "window/window_controller.h"
#include "window/window_peer_menu.h"
#include "data/data_feed_messages.h"
#include "data/data_photo.h"
#include "data/data_document.h"
@ -69,6 +71,18 @@ Widget::Widget(
_topBar->move(0, 0);
_topBar->resizeToWidth(width());
_topBar->show();
_topBar->forwardSelectionRequest(
) | rpl::start_with_next([=] {
forwardSelected();
}, _topBar->lifetime());
_topBar->deleteSelectionRequest(
) | rpl::start_with_next([=] {
confirmDeleteSelected();
}, _topBar->lifetime());
_topBar->clearSelectionRequest(
) | rpl::start_with_next([=] {
clearSelected();
}, _topBar->lifetime());
_topBarShadow->raise();
updateAdaptiveLayout();
@ -175,6 +189,30 @@ rpl::producer<Data::MessagesSlice> Widget::listSource(
limitAfter);
}
bool Widget::listAllowsMultiSelect() {
return true;
}
bool Widget::listIsLessInOrder(
not_null<HistoryItem*> first,
not_null<HistoryItem*> second) {
return first->position() < second->position();
}
void Widget::listSelectionChanged(HistoryView::SelectedItems &&items) {
HistoryView::TopBarWidget::SelectedState state;
state.count = items.size();
for (const auto item : items) {
if (item.canForward) {
++state.canForwardCount;
}
if (item.canDelete) {
++state.canDeleteCount;
}
}
_topBar->showSelected(state);
}
std::unique_ptr<Window::SectionMemento> Widget::createMemento() {
auto result = std::make_unique<Memento>(_feed);
saveState(result.get());
@ -280,4 +318,35 @@ QRect Widget::rectForFloatPlayer() const {
return mapToGlobal(_scroll->geometry());
}
void Widget::forwardSelected() {
auto items = _inner->getSelectedItems();
if (items.empty()) {
return;
}
const auto weak = make_weak(this);
Window::ShowForwardMessagesBox(std::move(items), [=] {
if (const auto strong = weak.data()) {
strong->clearSelected();
}
});
}
void Widget::confirmDeleteSelected() {
auto items = _inner->getSelectedItems();
if (items.empty()) {
return;
}
const auto weak = make_weak(this);
const auto box = Ui::show(Box<DeleteMessagesBox>(std::move(items)));
box->setDeleteConfirmedCallback([=] {
if (const auto strong = weak.data()) {
strong->clearSelected();
}
});
}
void Widget::clearSelected() {
_inner->cancelSelection();
}
} // namespace HistoryFeed

View File

@ -68,6 +68,12 @@ public:
Data::MessagePosition aroundId,
int limitBefore,
int limitAfter) override;
bool listAllowsMultiSelect() override;
bool listIsLessInOrder(
not_null<HistoryItem*> first,
not_null<HistoryItem*> second) override;
void listSelectionChanged(
HistoryView::SelectedItems &&items) override;
protected:
void resizeEvent(QResizeEvent *e) override;
@ -86,6 +92,10 @@ private:
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
void forwardSelected();
void confirmDeleteSelected();
void clearSelected();
not_null<Data::Feed*> _feed;
object_ptr<Ui::ScrollArea> _scroll;
QPointer<HistoryView::ListWidget> _inner;

View File

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_message.h"
#include "history/history_media_types.h"
#include "history/history_item_components.h"
#include "history/history_item_text.h"
#include "history/view/history_view_message.h"
#include "history/view/history_view_service_message.h"
#include "ui/text_options.h"
@ -1326,7 +1327,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
// -2 - has full selected items, but not over, -1 - has selection, but no over, 0 - no selection, 1 - over text, 2 - over full selected items
auto isUponSelected = 0;
auto hasSelected = 0;;
auto hasSelected = 0;
if (!_selected.empty()) {
isUponSelected = -1;
if (_selected.cbegin()->second == FullSelection) {
@ -1441,14 +1442,18 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
}
if (isUponSelected > 1) {
if (selectedState.count > 0 && selectedState.canForwardCount == selectedState.count) {
_menu->addAction(lang(lng_context_forward_selected), _widget, SLOT(onForwardSelected()));
_menu->addAction(lang(lng_context_forward_selected), [=] {
_widget->forwardSelected();
});
}
if (selectedState.count > 0 && selectedState.canDeleteCount == selectedState.count) {
_menu->addAction(lang(lng_context_delete_selected), [=] {
_widget->confirmDeleteSelectedItems();
_widget->confirmDeleteSelected();
});
}
_menu->addAction(lang(lng_context_clear_selection), _widget, SLOT(onClearSelected()));
_menu->addAction(lang(lng_context_clear_selection), [=] {
_widget->clearSelected();
});
} else if (item) {
const auto itemId = item->fullId();
if (isUponSelected != -2) {
@ -1576,14 +1581,18 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
}
if (isUponSelected > 1) {
if (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) {
_menu->addAction(lang(lng_context_forward_selected), _widget, SLOT(onForwardSelected()));
_menu->addAction(lang(lng_context_forward_selected), [=] {
_widget->forwardSelected();
});
}
if (selectedState.count > 0 && selectedState.count == selectedState.canDeleteCount) {
_menu->addAction(lang(lng_context_delete_selected), [=] {
_widget->confirmDeleteSelectedItems();
_widget->confirmDeleteSelected();
});
}
_menu->addAction(lang(lng_context_clear_selection), _widget, SLOT(onClearSelected()));
_menu->addAction(lang(lng_context_clear_selection), [=] {
_widget->clearSelected();
});
} else if (item && ((isUponSelected != -2 && (canForward || canDelete)) || item->id > 0)) {
if (isUponSelected != -2) {
if (canForward) {
@ -1709,10 +1718,8 @@ void HistoryInner::saveContextGif(FullMsgId itemId) {
void HistoryInner::copyContextText(FullMsgId itemId) {
if (const auto item = App::histItemById(itemId)) {
if (const auto view = item->mainView()) {
// #TODO check for a group
SetClipboardWithEntities(view->selectedText(FullSelection));
}
// #TODO check for a group
SetClipboardWithEntities(HistoryItemText(item));
}
}
@ -1743,13 +1750,11 @@ TextWithEntities HistoryInner::getSelectedText() const {
auto fullSize = 0;
auto texts = base::flat_map<std::pair<int, MsgId>, TextWithEntities>();
const auto addItem = [&](
not_null<HistoryView::Element*> view,
TextSelection selection) {
const auto addItem = [&](not_null<HistoryView::Element*> view) {
const auto item = view->data();
auto time = item->date.toString(timeFormat);
auto part = TextWithEntities();
auto unwrapped = view->selectedText(selection);
auto unwrapped = HistoryItemText(item);
auto size = item->author()->name.size()
+ time.size()
+ unwrapped.text.size();
@ -1780,17 +1785,17 @@ TextWithEntities HistoryInner::getSelectedText() const {
// group->leader);
//if (leaderSelection == FullSelection) {
// groupLeadersAdded.emplace(group->leader);
// addItem(group->leader, FullSelection);
// addItem(group->leader);
//} else if (view == group->leader) {
// const auto leaderFullSelection = AddGroupItemSelection(
// TextSelection(),
// int(group->others.size()));
// addItem(view, leaderFullSelection);
//} else {
// addItem(view, FullSelection);
// addItem(view);
//}
} else {
addItem(view, FullSelection);
addItem(view);
}
}
@ -1820,7 +1825,7 @@ void HistoryInner::keyPressEvent(QKeyEvent *e) {
auto selectedState = getSelectionState();
if (selectedState.count > 0
&& selectedState.canDeleteCount == selectedState.count) {
_widget->confirmDeleteSelectedItems();
_widget->confirmDeleteSelected();
}
} else {
e->ignore();
@ -2106,7 +2111,7 @@ bool HistoryInner::focusNextPrevChild(bool next) {
if (_selected.empty()) {
return TWidget::focusNextPrevChild(next);
} else {
clearSelectedItems();
clearSelected();
return true;
}
}
@ -2208,7 +2213,7 @@ auto HistoryInner::getSelectionState() const
return result;
}
void HistoryInner::clearSelectedItems(bool onlyTextSelection) {
void HistoryInner::clearSelected(bool onlyTextSelection) {
if (!_selected.empty() && (!onlyTextSelection || _selected.cbegin()->second != FullSelection)) {
_selected.clear();
_widget->updateTopBarSelection();
@ -2927,6 +2932,10 @@ not_null<HistoryView::ElementDelegate*> HistoryInner::ElementDelegate() {
not_null<HistoryService*> message) override {
return std::make_unique<HistoryView::Service>(this, message);
}
bool elementUnderCursor(
not_null<const HistoryView::Element*> view) override {
return (App::hoveredItem() == view);
}
void elementAnimationAutoplayAsync(
not_null<const HistoryView::Element*> view) override {
crl::on_main(&Auth(), [msgId = view->data()->fullId()] {

View File

@ -58,7 +58,7 @@ public:
bool canDeleteSelected() const;
HistoryView::TopBarWidget::SelectedState getSelectionState() const;
void clearSelectedItems(bool onlyTextSelection = false);
void clearSelected(bool onlyTextSelection = false);
MessageIdsList getSelectedItems() const;
void selectItem(not_null<HistoryItem*> item);

View File

@ -679,14 +679,25 @@ HistoryItem::~HistoryItem() {
}
}
ClickHandlerPtr goToMessageClickHandler(
not_null<HistoryItem*> item,
FullMsgId returnToId) {
return goToMessageClickHandler(
item->history()->peer,
item->id,
returnToId);
}
ClickHandlerPtr goToMessageClickHandler(
not_null<PeerData*> peer,
MsgId msgId) {
MsgId msgId,
FullMsgId returnToId) {
return std::make_shared<LambdaClickHandler>([=] {
if (App::main()) {
auto view = App::mousedItem();
if (view && view->data()->history()->peer == peer) {
App::main()->pushReplyReturn(view->data());
if (const auto main = App::main()) {
if (const auto returnTo = App::histItemById(returnToId)) {
if (returnTo->history()->peer == peer) {
main->pushReplyReturn(returnTo);
}
}
App::wnd()->controller()->showPeerHistory(
peer,
@ -696,10 +707,6 @@ ClickHandlerPtr goToMessageClickHandler(
});
}
ClickHandlerPtr goToMessageClickHandler(not_null<HistoryItem*> item) {
return goToMessageClickHandler(item->history()->peer, item->id);
}
not_null<HistoryItem*> HistoryItem::Create(
not_null<History*> history,
const MTPMessage &message) {

View File

@ -180,6 +180,9 @@ public:
virtual TextWithEntities originalText() const {
return { QString(), EntitiesInText() };
}
virtual TextWithEntities clipboardText() const {
return { QString(), EntitiesInText() };
}
virtual void setViewsCount(int32 count) {
}
@ -302,5 +305,8 @@ private:
ClickHandlerPtr goToMessageClickHandler(
not_null<PeerData*> peer,
MsgId msgId);
ClickHandlerPtr goToMessageClickHandler(not_null<HistoryItem*> item);
MsgId msgId,
FullMsgId returnToId = FullMsgId());
ClickHandlerPtr goToMessageClickHandler(
not_null<HistoryItem*> item,
FullMsgId returnToId = FullMsgId());

View File

@ -125,7 +125,9 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const {
}
}
bool HistoryMessageReply::updateData(HistoryMessage *holder, bool force) {
bool HistoryMessageReply::updateData(
not_null<HistoryMessage*> holder,
bool force) {
if (!force) {
if (replyToMsg || !replyToMsgId) {
return true;
@ -152,7 +154,7 @@ bool HistoryMessageReply::updateData(HistoryMessage *holder, bool force) {
updateName();
replyToLnk = goToMessageClickHandler(replyToMsg);
replyToLnk = goToMessageClickHandler(replyToMsg, holder->fullId());
if (!replyToMsg->Has<HistoryMessageForwarded>()) {
if (auto bot = replyToMsg->viaBot()) {
replyToVia = std::make_unique<HistoryMessageVia>();
@ -168,7 +170,7 @@ bool HistoryMessageReply::updateData(HistoryMessage *holder, bool force) {
return (replyToMsg || !replyToMsgId);
}
void HistoryMessageReply::clearData(HistoryMessage *holder) {
void HistoryMessageReply::clearData(not_null<HistoryMessage*> holder) {
replyToVia = nullptr;
if (replyToMsg) {
App::historyUnregDependency(holder, replyToMsg);

View File

@ -84,10 +84,10 @@ struct HistoryMessageReply : public RuntimeComponent<HistoryMessageReply, Histor
Expects(replyToVia == nullptr);
}
bool updateData(HistoryMessage *holder, bool force = false);
bool updateData(not_null<HistoryMessage*> holder, bool force = false);
// Must be called before destructor.
void clearData(HistoryMessage *holder);
void clearData(not_null<HistoryMessage*> holder);
bool isNameUpdated() const;
void updateName() const;

View File

@ -0,0 +1,106 @@
/*
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/history_item_text.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "data/data_media_types.h"
#include "data/data_web_page.h"
#include "lang/lang_keys.h"
#include "ui/text_options.h"
TextWithEntities WrapAsReply(
TextWithEntities &&text,
not_null<HistoryItem*> to) {
const auto name = to->author()->name;
auto result = TextWithEntities();
result.text.reserve(
lang(lng_in_reply_to).size()
+ name.size()
+ 4
+ text.text.size());
result.text.append('['
).append(lang(lng_in_reply_to)
).append(' '
).append(name
).append(qsl("]\n")
);
TextUtilities::Append(result, std::move(text));
return result;
}
TextWithEntities WrapAsForwarded(
TextWithEntities &&text,
not_null<HistoryMessageForwarded*> forwarded) {
auto info = forwarded->text.originalTextWithEntities(
AllTextSelection,
ExpandLinksAll);
auto result = TextWithEntities();
result.text.reserve(
info.text.size()
+ 4
+ text.text.size());
result.entities.reserve(
info.entities.size()
+ text.entities.size());
result.text.append('[');
TextUtilities::Append(result, std::move(info));
result.text.append(qsl("]\n"));
TextUtilities::Append(result, std::move(text));
return result;
}
TextWithEntities HistoryItemText(not_null<HistoryItem*> item) {
const auto media = item->media();
auto textResult = item->clipboardText();
auto mediaResult = media ? media->clipboardText() : TextWithEntities();
auto logEntryOriginalResult = [&] {
const auto entry = item->Get<HistoryMessageLogEntryOriginal>();
if (!entry) {
return TextWithEntities();
}
const auto title = TextUtilities::SingleLine(entry->page->title.isEmpty()
? entry->page->author
: entry->page->title);
auto titleResult = TextUtilities::ParseEntities(
title,
Ui::WebpageTextTitleOptions().flags);
auto descriptionResult = entry->page->description;
if (titleResult.text.isEmpty()) {
return descriptionResult;
} else if (descriptionResult.text.isEmpty()) {
return titleResult;
}
titleResult.text += '\n';
TextUtilities::Append(titleResult, std::move(descriptionResult));
return titleResult;
}();
auto result = textResult;
if (result.text.isEmpty()) {
result = std::move(mediaResult);
} else if (!mediaResult.text.isEmpty()) {
result.text += qstr("\n\n");
TextUtilities::Append(result, std::move(mediaResult));
}
if (result.text.isEmpty()) {
result = std::move(logEntryOriginalResult);
} else if (!logEntryOriginalResult.text.isEmpty()) {
result.text += qstr("\n\n");
TextUtilities::Append(result, std::move(logEntryOriginalResult));
}
if (const auto reply = item->Get<HistoryMessageReply>()) {
if (const auto message = reply->replyToMsg) {
result = WrapAsReply(std::move(result), message);
}
}
if (const auto forwarded = item->Get<HistoryMessageForwarded>()) {
result = WrapAsForwarded(std::move(result), forwarded);
}
return result;
}

View File

@ -0,0 +1,10 @@
/*
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
*/
#pragma once
TextWithEntities HistoryItemText(not_null<HistoryItem*> item);

View File

@ -59,7 +59,9 @@ public:
virtual HistoryMediaType type() const = 0;
virtual TextWithEntities selectedText(TextSelection selection) const = 0;
virtual TextWithEntities selectedText(TextSelection selection) const {
return TextWithEntities();
}
bool hasPoint(QPoint point) const {
return QRect(0, 0, width(), height()).contains(point);

View File

@ -261,15 +261,17 @@ TextSelection HistoryGroupedMedia::adjustSelection(
TextWithEntities HistoryGroupedMedia::selectedText(
TextSelection selection) const {
if (!IsSubGroupSelection(selection)) {
return WithCaptionSelectedText(
lang(lng_in_dlg_album),
_caption,
selection);
} else if (IsGroupItemSelection(selection, int(_parts.size()) - 1)) {
return main()->selectedText(FullSelection);
}
return TextWithEntities();
return _caption.originalTextWithEntities(selection, ExpandLinksAll);
// #TODO group select
//if (!IsSubGroupSelection(selection)) {
// return WithCaptionSelectedText(
// lang(lng_in_dlg_album),
// _caption,
// selection);
//} else if (IsGroupItemSelection(selection, int(_parts.size()) - 1)) {
// return main()->selectedText(FullSelection);
//}
//return TextWithEntities();
}
void HistoryGroupedMedia::clickHandlerActiveChanged(

View File

@ -94,27 +94,6 @@ std::unique_ptr<HistoryMedia> CreateAttach(
} // namespace
TextWithEntities WithCaptionSelectedText(
const QString &attachType,
const Text &caption,
TextSelection selection) {
if (selection != FullSelection) {
return caption.originalTextWithEntities(selection, ExpandLinksAll);
}
TextWithEntities result, original;
if (!caption.isEmpty()) {
original = caption.originalTextWithEntities(AllTextSelection, ExpandLinksAll);
}
result.text.reserve(5 + attachType.size() + original.text.size());
result.text.append(qstr("[ ")).append(attachType).append(qstr(" ]"));
if (!caption.isEmpty()) {
result.text.append(qstr("\n"));
TextUtilities::Append(result, std::move(original));
}
return result;
}
void HistoryFileMedia::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
if (p == _savel || p == _cancell) {
if (active && !dataLoaded()) {
@ -682,10 +661,7 @@ void HistoryPhoto::validateGroupedCache(
}
TextWithEntities HistoryPhoto::selectedText(TextSelection selection) const {
return WithCaptionSelectedText(
lang(lng_in_dlg_photo),
_caption,
selection);
return _caption.originalTextWithEntities(selection, ExpandLinksAll);
}
bool HistoryPhoto::needsBubble() const {
@ -1122,10 +1098,7 @@ void HistoryVideo::setStatusSize(int newSize) const {
}
TextWithEntities HistoryVideo::selectedText(TextSelection selection) const {
return WithCaptionSelectedText(
lang(lng_in_dlg_video),
_caption,
selection);
return _caption.originalTextWithEntities(selection, ExpandLinksAll);
}
bool HistoryVideo::needsBubble() const {
@ -1715,38 +1688,11 @@ bool HistoryDocument::hasTextForCopy() const {
}
TextWithEntities HistoryDocument::selectedText(TextSelection selection) const {
TextWithEntities result;
buildStringRepresentation([&result, selection](const QString &type, const QString &fileName, const Text &caption) {
auto fullType = type;
if (!fileName.isEmpty()) {
fullType.append(qstr(" : ")).append(fileName);
}
result = WithCaptionSelectedText(fullType, caption, selection);
});
return result;
}
template <typename Callback>
void HistoryDocument::buildStringRepresentation(Callback callback) const {
const Text emptyCaption;
const Text *caption = &emptyCaption;
if (auto captioned = Get<HistoryDocumentCaptioned>()) {
caption = &captioned->_caption;
if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
const auto &caption = captioned->_caption;
return caption.originalTextWithEntities(selection, ExpandLinksAll);
}
QString attachType = lang(lng_in_dlg_file);
if (Has<HistoryDocumentVoice>()) {
attachType = lang(lng_in_dlg_audio);
} else if (_data->isAudioFile()) {
attachType = lang(lng_in_dlg_audio_file);
}
QString attachFileName;
if (auto named = Get<HistoryDocumentNamed>()) {
if (!named->_name.isEmpty()) {
attachFileName = named->_name;
}
}
return callback(attachType, attachFileName, *caption);
return TextWithEntities();
}
void HistoryDocument::setStatusSize(int newSize, qint64 realDuration) const {
@ -2521,7 +2467,7 @@ HistoryTextState HistoryGif::getState(QPoint point, HistoryStateRequest request)
}
TextWithEntities HistoryGif::selectedText(TextSelection selection) const {
return WithCaptionSelectedText(mediaTypeString(), _caption, selection);
return _caption.originalTextWithEntities(selection, ExpandLinksAll);
}
bool HistoryGif::needsBubble() const {
@ -2979,17 +2925,6 @@ HistoryTextState HistorySticker::getState(QPoint point, HistoryStateRequest requ
return result;
}
QString HistorySticker::toString() const {
return _emoji.isEmpty() ? lang(lng_in_dlg_sticker) : lng_in_dlg_sticker_emoji(lt_emoji, _emoji);
}
TextWithEntities HistorySticker::selectedText(TextSelection selection) const {
if (selection != FullSelection) {
return TextWithEntities();
}
return { qsl("[ ") + toString() + qsl(" ]"), EntitiesInText() };
}
ImagePtr HistorySticker::replyPreview() {
return _data->makeReplyPreview();
}
@ -3197,13 +3132,6 @@ HistoryTextState HistoryContact::getState(QPoint point, HistoryStateRequest requ
return result;
}
TextWithEntities HistoryContact::selectedText(TextSelection selection) const {
if (selection != FullSelection) {
return TextWithEntities();
}
return { qsl("[ ") + lang(lng_in_dlg_contact) + qsl(" ]\n") + _name.originalText() + '\n' + _phone, EntitiesInText() };
}
HistoryCall::HistoryCall(
not_null<Element*> parent,
not_null<Data::Call*> call)
@ -3291,13 +3219,6 @@ HistoryTextState HistoryCall::getState(QPoint point, HistoryStateRequest request
return result;
}
TextWithEntities HistoryCall::selectedText(TextSelection selection) const {
if (selection != FullSelection) {
return TextWithEntities();
}
return { qsl("[ ") + _text + qsl(" ]"), EntitiesInText() };
}
namespace {
int articleThumbWidth(PhotoData *thumb, int height) {
@ -3841,11 +3762,12 @@ bool HistoryWebPage::isDisplayed() const {
}
TextWithEntities HistoryWebPage::selectedText(TextSelection selection) const {
if (selection == FullSelection && !isLogEntryOriginal()) {
return TextWithEntities();
}
auto titleResult = _title.originalTextWithEntities((selection == FullSelection) ? AllTextSelection : selection, ExpandLinksAll);
auto descriptionResult = _description.originalTextWithEntities(toDescriptionSelection((selection == FullSelection) ? AllTextSelection : selection), ExpandLinksAll);
auto titleResult = _title.originalTextWithEntities(
selection,
ExpandLinksAll);
auto descriptionResult = _description.originalTextWithEntities(
toDescriptionSelection(selection),
ExpandLinksAll);
if (titleResult.text.isEmpty()) {
return descriptionResult;
} else if (descriptionResult.text.isEmpty()) {
@ -4237,11 +4159,12 @@ void HistoryGame::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pres
}
TextWithEntities HistoryGame::selectedText(TextSelection selection) const {
if (selection == FullSelection) {
return TextWithEntities();
}
auto titleResult = _title.originalTextWithEntities(selection, ExpandLinksAll);
auto descriptionResult = _description.originalTextWithEntities(toDescriptionSelection(selection), ExpandLinksAll);
auto titleResult = _title.originalTextWithEntities(
selection,
ExpandLinksAll);
auto descriptionResult = _description.originalTextWithEntities(
toDescriptionSelection(selection),
ExpandLinksAll);
if (titleResult.text.isEmpty()) {
return descriptionResult;
} else if (descriptionResult.text.isEmpty()) {
@ -4637,11 +4560,12 @@ void HistoryInvoice::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool p
}
TextWithEntities HistoryInvoice::selectedText(TextSelection selection) const {
if (selection == FullSelection) {
return TextWithEntities();
}
auto titleResult = _title.originalTextWithEntities(selection, ExpandLinksAll);
auto descriptionResult = _description.originalTextWithEntities(toDescriptionSelection(selection), ExpandLinksAll);
auto titleResult = _title.originalTextWithEntities(
selection,
ExpandLinksAll);
auto descriptionResult = _description.originalTextWithEntities(
toDescriptionSelection(selection),
ExpandLinksAll);
if (titleResult.text.isEmpty()) {
return descriptionResult;
} else if (descriptionResult.text.isEmpty()) {
@ -4685,12 +4609,11 @@ HistoryLocation::HistoryLocation(
Ui::WebpageTextTitleOptions());
}
if (!description.isEmpty()) {
auto marked = TextWithEntities { TextUtilities::Clean(description) };
auto parseFlags = TextParseLinks | TextParseMultiline | TextParseRichText;
TextUtilities::ParseEntities(marked, parseFlags);
_description.setMarkedText(
st::webPageDescriptionStyle,
marked,
TextUtilities::ParseEntities(
TextUtilities::Clean(description),
TextParseLinks | TextParseMultiline | TextParseRichText),
Ui::WebpageTextDescriptionOptions());
}
}
@ -4924,17 +4847,6 @@ TextSelection HistoryLocation::adjustSelection(TextSelection selection, TextSele
}
TextWithEntities HistoryLocation::selectedText(TextSelection selection) const {
if (selection == FullSelection) {
TextWithEntities result = { qsl("[ ") + lang(lng_maps_point) + qsl(" ]\n"), EntitiesInText() };
auto info = selectedText(AllTextSelection);
if (!info.text.isEmpty()) {
TextUtilities::Append(result, std::move(info));
result.text.append('\n');
}
result.text += _link->dragText();
return result;
}
auto titleResult = _title.originalTextWithEntities(selection);
auto descriptionResult = _description.originalTextWithEntities(toDescriptionSelection(selection));
if (titleResult.text.isEmpty()) {

View File

@ -41,11 +41,6 @@ namespace Ui {
class EmptyUserpic;
} // namespace Ui
TextWithEntities WithCaptionSelectedText(
const QString &attachType,
const Text &caption,
TextSelection selection);
class HistoryFileMedia : public HistoryMedia {
public:
using HistoryMedia::HistoryMedia;
@ -390,11 +385,6 @@ private:
void setStatusSize(int newSize, qint64 realDuration = 0) const;
bool updateStatusText() const; // returns showPause
// Callback is a void(const QString &, const QString &, const Text &) functor.
// It will be called as callback(attachType, attachFileName, attachCaption).
template <typename Callback>
void buildStringRepresentation(Callback callback) const;
not_null<DocumentData*> _data;
};
@ -523,8 +513,6 @@ public:
return true;
}
TextWithEntities selectedText(TextSelection selection) const override;
DocumentData *getDocument() const override {
return _data;
}
@ -550,7 +538,6 @@ private:
int additionalWidth(const HistoryMessageVia *via, const HistoryMessageReply *reply) const;
int additionalWidth() const;
QString toString() const;
int _pixw = 1;
int _pixh = 1;
@ -584,8 +571,6 @@ public:
return true;
}
TextWithEntities selectedText(TextSelection selection) const override;
bool needsBubble() const override {
return true;
}
@ -643,8 +628,6 @@ public:
return false;
}
TextWithEntities selectedText(TextSelection selection) const override;
bool needsBubble() const override {
return true;
}

View File

@ -1017,6 +1017,13 @@ TextWithEntities HistoryMessage::originalText() const {
return _text.originalTextWithEntities();
}
TextWithEntities HistoryMessage::clipboardText() const {
if (emptyText()) {
return { QString(), EntitiesInText() };
}
return _text.originalTextWithEntities(AllTextSelection, ExpandLinksAll);
}
bool HistoryMessage::textHasLinks() const {
return emptyText() ? false : _text.hasLinks();
}

View File

@ -121,6 +121,7 @@ public:
void setText(const TextWithEntities &textWithEntities) override;
TextWithEntities originalText() const override;
TextWithEntities clipboardText() const override;
bool textHasLinks() const override;
int viewsCount() const override;

View File

@ -400,7 +400,10 @@ HistoryHider::~HistoryHider() {
parent()->noHider(this);
}
HistoryWidget::HistoryWidget(QWidget *parent, not_null<Window::Controller*> controller) : Window::AbstractSectionWidget(parent, controller)
HistoryWidget::HistoryWidget(
QWidget *parent,
not_null<Window::Controller*> controller)
: Window::AbstractSectionWidget(parent, controller)
, _fieldBarCancel(this, st::historyReplyCancel)
, _topBar(this, controller)
, _scroll(this, st::historyScroll, false)
@ -672,9 +675,21 @@ HistoryWidget::HistoryWidget(QWidget *parent, not_null<Window::Controller*> cont
}
}, lifetime());
_topBar->membersShowAreaActive(
) | rpl::start_with_next([this](bool active) {
) | rpl::start_with_next([=](bool active) {
setMembersShowAreaActive(active);
}, _topBar->lifetime());
_topBar->forwardSelectionRequest(
) | rpl::start_with_next([=] {
forwardSelected();
}, _topBar->lifetime());
_topBar->deleteSelectionRequest(
) | rpl::start_with_next([=] {
confirmDeleteSelected();
}, _topBar->lifetime());
_topBar->clearSelectionRequest(
) | rpl::start_with_next([=] {
clearSelected();
}, _topBar->lifetime());
Auth().api().sendActions(
) | rpl::start_with_next([this](const ApiWrap::SendOptions &options) {
@ -1675,11 +1690,16 @@ void HistoryWidget::showHistory(const PeerId &peerId, MsgId showAtMsgId, bool re
Auth().data().stopAutoplayAnimations();
}
clearReplyReturns();
clearAllLoadRequests();
if (_history) {
if (App::main()) App::main()->saveDraftToCloud();
if (Ui::InFocusChain(_list)) {
// Removing focus from list clears selected and updates top bar.
setFocus();
}
if (App::main()) {
App::main()->saveDraftToCloud();
}
if (_migrated) {
_migrated->clearLocalDraft(); // use migrated draft only once
_migrated->clearEditDraft();
@ -1710,7 +1730,7 @@ void HistoryWidget::showHistory(const PeerId &peerId, MsgId showAtMsgId, bool re
_fieldBarCancel->hide();
_membersDropdownShowTimer.stop();
_scroll->takeWidget<HistoryInner>().destroyDelayed();
_scroll->takeWidget<HistoryInner>().destroy();
_list = nullptr;
clearInlineBot();
@ -3972,7 +3992,9 @@ void HistoryWidget::onFieldResize() {
}
void HistoryWidget::onFieldFocused() {
if (_list) _list->clearSelectedItems(true);
if (_list) {
_list->clearSelected(true);
}
}
void HistoryWidget::onCheckFieldAutocomplete() {
@ -6178,25 +6200,37 @@ void HistoryWidget::handlePeerUpdate() {
}
}
void HistoryWidget::onForwardSelected() {
if (!_list) return;
auto weak = make_weak(this);
void HistoryWidget::forwardSelected() {
if (!_list) {
return;
}
const auto weak = make_weak(this);
Window::ShowForwardMessagesBox(getSelectedItems(), [=] {
if (weak) {
weak->onClearSelected();
if (const auto strong = weak.data()) {
strong->clearSelected();
}
});
}
void HistoryWidget::confirmDeleteSelectedItems() {
void HistoryWidget::confirmDeleteSelected() {
if (!_list) return;
App::main()->deleteLayer(_list->getSelectedItems());
auto items = _list->getSelectedItems();
if (items.empty()) {
return;
}
const auto weak = make_weak(this);
const auto box = Ui::show(Box<DeleteMessagesBox>(std::move(items)));
box->setDeleteConfirmedCallback([=] {
if (const auto strong = weak.data()) {
strong->clearSelected();
}
});
}
void HistoryWidget::onListEscapePressed() {
if (_nonEmptySelection && _list) {
onClearSelected();
clearSelected();
} else {
onCancel();
}
@ -6208,8 +6242,10 @@ void HistoryWidget::onListEnterPressed() {
}
}
void HistoryWidget::onClearSelected() {
if (_list) _list->clearSelectedItems();
void HistoryWidget::clearSelected() {
if (_list) {
_list->clearSelected();
}
}
HistoryItem *HistoryWidget::getItemFromHistoryOrMigrated(MsgId genericMsgId) const {

View File

@ -324,9 +324,9 @@ public:
void grapWithoutTopBarShadow();
void grabFinish() override;
bool isItemVisible(HistoryItem *item);
void confirmDeleteSelectedItems();
void forwardSelected();
void confirmDeleteSelected();
void clearSelected();
// Float player interface.
bool wheelEventFromFloatPlayer(QEvent *e) override;
@ -415,9 +415,6 @@ public slots:
void onCheckFieldAutocomplete();
void onScrollTimer();
void onForwardSelected();
void onClearSelected();
void onDraftSaveDelayed();
void onDraftSave(bool delayed = false);
void onCloudDraftSave();

View File

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_list_widget.h"
#include "history/history_item.h"
#include "history/history_item_text.h"
#include "history/history_media_types.h"
#include "ui/widgets/popup_menu.h"
#include "chat_helpers/message_field.h"
@ -218,7 +219,9 @@ base::unique_qptr<Ui::PopupMenu> FillContextMenu(
}
if (!link && (view->hasVisibleText() || mediaHasTextForCopy)) {
result->addAction(lang(lng_context_copy_text), [=] {
SetClipboardWithEntities(list->getItemText(itemId));
if (const auto item = App::histItemById(itemId)) {
SetClipboardWithEntities(HistoryItemText(item));
}
});
}
}

View File

@ -186,7 +186,7 @@ int Element::marginBottom() const {
}
bool Element::isUnderCursor() const {
return (App::hoveredItem() == this);
return _delegate->elementUnderCursor(this);
}
void Element::setPendingResize() {

View File

@ -37,6 +37,7 @@ public:
not_null<HistoryMessage*> message) = 0;
virtual std::unique_ptr<Element> elementCreate(
not_null<HistoryService*> message) = 0;
virtual bool elementUnderCursor(not_null<const Element*> view) = 0;
virtual void elementAnimationAutoplayAsync(
not_null<const Element*> element) = 0;

View File

@ -27,6 +27,18 @@ namespace HistoryView {
enum class Context : char;
struct SelectedItem {
explicit SelectedItem(FullMsgId msgId) : msgId(msgId) {
}
FullMsgId msgId;
bool canDelete = false;
bool canForward = false;
};
using SelectedItems = std::vector<SelectedItem>;
class ListDelegate {
public:
virtual Context listContext() = 0;
@ -36,9 +48,25 @@ public:
Data::MessagePosition aroundId,
int limitBefore,
int limitAfter) = 0;
virtual bool listAllowsMultiSelect() = 0;
virtual bool listIsLessInOrder(
not_null<HistoryItem*> first,
not_null<HistoryItem*> second) = 0;
virtual void listSelectionChanged(SelectedItems &&items) = 0;
};
struct SelectionData {
bool canDelete = false;
bool canForward = false;
};
using SelectedMap = base::flat_map<
FullMsgId,
SelectionData,
std::less<>>;
class ListMemento {
public:
struct ScrollTopState {
@ -100,7 +128,8 @@ public:
void restoreState(not_null<ListMemento*> memento);
TextWithEntities getSelectedText() const;
TextWithEntities getItemText(FullMsgId itemId) const;
MessageIdsList getSelectedItems() const;
void cancelSelection();
// AbstractTooltipShower interface
QString tooltipText() const override;
@ -112,6 +141,7 @@ public:
not_null<HistoryMessage*> message) override;
std::unique_ptr<Element> elementCreate(
not_null<HistoryService*> message) override;
bool elementUnderCursor(not_null<const Element*> view) override;
void elementAnimationAutoplayAsync(
not_null<const Element*> view) override;
@ -136,6 +166,21 @@ protected:
int resizeGetHeight(int newWidth) override;
private:
struct CursorState {
FullMsgId itemId;
int height = 0;
QPoint cursor;
bool inside = false;
inline bool operator==(const CursorState &other) const {
return (itemId == other.itemId)
&& (cursor == other.cursor);
}
inline bool operator!=(const CursorState &other) const {
return !(*this == other);
}
};
enum class Direction {
Up,
Down,
@ -144,12 +189,18 @@ private:
None,
PrepareDrag,
Dragging,
PrepareSelect,
Selecting,
};
enum class EnumItemsDirection {
TopToBottom,
BottomToTop,
};
enum class DragSelectAction {
None,
Selecting,
Deselecting,
};
using ScrollTopState = ListMemento::ScrollTopState;
void refreshViewer();
@ -159,16 +210,23 @@ private:
void saveScrollState();
void restoreScrollState();
Element *viewForItem(FullMsgId itemId) const;
Element *viewForItem(const HistoryItem *item) const;
not_null<Element*> enforceViewForItem(not_null<HistoryItem*> item);
void mouseActionStart(const QPoint &screenPos, Qt::MouseButton button);
void mouseActionUpdate(const QPoint &screenPos);
void mouseActionFinish(const QPoint &screenPos, Qt::MouseButton button);
void mouseActionStart(
const QPoint &globalPosition,
Qt::MouseButton button);
void mouseActionUpdate(const QPoint &globalPosition);
void mouseActionUpdate();
void mouseActionFinish(
const QPoint &globalPosition,
Qt::MouseButton button);
void mouseActionCancel();
void updateSelected();
void performDrag();
style::cursor computeMouseCursor() const;
int itemTop(not_null<const Element*> view) const;
void repaintItem(FullMsgId itemId);
void repaintItem(const Element *view);
void resizeItem(not_null<Element*> view);
void refreshItem(not_null<const Element*> view);
@ -176,7 +234,6 @@ private:
QPoint mapPointToItem(QPoint point, const Element *view) const;
void showContextMenu(QContextMenuEvent *e, bool showFromTouch = false);
void showStickerPackInfo(not_null<DocumentData*> document);
not_null<Element*> findItemByY(int y) const;
Element *strictFindItemByY(int y) const;
@ -197,6 +254,38 @@ private:
void scrollDateCheck();
void scrollDateHideByTimer();
void trySwitchToWordSelection();
void switchToWordSelection();
void validateTrippleClickStartTime();
SelectedItems collectSelectedItems() const;
MessageIdsList collectSelectedIds() const;
void pushSelectedItems();
void removeItemSelection(
const SelectedMap::const_iterator &i);
bool hasSelectedText() const;
bool hasSelectedItems() const;
void clearTextSelection();
void clearSelected();
void setTextSelection(
not_null<Element*> view,
TextSelection selection);
bool applyItemSelection(SelectedMap &applyTo, FullMsgId itemId) const;
void toggleItemSelection(FullMsgId itemId);
SelectedMap::iterator itemUnderPressSelection();
SelectedMap::const_iterator itemUnderPressSelection() const;
bool isItemUnderPressSelected() const;
bool requiredToStartDragging(not_null<Element*> view) const;
bool isPressInSelectedText(HistoryTextState state) const;
void updateDragSelection();
void clearDragSelection();
void applyDragSelection();
void applyDragSelection(SelectedMap &applyTo) const;
TextSelection itemRenderSelection(
not_null<const Element*> view) const;
TextSelection computeRenderSelection(
not_null<const SelectedMap*> selected,
not_null<const Element*> view) const;
// This function finds all history items that are displayed and calls template method
// for each found message (in given direction) in the passed history with passed top offset.
//
@ -231,7 +320,10 @@ private:
int _idsLimit = kMinimalIdsLimit;
Data::MessagesSlice _slice;
std::vector<not_null<Element*>> _items;
std::map<not_null<HistoryItem*>, std::unique_ptr<Element>, std::less<>> _views;
std::map<
not_null<HistoryItem*>,
std::unique_ptr<Element>,
std::less<>> _views;
int _itemsTop = 0;
int _itemsWidth = 0;
int _itemsHeight = 0;
@ -252,22 +344,29 @@ private:
MouseAction _mouseAction = MouseAction::None;
TextSelectType _mouseSelectType = TextSelectType::Letters;
QPoint _dragStartPosition;
QPoint _mousePosition;
Element *_mouseActionItem = nullptr;
CursorState _overState;
CursorState _pressState;
Element *_overItem = nullptr;
HistoryCursorState _mouseCursorState = HistoryDefaultCursorState;
uint16 _mouseTextSymbol = 0;
bool _pressWasInactive = false;
Element *_selectedItem = nullptr;
TextSelection _selectedText;
bool _wasSelectedText = false; // was some text selected in current drag action
bool _selectEnabled = false;
HistoryItem *_selectedTextItem = nullptr;
TextSelection _selectedTextRange;
TextWithEntities _selectedText;
SelectedMap _selected;
base::flat_set<FullMsgId> _dragSelected;
DragSelectAction _dragSelectAction = DragSelectAction::None;
// Was some text selected in current drag action.
bool _wasSelectedText = false;
Qt::CursorShape _cursor = style::cur_default;
base::unique_qptr<Ui::PopupMenu> _menu;
QPoint _trippleClickPoint;
base::Timer _trippleClickTimer;
TimeMs _trippleClickStartTime = 0;
rpl::lifetime _viewerLifetime;

View File

@ -970,13 +970,8 @@ TextWithEntities Message::selectedText(TextSelection selection) const {
const auto media = this->media();
TextWithEntities logEntryOriginalResult;
const auto textSelection = (selection == FullSelection)
? AllTextSelection
: IsSubGroupSelection(selection)
? TextSelection(0, 0)
: selection;
auto textResult = item->_text.originalTextWithEntities(
textSelection,
selection,
ExpandLinksAll);
auto skipped = skipTextSelection(selection);
auto mediaDisplayed = (media && media->isDisplayed());
@ -1002,28 +997,6 @@ TextWithEntities Message::selectedText(TextSelection selection) const {
result.text += qstr("\n\n");
TextUtilities::Append(result, std::move(logEntryOriginalResult));
}
if (auto reply = item->Get<HistoryMessageReply>()) {
if (selection == FullSelection && reply->replyToMsg) {
TextWithEntities wrapped;
wrapped.text.reserve(lang(lng_in_reply_to).size() + reply->replyToMsg->author()->name.size() + 4 + result.text.size());
wrapped.text.append('[').append(lang(lng_in_reply_to)).append(' ').append(reply->replyToMsg->author()->name).append(qsl("]\n"));
TextUtilities::Append(wrapped, std::move(result));
result = wrapped;
}
}
if (auto forwarded = item->Get<HistoryMessageForwarded>()) {
if (selection == FullSelection) {
auto fwdinfo = forwarded->text.originalTextWithEntities(AllTextSelection, ExpandLinksAll);
auto wrapped = TextWithEntities();
wrapped.text.reserve(fwdinfo.text.size() + 4 + result.text.size());
wrapped.entities.reserve(fwdinfo.entities.size() + result.entities.size());
wrapped.text.append('[');
TextUtilities::Append(wrapped, std::move(fwdinfo));
wrapped.text.append(qsl("]\n"));
TextUtilities::Append(wrapped, std::move(result));
result = wrapped;
}
}
return result;
}

View File

@ -508,8 +508,7 @@ void Service::updatePressed(QPoint point) {
}
TextWithEntities Service::selectedText(TextSelection selection) const {
return message()->_text.originalTextWithEntities(
(selection == FullSelection) ? AllTextSelection : selection);
return message()->_text.originalTextWithEntities(selection);
}
TextSelection Service::adjustSelection(

View File

@ -43,7 +43,7 @@ TopBarWidget::TopBarWidget(
not_null<Window::Controller*> controller)
: RpWidget(parent)
, _controller(controller)
, _clearSelection(this, langFactory(lng_selected_clear), st::topBarClearButton)
, _clear(this, langFactory(lng_selected_clear), st::topBarClearButton)
, _forward(this, langFactory(lng_selected_forward), st::defaultActiveButton)
, _delete(this, langFactory(lng_selected_delete), st::defaultActiveButton)
, _back(this, st::historyTopBarBack)
@ -55,11 +55,11 @@ TopBarWidget::TopBarWidget(
subscribe(Lang::Current().updated(), [this] { refreshLang(); });
setAttribute(Qt::WA_OpaquePaintEvent);
_forward->setClickedCallback([this] { onForwardSelection(); });
_forward->setClickedCallback([this] { _forwardSelection.fire({}); });
_forward->setWidthChangedCallback([this] { updateControlsGeometry(); });
_delete->setClickedCallback([this] { onDeleteSelection(); });
_delete->setClickedCallback([this] { _deleteSelection.fire({}); });
_delete->setWidthChangedCallback([this] { updateControlsGeometry(); });
_clearSelection->setClickedCallback([this] { onClearSelection(); });
_clear->setClickedCallback([this] { _clearSelection.fire({}); });
_call->setClickedCallback([this] { onCall(); });
_search->setClickedCallback([this] { onSearch(); });
_menuToggle->setClickedCallback([this] { showMenu(); });
@ -132,18 +132,6 @@ void TopBarWidget::refreshLang() {
InvokeQueued(this, [this] { updateControlsGeometry(); });
}
void TopBarWidget::onForwardSelection() {
if (App::main()) App::main()->forwardSelectedItems();
}
void TopBarWidget::onDeleteSelection() {
if (App::main()) App::main()->confirmDeleteSelectedItems();
}
void TopBarWidget::onClearSelection() {
if (App::main()) App::main()->clearSelectedItems();
}
void TopBarWidget::onSearch() {
if (_activeChat) {
App::main()->searchInChat(_activeChat);
@ -399,7 +387,7 @@ void TopBarWidget::updateControlsGeometry() {
auto selectedButtonsTop = countSelectedButtonsTop(_selectedShown.current(hasSelected ? 1. : 0.));
auto otherButtonsTop = selectedButtonsTop + st::topBarHeight;
auto buttonsLeft = st::topBarActionSkip + (Adaptive::OneColumn() ? 0 : st::lineWidth);
auto buttonsWidth = _forward->contentWidth() + _delete->contentWidth() + _clearSelection->width();
auto buttonsWidth = _forward->contentWidth() + _delete->contentWidth() + _clear->width();
buttonsWidth += buttonsLeft + st::topBarActionSkip * 3;
auto widthLeft = qMin(width() - buttonsWidth, -2 * st::defaultActiveButton.width);
@ -414,7 +402,7 @@ void TopBarWidget::updateControlsGeometry() {
}
_delete->moveToLeft(buttonsLeft, selectedButtonsTop);
_clearSelection->moveToRight(st::topBarActionSkip, selectedButtonsTop);
_clear->moveToRight(st::topBarActionSkip, selectedButtonsTop);
if (_unreadBadge) {
_unreadBadge->setGeometryToLeft(
@ -469,7 +457,7 @@ void TopBarWidget::updateControlsVisibility() {
hideChildren();
return;
}
_clearSelection->show();
_clear->show();
_delete->setVisible(_canDelete);
_forward->setVisible(_canForward);

View File

@ -48,6 +48,16 @@ public:
void setActiveChat(Dialogs::Key chat);
rpl::producer<> forwardSelectionRequest() const {
return _forwardSelection.events();
}
rpl::producer<> deleteSelectionRequest() const {
return _deleteSelection.events();
}
rpl::producer<> clearSelectionRequest() const {
return _clearSelection.events();
}
protected:
void paintEvent(QPaintEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
@ -62,9 +72,6 @@ private:
void selectedShowCallback();
void updateInfoToggleActive();
void onForwardSelection();
void onDeleteSelection();
void onClearSelection();
void onCall();
void onSearch();
void showMenu();
@ -95,7 +102,7 @@ private:
Animation _selectedShown;
object_ptr<Ui::RoundButton> _clearSelection;
object_ptr<Ui::RoundButton> _clear;
object_ptr<Ui::RoundButton> _forward, _delete;
object_ptr<Ui::IconButton> _back;
@ -121,6 +128,10 @@ private:
int _unreadCounterSubscription = 0;
base::Timer _onlineUpdater;
rpl::event_stream<> _forwardSelection;
rpl::event_stream<> _deleteSelection;
rpl::event_stream<> _clearSelection;
};
} // namespace HistoryView

View File

@ -509,11 +509,13 @@ void TopBar::performForward() {
_cancelSelectionClicks.fire({});
return;
}
Window::ShowForwardMessagesBox(std::move(items), [weak = make_weak(this)]{
if (weak) {
weak->_cancelSelectionClicks.fire({});
}
});
Window::ShowForwardMessagesBox(
std::move(items),
[weak = make_weak(this)] {
if (weak) {
weak->_cancelSelectionClicks.fire({});
}
});
}
void TopBar::performDelete() {
@ -521,7 +523,12 @@ void TopBar::performDelete() {
if (items.empty()) {
_cancelSelectionClicks.fire({});
} else {
Ui::show(Box<DeleteMessagesBox>(std::move(items)));
const auto box = Ui::show(Box<DeleteMessagesBox>(std::move(items)));
box->setDeleteConfirmedCallback([weak = make_weak(this)] {
if (weak) {
weak->_cancelSelectionClicks.fire({});
}
});
}
}

View File

@ -1387,7 +1387,14 @@ void ListWidget::forwardItems(MessageIdsList &&items) {
}
void ListWidget::deleteSelected() {
deleteItems(collectSelectedIds());
if (const auto box = deleteItems(collectSelectedIds())) {
const auto weak = make_weak(this);
box->setDeleteConfirmedCallback([=]{
if (const auto strong = weak.data()) {
strong->clearSelected();
}
});
}
}
void ListWidget::deleteItem(UniversalMsgId universalId) {
@ -1396,11 +1403,14 @@ void ListWidget::deleteItem(UniversalMsgId universalId) {
}
}
void ListWidget::deleteItems(MessageIdsList &&items) {
DeleteMessagesBox *ListWidget::deleteItems(MessageIdsList &&items) {
if (!items.empty()) {
const auto box = Ui::show(Box<DeleteMessagesBox>(std::move(items)));
setActionBoxWeak(box.data());
const auto box = Ui::show(
Box<DeleteMessagesBox>(std::move(items))).data();
setActionBoxWeak(box);
return box;
}
return nullptr;
}
void ListWidget::setActionBoxWeak(QPointer<Ui::RpWidget> box) {
@ -1575,7 +1585,7 @@ void ListWidget::enterEventHook(QEvent *e) {
}
void ListWidget::leaveEventHook(QEvent *e) {
if (auto item = _overLayout) {
if (const auto item = _overLayout) {
if (_overState.inside) {
repaintItem(item);
_overState.inside = false;
@ -1596,12 +1606,12 @@ QPoint ListWidget::clampMousePosition(QPoint position) const {
};
}
void ListWidget::mouseActionUpdate(const QPoint &screenPos) {
void ListWidget::mouseActionUpdate(const QPoint &globalPosition) {
if (_sections.empty() || _visibleBottom <= _visibleTop) {
return;
}
_mousePosition = screenPos;
_mousePosition = globalPosition;
auto local = mapFromGlobal(_mousePosition);
auto point = clampMousePosition(local);
@ -1625,14 +1635,14 @@ void ListWidget::mouseActionUpdate(const QPoint &screenPos) {
auto inTextSelection = _overState.inside
&& (_overState.itemId == _pressState.itemId)
&& hasSelectedText();
auto cursorDeltaLength = [&] {
auto cursorDelta = (_overState.cursor - _pressState.cursor);
return cursorDelta.manhattanLength();
};
auto dragStartLength = [] {
return QApplication::startDragDistance();
};
if (_overLayout) {
auto cursorDeltaLength = [&] {
auto cursorDelta = (_overState.cursor - _pressState.cursor);
return cursorDelta.manhattanLength();
};
auto dragStartLength = [] {
return QApplication::startDragDistance();
};
if (_overState.itemId != _pressState.itemId
|| cursorDeltaLength() >= dragStartLength()) {
if (_mouseAction == MouseAction::PrepareDrag) {
@ -1770,9 +1780,13 @@ void ListWidget::clearDragSelection() {
}
}
void ListWidget::mouseActionStart(const QPoint &screenPos, Qt::MouseButton button) {
mouseActionUpdate(screenPos);
if (button != Qt::LeftButton) return;
void ListWidget::mouseActionStart(
const QPoint &globalPosition,
Qt::MouseButton button) {
mouseActionUpdate(globalPosition);
if (button != Qt::LeftButton) {
return;
}
ClickHandler::pressed();
if (_pressState != _overState) {
@ -1799,9 +1813,9 @@ void ListWidget::mouseActionStart(const QPoint &screenPos, Qt::MouseButton butto
}
}
if (_mouseAction == MouseAction::None && pressLayout) {
HistoryTextState dragState;
validateTrippleClickStartTime();
auto startDistance = (screenPos - _trippleClickPoint).manhattanLength();
HistoryTextState dragState;
auto startDistance = (globalPosition - _trippleClickPoint).manhattanLength();
auto validStartPoint = startDistance < QApplication::startDragDistance();
if (_trippleClickStartTime != 0 && validStartPoint) {
HistoryStateRequest request;
@ -1950,8 +1964,10 @@ void ListWidget::performDrag() {
//}
}
void ListWidget::mouseActionFinish(const QPoint &screenPos, Qt::MouseButton button) {
mouseActionUpdate(screenPos);
void ListWidget::mouseActionFinish(
const QPoint &globalPosition,
Qt::MouseButton button) {
mouseActionUpdate(globalPosition);
auto pressState = base::take(_pressState);
repaintItem(pressState.itemId);

View File

@ -12,6 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_shared_media.h"
#include "history/view/history_view_cursor_state.h"
class DeleteMessagesBox;
namespace Ui {
class PopupMenu;
} // namespace Ui
@ -186,7 +188,7 @@ private:
void forwardItems(MessageIdsList &&items);
void deleteSelected();
void deleteItem(UniversalMsgId universalId);
void deleteItems(MessageIdsList &&items);
DeleteMessagesBox *deleteItems(MessageIdsList &&items);
void applyItemSelection(
UniversalMsgId universalId,
TextSelection selection);
@ -233,12 +235,12 @@ private:
QPoint clampMousePosition(QPoint position) const;
void mouseActionStart(
const QPoint &screenPos,
const QPoint &globalPosition,
Qt::MouseButton button);
void mouseActionUpdate(const QPoint &screenPos);
void mouseActionUpdate(const QPoint &globalPosition);
void mouseActionUpdate();
void mouseActionFinish(
const QPoint &screenPos,
const QPoint &globalPosition,
Qt::MouseButton button);
void mouseActionCancel();
void performDrag();

View File

@ -877,12 +877,6 @@ void MainWidget::showSendPathsLayer() {
hiderLayer(object_ptr<HistoryHider>(this));
}
void MainWidget::deleteLayer(MessageIdsList &&items) {
if (!items.empty()) {
Ui::show(Box<DeleteMessagesBox>(std::move(items)));
}
}
void MainWidget::deleteLayer(FullMsgId itemId) {
if (const auto item = App::histItemById(itemId)) {
const auto suggestModerateActions = true;
@ -1349,18 +1343,6 @@ void MainWidget::onCacheBackground() {
_cachedFor = _willCacheFor;
}
void MainWidget::forwardSelectedItems() {
_history->onForwardSelected();
}
void MainWidget::confirmDeleteSelectedItems() {
_history->confirmDeleteSelectedItems();
}
void MainWidget::clearSelectedItems() {
_history->onClearSelected();
}
Dialogs::IndexedList *MainWidget::contactsList() {
return _dialogs->contactsList();
}

View File

@ -168,7 +168,6 @@ public:
void showForwardLayer(MessageIdsList &&items);
void showSendPathsLayer();
void deleteLayer(MessageIdsList &&items);
void deleteLayer(FullMsgId itemId);
void cancelUploadLayer(not_null<HistoryItem*> item);
void shareUrlLayer(const QString &url, const QString &text);
@ -221,10 +220,6 @@ public:
bool sendMessageFail(const RPCError &error);
void forwardSelectedItems();
void confirmDeleteSelectedItems();
void clearSelectedItems();
Dialogs::IndexedList *contactsList();
Dialogs::IndexedList *dialogsList();
Dialogs::IndexedList *contactsNoDialogsList();

View File

@ -1780,6 +1780,13 @@ void ParseMarkdown(
}
}
TextWithEntities ParseEntities(const QString &text, int32 flags) {
const auto rich = ((flags & TextParseRichText) != 0);
auto result = TextWithEntities{ text, EntitiesInText() };
ParseEntities(result, flags, rich);
return result;
}
// Some code is duplicated in flattextarea.cpp!
void ParseEntities(TextWithEntities &result, int32 flags, bool rich) {
if (flags & TextParseMarkdown) { // parse markdown entities (bold, italic, code and pre)

View File

@ -214,6 +214,7 @@ MTPVector<MTPMessageEntity> EntitiesToMTP(const EntitiesInText &entities, Conver
// New entities are added to the ones that are already in result.
// Changes text if (flags & TextParseMarkdown).
TextWithEntities ParseEntities(const QString &text, int32 flags);
void ParseEntities(TextWithEntities &result, int32 flags, bool rich = false);
QString ApplyEntities(const TextWithEntities &text);

View File

@ -349,7 +349,7 @@ void Filler::addChatActions(not_null<ChatData*> chat) {
void Filler::addChannelActions(not_null<ChannelData*> channel) {
auto isGroup = channel->isMegagroup();
if (false && !isGroup) {
if (!isGroup) {
const auto grouped = (channel->feed() != nullptr);
_addAction(
lang(grouped ? lng_feed_ungroup : lng_feed_group),

View File

@ -252,6 +252,8 @@
<(src_loc)/history/history_item.h
<(src_loc)/history/history_item_components.cpp
<(src_loc)/history/history_item_components.h
<(src_loc)/history/history_item_text.cpp
<(src_loc)/history/history_item_text.h
<(src_loc)/history/history_inner_widget.cpp
<(src_loc)/history/history_inner_widget.h
<(src_loc)/history/history_location_manager.cpp