From b5c9b6f552d14ebbd36811dfbb4a325e09e2c146 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 7 Jul 2025 14:04:54 +0400 Subject: [PATCH] Make and display replies to tasks. --- Telegram/Resources/langs/lang.strings | 2 + .../SourceFiles/core/click_handler_types.h | 1 + .../history/history_inner_widget.cpp | 6 + Telegram/SourceFiles/history/history_item.cpp | 1 + .../SourceFiles/history/history_widget.cpp | 35 ++++-- Telegram/SourceFiles/history/history_widget.h | 3 +- .../history_view_compose_controls.cpp | 3 +- .../view/history_view_context_menu.cpp | 6 + .../history/view/history_view_reply.cpp | 114 +++++++++++++++++- .../history/view/history_view_reply.h | 2 +- .../view/media/history_view_todo_list.cpp | 17 ++- 11 files changed, 169 insertions(+), 21 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index b5c74ff866..d8566d25f5 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4260,6 +4260,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_to_msg" = "Go To Message"; "lng_context_reply_msg" = "Reply"; "lng_context_quote_and_reply" = "Quote & Reply"; +"lng_context_reply_to_task" = "Reply to Task"; "lng_context_edit_msg" = "Edit"; "lng_context_add_factcheck" = "Add Fact Check"; "lng_context_edit_factcheck" = "Edit Fact Check"; @@ -4450,6 +4451,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_inline_switch_cant" = "Sorry, no way to write here :("; "lng_preview_reply_to" = "Reply to {name}"; "lng_preview_reply_to_quote" = "Reply to quote from {name}"; +"lng_preview_reply_to_task" = "Reply to task from {title}"; "lng_suggest_bar_title" = "Suggest a Post Below"; "lng_suggest_bar_text" = "Click to offer a price for publishing."; diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index 43295e1965..520b1282a1 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -17,6 +17,7 @@ constexpr auto kSendReactionEmojiProperty = 0x04; constexpr auto kReactionsCountEmojiProperty = 0x05; constexpr auto kDocumentFilenameTooltipProperty = 0x06; constexpr auto kPhoneNumberLinkProperty = 0x07; +constexpr auto kTodoListItemIdProperty = 0x08; namespace Ui { class Show; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 2eab960029..06415a1aef 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -2342,6 +2342,9 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { const auto linkUserpicPeerId = (link && _dragStateUserpic) ? link->property(kPeerLinkPeerIdProperty).toULongLong() : 0; + const auto todoListTaskId = link + ? link->property(kTodoListItemIdProperty).toInt() + : 0; const auto session = &this->session(); _whoReactedMenuLifetime.destroy(); if (!clickedReaction.empty() @@ -2702,6 +2705,8 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { const auto selected = selectedQuote(item); auto text = (selected ? tr::lng_context_quote_and_reply + : todoListTaskId + ? tr::lng_context_reply_to_task : tr::lng_context_reply_msg)( tr::now, Ui::Text::FixAmpersandInAction); @@ -2714,6 +2719,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { .messageId = itemId, .quote = quote, .quoteOffset = quoteOffset, + .todoItemId = todoListTaskId, }); if (!quote.empty()) { _widget->clearSelected(); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index c803b59abc..2200581101 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -4213,6 +4213,7 @@ void HistoryItem::createComponentsHelper(HistoryItemCommonFields &&fields) { : replyTo.monoforumPeerId ? replyTo.monoforumPeerId : PeerId(); + config.reply.todoItemId = replyTo.todoItemId; const auto replyToTop = replyTo.topicRootId ? replyTo.topicRootId : LookupReplyToTop(_history, to); diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 4ab4561ec8..2d4a93f1ce 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -68,6 +68,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_drafts.h" #include "data/data_session.h" +#include "data/data_todo_list.h" #include "data/data_web_page.h" #include "data/data_document.h" #include "data/data_photo.h" @@ -8548,7 +8549,7 @@ void HistoryWidget::clearFieldText( void HistoryWidget::replyToMessage(FullReplyTo id) { if (const auto item = session().data().message(id.messageId)) { if (CanSendReply(item) && !base::IsCtrlPressed()) { - replyToMessage(item, id.quote, id.quoteOffset); + replyToMessage(item, id); } else if (item->allowsForward()) { const auto show = controller()->uiShow(); HistoryView::Controls::ShowReplyToChatBox(show, id); @@ -8561,16 +8562,12 @@ void HistoryWidget::replyToMessage(FullReplyTo id) { void HistoryWidget::replyToMessage( not_null item, - TextWithEntities quote, - int quoteOffset) { + FullReplyTo fields) { if (isJoinChannel()) { return; } - _processingReplyTo = { - .messageId = item->fullId(), - .quote = quote, - .quoteOffset = quoteOffset, - }; + fields.messageId = item->fullId(); + _processingReplyTo = fields; _processingReplyItem = item; processReply(); } @@ -9231,11 +9228,24 @@ void HistoryWidget::updateReplyEditText(not_null item) { .session = &session(), .repaint = [=] { updateField(); }, }); + const auto text = [&] { + const auto media = _replyTo.todoItemId ? item->media() : nullptr; + if (const auto todolist = media ? media->todolist() : nullptr) { + const auto i = ranges::find( + todolist->items, + _replyTo.todoItemId, + &TodoListItem::id); + if (i != end(todolist->items)) { + return i->text; + } + } + return (_editMsgId || _replyTo.quote.empty()) + ? item->inReplyText() + : _replyTo.quote; + }(); _replyEditMsgText.setMarkedText( st::defaultTextStyle, - ((_editMsgId || _replyTo.quote.empty()) - ? item->inReplyText() - : _replyTo.quote), + text, Ui::DialogTextOptions(), context); if (fieldOrDisabledShown() || isRecording()) { @@ -9321,10 +9331,9 @@ void HistoryWidget::updateReplyToName() { .customEmojiLoopLimit = 1, }); const auto to = _replyEditMsg ? _replyEditMsg : _kbReplyTo; - const auto replyToQuote = _replyTo && !_replyTo.quote.empty(); _replyToName.setMarkedText( st::fwdTextStyle, - HistoryView::Reply::ComposePreviewName(_history, to, replyToQuote), + HistoryView::Reply::ComposePreviewName(_history, to, _replyTo), Ui::NameTextOptions(), context); } diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 7166ea8768..d2606c9fc5 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -205,8 +205,7 @@ public: void replyToMessage(FullReplyTo id); void replyToMessage( not_null item, - TextWithEntities quote = {}, - int quoteOffset = 0); + FullReplyTo fields = {}); void editMessage( not_null item, const TextSelection &selection); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index ce7c9335d5..b104c9c498 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -492,10 +492,9 @@ void FieldHeader::setShownMessage(HistoryItem *item) { .customEmojiLoopLimit = 1, }); const auto replyTo = _replyTo.current(); - const auto quote = replyTo && !replyTo.quote.empty(); _shownMessageName.setMarkedText( st::fwdTextStyle, - HistoryView::Reply::ComposePreviewName(_history, item, quote), + HistoryView::Reply::ComposePreviewName(_history, item, replyTo), Ui::NameTextOptions(), context); } else { diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index a9b98a98d6..109ccc2fb0 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -639,9 +639,14 @@ bool AddReplyToMessageAction( return false; } + const auto todoListTaskId = request.link + ? request.link->property(kTodoListItemIdProperty).toInt() + : 0; const auto "e = request.quote; auto text = (quote.text.empty() ? tr::lng_context_reply_msg + : todoListTaskId + ? tr::lng_context_reply_to_task : tr::lng_context_quote_and_reply)( tr::now, Ui::Text::FixAmpersandInAction); @@ -650,6 +655,7 @@ bool AddReplyToMessageAction( .messageId = itemId, .quote = quote.text, .quoteOffset = quote.offset, + .todoItemId = todoListTaskId, }, base::IsCtrlPressed()); }, &st::menuIconReply); return true; diff --git a/Telegram/SourceFiles/history/view/history_view_reply.cpp b/Telegram/SourceFiles/history/view/history_view_reply.cpp index 3d29c806f6..9df096b737 100644 --- a/Telegram/SourceFiles/history/view/history_view_reply.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reply.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer.h" #include "data/data_session.h" #include "data/data_story.h" +#include "data/data_todo_list.h" #include "data/data_user.h" #include "history/view/history_view_item_preview.h" #include "history/history.h" @@ -38,6 +39,85 @@ namespace { constexpr auto kNonExpandedLinesLimit = 5; +[[nodiscard]] QImage MakeTaskImage() { + const auto diameter = st::normalFont->ascent; + const auto line = st::historyPollRadio.thickness; + const auto size = 2 * line + diameter; + const auto ratio = style::DevicePixelRatio(); + auto result = QImage( + QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + result.fill(Qt::transparent); + result.setDevicePixelRatio(ratio); + + auto p = QPainter(&result); + PainterHighQualityEnabler hq(p); + + p.setOpacity(st::historyPollRadioOpacity); + + const auto rect = QRectF(line, line, diameter, diameter).marginsRemoved( + QMarginsF(line / 2., line / 2., line / 2., line / 2.)); + auto pen = QPen(QColor(255, 255, 255)); + pen.setWidth(line); + p.setPen(pen); + p.drawEllipse(rect); + + p.end(); + + return result; +} + +[[nodiscard]] QImage MakeTaskDoneImage() { + const auto white = QColor(255, 255, 255); + const auto black = QColor(0, 0, 0); + + const auto diameter = st::normalFont->ascent; + const auto line = st::historyPollRadio.thickness; + const auto size = 2 * line + diameter; + const auto ratio = style::DevicePixelRatio(); + auto result = QImage( + QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + result.fill(black); + result.setDevicePixelRatio(ratio); + + auto p = QPainter(&result); + PainterHighQualityEnabler hq(p); + + const auto rect = QRectF(line, line, diameter, diameter).marginsRemoved( + QMarginsF(line / 2., line / 2., line / 2., line / 2.)); + auto pen = QPen(white); + pen.setWidth(line); + p.setPen(pen); + p.setBrush(white); + p.drawEllipse(rect); + const auto &icon = st::historyPollInChoiceRight; + icon.paint( + p, + line + (diameter - icon.width()) / 2, + line + (diameter - icon.height()) / 2, + size, + black); + p.end(); + + return style::colorizeImage(result, white); +} + +[[nodiscard]] TextWithEntities TaskDoneIcon( + not_null session) { + return Ui::Text::SingleCustomEmoji( + session->data().customEmojiManager().registerInternalEmoji( + MakeTaskDoneImage(), + QMargins(0, st::lineWidth, st::lineWidth, 0))); +} + +[[nodiscard]] TextWithEntities TaskIcon(not_null session) { + return Ui::Text::SingleCustomEmoji( + session->data().customEmojiManager().registerInternalEmoji( + MakeTaskImage(), + QMargins(0, st::lineWidth, st::lineWidth, 0))); +} + } // namespace void ValidateBackgroundEmoji( @@ -193,6 +273,22 @@ void Reply::update( const auto item = view->data(); const auto &fields = data->fields(); const auto message = data->resolvedMessage.get(); + const auto messageMedia = (message && fields.todoItemId) + ? message->media() + : nullptr; + const auto messageTodoList = messageMedia + ? messageMedia->todolist() + : nullptr; + const auto taskIndex = messageTodoList + ? int(ranges::find( + messageTodoList->items, + fields.todoItemId, + &TodoListItem::id) - begin(messageTodoList->items)) + : -1; + const auto task = (taskIndex >= 0 + && taskIndex < messageTodoList->items.size()) + ? &messageTodoList->items[taskIndex] + : nullptr; const auto story = data->resolvedStory.get(); const auto externalMedia = fields.externalMedia.get(); if (!_externalSender) { @@ -210,7 +306,6 @@ void Reply::update( _hiddenSenderColorIndexPlusOne = (!_colorPeer && message) ? (message->originalHiddenSenderInfo()->colorIndex + 1) : 0; - const auto hasPreview = (story && story->hasReplyPreview()) || (message && message->media() @@ -225,8 +320,13 @@ void Reply::update( && !fields.quote.empty(); _hasQuoteIcon = hasQuoteIcon ? 1 : 0; + const auto session = &view->history()->session(); const auto text = (!_displaying && data->unavailable()) ? TextWithEntities() + : task + ? Ui::Text::Colorized(task->completionDate + ? TaskDoneIcon(session) + : TaskIcon(session)).append(task->text) : (message && (fields.quote.empty() || !fields.manualQuote)) ? message->inReplyText() : !fields.quote.empty() @@ -867,18 +967,28 @@ TextWithEntities Reply::ForwardEmoji(not_null owner) { TextWithEntities Reply::ComposePreviewName( not_null history, not_null to, - bool quote) { + const FullReplyTo &replyTo) { const auto sender = [&] { if (const auto from = to->displayFrom()) { return not_null(from); } return to->author(); }(); + if (const auto media = replyTo.todoItemId ? to->media() : nullptr) { + if (const auto todolist = media->todolist()) { + return tr::lng_preview_reply_to_task( + tr::now, + lt_title, + todolist->title, + Ui::Text::WithEntities); + } + } const auto toPeer = to->history()->peer; const auto displayAsExternal = (to->history() != history); const auto groupNameAdded = displayAsExternal && (toPeer != sender) && (toPeer->isChat() || toPeer->isMegagroup()); + const auto quote = replyTo && !replyTo.quote.empty(); const auto shorten = groupNameAdded || quote; auto nameFull = TextWithEntities(); diff --git a/Telegram/SourceFiles/history/view/history_view_reply.h b/Telegram/SourceFiles/history/view/history_view_reply.h index 416f4c0097..3e1a55addc 100644 --- a/Telegram/SourceFiles/history/view/history_view_reply.h +++ b/Telegram/SourceFiles/history/view/history_view_reply.h @@ -110,7 +110,7 @@ public: [[nodiscard]] static TextWithEntities ComposePreviewName( not_null history, not_null to, - bool quote); + const FullReplyTo &replyTo); private: [[nodiscard]] Ui::Text::GeometryDescriptor textGeometry( diff --git a/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp b/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp index 4f37d36d0c..46bf0ab28d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp @@ -334,9 +334,11 @@ void TodoList::updateTasks(bool skipAnimations) { ClickHandlerPtr TodoList::createTaskClickHandler( const Task &task) { const auto id = task.id; - return std::make_shared(crl::guard(this, [=] { + auto result = std::make_shared(crl::guard(this, [=] { toggleCompletion(id); })); + result->setProperty(kTodoListItemIdProperty, id); + return result; } void TodoList::startToggleAnimation(Task &task) { @@ -375,11 +377,24 @@ void TodoList::toggleCompletion(int id) { if (i == end(_tasks)) { return; } + const auto selected = (i->completionDate != 0); i->completionDate = selected ? TimeId() : base::unixtime::now(); if (!selected) { i->setCompletedBy(_parent->history()->session().user()); } + + const auto parentMedia = _parent->data()->media(); + const auto baseList = parentMedia ? parentMedia->todolist() : nullptr; + if (baseList) { + const auto j = ranges::find(baseList->items, id, &TodoListItem::id); + if (j != end(baseList->items)) { + j->completionDate = i->completionDate; + j->completedBy = i->completedBy; + } + history()->owner().updateDependentMessages(_parent->data()); + } + startToggleAnimation(*i); repaint();