[poll-view] Added initial support of reply to poll answers.

This commit is contained in:
23rd
2026-03-24 16:55:13 +03:00
parent 9f887dda4b
commit 554ba411f6
23 changed files with 266 additions and 29 deletions
+2
View File
@@ -4973,6 +4973,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_reply_msg" = "Reply";
"lng_context_quote_and_reply" = "Quote & Reply";
"lng_context_reply_to_task" = "Reply to Task";
"lng_context_reply_to_poll_option" = "Reply to Option";
"lng_context_edit_msg" = "Edit";
"lng_context_draw" = "Edit Image";
"lng_context_add_factcheck" = "Add Fact Check";
@@ -5214,6 +5215,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"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_preview_reply_to_poll_option" = "Reply to poll option from {title}";
"lng_suggest_bar_title" = "Suggest a Post Below";
"lng_suggest_bar_text" = "Click to offer a price for publishing.";
+4 -1
View File
@@ -220,10 +220,13 @@ void Polls::reloadResults(not_null<HistoryItem*> item) {
if (!item->isRegular() || _pollReloadRequestIds.contains(itemId)) {
return;
}
const auto media = item->media();
const auto poll = media ? media->poll() : nullptr;
const auto pollHash = poll ? poll->hash : uint64(0);
const auto requestId = _api.request(MTPmessages_GetPollResults(
item->history()->peer->input(),
MTP_int(item->id),
MTP_long(0)
MTP_long(pollHash)
)).done([=](const MTPUpdates &result) {
_pollReloadRequestIds.erase(itemId);
_session->updates().applyUpdates(result);
@@ -19,7 +19,8 @@ constexpr auto kReactionsCountEmojiProperty = 0x05;
constexpr auto kDocumentFilenameTooltipProperty = 0x06;
constexpr auto kPhoneNumberLinkProperty = 0x07;
constexpr auto kTodoListItemIdProperty = 0x08;
constexpr auto kFastShareProperty = 0x09;
constexpr auto kPollOptionProperty = 0x09;
constexpr auto kFastShareProperty = 0x0a;
namespace Ui {
class Show;
+5 -2
View File
@@ -91,7 +91,10 @@ MTPInputReplyTo ReplyToForMTP(
| (quoteEntities.v.isEmpty()
? Flag()
: Flag::f_quote_entities)
| (replyTo.todoItemId ? Flag::f_todo_item_id : Flag())),
| (replyTo.todoItemId ? Flag::f_todo_item_id : Flag())
| (replyTo.pollOption.isEmpty()
? Flag()
: Flag::f_poll_option)),
MTP_int(replyTo.messageId ? replyTo.messageId.msg : 0),
MTP_int(replyTo.topicRootId),
(external
@@ -104,7 +107,7 @@ MTPInputReplyTo ReplyToForMTP(
? history->owner().peer(replyToMonoforumPeerId)->input()
: MTPInputPeer()),
MTP_int(replyTo.todoItemId),
MTPbytes()); // poll_option
MTP_bytes(replyTo.pollOption));
} else if (history->peer->amMonoforumAdmin()
&& replyTo.monoforumPeerId) {
const auto replyToMonoforumPeer = replyTo.monoforumPeerId
+4 -2
View File
@@ -176,9 +176,10 @@ struct MessageHighlightId {
TextWithEntities quote;
int quoteOffset = 0;
int todoItemId = 0;
QByteArray pollOption;
[[nodiscard]] bool empty() const {
return quote.empty() && !todoItemId;
return quote.empty() && !todoItemId && pollOption.isEmpty();
}
[[nodiscard]] friend inline bool operator==(
const MessageHighlightId &a,
@@ -193,9 +194,10 @@ struct FullReplyTo {
PeerId monoforumPeerId = 0;
int quoteOffset = 0;
int todoItemId = 0;
QByteArray pollOption;
[[nodiscard]] MessageHighlightId highlight() const {
return { quote, quoteOffset, todoItemId };
return { quote, quoteOffset, todoItemId, pollOption };
}
[[nodiscard]] bool replying() const {
return messageId || (storyId && storyId.peer);
+1
View File
@@ -161,6 +161,7 @@ bool PollData::applyChanges(const MTPDpoll &poll) {
}
}
}
hash = poll.vhash().v;
++version;
return true;
}
+1
View File
@@ -105,6 +105,7 @@ struct PollData {
TimeId closeDate = 0;
int totalVoters = 0;
int version = 0;
uint64 hash = 0;
static constexpr auto kMaxOptions = 32;
@@ -2501,6 +2501,9 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
const auto todoListTaskId = link
? link->property(kTodoListItemIdProperty).toInt()
: 0;
const auto pollOptionLink = link
? link->property(kPollOptionProperty).toByteArray()
: QByteArray();
const auto session = &this->session();
_whoReactedMenuLifetime.destroy();
if (!clickedReaction.empty() && leaderOrSelf) {
@@ -2909,6 +2912,8 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
? tr::lng_context_quote_and_reply
: todoListTaskId
? tr::lng_context_reply_to_task
: !pollOptionLink.isEmpty()
? tr::lng_context_reply_to_poll_option
: tr::lng_context_reply_msg)(
tr::now,
Ui::Text::FixAmpersandInAction);
@@ -2920,6 +2925,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
.quote = selected.highlight.quote,
.quoteOffset = selected.highlight.quoteOffset,
.todoItemId = todoListTaskId,
.pollOption = pollOptionLink,
});
if (!selected.highlight.quote.empty()) {
_widget->clearSelected();
@@ -361,6 +361,8 @@ ReplyFields ReplyFields::clone(not_null<HistoryItem*> parent) const {
.messageId = messageId,
.topMessageId = topMessageId,
.storyId = storyId,
.todoItemId = todoItemId,
.pollOption = pollOption,
.quoteOffset = quoteOffset,
.manualQuote = manualQuote,
.topicPost = topicPost,
@@ -389,6 +391,7 @@ ReplyFields ReplyFieldsFromMTP(
result.topicPost = data.is_forum_topic() ? 1 : 0;
}
result.todoItemId = data.vtodo_item_id().value_or_empty();
result.pollOption = data.vpoll_option().value_or_empty();
if (const auto header = data.vreply_from()) {
const auto &data = header->data();
result.externalPostAuthor
@@ -443,6 +446,8 @@ FullReplyTo ReplyToFromMTP(
data.vquote_entities().value_or_empty()),
};
result.quoteOffset = data.vquote_offset().value_or_empty();
result.todoItemId = data.vtodo_item_id().value_or_empty();
result.pollOption = data.vpoll_option().value_or_empty();
return result;
}, [&](const MTPDinputReplyToStory &data) {
if (const auto parsed = Data::PeerFromInputMTP(
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "data/data_cloud_file.h"
#include "data/data_poll.h"
#include "history/history_item.h"
#include "spellcheck/spellcheck_types.h" // LanguageId.
#include "ui/empty_userpic.h"
@@ -286,6 +287,7 @@ struct ReplyFields {
MsgId topMessageId = 0;
StoryId storyId = 0;
int todoItemId = 0;
QByteArray pollOption;
uint32 quoteOffset : 30 = 0;
uint32 manualQuote : 1 = 0;
uint32 topicPost : 1 = 0;
@@ -906,6 +906,9 @@ MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) {
? Flag()
: Flag::f_quote_entities)
| (replyTo.todoItemId ? Flag::f_todo_item_id : Flag())
| (replyTo.pollOption.isEmpty()
? Flag()
: Flag::f_poll_option)
| (topicPost ? Flag::f_forum_topic : Flag())),
MTP_int(replyTo.messageId.msg),
peerToMTP(externalPeerId),
@@ -916,7 +919,7 @@ MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) {
quoteEntities,
MTP_int(replyTo.quoteOffset),
MTP_int(replyTo.todoItemId),
MTPbytes()); // poll_option
MTP_bytes(replyTo.pollOption));
}
return MTPMessageReplyHeader();
}
@@ -66,6 +66,7 @@ Ui::ChatPaintHighlight ElementHighlighter::state(
auto result = _animation.state();
result.range = _highlighted.part;
result.todoItemId = _highlighted.todoListId;
result.pollOption = _highlighted.pollOption;
return result;
}
return {};
@@ -90,20 +91,37 @@ ElementHighlighter::Highlight ElementHighlighter::computeHighlight(
leaderId,
leaderView->selectionFromQuote(quote),
quote.highlight.todoItemId,
quote.highlight.pollOption,
};
}
}
return { leaderId, {}, quote.highlight.todoItemId };
return {
leaderId,
{},
quote.highlight.todoItemId,
quote.highlight.pollOption,
};
} else if (quote.highlight.quote.empty()) {
return { item->fullId(), {}, quote.highlight.todoItemId };
return {
item->fullId(),
{},
quote.highlight.todoItemId,
quote.highlight.pollOption,
};
} else if (const auto view = _viewForItem(item)) {
return {
item->fullId(),
view->selectionFromQuote(quote),
quote.highlight.todoItemId,
quote.highlight.pollOption,
};
}
return { item->fullId(), {}, quote.highlight.todoItemId };
return {
item->fullId(),
{},
quote.highlight.todoItemId,
quote.highlight.pollOption,
};
}
void ElementHighlighter::highlight(Highlight data) {
@@ -66,6 +66,7 @@ private:
FullMsgId itemId;
TextSelection part;
int todoListId = 0;
QByteArray pollOption;
explicit operator bool() const {
return itemId.operator bool();
@@ -1592,12 +1592,18 @@ int HistoryWidget::itemTopForHighlight(
if (heightLeft >= 0) {
return std::max(itemTop - (heightLeft / 2), 0);
} else if (const auto highlight = itemHighlight(item)
; (!highlight.range.empty() || highlight.todoItemId)
; (!highlight.range.empty()
|| highlight.todoItemId
|| !highlight.pollOption.isEmpty())
&& !IsSubGroupSelection(highlight.range)) {
const auto sel = highlight.range;
const auto single = st::messageTextStyle.font->height;
const auto todoy = sel.empty()
? HistoryView::FindViewTaskY(view, highlight.todoItemId)
? (highlight.todoItemId
? HistoryView::FindViewTaskY(view, highlight.todoItemId)
: !highlight.pollOption.isEmpty()
? HistoryView::FindViewPollOptionY(view, highlight.pollOption)
: 0)
: 0;
const auto begin = sel.empty()
? (todoy - 4 * single)
@@ -9656,7 +9662,10 @@ void HistoryWidget::updateReplyEditText(not_null<HistoryItem*> item) {
.repaint = [=] { updateField(); },
});
const auto text = [&] {
const auto media = _replyTo.todoItemId ? item->media() : nullptr;
const auto media = (_replyTo.todoItemId
|| !_replyTo.pollOption.isEmpty())
? item->media()
: nullptr;
if (const auto todolist = media ? media->todolist() : nullptr) {
const auto i = ranges::find(
todolist->items,
@@ -9666,6 +9675,12 @@ void HistoryWidget::updateReplyEditText(not_null<HistoryItem*> item) {
return i->text;
}
}
if (const auto poll = media ? media->poll() : nullptr) {
if (const auto answer = poll->answerByOption(
_replyTo.pollOption)) {
return answer->text;
}
}
return (_editMsgId || _replyTo.quote.empty())
? item->inReplyText()
: _replyTo.quote;
@@ -1065,6 +1065,7 @@ void ChatWidget::setupSwipeReplyAndBack() {
.quote = selected.highlight.quote,
.quoteOffset = selected.highlight.quoteOffset,
.todoItemId = selected.highlight.todoItemId,
.pollOption = selected.highlight.pollOption,
});
};
return result;
@@ -623,9 +623,14 @@ bool AddReplyToMessageAction(
const auto todoListTaskId = request.link
? request.link->property(kTodoListItemIdProperty).toInt()
: 0;
const auto pollOption = request.link
? request.link->property(kPollOptionProperty).toByteArray()
: QByteArray();
const auto &quote = request.quote;
auto text = (todoListTaskId
? tr::lng_context_reply_to_task
: !pollOption.isEmpty()
? tr::lng_context_reply_to_poll_option
: quote.highlight.quote.empty()
? tr::lng_context_reply_msg
: tr::lng_context_quote_and_reply)(
@@ -637,6 +642,7 @@ bool AddReplyToMessageAction(
.quote = quote.highlight.quote,
.quoteOffset = quote.highlight.quoteOffset,
.todoItemId = todoListTaskId,
.pollOption = pollOption,
}, base::IsCtrlPressed());
}, &st::menuIconReply);
return true;
@@ -3059,6 +3059,72 @@ int FindViewTaskY(not_null<Element*> view, int taskId, int yfrom) {
return origin.y() + (yfrom + ytill) / 2;
}
int FindViewPollOptionY(
not_null<Element*> view,
const QByteArray &option,
int yfrom) {
auto request = HistoryView::StateRequest();
request.flags = Ui::Text::StateRequest::Flag::LookupLink;
const auto single = st::messageTextStyle.font->height;
const auto inner = view->innerGeometry();
const auto origin = inner.topLeft();
if (origin.y() < 0
|| origin.y() + inner.height() > view->height()
|| inner.height() <= 0) {
return yfrom;
}
const auto media = view->data()->media();
const auto poll = media ? media->poll() : nullptr;
if (!poll) {
return yfrom;
}
const auto &answers = poll->answers;
const auto indexOf = [&](const QByteArray &opt) -> int {
return int(ranges::find(
answers, opt, &PollAnswer::option) - begin(answers));
};
const auto index = indexOf(option);
const auto count = int(answers.size());
if (index == count) {
return yfrom;
}
yfrom = std::max(yfrom - origin.y(), 0);
auto ytill = inner.height() - 1;
const auto middle = (yfrom + ytill) / 2;
const auto fory = [&](int y) {
const auto state = view->textState(origin + QPoint(0, y), request);
const auto &link = state.link;
const auto opt = link
? link->property(kPollOptionProperty).toByteArray()
: QByteArray();
const auto idx = !opt.isEmpty() ? indexOf(opt) : count;
return (idx < count) ? idx : (y < middle) ? -1 : count;
};
auto indexfrom = fory(yfrom);
auto indextill = fory(ytill);
if ((yfrom >= ytill) || (indexfrom >= index)) {
return origin.y() + yfrom;
} else if (indextill <= index) {
return origin.y() + ytill;
}
while (ytill - yfrom >= 2 * single) {
const auto mid = (yfrom + ytill) / 2;
const auto found = fory(mid);
if (found == index
|| indexfrom > found
|| indextill < found) {
return origin.y() + mid;
} else if (found < index) {
yfrom = mid;
indexfrom = found;
} else {
ytill = mid;
indextill = found;
}
}
return origin.y() + (yfrom + ytill) / 2;
}
Window::SessionController *ExtractController(const ClickContext &context) {
const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) {
@@ -789,6 +789,11 @@ private:
int taskId,
int yfrom = 0);
[[nodiscard]] int FindViewPollOptionY(
not_null<Element*> view,
const QByteArray &option,
int yfrom = 0);
[[nodiscard]] Window::SessionController *ExtractController(
const ClickContext &context);
@@ -764,12 +764,18 @@ std::optional<int> ListWidget::scrollTopForView(
if (heightLeft >= 0) {
return std::max(top - (heightLeft / 2), 0);
} else if (const auto highlight = _highlighter.state(view->data())
; (!highlight.range.empty() || highlight.todoItemId)
; (!highlight.range.empty()
|| highlight.todoItemId
|| !highlight.pollOption.isEmpty())
&& !IsSubGroupSelection(highlight.range)) {
const auto sel = highlight.range;
const auto single = st::messageTextStyle.font->height;
const auto todoy = sel.empty()
? HistoryView::FindViewTaskY(view, highlight.todoItemId)
? (highlight.todoItemId
? HistoryView::FindViewTaskY(view, highlight.todoItemId)
: !highlight.pollOption.isEmpty()
? HistoryView::FindViewPollOptionY(view, highlight.pollOption)
: 0)
: 0;
const auto begin = sel.empty()
? (todoy - 4 * single)
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/stickers/data_custom_emoji.h"
#include "data/data_channel.h"
#include "data/data_peer.h"
#include "data/data_poll.h"
#include "data/data_session.h"
#include "data/data_story.h"
#include "data/data_todo_list.h"
@@ -310,7 +311,8 @@ 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)
const auto messageMedia = (message
&& (fields.todoItemId || !fields.pollOption.isEmpty()))
? message->media()
: nullptr;
const auto messageTodoList = messageMedia
@@ -326,6 +328,12 @@ void Reply::update(
&& taskIndex < messageTodoList->items.size())
? &messageTodoList->items[taskIndex]
: nullptr;
const auto messagePoll = messageMedia
? messageMedia->poll()
: nullptr;
const auto pollAnswer = messagePoll
? messagePoll->answerByOption(fields.pollOption)
: nullptr;
const auto story = data->resolvedStory.get();
const auto externalMedia = fields.externalMedia.get();
if (!_externalSender) {
@@ -374,6 +382,11 @@ void Reply::update(
.image = MakeTaskImage(),
.margin = QMargins(0, st::lineWidth, st::lineWidth, 0),
})).append(task->text)
: pollAnswer
? Ui::Text::Colorized(helper.image({
.image = MakeTaskImage(),
.margin = QMargins(0, st::lineWidth, st::lineWidth, 0),
})).append(pollAnswer->text)
: (message && (fields.quote.empty() || !fields.manualQuote))
? message->inReplyText()
: !fields.quote.empty()
@@ -431,6 +444,7 @@ void Reply::setLinkFrom(
.quote = fields.manualQuote ? fields.quote : TextWithEntities(),
.quoteOffset = int(fields.quoteOffset),
.todoItemId = fields.todoItemId,
.pollOption = fields.pollOption,
};
const auto returnToId = view->data()->fullId();
const auto externalLink = [=](ClickContext context) {
@@ -1033,13 +1047,22 @@ TextWithEntities Reply::ComposePreviewName(
}
return to->author();
}();
if (const auto media = replyTo.todoItemId ? to->media() : nullptr) {
if (const auto media = (replyTo.todoItemId
|| !replyTo.pollOption.isEmpty())
? to->media()
: nullptr) {
if (const auto todolist = media->todolist()) {
return tr::lng_preview_reply_to_task(
tr::now,
lt_title,
todolist->title,
tr::marked);
} else if (const auto poll = media->poll()) {
return tr::lng_preview_reply_to_poll_option(
tr::now,
lt_title,
poll->question,
tr::marked);
}
}
const auto toPeer = to->history()->peer;
@@ -282,6 +282,11 @@ PollThumbnailData MakePollThumbnail(
});
});
}
if (result.handler) {
result.handler->setProperty(
kPollOptionProperty,
answer.option);
}
return result;
}
@@ -1202,17 +1207,29 @@ void Poll::updateAnswers() {
ClickHandlerPtr Poll::createAnswerClickHandler(
const Answer &answer) {
const auto option = answer.option;
auto result = ClickHandlerPtr();
if (_flags & PollData::Flag::MultiChoice) {
return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
toggleMultiOption(option);
result = std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
if (canVote()) {
toggleMultiOption(option);
} else if (showVotes()) {
showAnswerVotesTooltip(option);
}
}));
} else {
result = std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
if (canVote()) {
_votedFromHere = true;
history()->session().api().polls().sendVotes(
_parent->data()->fullId(),
{ option });
} else if (showVotes()) {
showAnswerVotesTooltip(option);
}
}));
}
return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
_votedFromHere = true;
history()->session().api().polls().sendVotes(
_parent->data()->fullId(),
{ option });
}));
result->setProperty(kPollOptionProperty, option);
return result;
}
void Poll::toggleMultiOption(const QByteArray &option) {
@@ -1255,6 +1272,25 @@ void Poll::sendMultiOptions() {
}
}
void Poll::showAnswerVotesTooltip(const QByteArray &option) {
const auto answer = _poll->answerByOption(option);
if (!answer) {
return;
}
const auto quiz = _poll->quiz();
const auto text = answer->votes
? (quiz
? tr::lng_polls_answers_count
: tr::lng_polls_votes_count)(
tr::now,
lt_count_decimal,
answer->votes)
: (quiz
? tr::lng_polls_answers_none
: tr::lng_polls_votes_none)(tr::now);
_parent->delegate()->elementShowTooltip({ text }, [] {});
}
void Poll::showResults() {
_parent->delegate()->elementShowPollResults(
_poll,
@@ -1919,6 +1955,30 @@ int Poll::paintAnswer(
int outerWidth,
const PaintContext &context) const {
const auto height = countAnswerHeight(answer, width);
if (!context.highlight.pollOption.isEmpty()
&& context.highlight.pollOption == answer.option
&& context.highlight.collapsion > 0.) {
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,
this->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(this->width(), to.width()),
lerp(height, to.height()));
}
}
const auto stm = context.messageStyle();
const auto &answerPadding = answer.thumbnail
? st::historyPollAnswerPadding
@@ -2623,10 +2683,13 @@ TextState Poll::textState(QPoint point, StateRequest request) const {
media,
media).contains(point)) {
result.link = answer.mediaHandler;
} else if (can) {
_lastLinkPoint = point;
} else {
if (can) {
_lastLinkPoint = point;
}
result.link = answer.handler;
} else if (show) {
}
if (!can && show) {
result.customTooltip = true;
using Flag = Ui::Text::StateRequest::Flag;
if (request.flags & Flag::LookupCustomTooltip) {
@@ -2734,7 +2797,9 @@ void Poll::clickHandlerPressedChanged(
handler,
&Answer::handler);
if (i != end(_answers)) {
toggleRipple(*i, pressed);
if (canVote()) {
toggleRipple(*i, pressed);
}
} else if (handler == _sendVotesLink || handler == _showResultsLink) {
toggleLinkRipple(pressed);
}
@@ -207,6 +207,7 @@ private:
void toggleMultiOption(const QByteArray &option);
void sendMultiOptions();
void showResults();
void showAnswerVotesTooltip(const QByteArray &option);
void checkQuizAnswered();
void showSolution() const;
void solutionToggled(
@@ -183,6 +183,7 @@ struct ChatPaintHighlight {
float64 collapsion = 0.;
TextSelection range;
int todoItemId = 0;
QByteArray pollOption;
};
struct ChatPaintContext {