mirror of
https://github.com/vale981/tdesktop
synced 2025-03-09 12:36:39 -04:00
620 lines
16 KiB
C++
620 lines
16 KiB
C++
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
#include "history/media/history_media_poll.h"
|
|
|
|
#include "lang/lang_keys.h"
|
|
#include "history/history.h"
|
|
#include "history/history_item.h"
|
|
#include "history/view/history_view_element.h"
|
|
#include "history/view/history_view_cursor_state.h"
|
|
#include "calls/calls_instance.h"
|
|
#include "ui/text_options.h"
|
|
#include "data/data_media_types.h"
|
|
#include "data/data_poll.h"
|
|
#include "data/data_session.h"
|
|
#include "layout.h"
|
|
#include "auth_session.h"
|
|
#include "apiwrap.h"
|
|
#include "styles/style_history.h"
|
|
#include "styles/style_widgets.h"
|
|
|
|
namespace {
|
|
|
|
using TextState = HistoryView::TextState;
|
|
|
|
struct FormattedLargeNumber {
|
|
int rounded = 0;
|
|
bool shortened = false;
|
|
QString text;
|
|
};
|
|
|
|
FormattedLargeNumber FormatLargeNumber(int64 number) {
|
|
auto result = FormattedLargeNumber();
|
|
const auto abs = std::abs(number);
|
|
const auto shorten = [&](int64 divider, char multiplier) {
|
|
const auto sign = (number > 0) ? 1 : -1;
|
|
const auto rounded = abs / (divider / 10);
|
|
result.rounded = sign * rounded * (divider / 10);
|
|
result.text = QString::number(sign * rounded / 10);
|
|
if (rounded % 10) {
|
|
result.text += '.' + QString::number(rounded % 10) + multiplier;
|
|
} else {
|
|
result.text += multiplier;
|
|
}
|
|
result.shortened = true;
|
|
};
|
|
if (abs >= 1'000'000) {
|
|
shorten(1'000'000, 'M');
|
|
} else if (abs >= 10'000) {
|
|
shorten(1'000, 'K');
|
|
} else {
|
|
result.rounded = number;
|
|
result.text = QString::number(number);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
HistoryPoll::Answer::Answer() : text(st::msgMinWidth / 2) {
|
|
}
|
|
|
|
void HistoryPoll::Answer::fillText(const PollAnswer &original) {
|
|
if (!text.isEmpty() && text.originalText() == original.text) {
|
|
return;
|
|
}
|
|
text.setText(
|
|
st::historyPollAnswerStyle,
|
|
original.text,
|
|
Ui::WebpageTextTitleOptions());
|
|
}
|
|
|
|
HistoryPoll::HistoryPoll(
|
|
not_null<Element*> parent,
|
|
not_null<PollData*> poll)
|
|
: HistoryMedia(parent)
|
|
, _poll(poll)
|
|
, _question(st::msgMinWidth / 2) {
|
|
Auth().data().registerPollView(_poll, _parent);
|
|
}
|
|
|
|
QSize HistoryPoll::countOptimalSize() {
|
|
updateTexts();
|
|
|
|
const auto paddings = st::msgPadding.left() + st::msgPadding.right();
|
|
|
|
auto maxWidth = st::msgFileMinWidth;
|
|
accumulate_max(maxWidth, paddings + _question.maxWidth());
|
|
for (const auto &answer : _answers) {
|
|
accumulate_max(
|
|
maxWidth,
|
|
paddings
|
|
+ st::historyPollAnswerPadding.left()
|
|
+ answer.text.maxWidth()
|
|
+ st::historyPollAnswerPadding.right());
|
|
}
|
|
|
|
const auto answersHeight = ranges::accumulate(ranges::view::all(
|
|
_answers
|
|
) | ranges::view::transform([](const Answer &answer) {
|
|
return st::historyPollAnswerPadding.top()
|
|
+ answer.text.minHeight()
|
|
+ st::historyPollAnswerPadding.bottom();
|
|
}), 0);
|
|
|
|
auto minHeight = st::historyPollQuestionTop
|
|
+ _question.minHeight()
|
|
+ st::historyPollSubtitleSkip
|
|
+ st::msgDateFont->height
|
|
+ st::historyPollAnswersSkip
|
|
+ answersHeight
|
|
+ st::msgPadding.bottom()
|
|
+ st::msgDateFont->height
|
|
+ st::msgPadding.bottom();
|
|
if (!isBubbleTop()) {
|
|
minHeight -= st::msgFileTopMinus;
|
|
}
|
|
return { maxWidth, minHeight };
|
|
}
|
|
|
|
bool HistoryPoll::canVote() const {
|
|
return !_voted && !_closed;
|
|
}
|
|
|
|
int HistoryPoll::countAnswerHeight(
|
|
const Answer &answer,
|
|
int innerWidth) const {
|
|
const auto answerWidth = innerWidth
|
|
- st::historyPollAnswerPadding.left()
|
|
- st::historyPollAnswerPadding.right();
|
|
return st::historyPollAnswerPadding.top()
|
|
+ answer.text.countHeight(answerWidth)
|
|
+ st::historyPollAnswerPadding.bottom();
|
|
}
|
|
|
|
QSize HistoryPoll::countCurrentSize(int newWidth) {
|
|
const auto paddings = st::msgPadding.left() + st::msgPadding.right();
|
|
|
|
accumulate_min(newWidth, maxWidth());
|
|
const auto innerWidth = newWidth
|
|
- st::msgPadding.left()
|
|
- st::msgPadding.right();
|
|
|
|
const auto answersHeight = ranges::accumulate(ranges::view::all(
|
|
_answers
|
|
) | ranges::view::transform([&](const Answer &answer) {
|
|
return countAnswerHeight(answer, innerWidth);
|
|
}), 0);
|
|
|
|
auto newHeight = st::historyPollQuestionTop
|
|
+ _question.countHeight(innerWidth)
|
|
+ st::historyPollSubtitleSkip
|
|
+ st::msgDateFont->height
|
|
+ st::historyPollAnswersSkip
|
|
+ answersHeight
|
|
+ st::historyPollTotalVotesSkip
|
|
+ st::msgDateFont->height
|
|
+ st::msgPadding.bottom();
|
|
if (!isBubbleTop()) {
|
|
newHeight -= st::msgFileTopMinus;
|
|
}
|
|
return { newWidth, newHeight };
|
|
}
|
|
|
|
void HistoryPoll::updateTexts() {
|
|
if (_pollVersion == _poll->version) {
|
|
return;
|
|
}
|
|
_pollVersion = _poll->version;
|
|
|
|
const auto willStartAnimation = checkAnimationStart();
|
|
|
|
_closed = _poll->closed;
|
|
_question.setText(
|
|
st::historyPollQuestionStyle,
|
|
_poll->question,
|
|
Ui::WebpageTextTitleOptions());
|
|
_subtitle.setText(
|
|
st::msgDateTextStyle,
|
|
lang(_closed ? lng_polls_closed : lng_polls_anonymous));
|
|
|
|
updateAnswers();
|
|
updateVotes();
|
|
|
|
if (willStartAnimation) {
|
|
startAnimation();
|
|
}
|
|
}
|
|
|
|
void HistoryPoll::updateAnswers() {
|
|
const auto changed = !ranges::equal(
|
|
_answers,
|
|
_poll->answers,
|
|
ranges::equal_to(),
|
|
&Answer::option,
|
|
&PollAnswer::option);
|
|
if (!changed) {
|
|
auto &&answers = ranges::view::zip(_answers, _poll->answers);
|
|
for (auto &&[answer, original] : answers) {
|
|
answer.fillText(original);
|
|
}
|
|
return;
|
|
}
|
|
_answers = ranges::view::all(
|
|
_poll->answers
|
|
) | ranges::view::transform([](const PollAnswer &answer) {
|
|
auto result = Answer();
|
|
result.option = answer.option;
|
|
result.fillText(answer);
|
|
return result;
|
|
}) | ranges::to_vector;
|
|
|
|
for (auto &answer : _answers) {
|
|
answer.handler = createAnswerClickHandler(answer);
|
|
}
|
|
|
|
_answersAnimation = nullptr;
|
|
}
|
|
|
|
ClickHandlerPtr HistoryPoll::createAnswerClickHandler(
|
|
const Answer &answer) const {
|
|
const auto option = answer.option;
|
|
const auto itemId = _parent->data()->fullId();
|
|
return std::make_shared<LambdaClickHandler>([=] {
|
|
Auth().api().sendPollVotes(itemId, { option });
|
|
});
|
|
}
|
|
|
|
void HistoryPoll::updateVotes() const {
|
|
_voted = _poll->voted();
|
|
updateAnswerVotes();
|
|
updateTotalVotes();
|
|
}
|
|
|
|
void HistoryPoll::updateVotesCheckAnimation() const {
|
|
const auto willStartAnimation = checkAnimationStart();
|
|
updateVotes();
|
|
if (willStartAnimation) {
|
|
startAnimation();
|
|
}
|
|
}
|
|
|
|
void HistoryPoll::updateTotalVotes() const {
|
|
if (_totalVotes == _poll->totalVoters) {
|
|
return;
|
|
}
|
|
_totalVotes = _poll->totalVoters;
|
|
const auto string = [&] {
|
|
if (!_totalVotes) {
|
|
return lang(lng_polls_votes_none);
|
|
}
|
|
const auto format = FormatLargeNumber(_totalVotes);
|
|
auto text = lng_polls_votes_count(lt_count, format.rounded);
|
|
if (format.shortened) {
|
|
text.replace(QString::number(format.rounded), format.text);
|
|
}
|
|
return text;
|
|
}();
|
|
_totalVotesLabel.setText(st::msgDateTextStyle, string);
|
|
}
|
|
|
|
void HistoryPoll::updateAnswerVotesFromOriginal(
|
|
const Answer &answer,
|
|
const PollAnswer &original,
|
|
int totalVotes,
|
|
int maxVotes) const {
|
|
if (canVote()) {
|
|
answer.votesPercent.clear();
|
|
} else if (answer.votes != original.votes
|
|
|| answer.votesPercent.isEmpty()
|
|
|| std::max(_totalVotes, 1) != totalVotes) {
|
|
const auto percent = int(std::round(
|
|
original.votes * 100. / totalVotes));
|
|
answer.votesPercent = QString::number(percent) + '%';
|
|
answer.votesPercentWidth = st::historyPollPercentFont->width(
|
|
answer.votesPercent);
|
|
}
|
|
answer.votes = original.votes;
|
|
answer.filling = answer.votes / float64(maxVotes);
|
|
}
|
|
|
|
void HistoryPoll::updateAnswerVotes() const {
|
|
if (_poll->answers.size() != _answers.size()
|
|
|| _poll->answers.empty()) {
|
|
return;
|
|
}
|
|
const auto totalVotes = std::max(1, _poll->totalVoters);
|
|
const auto maxVotes = std::max(1, ranges::max_element(
|
|
_poll->answers,
|
|
ranges::less(),
|
|
&PollAnswer::votes)->votes);
|
|
auto &&answers = ranges::view::zip(_answers, _poll->answers);
|
|
for (auto &&[answer, original] : answers) {
|
|
updateAnswerVotesFromOriginal(
|
|
answer,
|
|
original,
|
|
totalVotes,
|
|
maxVotes);
|
|
}
|
|
}
|
|
|
|
void HistoryPoll::draw(Painter &p, const QRect &r, TextSelection selection, TimeMs ms) const {
|
|
if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return;
|
|
auto paintx = 0, painty = 0, paintw = width(), painth = height();
|
|
|
|
updateVotesCheckAnimation();
|
|
|
|
const auto outbg = _parent->hasOutLayout();
|
|
const auto selected = (selection == FullSelection);
|
|
const auto ®ular = selected ? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected) : (outbg ? st::msgOutDateFg : st::msgInDateFg);
|
|
|
|
const auto padding = st::msgPadding;
|
|
auto tshift = st::historyPollQuestionTop;
|
|
if (!isBubbleTop()) {
|
|
tshift -= st::msgFileTopMinus;
|
|
}
|
|
paintw -= padding.left() + padding.right();
|
|
|
|
p.setPen(outbg ? st::webPageTitleOutFg : st::webPageTitleInFg);
|
|
_question.drawLeft(p, padding.left(), tshift, paintw, width(), style::al_left, 0, -1, selection);
|
|
tshift += _question.countHeight(paintw) + st::historyPollSubtitleSkip;
|
|
|
|
p.setPen(regular);
|
|
_subtitle.drawLeftElided(p, padding.left(), tshift, paintw, width());
|
|
tshift += st::msgDateFont->height + st::historyPollAnswersSkip;
|
|
|
|
const auto progress = _answersAnimation
|
|
? _answersAnimation->progress.current(ms, 1.)
|
|
: 1.;
|
|
if (progress == 1.) {
|
|
_answersAnimation = nullptr;
|
|
}
|
|
|
|
auto &&answers = ranges::view::zip(
|
|
_answers,
|
|
ranges::view::ints(0, int(_answers.size())));
|
|
for (const auto &[answer, index] : answers) {
|
|
const auto animation = _answersAnimation
|
|
? &_answersAnimation->data[index]
|
|
: nullptr;
|
|
if (animation) {
|
|
animation->percent.update(progress, anim::linear);
|
|
animation->filling.update(progress, anim::linear);
|
|
animation->opacity.update(progress, anim::linear);
|
|
}
|
|
const auto height = paintAnswer(
|
|
p,
|
|
answer,
|
|
animation,
|
|
padding.left(),
|
|
tshift,
|
|
paintw,
|
|
width(),
|
|
selection,
|
|
ms);
|
|
tshift += height;
|
|
}
|
|
if (!_totalVotesLabel.isEmpty()) {
|
|
tshift += st::msgPadding.bottom();
|
|
p.setPen(regular);
|
|
_totalVotesLabel.drawLeftElided(p, padding.left(), tshift, paintw, width());
|
|
}
|
|
}
|
|
|
|
int HistoryPoll::paintAnswer(
|
|
Painter &p,
|
|
const Answer &answer,
|
|
const AnswerAnimation *animation,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
int outerWidth,
|
|
TextSelection selection,
|
|
TimeMs ms) const {
|
|
const auto height = countAnswerHeight(answer, width);
|
|
const auto outbg = _parent->hasOutLayout();
|
|
const auto aleft = left + st::historyPollAnswerPadding.left();
|
|
const auto awidth = width
|
|
- st::historyPollAnswerPadding.left()
|
|
- st::historyPollAnswerPadding.right();
|
|
|
|
if (animation) {
|
|
const auto opacity = animation->opacity.current();
|
|
if (opacity < 1.) {
|
|
p.setOpacity(1. - opacity);
|
|
paintRadio(p, answer, left, top, selection);
|
|
}
|
|
if (opacity > 0.) {
|
|
const auto percent = QString::number(
|
|
int(std::round(animation->percent.current()))) + '%';
|
|
const auto percentWidth = st::historyPollPercentFont->width(
|
|
percent);
|
|
p.setOpacity(opacity);
|
|
paintPercent(
|
|
p,
|
|
percent,
|
|
percentWidth,
|
|
left,
|
|
top,
|
|
outerWidth,
|
|
selection);
|
|
p.setOpacity(sqrt(opacity));
|
|
paintFilling(
|
|
p,
|
|
animation->filling.current(),
|
|
left,
|
|
top,
|
|
width,
|
|
height,
|
|
selection);
|
|
p.setOpacity(1.);
|
|
}
|
|
} else if (canVote()) {
|
|
paintRadio(p, answer, left, top, selection);
|
|
} else {
|
|
paintPercent(
|
|
p,
|
|
answer.votesPercent,
|
|
answer.votesPercentWidth,
|
|
left,
|
|
top,
|
|
outerWidth,
|
|
selection);
|
|
paintFilling(
|
|
p,
|
|
answer.filling,
|
|
left,
|
|
top,
|
|
width,
|
|
height,
|
|
selection);
|
|
}
|
|
|
|
top += st::historyPollAnswerPadding.top();
|
|
p.setPen(outbg ? st::webPageDescriptionOutFg : st::webPageDescriptionInFg);
|
|
answer.text.drawLeft(p, aleft, top, awidth, outerWidth);
|
|
|
|
return height;
|
|
}
|
|
|
|
void HistoryPoll::paintRadio(
|
|
Painter &p,
|
|
const Answer &answer,
|
|
int left,
|
|
int top,
|
|
TextSelection selection) const {
|
|
top += st::historyPollAnswerPadding.top();
|
|
|
|
const auto outbg = _parent->hasOutLayout();
|
|
const auto selected = (selection == FullSelection);
|
|
|
|
PainterHighQualityEnabler hq(p);
|
|
const auto &st = st::historyPollRadio;
|
|
const auto over = ClickHandler::showAsActive(answer.handler);
|
|
const auto ®ular = selected ? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected) : (outbg ? st::msgOutDateFg : st::msgInDateFg);
|
|
auto pen = regular->p;
|
|
pen.setWidth(st.thickness);
|
|
p.setPen(pen);
|
|
p.setBrush(Qt::NoBrush);
|
|
const auto o = p.opacity();
|
|
p.setOpacity(o * (over ? st::historyPollRadioOpacityOver : st::historyPollRadioOpacity));
|
|
p.drawEllipse(QRectF(left, top, st.diameter, st.diameter).marginsRemoved(QMarginsF(st.thickness / 2., st.thickness / 2., st.thickness / 2., st.thickness / 2.)));
|
|
p.setOpacity(o);
|
|
}
|
|
|
|
void HistoryPoll::paintPercent(
|
|
Painter &p,
|
|
const QString &percent,
|
|
int percentWidth,
|
|
int left,
|
|
int top,
|
|
int outerWidth,
|
|
TextSelection selection) const {
|
|
const auto outbg = _parent->hasOutLayout();
|
|
const auto selected = (selection == FullSelection);
|
|
const auto aleft = left + st::historyPollAnswerPadding.left();
|
|
|
|
top += st::historyPollAnswerPadding.top();
|
|
|
|
p.setFont(st::historyPollPercentFont);
|
|
p.setPen(outbg ? st::webPageDescriptionOutFg : st::webPageDescriptionInFg);
|
|
const auto pleft = aleft - percentWidth - st::historyPollPercentSkip;
|
|
p.drawTextLeft(pleft, top + st::historyPollPercentTop, outerWidth, percent, percentWidth);
|
|
}
|
|
|
|
void HistoryPoll::paintFilling(
|
|
Painter &p,
|
|
float64 filling,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
int height,
|
|
TextSelection selection) const {
|
|
const auto bottom = top + height;
|
|
const auto outbg = _parent->hasOutLayout();
|
|
const auto selected = (selection == FullSelection);
|
|
const auto aleft = left + st::historyPollAnswerPadding.left();
|
|
const auto awidth = width
|
|
- st::historyPollAnswerPadding.left()
|
|
- st::historyPollAnswerPadding.right();
|
|
|
|
top += st::historyPollAnswerPadding.top();
|
|
|
|
const auto bar = outbg ? (selected ? st::msgWaveformOutActiveSelected : st::msgWaveformOutActive) : (selected ? st::msgWaveformInActiveSelected : st::msgWaveformInActive);
|
|
PainterHighQualityEnabler hq(p);
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(bar);
|
|
const auto max = awidth - st::historyPollFillingRight;
|
|
const auto size = anim::interpolate(st::historyPollFillingMin, max, filling);
|
|
const auto radius = st::historyPollFillingRadius;
|
|
const auto ftop = bottom - st::historyPollFillingBottom - st::historyPollFillingHeight;
|
|
p.drawRoundedRect(aleft, ftop, size, st::historyPollFillingHeight, radius, radius);
|
|
}
|
|
|
|
bool HistoryPoll::answerVotesChanged() const {
|
|
if (_poll->answers.size() != _answers.size()
|
|
|| _poll->answers.empty()) {
|
|
return false;
|
|
}
|
|
return !ranges::equal(
|
|
_answers,
|
|
_poll->answers,
|
|
ranges::equal_to(),
|
|
&Answer::votes,
|
|
&PollAnswer::votes);
|
|
}
|
|
|
|
void HistoryPoll::saveStateInAnimation() const {
|
|
if (_answersAnimation) {
|
|
return;
|
|
}
|
|
const auto can = canVote();
|
|
_answersAnimation = std::make_unique<AnswersAnimation>();
|
|
_answersAnimation->data.reserve(_answers.size());
|
|
const auto convert = [&](const Answer &answer) {
|
|
auto result = AnswerAnimation();
|
|
result.percent = can
|
|
? 0.
|
|
: (answer.votes * 100. / std::max(_totalVotes, 1));
|
|
result.filling = can ? 0. : answer.filling;
|
|
result.opacity = can ? 0. : 1.;
|
|
return result;
|
|
};
|
|
ranges::transform(
|
|
_answers,
|
|
ranges::back_inserter(_answersAnimation->data),
|
|
convert);
|
|
}
|
|
|
|
bool HistoryPoll::checkAnimationStart() const {
|
|
if (_poll->answers.size() != _answers.size()) {
|
|
// Skip initial changes.
|
|
return false;
|
|
}
|
|
const auto result = (canVote() != (!_poll->voted() && !_poll->closed))
|
|
|| answerVotesChanged();
|
|
if (result) {
|
|
saveStateInAnimation();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void HistoryPoll::startAnimation() const {
|
|
if (!_answersAnimation) {
|
|
return;
|
|
}
|
|
|
|
const auto can = canVote();
|
|
auto &&both = ranges::view::zip(_answers, _answersAnimation->data);
|
|
for (auto &&[answer, data] : both) {
|
|
data.percent.start(can
|
|
? 0.
|
|
: answer.votes * 100. / std::max(_totalVotes, 1));
|
|
data.filling.start(can ? 0. : answer.filling);
|
|
data.opacity.start(can ? 0. : 1.);
|
|
}
|
|
_answersAnimation->progress.start(
|
|
[=] { Auth().data().requestViewRepaint(_parent); },
|
|
0.,
|
|
1.,
|
|
st::historyPollDuration);
|
|
}
|
|
|
|
TextState HistoryPoll::textState(QPoint point, StateRequest request) const {
|
|
auto result = TextState(_parent);
|
|
if (!canVote()) {
|
|
return result;
|
|
}
|
|
|
|
const auto padding = st::msgPadding;
|
|
auto paintw = width();
|
|
auto tshift = st::historyPollQuestionTop;
|
|
if (!isBubbleTop()) {
|
|
tshift -= st::msgFileTopMinus;
|
|
}
|
|
paintw -= padding.left() + padding.right();
|
|
|
|
tshift += _question.countHeight(paintw) + st::historyPollSubtitleSkip;
|
|
tshift += st::msgDateFont->height + st::historyPollAnswersSkip;
|
|
const auto awidth = paintw
|
|
- st::historyPollAnswerPadding.left()
|
|
- st::historyPollAnswerPadding.right();
|
|
for (const auto &answer : _answers) {
|
|
const auto height = countAnswerHeight(answer, paintw);
|
|
if (point.y() >= tshift && point.y() < tshift + height) {
|
|
result.link = answer.handler;
|
|
return result;
|
|
}
|
|
tshift += height;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
HistoryPoll::~HistoryPoll() {
|
|
Auth().data().unregisterPollView(_poll, _parent);
|
|
}
|