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);
struct MessageHighlightId {
TextWithEntities quote;
int quoteOffset = 0;
int todoItemId = 0;
[[nodiscard]] bool empty() const {
return quote.empty() && !todoItemId;
}
};
struct FullReplyTo {
FullMsgId messageId;
TextWithEntities quote;
@ -181,6 +191,9 @@ struct FullReplyTo {
int quoteOffset = 0;
int todoItemId = 0;
[[nodiscard]] MessageHighlightId highlight() const {
return { quote, quoteOffset, todoItemId };
}
[[nodiscard]] bool replying() const {
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()) {
auto params = Window::SectionShow(
Window::SectionShow::Way::ClearStack);
params.highlightPart.text = _searchState.query;
if (!params.highlightPart.empty()) {
params.highlightPartOffsetHint = kSearchQueryOffsetHint;
}
params.highlight = Window::SearchHighlightId(_searchState.query);
if (row.newWindow) {
controller()->showInNewWindow(
Window::SeparateId(topic),
@ -972,10 +969,7 @@ void Widget::chosenRow(const ChosenRow &row) {
) ? ShowAtUnreadMsgId : row.message.fullId.msg;
auto params = Window::SectionShow(
Window::SectionShow::Way::ClearStack);
params.highlightPart.text = _searchState.query;
if (!params.highlightPart.empty()) {
params.highlightPartOffsetHint = kSearchQueryOffsetHint;
}
params.highlight = Window::SearchHighlightId(_searchState.query);
if (row.newWindow) {
controller()->showInNewWindow(peer, showAtMsgId);
} else {

View file

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

View file

@ -909,12 +909,26 @@ void HistoryItem::updateServiceDependent(bool force) {
}
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->peerId
? _history->owner().peer(dependent->peerId)
: _history->peer),
dependent->msgId,
fullId());
fullId(),
{ .todoItemId = todoItemId });
}
auto gotDependencyItem = false;
if (!dependent->msg) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -818,10 +818,9 @@ bool ListWidget::isBelowPosition(Data::MessagePosition position) const {
void ListWidget::highlightMessage(
FullMsgId itemId,
const TextWithEntities &part,
int partOffsetHint) {
const MessageHighlightId &highlight) {
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
&& position != Data::UnreadMessagePosition) {
const auto hasHighlight = !params.highlightPart.empty();
highlightMessage(
position.fullId,
params.highlightPart,
params.highlightPartOffsetHint);
const auto hasHighlight = !params.highlight.empty();
highlightMessage(position.fullId, params.highlight);
if (hasHighlight) {
// We may want to scroll to a different part of the message.
scrollTop = scrollTopForPosition(position);

View file

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

View file

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

View file

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

View file

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

View file

@ -482,6 +482,7 @@ void TodoList::draw(Painter &p, const PaintContext &context) const {
paintw,
width(),
context);
appendTaskHighlight(task.id, tshift, height, context);
if (was) {
heavy = true;
} else if (!task.userpic.null()) {
@ -576,6 +577,33 @@ int TodoList::paintTask(
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(
Painter &p,
const Task &task,

View file

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

View file

@ -153,6 +153,7 @@ struct ChatPaintHighlight {
float64 opacity = 0.;
float64 collapsion = 0.;
TextSelection range;
int todoItemId = 0;
};
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)
: _session(session)
, _api(&_session->mtp()) {
@ -1146,8 +1154,7 @@ void SessionNavigation::showRepliesForMessage(
.repliesRootId = rootId,
},
commentId,
params.highlightPart,
params.highlightPartOffsetHint);
params.highlight);
memento->setFromTopic(topic);
showSection(std::move(memento), params);
return;
@ -1269,8 +1276,7 @@ void SessionNavigation::showSublist(
.sublist = sublist,
},
itemId,
params.highlightPart,
params.highlightPartOffsetHint);
params.highlight);
showSection(std::move(memento), params);
}

View file

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