Allow sending multiple votes in a poll.

This commit is contained in:
John Preston 2020-01-10 15:47:36 +03:00
parent afff7634f9
commit 2981a16e17
15 changed files with 242 additions and 38 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 B

View File

@ -2173,6 +2173,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_polls_votes_count#one" = "{count} vote"; "lng_polls_votes_count#one" = "{count} vote";
"lng_polls_votes_count#other" = "{count} votes"; "lng_polls_votes_count#other" = "{count} votes";
"lng_polls_votes_none" = "No votes"; "lng_polls_votes_none" = "No votes";
"lng_polls_submit_votes" = "Submit votes";
"lng_polls_view_results" = "View results";
"lng_polls_retract" = "Retract vote"; "lng_polls_retract" = "Retract vote";
"lng_polls_stop" = "Stop poll"; "lng_polls_stop" = "Stop poll";
"lng_polls_stop_warning" = "If you stop this poll now, nobody will be able to vote in it anymore. This action cannot be undone."; "lng_polls_stop_warning" = "If you stop this poll now, nobody will be able to vote in it anymore. This action cannot be undone.";

View File

@ -5882,13 +5882,13 @@ void ApiWrap::sendPollVotes(
const auto hideSending = [=] { const auto hideSending = [=] {
if (showSending) { if (showSending) {
if (const auto item = _session->data().message(itemId)) { if (const auto item = _session->data().message(itemId)) {
poll->sendingVote = QByteArray(); poll->sendingVotes.clear();
_session->data().requestItemRepaint(item); _session->data().requestItemRepaint(item);
} }
} }
}; };
if (showSending) { if (showSending) {
poll->sendingVote = options.front(); poll->sendingVotes = options;
_session->data().requestItemRepaint(item); _session->data().requestItemRepaint(item);
} }

View File

@ -60,7 +60,7 @@ struct PollData {
std::vector<PollAnswer> answers; std::vector<PollAnswer> answers;
std::vector<not_null<UserData*>> recentVoters; std::vector<not_null<UserData*>> recentVoters;
int totalVoters = 0; int totalVoters = 0;
QByteArray sendingVote; std::vector<QByteArray> sendingVotes;
crl::time lastResultsUpdate = 0; crl::time lastResultsUpdate = 0;
int version = 0; int version = 0;

View File

@ -573,6 +573,14 @@ historyPollRippleOpacity: 0.3;
historyPollRecentVotersSkip: 4px; historyPollRecentVotersSkip: 4px;
historyPollRecentVoterSize: 18px; historyPollRecentVoterSize: 18px;
historyPollRecentVoterSkip: 13px; historyPollRecentVoterSkip: 13px;
historyPollBottomButtonSkip: 15px;
historyPollBottomButtonTop: 4px;
historyPollChoiceRight: icon {{ "poll_choice_right", activeButtonFg }};
historyPollChoiceWrong: icon {{ "poll_choice_wrong", activeButtonFg }};
historyPollOutChosen: icon {{ "poll_select_check", historyFileOutIconFg }};
historyPollOutChosenSelected: icon {{ "poll_select_check", historyFileOutIconFgSelected }};
historyPollInChosen: icon {{ "poll_select_check", historyFileInIconFg }};
historyPollInChosenSelected: icon {{ "poll_select_check", historyFileInIconFgSelected }};
boxAttachEmoji: IconButton(historyAttachEmoji) { boxAttachEmoji: IconButton(historyAttachEmoji) {
width: 30px; width: 30px;

View File

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_cursor_state.h" #include "history/view/history_view_cursor_state.h"
#include "calls/calls_instance.h" #include "calls/calls_instance.h"
#include "ui/text_options.h" #include "ui/text_options.h"
#include "ui/text/text_utilities.h"
#include "ui/effects/animations.h" #include "ui/effects/animations.h"
#include "ui/effects/radial_animation.h" #include "ui/effects/radial_animation.h"
#include "ui/effects/ripple_animation.h" #include "ui/effects/ripple_animation.h"
@ -148,6 +149,7 @@ struct Poll::Answer {
QString votesPercentString; QString votesPercentString;
bool chosen = false; bool chosen = false;
bool correct = false; bool correct = false;
bool selected = false;
ClickHandlerPtr handler; ClickHandlerPtr handler;
mutable std::unique_ptr<Ui::RippleAnimation> ripple; mutable std::unique_ptr<Ui::RippleAnimation> ripple;
}; };
@ -184,7 +186,15 @@ Poll::Poll(
not_null<PollData*> poll) not_null<PollData*> poll)
: Media(parent) : Media(parent)
, _poll(poll) , _poll(poll)
, _question(st::msgMinWidth / 2) { , _question(st::msgMinWidth / 2)
, _showResultsLink(
std::make_shared<LambdaClickHandler>(crl::guard(
this,
[=] { showResults(); })))
, _sendVotesLink(
std::make_shared<LambdaClickHandler>(crl::guard(
this,
[=] { sendMultiOptions(); }))) {
history()->owner().registerPollView(_poll, _parent); history()->owner().registerPollView(_poll, _parent);
} }
@ -212,6 +222,9 @@ QSize Poll::countOptimalSize() {
+ st::historyPollAnswerPadding.bottom(); + st::historyPollAnswerPadding.bottom();
}), 0); }), 0);
const auto bottomButtonHeight = inlineFooter()
? 0
: st::historyPollBottomButtonSkip;
auto minHeight = st::historyPollQuestionTop auto minHeight = st::historyPollQuestionTop
+ _question.minHeight() + _question.minHeight()
+ st::historyPollSubtitleSkip + st::historyPollSubtitleSkip
@ -219,6 +232,7 @@ QSize Poll::countOptimalSize() {
+ st::historyPollAnswersSkip + st::historyPollAnswersSkip
+ answersHeight + answersHeight
+ st::msgPadding.bottom() + st::msgPadding.bottom()
+ bottomButtonHeight
+ st::msgDateFont->height + st::msgDateFont->height
+ st::msgPadding.bottom(); + st::msgPadding.bottom();
if (!isBubbleTop()) { if (!isBubbleTop()) {
@ -235,6 +249,21 @@ bool Poll::canVote() const {
return !showVotes() && IsServerMsgId(_parent->data()->id); return !showVotes() && IsServerMsgId(_parent->data()->id);
} }
bool Poll::canSendVotes() const {
return canVote() && _hasSelected;
}
bool Poll::showVotersCount() const {
return showVotes()
? !(_flags & PollData::Flag::PublicVotes)
: !(_flags & PollData::Flag::MultiChoice);
}
bool Poll::inlineFooter() const {
return !(_flags
& (PollData::Flag::PublicVotes | PollData::Flag::MultiChoice));
}
int Poll::countAnswerTop( int Poll::countAnswerTop(
const Answer &answer, const Answer &answer,
int innerWidth) const { int innerWidth) const {
@ -288,6 +317,9 @@ QSize Poll::countCurrentSize(int newWidth) {
return countAnswerHeight(answer, innerWidth); return countAnswerHeight(answer, innerWidth);
}), 0); }), 0);
const auto bottomButtonHeight = inlineFooter()
? 0
: st::historyPollBottomButtonSkip;
auto newHeight = st::historyPollQuestionTop auto newHeight = st::historyPollQuestionTop
+ _question.countHeight(innerWidth) + _question.countHeight(innerWidth)
+ st::historyPollSubtitleSkip + st::historyPollSubtitleSkip
@ -295,6 +327,7 @@ QSize Poll::countCurrentSize(int newWidth) {
+ st::historyPollAnswersSkip + st::historyPollAnswersSkip
+ answersHeight + answersHeight
+ st::historyPollTotalVotesSkip + st::historyPollTotalVotesSkip
+ bottomButtonHeight
+ st::msgDateFont->height + st::msgDateFont->height
+ st::msgPadding.bottom(); + st::msgPadding.bottom();
if (!isBubbleTop()) { if (!isBubbleTop()) {
@ -384,12 +417,59 @@ void Poll::updateAnswers() {
} }
ClickHandlerPtr Poll::createAnswerClickHandler( ClickHandlerPtr Poll::createAnswerClickHandler(
const Answer &answer) const { const Answer &answer) {
const auto option = answer.option; const auto option = answer.option;
const auto itemId = _parent->data()->fullId(); if (_flags & PollData::Flag::MultiChoice) {
return std::make_shared<LambdaClickHandler>([=] { return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
history()->session().api().sendPollVotes(itemId, { option }); toggleMultiOption(option);
}); }));
}
return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
history()->session().api().sendPollVotes(
_parent->data()->fullId(),
{ option });
}));
}
void Poll::toggleMultiOption(const QByteArray &option) {
const auto i = ranges::find(
_answers,
option,
&Answer::option);
if (i != end(_answers)) {
const auto selected = i->selected;
i->selected = !selected;
if (selected) {
const auto j = ranges::find(
_answers,
true,
&Answer::selected);
_hasSelected = (j != end(_answers));
} else {
_hasSelected = true;
}
history()->owner().requestViewRepaint(_parent);
}
}
void Poll::sendMultiOptions() {
auto chosen = _answers | ranges::view::filter(
&Answer::selected
) | ranges::view::transform(
&Answer::option
) | ranges::to_vector;
if (!chosen.empty()) {
for (auto &answer : _answers) {
answer.selected = false;
}
history()->session().api().sendPollVotes(
_parent->data()->fullId(),
std::move(chosen));
}
}
void Poll::showResults() {
// #TODO polls
} }
void Poll::updateVotes() { void Poll::updateVotes() {
@ -399,21 +479,23 @@ void Poll::updateVotes() {
} }
void Poll::checkSendingAnimation() const { void Poll::checkSendingAnimation() const {
const auto &sending = _poll->sendingVote; const auto &sending = _poll->sendingVotes;
if (sending.isEmpty() == !_sendingAnimation) { const auto sendingRadial = (sending.size() == 1)
&& !(_flags & PollData::Flag::MultiChoice);
if (sendingRadial == (_sendingAnimation != nullptr)) {
if (_sendingAnimation) { if (_sendingAnimation) {
_sendingAnimation->option = sending; _sendingAnimation->option = sending.front();
} }
return; return;
} }
if (sending.isEmpty()) { if (!sendingRadial) {
if (!_answersAnimation) { if (!_answersAnimation) {
_sendingAnimation = nullptr; _sendingAnimation = nullptr;
} }
return; return;
} }
_sendingAnimation = std::make_unique<SendingAnimation>( _sendingAnimation = std::make_unique<SendingAnimation>(
sending, sending.front(),
[=] { radialAnimationCallback(); }); [=] { radialAnimationCallback(); });
_sendingAnimation->animation.start(); _sendingAnimation->animation.start();
} }
@ -547,23 +629,72 @@ void Poll::draw(Painter &p, const QRect &r, TextSelection selection, crl::time m
selection); selection);
tshift += height; tshift += height;
} }
if (!_totalVotesLabel.isEmpty()) {
tshift += st::msgPadding.bottom(); tshift += st::msgPadding.bottom();
if (!inlineFooter()) {
paintBottom(p, padding.left(), tshift, paintw, selection);
} else if (!_totalVotesLabel.isEmpty()) {
paintInlineFooter(p, padding.left(), tshift, paintw, selection);
}
}
void Poll::paintInlineFooter(
Painter &p,
int left,
int top,
int paintw,
TextSelection selection) const {
const auto selected = (selection == FullSelection);
const auto outbg = _parent->hasOutLayout();
const auto &regular = selected ? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected) : (outbg ? st::msgOutDateFg : st::msgInDateFg);
p.setPen(regular); p.setPen(regular);
_totalVotesLabel.drawLeftElided( _totalVotesLabel.drawLeftElided(
p, p,
padding.left(), left,
tshift, top,
std::min( std::min(
_totalVotesLabel.maxWidth(), _totalVotesLabel.maxWidth(),
paintw - _parent->infoWidth()), paintw - _parent->infoWidth()),
width()); width());
} }
void Poll::paintBottom(
Painter &p,
int left,
int top,
int paintw,
TextSelection selection) const {
const auto stringtop = top + st::historyPollBottomButtonTop;
const auto selected = (selection == FullSelection);
const auto outbg = _parent->hasOutLayout();
const auto &regular = selected ? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected) : (outbg ? st::msgOutDateFg : st::msgInDateFg);
if (showVotersCount()) {
p.setPen(regular);
_totalVotesLabel.draw(p, left, stringtop, paintw, style::al_top);
} else {
const auto link = showVotes()
? _showResultsLink
: canSendVotes()
? _sendVotesLink
: nullptr;
const auto over = link ? ClickHandler::showAsActive(link) : false;
p.setFont(over ? st::semiboldFont->underline() : st::semiboldFont);
if (!link) {
p.setPen(regular);
} else {
p.setPen(outbg ? (selected ? st::msgFileThumbLinkOutFgSelected : st::msgFileThumbLinkOutFg) : (selected ? st::msgFileThumbLinkInFgSelected : st::msgFileThumbLinkInFg));
}
const auto string = showVotes()
? tr::lng_polls_view_results(tr::now, Ui::Text::Upper)
: tr::lng_polls_submit_votes(tr::now, Ui::Text::Upper);
const auto stringw = st::semiboldFont->width(string);
p.drawTextLeft(left + (paintw - stringw) / 2, stringtop, width(), string, stringw);
}
} }
void Poll::resetAnswersAnimation() const { void Poll::resetAnswersAnimation() const {
_answersAnimation = nullptr; _answersAnimation = nullptr;
if (_poll->sendingVote.isEmpty()) { if (_poll->sendingVotes.size() != 1
|| (_flags & PollData::Flag::MultiChoice)) {
_sendingAnimation = nullptr; _sendingAnimation = nullptr;
} }
} }
@ -708,9 +839,19 @@ void Poll::paintRadio(
const auto over = ClickHandler::showAsActive(answer.handler); const auto over = ClickHandler::showAsActive(answer.handler);
const auto &regular = selected ? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected) : (outbg ? st::msgOutDateFg : st::msgInDateFg); const auto &regular = selected ? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected) : (outbg ? st::msgOutDateFg : st::msgInDateFg);
p.setBrush(Qt::NoBrush); const auto checkmark = answer.selected;
const auto o = p.opacity(); const auto o = p.opacity();
if (checkmark) {
const auto color = outbg ? (selected ? st::msgFileThumbLinkOutFgSelected : st::msgFileThumbLinkOutFg) : (selected ? st::msgFileThumbLinkInFgSelected : st::msgFileThumbLinkInFg);
auto pen = color->p;
pen.setWidth(st.thickness);
p.setPen(pen);
p.setBrush(color);
} else {
p.setBrush(Qt::NoBrush);
p.setOpacity(o * (over ? st::historyPollRadioOpacityOver : st::historyPollRadioOpacity)); p.setOpacity(o * (over ? st::historyPollRadioOpacityOver : st::historyPollRadioOpacity));
}
const auto rect = QRectF(left, top, st.diameter, st.diameter).marginsRemoved(QMarginsF(st.thickness / 2., st.thickness / 2., st.thickness / 2., st.thickness / 2.)); const auto rect = QRectF(left, top, st.diameter, st.diameter).marginsRemoved(QMarginsF(st.thickness / 2., st.thickness / 2., st.thickness / 2., st.thickness / 2.));
if (_sendingAnimation && _sendingAnimation->option == answer.option) { if (_sendingAnimation && _sendingAnimation->option == answer.option) {
@ -729,10 +870,16 @@ void Poll::paintRadio(
state.arcLength); state.arcLength);
} }
} else { } else {
if (!checkmark) {
auto pen = regular->p; auto pen = regular->p;
pen.setWidth(st.thickness); pen.setWidth(st.thickness);
p.setPen(pen); p.setPen(pen);
}
p.drawEllipse(rect); p.drawEllipse(rect);
if (checkmark) {
const auto &icon = outbg ? (selected ? st::historyPollOutChosenSelected : st::historyPollOutChosen) : (selected ? st::historyPollInChosenSelected : st::historyPollInChosen);
icon.paint(p, left + (st.diameter - icon.width()) / 2, top + (st.diameter - icon.height()) / 2, width());
}
} }
p.setOpacity(o); p.setOpacity(o);
@ -795,13 +942,19 @@ void Poll::paintFilling(
auto barleft = aleft; auto barleft = aleft;
auto barwidth = size; auto barwidth = size;
if (chosen || correct) { if (chosen || correct) {
p.drawEllipse(aleft, ftop - thickness, thickness * 3, thickness * 3); const auto &icon = (chosen && !correct)
barleft += thickness * 3 - radius; ? st::historyPollChoiceWrong
barwidth -= thickness * 3 - radius; : st::historyPollChoiceRight;
const auto ctop = ftop - (icon.height() - thickness) / 2;
p.drawEllipse(aleft, ctop, icon.width(), icon.height());
icon.paint(p, aleft, ctop, width);
barleft += icon.width() - radius;
barwidth -= icon.width() - radius;
} }
if (barwidth > 0) {
p.drawRoundedRect(barleft, ftop, barwidth, thickness, radius, radius); p.drawRoundedRect(barleft, ftop, barwidth, thickness, radius, radius);
} }
}
bool Poll::answerVotesChanged() const { bool Poll::answerVotesChanged() const {
if (_poll->answers.size() != _answers.size() if (_poll->answers.size() != _answers.size()
@ -874,7 +1027,7 @@ void Poll::startAnswersAnimation() const {
TextState Poll::textState(QPoint point, StateRequest request) const { TextState Poll::textState(QPoint point, StateRequest request) const {
auto result = TextState(_parent); auto result = TextState(_parent);
if (!_poll->sendingVote.isEmpty()) { if (!_poll->sendingVotes.empty()) {
return result; return result;
} }
@ -912,6 +1065,25 @@ TextState Poll::textState(QPoint point, StateRequest request) const {
} }
tshift += height; tshift += height;
} }
tshift += st::msgPadding.bottom();
if (!showVotersCount()) {
const auto link = showVotes()
? _showResultsLink
: canSendVotes()
? _sendVotesLink
: nullptr;
if (link) {
const auto string = showVotes()
? tr::lng_polls_view_results(tr::now, Ui::Text::Upper)
: tr::lng_polls_submit_votes(tr::now, Ui::Text::Upper);
const auto stringw = st::semiboldFont->width(string);
const auto stringtop = tshift + st::historyPollBottomButtonTop;
if (QRect(padding.left() + (paintw - stringw) / 2, stringtop, stringw, st::semiboldFont->height).contains(point)) {
result.link = link;
return result;
}
}
}
return result; return result;
} }

View File

@ -9,10 +9,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/media/history_view_media.h" #include "history/view/media/history_view_media.h"
#include "data/data_poll.h" #include "data/data_poll.h"
#include "base/weak_ptr.h"
namespace HistoryView { namespace HistoryView {
class Poll : public Media { class Poll : public Media, public base::has_weak_ptr {
public: public:
Poll( Poll(
not_null<Element*> parent, not_null<Element*> parent,
@ -52,6 +53,7 @@ private:
[[nodiscard]] bool showVotes() const; [[nodiscard]] bool showVotes() const;
[[nodiscard]] bool canVote() const; [[nodiscard]] bool canVote() const;
[[nodiscard]] bool canSendVotes() const;
[[nodiscard]] int countAnswerTop( [[nodiscard]] int countAnswerTop(
const Answer &answer, const Answer &answer,
@ -60,12 +62,14 @@ private:
const Answer &answer, const Answer &answer,
int innerWidth) const; int innerWidth) const;
[[nodiscard]] ClickHandlerPtr createAnswerClickHandler( [[nodiscard]] ClickHandlerPtr createAnswerClickHandler(
const Answer &answer) const; const Answer &answer);
void updateTexts(); void updateTexts();
void updateRecentVoters(); void updateRecentVoters();
void updateAnswers(); void updateAnswers();
void updateVotes(); void updateVotes();
void updateTotalVotes(); void updateTotalVotes();
bool showVotersCount() const;
bool inlineFooter() const;
void updateAnswerVotes(); void updateAnswerVotes();
void updateAnswerVotesFromOriginal( void updateAnswerVotesFromOriginal(
Answer &answer, Answer &answer,
@ -112,6 +116,18 @@ private:
int width, int width,
int height, int height,
TextSelection selection) const; TextSelection selection) const;
void paintInlineFooter(
Painter &p,
int left,
int top,
int paintw,
TextSelection selection) const;
void paintBottom(
Painter &p,
int left,
int top,
int paintw,
TextSelection selection) const;
bool checkAnimationStart() const; bool checkAnimationStart() const;
bool answerVotesChanged() const; bool answerVotesChanged() const;
@ -121,6 +137,9 @@ private:
void radialAnimationCallback() const; void radialAnimationCallback() const;
void toggleRipple(Answer &answer, bool pressed); void toggleRipple(Answer &answer, bool pressed);
void toggleMultiOption(const QByteArray &option);
void sendMultiOptions();
void showResults();
const not_null<PollData*> _poll; const not_null<PollData*> _poll;
int _pollVersion = 0; int _pollVersion = 0;
@ -135,6 +154,9 @@ private:
std::vector<Answer> _answers; std::vector<Answer> _answers;
Ui::Text::String _totalVotesLabel; Ui::Text::String _totalVotesLabel;
ClickHandlerPtr _showResultsLink;
ClickHandlerPtr _sendVotesLink;
bool _hasSelected = false;
mutable std::unique_ptr<AnswersAnimation> _answersAnimation; mutable std::unique_ptr<AnswersAnimation> _answersAnimation;
mutable std::unique_ptr<SendingAnimation> _sendingAnimation; mutable std::unique_ptr<SendingAnimation> _sendingAnimation;