Highlight tasks from reply/service messages.

This commit is contained in:
John Preston 2025-07-07 16:26:11 +04:00
parent b5c9b6f552
commit bff86b90fb
25 changed files with 170 additions and 122 deletions

View file

@ -172,6 +172,16 @@ inline QDebug operator<<(QDebug debug, const FullMsgId &fullMsgId) {
Q_DECLARE_METATYPE(FullMsgId); Q_DECLARE_METATYPE(FullMsgId);
struct MessageHighlightId {
TextWithEntities quote;
int quoteOffset = 0;
int todoItemId = 0;
[[nodiscard]] bool empty() const {
return quote.empty() && !todoItemId;
}
};
struct FullReplyTo { struct FullReplyTo {
FullMsgId messageId; FullMsgId messageId;
TextWithEntities quote; TextWithEntities quote;
@ -181,6 +191,9 @@ struct FullReplyTo {
int quoteOffset = 0; int quoteOffset = 0;
int todoItemId = 0; int todoItemId = 0;
[[nodiscard]] MessageHighlightId highlight() const {
return { quote, quoteOffset, todoItemId };
}
[[nodiscard]] bool replying() const { [[nodiscard]] bool replying() const {
return messageId || (storyId && storyId.peer); return messageId || (storyId && storyId.peer);
} }

View file

@ -898,10 +898,7 @@ void Widget::chosenRow(const ChosenRow &row) {
} else if (const auto topic = row.key.topic()) { } else if (const auto topic = row.key.topic()) {
auto params = Window::SectionShow( auto params = Window::SectionShow(
Window::SectionShow::Way::ClearStack); Window::SectionShow::Way::ClearStack);
params.highlightPart.text = _searchState.query; params.highlight = Window::SearchHighlightId(_searchState.query);
if (!params.highlightPart.empty()) {
params.highlightPartOffsetHint = kSearchQueryOffsetHint;
}
if (row.newWindow) { if (row.newWindow) {
controller()->showInNewWindow( controller()->showInNewWindow(
Window::SeparateId(topic), Window::SeparateId(topic),
@ -972,10 +969,7 @@ void Widget::chosenRow(const ChosenRow &row) {
) ? ShowAtUnreadMsgId : row.message.fullId.msg; ) ? ShowAtUnreadMsgId : row.message.fullId.msg;
auto params = Window::SectionShow( auto params = Window::SectionShow(
Window::SectionShow::Way::ClearStack); Window::SectionShow::Way::ClearStack);
params.highlightPart.text = _searchState.query; params.highlight = Window::SearchHighlightId(_searchState.query);
if (!params.highlightPart.empty()) {
params.highlightPartOffsetHint = kSearchQueryOffsetHint;
}
if (row.newWindow) { if (row.newWindow) {
controller()->showInNewWindow(peer, showAtMsgId); controller()->showInNewWindow(peer, showAtMsgId);
} else { } else {

View file

@ -622,10 +622,10 @@ void HistoryInner::setupSwipeReplyAndBack() {
: still)->fullId(); : still)->fullId();
_widget->replyToMessage({ _widget->replyToMessage({
.messageId = replyToItemId, .messageId = replyToItemId,
.quote = selected.text, .quote = selected.highlight.quote,
.quoteOffset = selected.offset, .quoteOffset = selected.highlight.quoteOffset,
}); });
if (!selected.text.empty()) { if (!selected.highlight.quote.empty()) {
_widget->clearSelected(); _widget->clearSelected();
} }
}; };
@ -2712,16 +2712,14 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
Ui::Text::FixAmpersandInAction); Ui::Text::FixAmpersandInAction);
const auto replyToItem = selected.item ? selected.item : item; const auto replyToItem = selected.item ? selected.item : item;
const auto itemId = replyToItem->fullId(); const auto itemId = replyToItem->fullId();
const auto quote = selected.text;
const auto quoteOffset = selected.offset;
_menu->addAction(std::move(text), [=] { _menu->addAction(std::move(text), [=] {
_widget->replyToMessage({ _widget->replyToMessage({
.messageId = itemId, .messageId = itemId,
.quote = quote, .quote = selected.highlight.quote,
.quoteOffset = quoteOffset, .quoteOffset = selected.highlight.quoteOffset,
.todoItemId = todoListTaskId, .todoItemId = todoListTaskId,
}); });
if (!quote.empty()) { if (!selected.highlight.quote.empty()) {
_widget->clearSelected(); _widget->clearSelected();
} }
}, &st::menuIconReply); }, &st::menuIconReply);

View file

@ -909,12 +909,26 @@ void HistoryItem::updateServiceDependent(bool force) {
} }
if (!dependent->lnk) { if (!dependent->lnk) {
auto todoItemId = 0;
if (const auto done = Get<HistoryServiceTodoCompletions>()) {
const auto &items = !done->completed.empty()
? done->completed
: done->incompleted;
if (items.size() == 1) {
todoItemId = items.front();
}
} else if (const auto append = Get<HistoryServiceTodoAppendTasks>()) {
if (append->list.size() == 1) {
todoItemId = append->list.front().id;
}
}
dependent->lnk = JumpToMessageClickHandler( dependent->lnk = JumpToMessageClickHandler(
(dependent->peerId (dependent->peerId
? _history->owner().peer(dependent->peerId) ? _history->owner().peer(dependent->peerId)
: _history->peer), : _history->peer),
dependent->msgId, dependent->msgId,
fullId()); fullId(),
{ .todoItemId = todoItemId });
} }
auto gotDependencyItem = false; auto gotDependencyItem = false;
if (!dependent->msg) { if (!dependent->msg) {

View file

@ -713,22 +713,19 @@ bool IsItemScheduledUntilOnline(not_null<const HistoryItem*> item) {
ClickHandlerPtr JumpToMessageClickHandler( ClickHandlerPtr JumpToMessageClickHandler(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
FullMsgId returnToId, FullMsgId returnToId,
TextWithEntities highlightPart, MessageHighlightId highlight) {
int highlightPartOffsetHint) {
return JumpToMessageClickHandler( return JumpToMessageClickHandler(
item->history()->peer, item->history()->peer,
item->id, item->id,
returnToId, returnToId,
std::move(highlightPart), std::move(highlight));
highlightPartOffsetHint);
} }
ClickHandlerPtr JumpToMessageClickHandler( ClickHandlerPtr JumpToMessageClickHandler(
not_null<PeerData*> peer, not_null<PeerData*> peer,
MsgId msgId, MsgId msgId,
FullMsgId returnToId, FullMsgId returnToId,
TextWithEntities highlightPart, MessageHighlightId highlight) {
int highlightPartOffsetHint) {
return std::make_shared<LambdaClickHandler>([=] { return std::make_shared<LambdaClickHandler>([=] {
const auto separate = Core::App().separateWindowFor(peer); const auto separate = Core::App().separateWindowFor(peer);
const auto controller = separate const auto controller = separate
@ -738,8 +735,7 @@ ClickHandlerPtr JumpToMessageClickHandler(
auto params = Window::SectionShow{ auto params = Window::SectionShow{
Window::SectionShow::Way::Forward Window::SectionShow::Way::Forward
}; };
params.highlightPart = highlightPart; params.highlight = highlight;
params.highlightPartOffsetHint = highlightPartOffsetHint;
params.origin = Window::SectionShow::OriginMessage{ params.origin = Window::SectionShow::OriginMessage{
returnToId returnToId
}; };

View file

@ -229,13 +229,11 @@ private:
not_null<PeerData*> peer, not_null<PeerData*> peer,
MsgId msgId, MsgId msgId,
FullMsgId returnToId = FullMsgId(), FullMsgId returnToId = FullMsgId(),
TextWithEntities highlightPart = {}, MessageHighlightId highlight = {});
int highlightPartOffsetHint = 0);
[[nodiscard]] ClickHandlerPtr JumpToMessageClickHandler( [[nodiscard]] ClickHandlerPtr JumpToMessageClickHandler(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
FullMsgId returnToId = FullMsgId(), FullMsgId returnToId = FullMsgId(),
TextWithEntities highlightPart = {}, MessageHighlightId highlight = {});
int highlightPartOffsetHint = 0);
[[nodiscard]] ClickHandlerPtr JumpToStoryClickHandler( [[nodiscard]] ClickHandlerPtr JumpToStoryClickHandler(
not_null<Data::Story*> story); not_null<Data::Story*> story);
ClickHandlerPtr JumpToStoryClickHandler( ClickHandlerPtr JumpToStoryClickHandler(

View file

@ -65,6 +65,7 @@ Ui::ChatPaintHighlight ElementHighlighter::state(
if (item->fullId() == _highlighted.itemId) { if (item->fullId() == _highlighted.itemId) {
auto result = _animation.state(); auto result = _animation.state();
result.range = _highlighted.part; result.range = _highlighted.part;
result.todoItemId = _highlighted.todoListId;
return result; return result;
} }
return {}; return {};
@ -82,19 +83,27 @@ ElementHighlighter::Highlight ElementHighlighter::computeHighlight(
const auto i = ranges::find(group->items, item); const auto i = ranges::find(group->items, item);
if (i != end(group->items)) { if (i != end(group->items)) {
const auto index = int(i - begin(group->items)); const auto index = int(i - begin(group->items));
if (quote.text.empty()) { if (quote.highlight.empty()) {
return { leaderId, AddGroupItemSelection({}, index) }; return { leaderId, AddGroupItemSelection({}, index) };
} else if (const auto leaderView = _viewForItem(leader)) { } else if (const auto leaderView = _viewForItem(leader)) {
return { leaderId, leaderView->selectionFromQuote(quote) }; return {
leaderId,
leaderView->selectionFromQuote(quote),
quote.highlight.todoItemId,
};
} }
} }
return { leaderId }; return { leaderId, {}, quote.highlight.todoItemId };
} else if (quote.text.empty()) { } else if (quote.highlight.quote.empty()) {
return { item->fullId() }; return { item->fullId(), {}, quote.highlight.todoItemId };
} else if (const auto view = _viewForItem(item)) { } else if (const auto view = _viewForItem(item)) {
return { item->fullId(), view->selectionFromQuote(quote) }; return {
item->fullId(),
view->selectionFromQuote(quote),
quote.highlight.todoItemId,
};
} }
return { item->fullId() }; return { item->fullId(), {}, quote.highlight.todoItemId };
} }
void ElementHighlighter::highlight(Highlight data) { void ElementHighlighter::highlight(Highlight data) {
@ -108,7 +117,7 @@ void ElementHighlighter::highlight(Highlight data) {
} }
} }
_highlighted = data; _highlighted = data;
_animation.start(!data.part.empty() _animation.start((!data.part.empty() || data.todoListId)
&& !IsSubGroupSelection(data.part)); && !IsSubGroupSelection(data.part));
repaintHighlightedItem(view); repaintHighlightedItem(view);

View file

@ -65,6 +65,7 @@ private:
struct Highlight { struct Highlight {
FullMsgId itemId; FullMsgId itemId;
TextSelection part; TextSelection part;
int todoListId = 0;
explicit operator bool() const { explicit operator bool() const {
return itemId.operator bool(); return itemId.operator bool();

View file

@ -5640,8 +5640,7 @@ void HistoryWidget::switchToSearch(QString query) {
const auto item = activation.item; const auto item = activation.item;
auto params = ::Window::SectionShow( auto params = ::Window::SectionShow(
::Window::SectionShow::Way::ClearStack); ::Window::SectionShow::Way::ClearStack);
params.highlightPart = { activation.query }; params.highlight = Window::SearchHighlightId(activation.query);
params.highlightPartOffsetHint = kSearchQueryOffsetHint;
controller()->showPeerHistory( controller()->showPeerHistory(
item->history()->peer->id, item->history()->peer->id,
params, params,
@ -6743,8 +6742,7 @@ int HistoryWidget::countInitialScrollTop() {
enqueueMessageHighlight({ enqueueMessageHighlight({
item, item,
base::take(_showAtMsgParams.highlightPart), base::take(_showAtMsgParams.highlight),
base::take(_showAtMsgParams.highlightPartOffsetHint),
}); });
const auto result = itemTopForHighlight(view); const auto result = itemTopForHighlight(view);
createUnreadBarIfBelowVisibleArea(result); createUnreadBarIfBelowVisibleArea(result);
@ -7501,12 +7499,7 @@ void HistoryWidget::editDraftOptions() {
void HistoryWidget::jumpToReply(FullReplyTo to) { void HistoryWidget::jumpToReply(FullReplyTo to) {
if (const auto item = session().data().message(to.messageId)) { if (const auto item = session().data().message(to.messageId)) {
JumpToMessageClickHandler( JumpToMessageClickHandler(item, {}, to.highlight())->onClick({});
item,
{},
to.quote,
to.quoteOffset
)->onClick({});
} }
} }

View file

@ -783,7 +783,7 @@ void DraftOptionsBox(
box->setTitle(hasLink box->setTitle(hasLink
? tr::lng_link_options_header() ? tr::lng_link_options_header()
: hasReply : hasReply
? (state->quote.current().text.empty() ? (state->quote.current().highlight.quote.empty()
? tr::lng_reply_options_header() ? tr::lng_reply_options_header()
: tr::lng_reply_options_quote()) : tr::lng_reply_options_quote())
: (forwardCount == 1) : (forwardCount == 1)
@ -807,10 +807,12 @@ void DraftOptionsBox(
auto result = draft.reply; auto result = draft.reply;
if (const auto current = state->quote.current()) { if (const auto current = state->quote.current()) {
result.messageId = current.item->fullId(); result.messageId = current.item->fullId();
result.quote = current.text; result.quote = current.highlight.quote;
result.quoteOffset = current.offset; result.quoteOffset = current.highlight.quoteOffset;
// result.todoItemId = current.highlight.todoItemId;
} else { } else {
result.quote = {}; result.quote = {};
// result.todoItemId = 0;
} }
return result; return result;
}; };
@ -1112,7 +1114,7 @@ void DraftOptionsBox(
state->quote.value(), state->quote.value(),
state->shown.value() state->shown.value()
) | rpl::map([=](const SelectedQuote &quote, Section shown) { ) | rpl::map([=](const SelectedQuote &quote, Section shown) {
return (quote.text.empty() || shown != Section::Reply) return (quote.highlight.quote.empty() || shown != Section::Reply)
? tr::lng_settings_save() ? tr::lng_settings_save()
: tr::lng_reply_quote_selected(); : tr::lng_reply_quote_selected();
}) | rpl::flatten_latest(); }) | rpl::flatten_latest();

View file

@ -119,12 +119,10 @@ rpl::producer<Ui::MessageBarContent> RootViewContent(
ChatMemento::ChatMemento( ChatMemento::ChatMemento(
ChatViewId id, ChatViewId id,
MsgId highlightId, MsgId highlightId,
const TextWithEntities &highlightPart, MessageHighlightId highlight)
int highlightPartOffsetHint)
: _id(id) : _id(id)
, _highlightPart(highlightPart) , _highlightId(highlightId)
, _highlightPartOffsetHint(highlightPartOffsetHint) , _highlight(std::move(highlight)) {
, _highlightId(highlightId) {
if (highlightId || _id.sublist) { if (highlightId || _id.sublist) {
_list.setAroundPosition({ _list.setAroundPosition({
.fullId = FullMsgId(_id.history->peer->id, highlightId), .fullId = FullMsgId(_id.history->peer->id, highlightId),
@ -876,12 +874,7 @@ void ChatWidget::setupComposeControls() {
_composeControls->jumpToItemRequests( _composeControls->jumpToItemRequests(
) | rpl::start_with_next([=](FullReplyTo to) { ) | rpl::start_with_next([=](FullReplyTo to) {
if (const auto item = session().data().message(to.messageId)) { if (const auto item = session().data().message(to.messageId)) {
JumpToMessageClickHandler( JumpToMessageClickHandler(item, {}, to.highlight())->onClick({});
item,
{},
to.quote,
to.quoteOffset
)->onClick({});
} }
}, lifetime()); }, lifetime());
@ -1039,8 +1032,9 @@ void ChatWidget::setupSwipeReplyAndBack() {
: still)->fullId(); : still)->fullId();
_inner->replyToMessageRequestNotify({ _inner->replyToMessageRequestNotify({
.messageId = replyToItemId, .messageId = replyToItemId,
.quote = selected.text, .quote = selected.highlight.quote,
.quoteOffset = selected.offset, .quoteOffset = selected.highlight.quoteOffset,
.todoItemId = selected.highlight.todoItemId,
}); });
}; };
return result; return result;
@ -2639,8 +2633,7 @@ void ChatWidget::restoreState(not_null<ChatMemento*> memento) {
auto params = Window::SectionShow( auto params = Window::SectionShow(
Window::SectionShow::Way::Forward, Window::SectionShow::Way::Forward,
anim::type::instant); anim::type::instant);
params.highlightPart = memento->highlightPart(); params.highlight = memento->highlight();
params.highlightPartOffsetHint = memento->highlightPartOffsetHint();
showAtPosition(Data::MessagePosition{ showAtPosition(Data::MessagePosition{
.fullId = FullMsgId(_peer->id, highlight), .fullId = FullMsgId(_peer->id, highlight),
.date = TimeId(0), .date = TimeId(0),
@ -3443,8 +3436,7 @@ bool ChatWidget::searchInChatEmbedded(
const auto item = activation.item; const auto item = activation.item;
auto params = ::Window::SectionShow( auto params = ::Window::SectionShow(
::Window::SectionShow::Way::ClearStack); ::Window::SectionShow::Way::ClearStack);
params.highlightPart = { activation.query }; params.highlight = Window::SearchHighlightId(activation.query);
params.highlightPartOffsetHint = kSearchQueryOffsetHint;
controller()->showPeerHistory( controller()->showPeerHistory(
item->history()->peer->id, item->history()->peer->id,
params, params,

View file

@ -461,8 +461,7 @@ public:
explicit ChatMemento( explicit ChatMemento(
ChatViewId id, ChatViewId id,
MsgId highlightId = 0, MsgId highlightId = 0,
const TextWithEntities &highlightPart = {}, MessageHighlightId highlight = {});
int highlightPartOffsetHint = 0);
struct Comments { struct Comments {
}; };
@ -511,20 +510,16 @@ public:
[[nodiscard]] MsgId highlightId() const { [[nodiscard]] MsgId highlightId() const {
return _highlightId; return _highlightId;
} }
[[nodiscard]] const TextWithEntities &highlightPart() const { [[nodiscard]] const MessageHighlightId &highlight() const {
return _highlightPart; return _highlight;
}
[[nodiscard]] int highlightPartOffsetHint() const {
return _highlightPartOffsetHint;
} }
private: private:
void setupTopicViewer(); void setupTopicViewer();
ChatViewId _id; ChatViewId _id;
const TextWithEntities _highlightPart;
const int _highlightPartOffsetHint = 0;
const MsgId _highlightId = 0; const MsgId _highlightId = 0;
const MessageHighlightId _highlight;
ListMemento _list; ListMemento _list;
std::shared_ptr<Data::RepliesList> _replies; std::shared_ptr<Data::RepliesList> _replies;
QVector<FullMsgId> _replyReturns; QVector<FullMsgId> _replyReturns;

View file

@ -643,18 +643,18 @@ bool AddReplyToMessageAction(
? request.link->property(kTodoListItemIdProperty).toInt() ? request.link->property(kTodoListItemIdProperty).toInt()
: 0; : 0;
const auto &quote = request.quote; const auto &quote = request.quote;
auto text = (quote.text.empty() auto text = (todoListTaskId
? tr::lng_context_reply_msg
: todoListTaskId
? tr::lng_context_reply_to_task ? tr::lng_context_reply_to_task
: quote.highlight.quote.empty()
? tr::lng_context_reply_msg
: tr::lng_context_quote_and_reply)( : tr::lng_context_quote_and_reply)(
tr::now, tr::now,
Ui::Text::FixAmpersandInAction); Ui::Text::FixAmpersandInAction);
menu->addAction(std::move(text), [=, itemId = item->fullId()] { menu->addAction(std::move(text), [=, itemId = item->fullId()] {
list->replyToMessageRequestNotify({ list->replyToMessageRequestNotify({
.messageId = itemId, .messageId = itemId,
.quote = quote.text, .quote = quote.highlight.quote,
.quoteOffset = quote.offset, .quoteOffset = quote.highlight.quoteOffset,
.todoItemId = todoListTaskId, .todoItemId = todoListTaskId,
}, base::IsCtrlPressed()); }, base::IsCtrlPressed());
}, &st::menuIconReply); }, &st::menuIconReply);

View file

@ -1341,9 +1341,18 @@ void Element::validateText() {
if (const auto done = item->Get<HistoryServiceTodoCompletions>()) { if (const auto done = item->Get<HistoryServiceTodoCompletions>()) {
if (!done->completed.empty() && !done->incompleted.empty()) { if (!done->completed.empty() && !done->incompleted.empty()) {
const auto todoItemId = (done->incompleted.size() == 1)
? done->incompleted.front()
: 0;
setServicePreMessage( setServicePreMessage(
item->composeTodoIncompleted(done), item->composeTodoIncompleted(done),
done->lnk); JumpToMessageClickHandler(
(done->peerId
? history()->owner().peer(done->peerId)
: history()->peer),
done->msgId,
item->fullId(),
{ .todoItemId = todoItemId }));
} else { } else {
setServicePreMessage({}); setServicePreMessage({});
} }
@ -2190,17 +2199,18 @@ TextSelection Element::FindSelectionFromQuote(
const SelectedQuote &quote) { const SelectedQuote &quote) {
Expects(quote.item != nullptr); Expects(quote.item != nullptr);
if (quote.text.empty()) { const auto &rich = quote.highlight.quote;
if (rich.empty()) {
return {}; return {};
} }
const auto &original = quote.item->originalText(); const auto &original = quote.item->originalText();
if (quote.offset == kSearchQueryOffsetHint) { if (quote.highlight.quoteOffset == kSearchQueryOffsetHint) {
return ApplyModificationsFrom( return ApplyModificationsFrom(
FindSearchQueryHighlight(original.text, quote.text.text), FindSearchQueryHighlight(original.text, rich.text),
text); text);
} }
const auto length = int(original.text.size()); const auto length = int(original.text.size());
const auto qlength = int(quote.text.text.size()); const auto qlength = int(rich.text.size());
const auto checkAt = [&](int offset) { const auto checkAt = [&](int offset) {
return TextSelection{ return TextSelection{
uint16(offset), uint16(offset),
@ -2211,7 +2221,7 @@ TextSelection Element::FindSelectionFromQuote(
if (offset > length - qlength) { if (offset > length - qlength) {
return TextSelection(); return TextSelection();
} }
const auto i = original.text.indexOf(quote.text.text, offset); const auto i = original.text.indexOf(rich.text, offset);
return (i >= 0) ? checkAt(i) : TextSelection(); return (i >= 0) ? checkAt(i) : TextSelection();
}; };
const auto findOneBefore = [&](int offset) { const auto findOneBefore = [&](int offset) {
@ -2220,7 +2230,7 @@ TextSelection Element::FindSelectionFromQuote(
} }
const auto end = std::min(offset + qlength - 1, length); const auto end = std::min(offset + qlength - 1, length);
const auto from = end - length - 1; const auto from = end - length - 1;
const auto i = original.text.lastIndexOf(quote.text.text, from); const auto i = original.text.lastIndexOf(rich.text, from);
return (i >= 0) ? checkAt(i) : TextSelection(); return (i >= 0) ? checkAt(i) : TextSelection();
}; };
const auto findAfter = [&](int offset) { const auto findAfter = [&](int offset) {
@ -2258,7 +2268,7 @@ TextSelection Element::FindSelectionFromQuote(
? before ? before
: after; : after;
}; };
auto result = findTwoWays(quote.offset); auto result = findTwoWays(quote.highlight.quoteOffset);
if (result.empty()) { if (result.empty()) {
return {}; return {};
} }

View file

@ -357,12 +357,11 @@ struct TopicButton {
struct SelectedQuote { struct SelectedQuote {
HistoryItem *item = nullptr; HistoryItem *item = nullptr;
TextWithEntities text; MessageHighlightId highlight;
int offset = 0;
bool overflown = false; bool overflown = false;
explicit operator bool() const { explicit operator bool() const {
return item && !text.empty(); return item && !highlight.quote.empty();
} }
friend inline bool operator==(SelectedQuote, SelectedQuote) = default; friend inline bool operator==(SelectedQuote, SelectedQuote) = default;
}; };

View file

@ -818,10 +818,9 @@ bool ListWidget::isBelowPosition(Data::MessagePosition position) const {
void ListWidget::highlightMessage( void ListWidget::highlightMessage(
FullMsgId itemId, FullMsgId itemId,
const TextWithEntities &part, const MessageHighlightId &highlight) {
int partOffsetHint) {
if (const auto view = viewForItem(itemId)) { if (const auto view = viewForItem(itemId)) {
_highlighter.highlight({ view->data(), part, partOffsetHint }); _highlighter.highlight({ view->data(), highlight });
} }
} }
@ -899,11 +898,8 @@ bool ListWidget::showAtPositionNow(
} }
if (position != Data::MaxMessagePosition if (position != Data::MaxMessagePosition
&& position != Data::UnreadMessagePosition) { && position != Data::UnreadMessagePosition) {
const auto hasHighlight = !params.highlightPart.empty(); const auto hasHighlight = !params.highlight.empty();
highlightMessage( highlightMessage(position.fullId, params.highlight);
position.fullId,
params.highlightPart,
params.highlightPartOffsetHint);
if (hasHighlight) { if (hasHighlight) {
// We may want to scroll to a different part of the message. // We may want to scroll to a different part of the message.
scrollTop = scrollTopForPosition(position); scrollTop = scrollTopForPosition(position);

View file

@ -314,8 +314,7 @@ public:
bool isBelowPosition(Data::MessagePosition position) const; bool isBelowPosition(Data::MessagePosition position) const;
void highlightMessage( void highlightMessage(
FullMsgId itemId, FullMsgId itemId,
const TextWithEntities &part, const MessageHighlightId &highlight);
int partOffsetHint);
void showAtPosition( void showAtPosition(
Data::MessagePosition position, Data::MessagePosition position,

View file

@ -3303,7 +3303,7 @@ TextSelection Message::selectionFromQuote(
const SelectedQuote &quote) const { const SelectedQuote &quote) const {
Expects(quote.item != nullptr); Expects(quote.item != nullptr);
if (quote.text.empty()) { if (quote.highlight.quote.empty()) {
return {}; return {};
} }
const auto item = quote.item; const auto item = quote.item;

View file

@ -384,10 +384,11 @@ void Reply::setLinkFrom(
const auto &fields = data->fields(); const auto &fields = data->fields();
const auto externalChannelId = peerToChannel(fields.externalPeerId); const auto externalChannelId = peerToChannel(fields.externalPeerId);
const auto messageId = fields.messageId; const auto messageId = fields.messageId;
const auto quote = fields.manualQuote const auto highlight = MessageHighlightId{
? fields.quote .quote = fields.manualQuote ? fields.quote : TextWithEntities(),
: TextWithEntities(); .quoteOffset = int(fields.quoteOffset),
const auto quoteOffset = fields.quoteOffset; .todoItemId = fields.todoItemId,
};
const auto returnToId = view->data()->fullId(); const auto returnToId = view->data()->fullId();
const auto externalLink = [=](ClickContext context) { const auto externalLink = [=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>(); const auto my = context.other.value<ClickHandlerContext>();
@ -410,8 +411,7 @@ void Reply::setLinkFrom(
channel, channel,
messageId, messageId,
returnToId, returnToId,
quote, highlight
quoteOffset
)->onClick(context); )->onClick(context);
} else { } else {
controller->showPeerInfo(channel); controller->showPeerInfo(channel);
@ -432,7 +432,7 @@ void Reply::setLinkFrom(
const auto message = data->resolvedMessage.get(); const auto message = data->resolvedMessage.get();
const auto story = data->resolvedStory.get(); const auto story = data->resolvedStory.get();
_link = message _link = message
? JumpToMessageClickHandler(message, returnToId, quote, quoteOffset) ? JumpToMessageClickHandler(message, returnToId, highlight)
: story : story
? JumpToStoryClickHandler(story) ? JumpToStoryClickHandler(story)
: (data->external() : (data->external()

View file

@ -430,12 +430,8 @@ void ScheduledWidget::setupComposeControls() {
if (item->isScheduled() && item->history() == _history) { if (item->isScheduled() && item->history() == _history) {
showAtPosition(item->position()); showAtPosition(item->position());
} else { } else {
JumpToMessageClickHandler( const auto highlight = to.highlight();
item, JumpToMessageClickHandler(item, {}, highlight)->onClick({});
{},
to.quote,
to.quoteOffset
)->onClick({});
} }
} }
}, lifetime()); }, lifetime());

View file

@ -482,6 +482,7 @@ void TodoList::draw(Painter &p, const PaintContext &context) const {
paintw, paintw,
width(), width(),
context); context);
appendTaskHighlight(task.id, tshift, height, context);
if (was) { if (was) {
heavy = true; heavy = true;
} else if (!task.userpic.null()) { } else if (!task.userpic.null()) {
@ -576,6 +577,33 @@ int TodoList::paintTask(
return height; return height;
} }
void TodoList::appendTaskHighlight(
int id,
int top,
int height,
const PaintContext &context) const {
if (context.highlight.todoItemId != id
|| context.highlight.collapsion <= 0.) {
return;
}
const auto to = context.highlightInterpolateTo;
const auto toProgress = (1. - context.highlight.collapsion);
if (toProgress >= 1.) {
context.highlightPathCache->addRect(to);
} else if (toProgress <= 0.) {
context.highlightPathCache->addRect(0, top, width(), height);
} else {
const auto lerp = [=](int from, int to) {
return from + (to - from) * toProgress;
};
context.highlightPathCache->addRect(
lerp(0, to.x()),
lerp(top, to.y()),
lerp(width(), to.width()),
lerp(height, to.height()));
}
}
void TodoList::paintRadio( void TodoList::paintRadio(
Painter &p, Painter &p,
const Task &task, const Task &task,

View file

@ -117,6 +117,11 @@ private:
int top, int top,
int paintw, int paintw,
const PaintContext &context) const; const PaintContext &context) const;
void appendTaskHighlight(
int id,
int top,
int height,
const PaintContext &context) const;
void radialAnimationCallback() const; void radialAnimationCallback() const;

View file

@ -153,6 +153,7 @@ struct ChatPaintHighlight {
float64 opacity = 0.; float64 opacity = 0.;
float64 collapsion = 0.; float64 collapsion = 0.;
TextSelection range; TextSelection range;
int todoItemId = 0;
}; };
struct ChatPaintContext { struct ChatPaintContext {

View file

@ -358,6 +358,14 @@ void DateClickHandler::onClick(ClickContext context) const {
} }
} }
MessageHighlightId SearchHighlightId(const QString &query) {
auto result = MessageHighlightId{ .quote = { query } };
if (!result.quote.empty()) {
result.quoteOffset = kSearchQueryOffsetHint;
}
return result;
}
SessionNavigation::SessionNavigation(not_null<Main::Session*> session) SessionNavigation::SessionNavigation(not_null<Main::Session*> session)
: _session(session) : _session(session)
, _api(&_session->mtp()) { , _api(&_session->mtp()) {
@ -1146,8 +1154,7 @@ void SessionNavigation::showRepliesForMessage(
.repliesRootId = rootId, .repliesRootId = rootId,
}, },
commentId, commentId,
params.highlightPart, params.highlight);
params.highlightPartOffsetHint);
memento->setFromTopic(topic); memento->setFromTopic(topic);
showSection(std::move(memento), params); showSection(std::move(memento), params);
return; return;
@ -1269,8 +1276,7 @@ void SessionNavigation::showSublist(
.sublist = sublist, .sublist = sublist,
}, },
itemId, itemId,
params.highlightPart, params.highlight);
params.highlightPartOffsetHint);
showSection(std::move(memento), params); showSection(std::move(memento), params);
} }

View file

@ -166,8 +166,9 @@ struct SectionShow {
return copy; return copy;
} }
TextWithEntities highlightPart; MessageHighlightId highlight;
int highlightPartOffsetHint = 0; int highlightPartOffsetHint = 0;
int highlightTodoItemId = 0;
std::optional<TimeId> videoTimestamp; std::optional<TimeId> videoTimestamp;
Way way = Way::Forward; Way way = Way::Forward;
anim::type animated = anim::type::normal; anim::type animated = anim::type::normal;
@ -182,6 +183,8 @@ struct SectionShow {
}; };
[[nodiscard]] MessageHighlightId SearchHighlightId(const QString &query);
class SessionController; class SessionController;
class SessionNavigation : public base::has_weak_ptr { class SessionNavigation : public base::has_weak_ptr {