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_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.";

View file

@ -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;

View file

@ -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();

View file

@ -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);

View file

@ -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<HistoryItem*> 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<HistoryItem*> 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);
}

View file

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

View file

@ -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 {

View file

@ -639,9 +639,14 @@ bool AddReplyToMessageAction(
return false;
}
const auto todoListTaskId = request.link
? request.link->property(kTodoListItemIdProperty).toInt()
: 0;
const auto &quote = 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;

View file

@ -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<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
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<Data::Session*> owner) {
TextWithEntities Reply::ComposePreviewName(
not_null<History*> history,
not_null<HistoryItem*> 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();

View file

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

View file

@ -334,9 +334,11 @@ void TodoList::updateTasks(bool skipAnimations) {
ClickHandlerPtr TodoList::createTaskClickHandler(
const Task &task) {
const auto id = task.id;
return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
auto result = std::make_shared<LambdaClickHandler>(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();