Highlight quotes in replies to albums.

This commit is contained in:
John Preston 2023-10-31 22:37:59 +04:00
parent 6493cb9ed8
commit 0dbb195106
26 changed files with 364 additions and 237 deletions

View File

@ -2106,7 +2106,7 @@ void HistoryInner::toggleFavoriteReaction(not_null<Element*> view) const {
item->toggleReaction(favorite, HistoryItem::ReactionSource::Quick); item->toggleReaction(favorite, HistoryItem::ReactionSource::Quick);
} }
TextWithEntities HistoryInner::selectedQuote( HistoryView::SelectedQuote HistoryInner::selectedQuote(
not_null<HistoryItem*> item) const { not_null<HistoryItem*> item) const {
if (_selected.size() != 1 if (_selected.size() != 1
|| _selected.begin()->first != item || _selected.begin()->first != item
@ -2393,11 +2393,13 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
}(); }();
const auto canReply = canSendReply || item->allowsForward(); const auto canReply = canSendReply || item->allowsForward();
if (canReply) { if (canReply) {
const auto itemId = item->fullId(); const auto selected = selectedQuote(item);
const auto quote = selectedQuote(item); auto text = selected
auto text = quote.empty() ? tr::lng_context_quote_and_reply(tr::now)
? tr::lng_context_reply_msg(tr::now) : tr::lng_context_reply_msg(tr::now);
: tr::lng_context_quote_and_reply(tr::now); const auto replyToItem = selected.item ? selected.item : item;
const auto itemId = replyToItem->fullId();
const auto quote = selected.text;
text.replace('&', u"&&"_q); text.replace('&', u"&&"_q);
_menu->addAction(text, [=] { _menu->addAction(text, [=] {
if (canSendReply) { if (canSendReply) {

View File

@ -35,6 +35,7 @@ class EmptyPainter;
class Element; class Element;
class TranslateTracker; class TranslateTracker;
struct PinnedId; struct PinnedId;
struct SelectedQuote;
} // namespace HistoryView } // namespace HistoryView
namespace HistoryView::Reactions { namespace HistoryView::Reactions {
@ -314,7 +315,7 @@ private:
QPoint mapPointToItem(QPoint p, const Element *view) const; QPoint mapPointToItem(QPoint p, const Element *view) const;
QPoint mapPointToItem(QPoint p, const HistoryItem *item) const; QPoint mapPointToItem(QPoint p, const HistoryItem *item) const;
[[nodiscard]] TextWithEntities selectedQuote( [[nodiscard]] HistoryView::SelectedQuote selectedQuote(
not_null<HistoryItem*> item) const; not_null<HistoryItem*> item) const;
void showContextMenu(QContextMenuEvent *e, bool showFromTouch = false); void showContextMenu(QContextMenuEvent *e, bool showFromTouch = false);

View File

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_view_highlight_manager.h" #include "history/history_view_highlight_manager.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "history/history.h"
#include "history/history_item.h" #include "history/history_item.h"
#include "history/view/history_view_element.h" #include "history/view/history_view_element.h"
#include "ui/chat/chat_style.h" #include "ui/chat/chat_style.h"
@ -26,17 +27,22 @@ ElementHighlighter::ElementHighlighter(
void ElementHighlighter::enqueue( void ElementHighlighter::enqueue(
not_null<Element*> view, not_null<Element*> view,
TextSelection part) { const TextWithEntities &part) {
const auto item = view->data(); const auto data = computeHighlight(view, part);
const auto data = Highlight{ item->fullId(), part };
if (_queue.empty() && !_animation.animating()) { if (_queue.empty() && !_animation.animating()) {
highlight(data.itemId, data.part); highlight(data);
} else if (_highlighted != data && !base::contains(_queue, data)) { } else if (_highlighted != data && !base::contains(_queue, data)) {
_queue.push_back(data); _queue.push_back(data);
checkNextHighlight(); checkNextHighlight();
} }
} }
void ElementHighlighter::highlight(
not_null<Element*> view,
const TextWithEntities &part) {
highlight(computeHighlight(view, part));
}
void ElementHighlighter::checkNextHighlight() { void ElementHighlighter::checkNextHighlight() {
if (_animation.animating()) { if (_animation.animating()) {
return; return;
@ -53,10 +59,9 @@ void ElementHighlighter::checkNextHighlight() {
} }
return Highlight(); return Highlight();
}(); }();
if (!next) { if (next) {
return; highlight(next);
} }
highlight(next.itemId, next.part);
} }
Ui::ChatPaintHighlight ElementHighlighter::state( Ui::ChatPaintHighlight ElementHighlighter::state(
@ -69,18 +74,46 @@ Ui::ChatPaintHighlight ElementHighlighter::state(
return {}; return {};
} }
void ElementHighlighter::highlight(FullMsgId itemId, TextSelection part) { ElementHighlighter::Highlight ElementHighlighter::computeHighlight(
if (const auto item = _data->message(itemId)) { not_null<const Element*> view,
const TextWithEntities &part) {
const auto item = view->data();
const auto owner = &item->history()->owner();
if (const auto group = owner->groups().find(item)) {
const auto leader = group->items.front();
const auto leaderId = leader->fullId();
const auto i = ranges::find(group->items, item);
if (i != end(group->items)) {
const auto index = int(i - begin(group->items));
if (part.empty()) {
return { leaderId, AddGroupItemSelection({}, index) };
} else if (const auto leaderView = _viewForItem(leader)) {
return {
leaderId,
leaderView->selectionFromQuote(item, part),
};
}
}
return { leaderId };
} else if (part.empty()) {
return { item->fullId() };
}
return { item->fullId(), view->selectionFromQuote(item, part) };
}
void ElementHighlighter::highlight(Highlight data) {
if (const auto item = _data->message(data.itemId)) {
if (const auto view = _viewForItem(item)) { if (const auto view = _viewForItem(item)) {
if (_highlighted && _highlighted.itemId != itemId) { if (_highlighted && _highlighted.itemId != data.itemId) {
if (const auto was = _data->message(_highlighted.itemId)) { if (const auto was = _data->message(_highlighted.itemId)) {
if (const auto view = _viewForItem(was)) { if (const auto view = _viewForItem(was)) {
repaintHighlightedItem(view); repaintHighlightedItem(view);
} }
} }
} }
_highlighted = { itemId, part }; _highlighted = data;
_animation.start(!part.empty()); _animation.start(!data.part.empty()
&& !IsSubGroupSelection(data.part));
repaintHighlightedItem(view); repaintHighlightedItem(view);
} }

View File

@ -33,8 +33,8 @@ public:
ViewForItem viewForItem, ViewForItem viewForItem,
RepaintView repaintView); RepaintView repaintView);
void enqueue(not_null<Element*> view, TextSelection part); void enqueue(not_null<Element*> view, const TextWithEntities &part);
void highlight(FullMsgId itemId, TextSelection part); void highlight(not_null<Element*> view, const TextWithEntities &part);
void clear(); void clear();
[[nodiscard]] Ui::ChatPaintHighlight state( [[nodiscard]] Ui::ChatPaintHighlight state(
@ -72,6 +72,10 @@ private:
friend inline bool operator==(Highlight, Highlight) = default; friend inline bool operator==(Highlight, Highlight) = default;
}; };
[[nodiscard]] Highlight computeHighlight(
not_null<const Element*> view,
const TextWithEntities &part);
void highlight(Highlight data);
void checkNextHighlight(); void checkNextHighlight();
void repaintHighlightedItem(not_null<const Element*> view); void repaintHighlightedItem(not_null<const Element*> view);
void updateMessage(); void updateMessage();

View File

@ -1270,7 +1270,7 @@ void HistoryWidget::scrollToAnimationCallback(
void HistoryWidget::enqueueMessageHighlight( void HistoryWidget::enqueueMessageHighlight(
not_null<HistoryView::Element*> view, not_null<HistoryView::Element*> view,
TextSelection part) { const TextWithEntities &part) {
_highlighter.enqueue(view, part); _highlighter.enqueue(view, part);
} }
@ -5709,8 +5709,7 @@ int HistoryWidget::countInitialScrollTop() {
enqueueMessageHighlight( enqueueMessageHighlight(
view, view,
view->selectionFromQuote( base::take(_showAtMsgHighlightPart));
base::take(_showAtMsgHighlightPart)));
const auto result = itemTopForHighlight(view); const auto result = itemTopForHighlight(view);
createUnreadBarIfBelowVisibleArea(result); createUnreadBarIfBelowVisibleArea(result);
return result; return result;

View File

@ -184,7 +184,7 @@ public:
void enqueueMessageHighlight( void enqueueMessageHighlight(
not_null<HistoryView::Element*> view, not_null<HistoryView::Element*> view,
TextSelection part); const TextWithEntities &part);
[[nodiscard]] Ui::ChatPaintHighlight itemHighlight( [[nodiscard]] Ui::ChatPaintHighlight itemHighlight(
not_null<const HistoryItem*> item) const; not_null<const HistoryItem*> item) const;

View File

@ -217,7 +217,9 @@ rpl::producer<TextWithEntities> PreviewWrap::showQuoteSelector(
const TextWithEntities &quote) { const TextWithEntities &quote) {
_selection.reset(TextSelection()); _selection.reset(TextSelection());
_element = item->createView(_delegate.get()); const auto group = item->history()->owner().groups().find(item);
const auto leader = group ? group->items.front() : item;
_element = leader->createView(_delegate.get());
_link = _pressedLink = nullptr; _link = _pressedLink = nullptr;
if (const auto was = base::take(_draftItem)) { if (const auto was = base::take(_draftItem)) {
@ -233,10 +235,10 @@ rpl::producer<TextWithEntities> PreviewWrap::showQuoteSelector(
initElement(); initElement();
_selection = _element->selectionFromQuote(quote); _selection = _element->selectionFromQuote(item, quote);
return _selection.value( return _selection.value(
) | rpl::map([=](TextSelection selection) { ) | rpl::map([=](TextSelection selection) {
return _element->selectedQuote(selection); return _element->selectedQuote(selection).text;
}); });
} }

View File

@ -48,6 +48,7 @@ struct ContextMenuRequest {
SelectedItems selectedItems; SelectedItems selectedItems;
TextForMimeData selectedText; TextForMimeData selectedText;
TextWithEntities quote; TextWithEntities quote;
HistoryItem *quoteItem = nullptr;
bool overSelection = false; bool overSelection = false;
PointState pointState = PointState(); PointState pointState = PointState();
}; };

View File

@ -94,6 +94,42 @@ Element *MousedElement/* = nullptr*/;
return session->tryResolveWindow(); return session->tryResolveWindow();
} }
[[nodiscard]] bool CheckQuoteEntities(
const EntitiesInText &quoteEntities,
const TextWithEntities &original,
TextSelection selection) {
auto left = quoteEntities;
const auto allowed = std::array{
EntityType::Bold,
EntityType::Italic,
EntityType::Underline,
EntityType::StrikeOut,
EntityType::Spoiler,
EntityType::CustomEmoji,
};
for (const auto &entity : original.entities) {
const auto from = entity.offset();
const auto till = from + entity.length();
if (till <= selection.from || from >= selection.to) {
continue;
}
const auto quoteFrom = std::max(from, int(selection.from));
const auto quoteTill = std::min(till, int(selection.to));
const auto cut = EntityInText(
entity.type(),
quoteFrom - int(selection.from),
quoteTill - quoteFrom,
entity.data());
const auto i = ranges::find(left, cut);
if (i != left.end()) {
left.erase(i);
} else if (ranges::contains(allowed, cut.type())) {
return false;
}
}
return left.empty();
};
} // namespace } // namespace
std::unique_ptr<Ui::PathShiftGradient> MakePathShiftGradient( std::unique_ptr<Ui::PathShiftGradient> MakePathShiftGradient(
@ -1559,6 +1595,105 @@ TextSelection Element::adjustSelection(
return selection; return selection;
} }
SelectedQuote Element::FindSelectedQuote(
const Ui::Text::String &text,
TextSelection selection,
not_null<HistoryItem*> item) {
if (selection.to > text.length()) {
return {};
}
auto modified = selection;
for (const auto &modification : text.modifications()) {
if (modification.position >= selection.to) {
break;
} else if (modification.position <= selection.from) {
modified.from += modification.skipped;
if (modification.added
&& modification.position < selection.from) {
--modified.from;
}
}
modified.to += modification.skipped;
if (modification.added && modified.to > modified.from) {
--modified.to;
}
}
auto result = item->originalText();
if (modified.empty() || modified.to > result.text.size()) {
return {};
}
result.text = result.text.mid(
modified.from,
modified.to - modified.from);
const auto allowed = std::array{
EntityType::Bold,
EntityType::Italic,
EntityType::Underline,
EntityType::StrikeOut,
EntityType::Spoiler,
EntityType::CustomEmoji,
};
for (auto i = result.entities.begin(); i != result.entities.end();) {
const auto offset = i->offset();
const auto till = offset + i->length();
if ((till <= modified.from)
|| (offset >= modified.to)
|| !ranges::contains(allowed, i->type())) {
i = result.entities.erase(i);
} else {
if (till > modified.to) {
i->shrinkFromRight(till - modified.to);
}
i->shiftLeft(modified.from);
++i;
}
}
return { item, result };
}
TextSelection Element::FindSelectionFromQuote(
const Ui::Text::String &text,
not_null<HistoryItem*> item,
const TextWithEntities &quote) {
if (quote.empty()) {
return {};
}
const auto &original = item->originalText();
auto result = TextSelection();
auto offset = 0;
while (true) {
const auto i = original.text.indexOf(quote.text, offset);
if (i < 0) {
return {};
}
auto selection = TextSelection{
uint16(i),
uint16(i + quote.text.size()),
};
if (CheckQuoteEntities(quote.entities, original, selection)) {
result = selection;
break;
}
offset = i + 1;
}
//for (const auto &modification : text.modifications()) {
// if (modification.position >= selection.to) {
// break;
// } else if (modification.position <= selection.from) {
// modified.from += modification.skipped;
// if (modification.added
// && modification.position < selection.from) {
// --modified.from;
// }
// }
// modified.to += modification.skipped;
// if (modification.added && modified.to > modified.from) {
// --modified.to;
// }
//}
return result;
}
Reactions::ButtonParameters Element::reactionButtonParameters( Reactions::ButtonParameters Element::reactionButtonParameters(
QPoint position, QPoint position,
const TextState &reactionState) const { const TextState &reactionState) const {

View File

@ -266,6 +266,15 @@ struct TopicButton {
int nameVersion = 0; int nameVersion = 0;
}; };
struct SelectedQuote {
HistoryItem *item = nullptr;
TextWithEntities text;
explicit operator bool() const {
return item && !text.empty();
}
};
class Element class Element
: public Object : public Object
, public RuntimeComposer<Element> , public RuntimeComposer<Element>
@ -387,19 +396,24 @@ public:
QPoint point, QPoint point,
InfoDisplayType type) const; InfoDisplayType type) const;
virtual TextForMimeData selectedText(TextSelection selection) const = 0; virtual TextForMimeData selectedText(TextSelection selection) const = 0;
virtual TextWithEntities selectedQuote(TextSelection selection) const = 0; virtual SelectedQuote selectedQuote(
virtual TextWithEntities selectedQuote(
const Ui::Text::String &text,
TextSelection selection) const = 0; TextSelection selection) const = 0;
virtual TextSelection selectionFromQuote( virtual TextSelection selectionFromQuote(
const TextWithEntities &quote) const = 0; not_null<HistoryItem*> item,
virtual TextSelection selectionFromQuote(
const Ui::Text::String &text,
const TextWithEntities &quote) const = 0; const TextWithEntities &quote) const = 0;
[[nodiscard]] virtual TextSelection adjustSelection( [[nodiscard]] virtual TextSelection adjustSelection(
TextSelection selection, TextSelection selection,
TextSelectType type) const; TextSelectType type) const;
[[nodiscard]] static SelectedQuote FindSelectedQuote(
const Ui::Text::String &text,
TextSelection selection,
not_null<HistoryItem*> item);
[[nodiscard]] static TextSelection FindSelectionFromQuote(
const Ui::Text::String &text,
not_null<HistoryItem*> item,
const TextWithEntities &quote);
[[nodiscard]] virtual auto reactionButtonParameters( [[nodiscard]] virtual auto reactionButtonParameters(
QPoint position, QPoint position,
const TextState &reactionState) const -> Reactions::ButtonParameters; const TextState &reactionState) const -> Reactions::ButtonParameters;

View File

@ -709,13 +709,10 @@ bool ListWidget::isBelowPosition(Data::MessagePosition position) const {
void ListWidget::highlightMessage( void ListWidget::highlightMessage(
FullMsgId itemId, FullMsgId itemId,
const TextWithEntities &highlightPart) { const TextWithEntities &part) {
const auto view = !highlightPart.empty() if (const auto view = viewForItem(itemId)) {
? viewForItem(itemId) _highlighter.highlight(view, part);
: nullptr; }
_highlighter.highlight(
itemId,
view ? view->selectionFromQuote(highlightPart) : TextSelection());
} }
void ListWidget::showAroundPosition( void ListWidget::showAroundPosition(
@ -2607,9 +2604,11 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
request.view = _overElement; request.view = _overElement;
request.item = overItem; request.item = overItem;
request.pointState = _overState.pointState; request.pointState = _overState.pointState;
request.quote = (overItemView && _selectedTextItem == overItem) const auto quote = (overItemView && _selectedTextItem == overItem)
? overItemView->selectedQuote(_selectedTextRange) ? overItemView->selectedQuote(_selectedTextRange)
: TextWithEntities(); : SelectedQuote();
request.quote = quote.text;
request.quoteItem = quote.item;
request.selectedText = _selectedText; request.selectedText = _selectedText;
request.selectedItems = collectSelectedItems(); request.selectedItems = collectSelectedItems();
const auto hasSelection = !request.selectedItems.empty() const auto hasSelection = !request.selectedItems.empty()

View File

@ -233,7 +233,7 @@ public:
bool isBelowPosition(Data::MessagePosition position) const; bool isBelowPosition(Data::MessagePosition position) const;
void highlightMessage( void highlightMessage(
FullMsgId itemId, FullMsgId itemId,
const TextWithEntities &highlightPart); const TextWithEntities &part);
void showAtPosition( void showAtPosition(
Data::MessagePosition position, Data::MessagePosition position,

View File

@ -65,42 +65,6 @@ const auto kPsaTooltipPrefix = "cloud_lng_tooltip_psa_";
return std::nullopt; return std::nullopt;
} }
[[nodiscard]] bool CheckQuoteEntities(
const EntitiesInText &quoteEntities,
const TextWithEntities &original,
TextSelection selection) {
auto left = quoteEntities;
const auto allowed = std::array{
EntityType::Bold,
EntityType::Italic,
EntityType::Underline,
EntityType::StrikeOut,
EntityType::Spoiler,
EntityType::CustomEmoji,
};
for (const auto &entity : original.entities) {
const auto from = entity.offset();
const auto till = from + entity.length();
if (till <= selection.from || from >= selection.to) {
continue;
}
const auto quoteFrom = std::max(from, int(selection.from));
const auto quoteTill = std::min(till, int(selection.to));
const auto cut = EntityInText(
entity.type(),
quoteFrom - int(selection.from),
quoteTill - quoteFrom,
entity.data());
const auto i = ranges::find(left, cut);
if (i != left.end()) {
left.erase(i);
} else if (ranges::contains(allowed, cut.type())) {
return false;
}
}
return left.empty();
};
class KeyboardStyle : public ReplyKeyboard::Style { class KeyboardStyle : public ReplyKeyboard::Style {
public: public:
KeyboardStyle(const style::BotKeyboardButton &st); KeyboardStyle(const style::BotKeyboardButton &st);
@ -2682,7 +2646,7 @@ TextForMimeData Message::selectedText(TextSelection selection) const {
return result; return result;
} }
TextWithEntities Message::selectedQuote(TextSelection selection) const { SelectedQuote Message::selectedQuote(TextSelection selection) const {
const auto item = data(); const auto item = data();
const auto &translated = item->translatedText(); const auto &translated = item->translatedText();
const auto &original = item->originalText(); const auto &original = item->originalText();
@ -2697,7 +2661,7 @@ TextWithEntities Message::selectedQuote(TextSelection selection) const {
const auto textSelection = mediaBefore const auto textSelection = mediaBefore
? media->skipSelection(selection) ? media->skipSelection(selection)
: selection; : selection;
return selectedQuote(text(), textSelection); return FindSelectedQuote(text(), textSelection, data());
} else if (const auto media = this->media()) { } else if (const auto media = this->media()) {
if (media->isDisplayed() || isHiddenByGroup()) { if (media->isDisplayed() || isHiddenByGroup()) {
return media->selectedQuote(selection); return media->selectedQuote(selection);
@ -2706,67 +2670,12 @@ TextWithEntities Message::selectedQuote(TextSelection selection) const {
return {}; return {};
} }
TextWithEntities Message::selectedQuote(
const Ui::Text::String &text,
TextSelection selection) const {
if (selection.to > text.length()) {
return {};
}
auto modified = selection;
for (const auto &modification : text.modifications()) {
if (modification.position >= selection.to) {
break;
} else if (modification.position <= selection.from) {
modified.from += modification.skipped;
if (modification.added
&& modification.position < selection.from) {
--modified.from;
}
}
modified.to += modification.skipped;
if (modification.added && modified.to > modified.from) {
--modified.to;
}
}
auto result = data()->originalText();
if (modified.empty() || modified.to > result.text.size()) {
return {};
}
result.text = result.text.mid(
modified.from,
modified.to - modified.from);
const auto allowed = std::array{
EntityType::Bold,
EntityType::Italic,
EntityType::Underline,
EntityType::StrikeOut,
EntityType::Spoiler,
EntityType::CustomEmoji,
};
for (auto i = result.entities.begin(); i != result.entities.end();) {
const auto offset = i->offset();
const auto till = offset + i->length();
if ((till <= modified.from)
|| (offset >= modified.to)
|| !ranges::contains(allowed, i->type())) {
i = result.entities.erase(i);
} else {
if (till > modified.to) {
i->shrinkFromRight(till - modified.to);
}
i->shiftLeft(modified.from);
++i;
}
}
return result;
}
TextSelection Message::selectionFromQuote( TextSelection Message::selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const { const TextWithEntities &quote) const {
if (quote.empty()) { if (quote.empty()) {
return {}; return {};
} }
const auto item = data();
const auto &translated = item->translatedText(); const auto &translated = item->translatedText();
const auto &original = item->originalText(); const auto &original = item->originalText();
if (&translated != &original) { if (&translated != &original) {
@ -2775,58 +2684,16 @@ TextSelection Message::selectionFromQuote(
const auto media = this->media(); const auto media = this->media();
const auto mediaDisplayed = media && media->isDisplayed(); const auto mediaDisplayed = media && media->isDisplayed();
const auto mediaBefore = mediaDisplayed && invertMedia(); const auto mediaBefore = mediaDisplayed && invertMedia();
const auto result = selectionFromQuote(text(), quote); const auto result = FindSelectionFromQuote(text(), item, quote);
return mediaBefore ? media->unskipSelection(result) : result; return mediaBefore ? media->unskipSelection(result) : result;
} else if (const auto media = this->media()) { } else if (const auto media = this->media()) {
if (media->isDisplayed() || isHiddenByGroup()) { if (media->isDisplayed() || isHiddenByGroup()) {
return media->selectionFromQuote(quote); return media->selectionFromQuote(item, quote);
} }
} }
return {}; return {};
} }
TextSelection Message::selectionFromQuote(
const Ui::Text::String &text,
const TextWithEntities &quote) const {
if (quote.empty()) {
return {};
}
const auto &original = data()->originalText();
auto result = TextSelection();
auto offset = 0;
while (true) {
const auto i = original.text.indexOf(quote.text, offset);
if (i < 0) {
return {};
}
auto selection = TextSelection{
uint16(i),
uint16(i + quote.text.size()),
};
if (CheckQuoteEntities(quote.entities, original, selection)) {
result = selection;
break;
}
offset = i + 1;
}
//for (const auto &modification : text.modifications()) {
// if (modification.position >= selection.to) {
// break;
// } else if (modification.position <= selection.from) {
// modified.from += modification.skipped;
// if (modification.added
// && modification.position < selection.from) {
// --modified.from;
// }
// }
// modified.to += modification.skipped;
// if (modification.added && modified.to > modified.from) {
// --modified.to;
// }
//}
return result;
}
TextSelection Message::adjustSelection( TextSelection Message::adjustSelection(
TextSelection selection, TextSelection selection,
TextSelectType type) const { TextSelectType type) const {

View File

@ -95,14 +95,9 @@ public:
QPoint point, QPoint point,
InfoDisplayType type) const override; InfoDisplayType type) const override;
TextForMimeData selectedText(TextSelection selection) const override; TextForMimeData selectedText(TextSelection selection) const override;
TextWithEntities selectedQuote(TextSelection selection) const override; SelectedQuote selectedQuote(TextSelection selection) const override;
TextWithEntities selectedQuote(
const Ui::Text::String &text,
TextSelection selection) const override;
TextSelection selectionFromQuote( TextSelection selectionFromQuote(
const TextWithEntities &quote) const override; not_null<HistoryItem*> item,
TextSelection selectionFromQuote(
const Ui::Text::String &text,
const TextWithEntities &quote) const override; const TextWithEntities &quote) const override;
TextSelection adjustSelection( TextSelection adjustSelection(
TextSelection selection, TextSelection selection,

View File

@ -669,23 +669,12 @@ TextForMimeData Service::selectedText(TextSelection selection) const {
return text().toTextForMimeData(selection); return text().toTextForMimeData(selection);
} }
TextWithEntities Service::selectedQuote(TextSelection selection) const { SelectedQuote Service::selectedQuote(TextSelection selection) const {
return {};
}
TextWithEntities Service::selectedQuote(
const Ui::Text::String &text,
TextSelection selection) const {
return {}; return {};
} }
TextSelection Service::selectionFromQuote( TextSelection Service::selectionFromQuote(
const TextWithEntities &quote) const { not_null<HistoryItem*> item,
return {};
}
TextSelection Service::selectionFromQuote(
const Ui::Text::String &text,
const TextWithEntities &quote) const { const TextWithEntities &quote) const {
return {}; return {};
} }

View File

@ -43,14 +43,9 @@ public:
StateRequest request) const override; StateRequest request) const override;
void updatePressed(QPoint point) override; void updatePressed(QPoint point) override;
TextForMimeData selectedText(TextSelection selection) const override; TextForMimeData selectedText(TextSelection selection) const override;
TextWithEntities selectedQuote(TextSelection selection) const override; SelectedQuote selectedQuote(TextSelection selection) const override;
TextWithEntities selectedQuote(
const Ui::Text::String &text,
TextSelection selection) const override;
TextSelection selectionFromQuote( TextSelection selectionFromQuote(
const TextWithEntities &quote) const override; not_null<HistoryItem*> item,
TextSelection selectionFromQuote(
const Ui::Text::String &text,
const TextWithEntities &quote) const override; const TextWithEntities &quote) const override;
TextSelection adjustSelection( TextSelection adjustSelection(
TextSelection selection, TextSelection selection,

View File

@ -1212,7 +1212,7 @@ TextForMimeData Document::selectedText(TextSelection selection) const {
return result; return result;
} }
TextWithEntities Document::selectedQuote(TextSelection selection) const { SelectedQuote Document::selectedQuote(TextSelection selection) const {
if (const auto voice = Get<HistoryDocumentVoice>()) { if (const auto voice = Get<HistoryDocumentVoice>()) {
const auto length = voice->transcribeText.length(); const auto length = voice->transcribeText.length();
if (selection.from < length) { if (selection.from < length) {
@ -1223,16 +1223,21 @@ TextWithEntities Document::selectedQuote(TextSelection selection) const {
voice->transcribeText); voice->transcribeText);
} }
if (const auto captioned = Get<HistoryDocumentCaptioned>()) { if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
return parent()->selectedQuote(captioned->caption, selection); return Element::FindSelectedQuote(
captioned->caption,
selection,
_realParent);
} }
return {}; return {};
} }
TextSelection Document::selectionFromQuote( TextSelection Document::selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const { const TextWithEntities &quote) const {
if (const auto captioned = Get<HistoryDocumentCaptioned>()) { if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
const auto result = parent()->selectionFromQuote( const auto result = Element::FindSelectionFromQuote(
captioned->caption, captioned->caption,
item,
quote); quote);
if (result.empty()) { if (result.empty()) {
return {}; return {};

View File

@ -46,8 +46,9 @@ public:
bool hasTextForCopy() const override; bool hasTextForCopy() const override;
TextForMimeData selectedText(TextSelection selection) const override; TextForMimeData selectedText(TextSelection selection) const override;
TextWithEntities selectedQuote(TextSelection selection) const override; SelectedQuote selectedQuote(TextSelection selection) const override;
TextSelection selectionFromQuote( TextSelection selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const override; const TextWithEntities &quote) const override;
bool uploading() const override; bool uploading() const override;

View File

@ -1205,13 +1205,14 @@ TextForMimeData Gif::selectedText(TextSelection selection) const {
return _caption.toTextForMimeData(selection); return _caption.toTextForMimeData(selection);
} }
TextWithEntities Gif::selectedQuote(TextSelection selection) const { SelectedQuote Gif::selectedQuote(TextSelection selection) const {
return parent()->selectedQuote(_caption, selection); return Element::FindSelectedQuote(_caption, selection, _realParent);
} }
TextSelection Gif::selectionFromQuote( TextSelection Gif::selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const { const TextWithEntities &quote) const {
return parent()->selectionFromQuote(_caption, quote); return Element::FindSelectionFromQuote(_caption, item, quote);
} }
bool Gif::fullFeaturedGrouped(RectParts sides) const { bool Gif::fullFeaturedGrouped(RectParts sides) const {

View File

@ -68,8 +68,9 @@ public:
} }
TextForMimeData selectedText(TextSelection selection) const override; TextForMimeData selectedText(TextSelection selection) const override;
TextWithEntities selectedQuote(TextSelection selection) const override; SelectedQuote selectedQuote(TextSelection selection) const override;
TextSelection selectionFromQuote( TextSelection selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const override; const TextWithEntities &quote) const override;
bool uploading() const override; bool uploading() const override;

View File

@ -189,6 +189,10 @@ not_null<History*> Media::history() const {
return _parent->history(); return _parent->history();
} }
SelectedQuote Media::selectedQuote(TextSelection selection) const {
return {};
}
bool Media::isDisplayed() const { bool Media::isDisplayed() const {
return true; return true;
} }

View File

@ -49,6 +49,7 @@ struct StateRequest;
struct MediaSpoiler; struct MediaSpoiler;
class StickerPlayer; class StickerPlayer;
class Element; class Element;
struct SelectedQuote;
using PaintContext = Ui::ChatPaintContext; using PaintContext = Ui::ChatPaintContext;
@ -88,11 +89,10 @@ public:
TextSelection selection) const { TextSelection selection) const {
return {}; return {};
} }
[[nodiscard]] virtual TextWithEntities selectedQuote( [[nodiscard]] virtual SelectedQuote selectedQuote(
TextSelection selection) const { TextSelection selection) const;
return {};
}
[[nodiscard]] virtual TextSelection selectionFromQuote( [[nodiscard]] virtual TextSelection selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const { const TextWithEntities &quote) const {
return {}; return {};
} }

View File

@ -286,16 +286,44 @@ void GroupedMedia::drawHighlight(
Painter &p, Painter &p,
const PaintContext &context, const PaintContext &context,
int top) const { int top) const {
auto selection = context.highlight.range;
if (_mode != Mode::Column) { if (_mode != Mode::Column) {
if (!selection.empty() && !IsSubGroupSelection(selection)) {
_parent->paintCustomHighlight(
p,
context,
top,
height(),
_parent->data().get());
}
return; return;
} }
const auto empty = selection.empty();
const auto subpart = IsSubGroupSelection(selection);
const auto skip = top + groupedPadding().top(); const auto skip = top + groupedPadding().top();
for (auto i = 0, count = int(_parts.size()); i != count; ++i) { for (auto i = 0, count = int(_parts.size()); i != count; ++i) {
const auto &part = _parts[i]; const auto &part = _parts[i];
const auto rect = part.geometry.translated(0, skip); const auto rect = part.geometry.translated(0, skip);
const auto full = (!i && empty)
|| (subpart && IsGroupItemSelection(selection, i));
auto copy = context;
if (full) {
copy.highlight.range = {};
_parent->paintCustomHighlight(
p,
copy,
rect.y(),
rect.height(),
part.item);
} else if (!selection.empty()) {
copy.highlight.range = selection;
selection = part.content->skipSelection(selection);
} else {
break;
}
_parent->paintCustomHighlight( _parent->paintCustomHighlight(
p, p,
context, copy,
rect.y(), rect.y(),
rect.height(), rect.height(),
part.item); part.item);
@ -316,6 +344,7 @@ void GroupedMedia::draw(Painter &p, const PaintContext &context) const {
const auto rounding = inWebPage const auto rounding = inWebPage
? Ui::BubbleRounding{ kSmall, kSmall, kSmall, kSmall } ? Ui::BubbleRounding{ kSmall, kSmall, kSmall, kSmall }
: adjustedBubbleRoundingWithCaption(_caption); : adjustedBubbleRoundingWithCaption(_caption);
const auto highlight = context.highlight.range;
for (auto i = 0, count = int(_parts.size()); i != count; ++i) { for (auto i = 0, count = int(_parts.size()); i != count; ++i) {
const auto &part = _parts[i]; const auto &part = _parts[i];
const auto partContext = context.withSelection(fullSelection const auto partContext = context.withSelection(fullSelection
@ -325,10 +354,11 @@ void GroupedMedia::draw(Painter &p, const PaintContext &context) const {
: IsGroupItemSelection(selection, i) : IsGroupItemSelection(selection, i)
? FullSelection ? FullSelection
: TextSelection()); : TextSelection());
const auto highlightOpacity = IsGroupItemSelection( const auto highlighted = (highlight.empty() && !i)
context.highlight.range, || IsGroupItemSelection(highlight, i);
i const auto highlightOpacity = highlighted
) ? context.highlight.opacity : 0.; ? context.highlight.opacity
: 0.;
if (textSelection) { if (textSelection) {
selection = part.content->skipSelection(selection); selection = part.content->skipSelection(selection);
} }
@ -517,6 +547,7 @@ TextSelection GroupedMedia::adjustSelection(
selection.to = modified.to; selection.to = modified.to;
return selection; return selection;
} }
checked = till;
} }
return selection; return selection;
} }
@ -564,6 +595,50 @@ TextForMimeData GroupedMedia::selectedText(
return result; return result;
} }
SelectedQuote GroupedMedia::selectedQuote(TextSelection selection) const {
if (_mode != Mode::Column) {
return _captionItem
? Element::FindSelectedQuote(_caption, selection, _captionItem)
: SelectedQuote();
}
for (const auto &part : _parts) {
const auto next = part.content->skipSelection(selection);
if (next.to - next.from != selection.to - selection.from) {
if (!next.empty()) {
return SelectedQuote();
}
auto result = part.content->selectedQuote(selection);
result.item = part.item;
return result;
}
selection = next;
}
return {};
}
TextSelection GroupedMedia::selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const {
if (_mode != Mode::Column) {
return (_captionItem == item)
? Element::FindSelectionFromQuote(_caption, item, quote)
: TextSelection();
}
const auto i = ranges::find(_parts, item, &Part::item);
if (i == end(_parts)) {
return {};
}
const auto index = int(i - begin(_parts));
auto result = i->content->selectionFromQuote(item, quote);
if (result.empty()) {
return AddGroupItemSelection({}, index);
}
for (auto j = i; j != begin(_parts);) {
result = (--j)->content->unskipSelection(result);
}
return result;
}
auto GroupedMedia::getBubbleSelectionIntervals( auto GroupedMedia::getBubbleSelectionIntervals(
TextSelection selection) const TextSelection selection) const
-> std::vector<Ui::BubbleSelectionInterval> { -> std::vector<Ui::BubbleSelectionInterval> {
@ -666,16 +741,15 @@ bool GroupedMedia::validateGroupParts(
} }
void GroupedMedia::refreshCaption() { void GroupedMedia::refreshCaption() {
using PartPtrOpt = std::optional<const Part*>; const auto part = [&]() -> const Part* {
const auto captionPart = [&]() -> PartPtrOpt {
if (_mode == Mode::Column) { if (_mode == Mode::Column) {
return std::nullopt; return nullptr;
} }
auto result = PartPtrOpt(); auto result = (const Part*)nullptr;
for (const auto &part : _parts) { for (const auto &part : _parts) {
if (!part.item->emptyText()) { if (!part.item->emptyText()) {
if (result) { if (result) {
return std::nullopt; return nullptr;
} else { } else {
result = &part; result = &part;
} }
@ -683,8 +757,7 @@ void GroupedMedia::refreshCaption() {
} }
return result; return result;
}(); }();
if (captionPart) { if (part) {
const auto &part = (*captionPart);
_caption = createCaption(part->item); _caption = createCaption(part->item);
_captionItem = part->item; _captionItem = part->item;
} else { } else {

View File

@ -55,6 +55,10 @@ public:
DocumentData *getDocument() const override; DocumentData *getDocument() const override;
TextForMimeData selectedText(TextSelection selection) const override; TextForMimeData selectedText(TextSelection selection) const override;
SelectedQuote selectedQuote(TextSelection selection) const override;
TextSelection selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const override;
std::vector<Ui::BubbleSelectionInterval> getBubbleSelectionIntervals( std::vector<Ui::BubbleSelectionInterval> getBubbleSelectionIntervals(
TextSelection selection) const override; TextSelection selection) const override;

View File

@ -1051,13 +1051,14 @@ TextForMimeData Photo::selectedText(TextSelection selection) const {
return _caption.toTextForMimeData(selection); return _caption.toTextForMimeData(selection);
} }
TextWithEntities Photo::selectedQuote(TextSelection selection) const { SelectedQuote Photo::selectedQuote(TextSelection selection) const {
return parent()->selectedQuote(_caption, selection); return Element::FindSelectedQuote(_caption, selection, _realParent);
} }
TextSelection Photo::selectionFromQuote( TextSelection Photo::selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const { const TextWithEntities &quote) const {
return parent()->selectionFromQuote(_caption, quote); return Element::FindSelectionFromQuote(_caption, item, quote);
} }
void Photo::hideSpoilers() { void Photo::hideSpoilers() {

View File

@ -57,8 +57,9 @@ public:
} }
TextForMimeData selectedText(TextSelection selection) const override; TextForMimeData selectedText(TextSelection selection) const override;
TextWithEntities selectedQuote(TextSelection selection) const override; SelectedQuote selectedQuote(TextSelection selection) const override;
TextSelection selectionFromQuote( TextSelection selectionFromQuote(
not_null<HistoryItem*> item,
const TextWithEntities &quote) const override; const TextWithEntities &quote) const override;
PhotoData *getPhoto() const override { PhotoData *getPhoto() const override {