Make and display replies to tasks.

This commit is contained in:
John Preston 2025-07-07 14:04:54 +04:00
parent 23f5102f1b
commit b5c9b6f552
11 changed files with 169 additions and 21 deletions

View file

@ -4260,6 +4260,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_to_msg" = "Go To Message"; "lng_context_to_msg" = "Go To Message";
"lng_context_reply_msg" = "Reply"; "lng_context_reply_msg" = "Reply";
"lng_context_quote_and_reply" = "Quote & Reply"; "lng_context_quote_and_reply" = "Quote & Reply";
"lng_context_reply_to_task" = "Reply to Task";
"lng_context_edit_msg" = "Edit"; "lng_context_edit_msg" = "Edit";
"lng_context_add_factcheck" = "Add Fact Check"; "lng_context_add_factcheck" = "Add Fact Check";
"lng_context_edit_factcheck" = "Edit 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_inline_switch_cant" = "Sorry, no way to write here :(";
"lng_preview_reply_to" = "Reply to {name}"; "lng_preview_reply_to" = "Reply to {name}";
"lng_preview_reply_to_quote" = "Reply to quote from {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_title" = "Suggest a Post Below";
"lng_suggest_bar_text" = "Click to offer a price for publishing."; "lng_suggest_bar_text" = "Click to offer a price for publishing.";

View file

@ -17,6 +17,7 @@ constexpr auto kSendReactionEmojiProperty = 0x04;
constexpr auto kReactionsCountEmojiProperty = 0x05; constexpr auto kReactionsCountEmojiProperty = 0x05;
constexpr auto kDocumentFilenameTooltipProperty = 0x06; constexpr auto kDocumentFilenameTooltipProperty = 0x06;
constexpr auto kPhoneNumberLinkProperty = 0x07; constexpr auto kPhoneNumberLinkProperty = 0x07;
constexpr auto kTodoListItemIdProperty = 0x08;
namespace Ui { namespace Ui {
class Show; class Show;

View file

@ -2342,6 +2342,9 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
const auto linkUserpicPeerId = (link && _dragStateUserpic) const auto linkUserpicPeerId = (link && _dragStateUserpic)
? link->property(kPeerLinkPeerIdProperty).toULongLong() ? link->property(kPeerLinkPeerIdProperty).toULongLong()
: 0; : 0;
const auto todoListTaskId = link
? link->property(kTodoListItemIdProperty).toInt()
: 0;
const auto session = &this->session(); const auto session = &this->session();
_whoReactedMenuLifetime.destroy(); _whoReactedMenuLifetime.destroy();
if (!clickedReaction.empty() if (!clickedReaction.empty()
@ -2702,6 +2705,8 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
const auto selected = selectedQuote(item); const auto selected = selectedQuote(item);
auto text = (selected auto text = (selected
? tr::lng_context_quote_and_reply ? tr::lng_context_quote_and_reply
: todoListTaskId
? tr::lng_context_reply_to_task
: tr::lng_context_reply_msg)( : tr::lng_context_reply_msg)(
tr::now, tr::now,
Ui::Text::FixAmpersandInAction); Ui::Text::FixAmpersandInAction);
@ -2714,6 +2719,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
.messageId = itemId, .messageId = itemId,
.quote = quote, .quote = quote,
.quoteOffset = quoteOffset, .quoteOffset = quoteOffset,
.todoItemId = todoListTaskId,
}); });
if (!quote.empty()) { if (!quote.empty()) {
_widget->clearSelected(); _widget->clearSelected();

View file

@ -4213,6 +4213,7 @@ void HistoryItem::createComponentsHelper(HistoryItemCommonFields &&fields) {
: replyTo.monoforumPeerId : replyTo.monoforumPeerId
? replyTo.monoforumPeerId ? replyTo.monoforumPeerId
: PeerId(); : PeerId();
config.reply.todoItemId = replyTo.todoItemId;
const auto replyToTop = replyTo.topicRootId const auto replyToTop = replyTo.topicRootId
? replyTo.topicRootId ? replyTo.topicRootId
: LookupReplyToTop(_history, to); : LookupReplyToTop(_history, to);

View file

@ -68,6 +68,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_changes.h" #include "data/data_changes.h"
#include "data/data_drafts.h" #include "data/data_drafts.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_todo_list.h"
#include "data/data_web_page.h" #include "data/data_web_page.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_photo.h" #include "data/data_photo.h"
@ -8548,7 +8549,7 @@ void HistoryWidget::clearFieldText(
void HistoryWidget::replyToMessage(FullReplyTo id) { void HistoryWidget::replyToMessage(FullReplyTo id) {
if (const auto item = session().data().message(id.messageId)) { if (const auto item = session().data().message(id.messageId)) {
if (CanSendReply(item) && !base::IsCtrlPressed()) { if (CanSendReply(item) && !base::IsCtrlPressed()) {
replyToMessage(item, id.quote, id.quoteOffset); replyToMessage(item, id);
} else if (item->allowsForward()) { } else if (item->allowsForward()) {
const auto show = controller()->uiShow(); const auto show = controller()->uiShow();
HistoryView::Controls::ShowReplyToChatBox(show, id); HistoryView::Controls::ShowReplyToChatBox(show, id);
@ -8561,16 +8562,12 @@ void HistoryWidget::replyToMessage(FullReplyTo id) {
void HistoryWidget::replyToMessage( void HistoryWidget::replyToMessage(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
TextWithEntities quote, FullReplyTo fields) {
int quoteOffset) {
if (isJoinChannel()) { if (isJoinChannel()) {
return; return;
} }
_processingReplyTo = { fields.messageId = item->fullId();
.messageId = item->fullId(), _processingReplyTo = fields;
.quote = quote,
.quoteOffset = quoteOffset,
};
_processingReplyItem = item; _processingReplyItem = item;
processReply(); processReply();
} }
@ -9231,11 +9228,24 @@ void HistoryWidget::updateReplyEditText(not_null<HistoryItem*> item) {
.session = &session(), .session = &session(),
.repaint = [=] { updateField(); }, .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( _replyEditMsgText.setMarkedText(
st::defaultTextStyle, st::defaultTextStyle,
((_editMsgId || _replyTo.quote.empty()) text,
? item->inReplyText()
: _replyTo.quote),
Ui::DialogTextOptions(), Ui::DialogTextOptions(),
context); context);
if (fieldOrDisabledShown() || isRecording()) { if (fieldOrDisabledShown() || isRecording()) {
@ -9321,10 +9331,9 @@ void HistoryWidget::updateReplyToName() {
.customEmojiLoopLimit = 1, .customEmojiLoopLimit = 1,
}); });
const auto to = _replyEditMsg ? _replyEditMsg : _kbReplyTo; const auto to = _replyEditMsg ? _replyEditMsg : _kbReplyTo;
const auto replyToQuote = _replyTo && !_replyTo.quote.empty();
_replyToName.setMarkedText( _replyToName.setMarkedText(
st::fwdTextStyle, st::fwdTextStyle,
HistoryView::Reply::ComposePreviewName(_history, to, replyToQuote), HistoryView::Reply::ComposePreviewName(_history, to, _replyTo),
Ui::NameTextOptions(), Ui::NameTextOptions(),
context); context);
} }

View file

@ -205,8 +205,7 @@ public:
void replyToMessage(FullReplyTo id); void replyToMessage(FullReplyTo id);
void replyToMessage( void replyToMessage(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
TextWithEntities quote = {}, FullReplyTo fields = {});
int quoteOffset = 0);
void editMessage( void editMessage(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
const TextSelection &selection); const TextSelection &selection);

View file

@ -492,10 +492,9 @@ void FieldHeader::setShownMessage(HistoryItem *item) {
.customEmojiLoopLimit = 1, .customEmojiLoopLimit = 1,
}); });
const auto replyTo = _replyTo.current(); const auto replyTo = _replyTo.current();
const auto quote = replyTo && !replyTo.quote.empty();
_shownMessageName.setMarkedText( _shownMessageName.setMarkedText(
st::fwdTextStyle, st::fwdTextStyle,
HistoryView::Reply::ComposePreviewName(_history, item, quote), HistoryView::Reply::ComposePreviewName(_history, item, replyTo),
Ui::NameTextOptions(), Ui::NameTextOptions(),
context); context);
} else { } else {

View file

@ -639,9 +639,14 @@ bool AddReplyToMessageAction(
return false; return false;
} }
const auto todoListTaskId = request.link
? request.link->property(kTodoListItemIdProperty).toInt()
: 0;
const auto &quote = request.quote; const auto &quote = request.quote;
auto text = (quote.text.empty() auto text = (quote.text.empty()
? tr::lng_context_reply_msg ? tr::lng_context_reply_msg
: todoListTaskId
? tr::lng_context_reply_to_task
: tr::lng_context_quote_and_reply)( : tr::lng_context_quote_and_reply)(
tr::now, tr::now,
Ui::Text::FixAmpersandInAction); Ui::Text::FixAmpersandInAction);
@ -650,6 +655,7 @@ bool AddReplyToMessageAction(
.messageId = itemId, .messageId = itemId,
.quote = quote.text, .quote = quote.text,
.quoteOffset = quote.offset, .quoteOffset = quote.offset,
.todoItemId = todoListTaskId,
}, base::IsCtrlPressed()); }, base::IsCtrlPressed());
}, &st::menuIconReply); }, &st::menuIconReply);
return true; return true;

View file

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_peer.h" #include "data/data_peer.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_story.h" #include "data/data_story.h"
#include "data/data_todo_list.h"
#include "data/data_user.h" #include "data/data_user.h"
#include "history/view/history_view_item_preview.h" #include "history/view/history_view_item_preview.h"
#include "history/history.h" #include "history/history.h"
@ -38,6 +39,85 @@ namespace {
constexpr auto kNonExpandedLinesLimit = 5; 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<Main::Session*> session) {
return Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
MakeTaskDoneImage(),
QMargins(0, st::lineWidth, st::lineWidth, 0)));
}
[[nodiscard]] TextWithEntities TaskIcon(not_null<Main::Session*> session) {
return Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
MakeTaskImage(),
QMargins(0, st::lineWidth, st::lineWidth, 0)));
}
} // namespace } // namespace
void ValidateBackgroundEmoji( void ValidateBackgroundEmoji(
@ -193,6 +273,22 @@ void Reply::update(
const auto item = view->data(); const auto item = view->data();
const auto &fields = data->fields(); const auto &fields = data->fields();
const auto message = data->resolvedMessage.get(); 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 story = data->resolvedStory.get();
const auto externalMedia = fields.externalMedia.get(); const auto externalMedia = fields.externalMedia.get();
if (!_externalSender) { if (!_externalSender) {
@ -210,7 +306,6 @@ void Reply::update(
_hiddenSenderColorIndexPlusOne = (!_colorPeer && message) _hiddenSenderColorIndexPlusOne = (!_colorPeer && message)
? (message->originalHiddenSenderInfo()->colorIndex + 1) ? (message->originalHiddenSenderInfo()->colorIndex + 1)
: 0; : 0;
const auto hasPreview = (story && story->hasReplyPreview()) const auto hasPreview = (story && story->hasReplyPreview())
|| (message || (message
&& message->media() && message->media()
@ -225,8 +320,13 @@ void Reply::update(
&& !fields.quote.empty(); && !fields.quote.empty();
_hasQuoteIcon = hasQuoteIcon ? 1 : 0; _hasQuoteIcon = hasQuoteIcon ? 1 : 0;
const auto session = &view->history()->session();
const auto text = (!_displaying && data->unavailable()) const auto text = (!_displaying && data->unavailable())
? TextWithEntities() ? TextWithEntities()
: task
? Ui::Text::Colorized(task->completionDate
? TaskDoneIcon(session)
: TaskIcon(session)).append(task->text)
: (message && (fields.quote.empty() || !fields.manualQuote)) : (message && (fields.quote.empty() || !fields.manualQuote))
? message->inReplyText() ? message->inReplyText()
: !fields.quote.empty() : !fields.quote.empty()
@ -867,18 +967,28 @@ TextWithEntities Reply::ForwardEmoji(not_null<Data::Session*> owner) {
TextWithEntities Reply::ComposePreviewName( TextWithEntities Reply::ComposePreviewName(
not_null<History*> history, not_null<History*> history,
not_null<HistoryItem*> to, not_null<HistoryItem*> to,
bool quote) { const FullReplyTo &replyTo) {
const auto sender = [&] { const auto sender = [&] {
if (const auto from = to->displayFrom()) { if (const auto from = to->displayFrom()) {
return not_null(from); return not_null(from);
} }
return to->author(); 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 toPeer = to->history()->peer;
const auto displayAsExternal = (to->history() != history); const auto displayAsExternal = (to->history() != history);
const auto groupNameAdded = displayAsExternal const auto groupNameAdded = displayAsExternal
&& (toPeer != sender) && (toPeer != sender)
&& (toPeer->isChat() || toPeer->isMegagroup()); && (toPeer->isChat() || toPeer->isMegagroup());
const auto quote = replyTo && !replyTo.quote.empty();
const auto shorten = groupNameAdded || quote; const auto shorten = groupNameAdded || quote;
auto nameFull = TextWithEntities(); auto nameFull = TextWithEntities();

View file

@ -110,7 +110,7 @@ public:
[[nodiscard]] static TextWithEntities ComposePreviewName( [[nodiscard]] static TextWithEntities ComposePreviewName(
not_null<History*> history, not_null<History*> history,
not_null<HistoryItem*> to, not_null<HistoryItem*> to,
bool quote); const FullReplyTo &replyTo);
private: private:
[[nodiscard]] Ui::Text::GeometryDescriptor textGeometry( [[nodiscard]] Ui::Text::GeometryDescriptor textGeometry(

View file

@ -334,9 +334,11 @@ void TodoList::updateTasks(bool skipAnimations) {
ClickHandlerPtr TodoList::createTaskClickHandler( ClickHandlerPtr TodoList::createTaskClickHandler(
const Task &task) { const Task &task) {
const auto id = task.id; const auto id = task.id;
return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] { auto result = std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
toggleCompletion(id); toggleCompletion(id);
})); }));
result->setProperty(kTodoListItemIdProperty, id);
return result;
} }
void TodoList::startToggleAnimation(Task &task) { void TodoList::startToggleAnimation(Task &task) {
@ -375,11 +377,24 @@ void TodoList::toggleCompletion(int id) {
if (i == end(_tasks)) { if (i == end(_tasks)) {
return; return;
} }
const auto selected = (i->completionDate != 0); const auto selected = (i->completionDate != 0);
i->completionDate = selected ? TimeId() : base::unixtime::now(); i->completionDate = selected ? TimeId() : base::unixtime::now();
if (!selected) { if (!selected) {
i->setCompletedBy(_parent->history()->session().user()); 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); startToggleAnimation(*i);
repaint(); repaint();