Merge remote-tracking branch 'tdesktop/dev' into dev
This commit is contained in:
commit
07509deee9
28 changed files with 582 additions and 93 deletions
|
|
@ -853,6 +853,8 @@ PRIVATE
|
|||
support/support_helper.h
|
||||
support/support_templates.cpp
|
||||
support/support_templates.h
|
||||
ui/effects/fireworks_animation.cpp
|
||||
ui/effects/fireworks_animation.h
|
||||
ui/effects/round_checkbox.cpp
|
||||
ui/effects/round_checkbox.h
|
||||
ui/effects/send_action_animations.cpp
|
||||
|
|
|
|||
|
|
@ -2204,6 +2204,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_polls_create_multiple_choice" = "Multiple Answers";
|
||||
"lng_polls_create_quiz_mode" = "Quiz Mode";
|
||||
"lng_polls_create_button" = "Create";
|
||||
"lng_polls_choose_question" = "Please enter a question.";
|
||||
"lng_polls_choose_answers" = "Please enter at least two options.";
|
||||
"lng_polls_choose_correct" = "Please choose the correct answer.";
|
||||
|
||||
"lng_polls_poll_results_title" = "Poll results";
|
||||
"lng_polls_quiz_results_title" = "Quiz results";
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="1.9.7.0" />
|
||||
Version="1.9.8.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram FZ-LLC</PublisherDisplayName>
|
||||
|
|
|
|||
|
|
@ -48,8 +48,9 @@ public:
|
|||
not_null<Main::Session*> session,
|
||||
bool chooseCorrectEnabled);
|
||||
|
||||
[[nodiscard]] bool hasOptions() const;
|
||||
[[nodiscard]] bool isValid() const;
|
||||
[[nodiscard]] rpl::producer<bool> isValidChanged() const;
|
||||
[[nodiscard]] bool hasCorrect() const;
|
||||
[[nodiscard]] std::vector<PollAnswer> toPollAnswers() const;
|
||||
void focusFirst();
|
||||
|
||||
|
|
@ -139,8 +140,10 @@ private:
|
|||
int _position = 0;
|
||||
std::vector<std::unique_ptr<Option>> _list;
|
||||
std::vector<std::unique_ptr<Option>> _destroyed;
|
||||
rpl::variable<bool> _valid = false;
|
||||
rpl::variable<int> _usedCount = 0;
|
||||
bool _hasOptions = false;
|
||||
bool _isValid = false;
|
||||
bool _hasCorrect = false;
|
||||
rpl::event_stream<not_null<QWidget*>> _scrollToWidget;
|
||||
rpl::event_stream<> _backspaceInFront;
|
||||
|
||||
|
|
@ -470,12 +473,16 @@ bool Options::full() const {
|
|||
return (_list.size() == kMaxOptionsCount);
|
||||
}
|
||||
|
||||
bool Options::isValid() const {
|
||||
return _valid.current();
|
||||
bool Options::hasOptions() const {
|
||||
return _hasOptions;
|
||||
}
|
||||
|
||||
rpl::producer<bool> Options::isValidChanged() const {
|
||||
return _valid.changes();
|
||||
bool Options::isValid() const {
|
||||
return _isValid;
|
||||
}
|
||||
|
||||
bool Options::hasCorrect() const {
|
||||
return _hasCorrect;
|
||||
}
|
||||
|
||||
rpl::producer<int> Options::usedCount() const {
|
||||
|
|
@ -539,10 +546,10 @@ void Options::enableChooseCorrect(bool enabled) {
|
|||
_chooseCorrectGroup = enabled
|
||||
? createChooseCorrectGroup()
|
||||
: nullptr;
|
||||
validateState();
|
||||
for (auto &option : _list) {
|
||||
option->enableChooseCorrect(_chooseCorrectGroup);
|
||||
}
|
||||
validateState();
|
||||
}
|
||||
|
||||
bool Options::correctShadows() const {
|
||||
|
|
@ -696,10 +703,11 @@ void Options::removeDestroyed(not_null<Option*> option) {
|
|||
|
||||
void Options::validateState() {
|
||||
checkLastOption();
|
||||
_valid = (ranges::count_if(_list, &Option::isGood) > 1)
|
||||
&& (ranges::find_if(_list, &Option::isTooLong) == end(_list))
|
||||
&& (!_chooseCorrectGroup
|
||||
|| ranges::find_if(_list, &Option::isCorrect) != end(_list));
|
||||
_hasOptions = (ranges::count_if(_list, &Option::isGood) > 1);
|
||||
_isValid = _hasOptions
|
||||
&& (ranges::find_if(_list, &Option::isTooLong) == end(_list));
|
||||
_hasCorrect = ranges::find_if(_list, &Option::isCorrect) != end(_list);
|
||||
|
||||
const auto lastEmpty = !_list.empty() && _list.back()->isEmpty();
|
||||
_usedCount = _list.size() - (lastEmpty ? 1 : 0);
|
||||
}
|
||||
|
|
@ -789,7 +797,7 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||
using namespace Settings;
|
||||
|
||||
const auto id = rand_value<uint64>();
|
||||
const auto valid = lifetime().make_state<rpl::event_stream<bool>>();
|
||||
const auto error = lifetime().make_state<Errors>(Error::Question);
|
||||
|
||||
auto result = object_ptr<Ui::VerticalLayout>(this);
|
||||
const auto container = result.data();
|
||||
|
|
@ -910,8 +918,41 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||
| (quiz->checked() ? Flag::Quiz : Flag(0)));
|
||||
return result;
|
||||
};
|
||||
const auto send = [=](Api::SendOptions options) {
|
||||
_submitRequests.fire({ collectResult(), options });
|
||||
const auto collectError = [=] {
|
||||
if (isValidQuestion()) {
|
||||
*error &= ~Error::Question;
|
||||
} else {
|
||||
*error |= Error::Question;
|
||||
}
|
||||
if (!options->hasOptions()) {
|
||||
*error |= Error::Options;
|
||||
} else if (!options->isValid()) {
|
||||
*error |= Error::Other;
|
||||
} else {
|
||||
*error &= ~(Error::Options | Error::Other);
|
||||
}
|
||||
if (quiz->checked() && !options->hasCorrect()) {
|
||||
*error |= Error::Correct;
|
||||
} else {
|
||||
*error &= ~Error::Correct;
|
||||
}
|
||||
};
|
||||
const auto showError = [=](const QString &text) {
|
||||
Ui::Toast::Show(text);
|
||||
};
|
||||
const auto send = [=](Api::SendOptions sendOptions) {
|
||||
collectError();
|
||||
if (*error & Error::Question) {
|
||||
showError(tr::lng_polls_choose_question(tr::now));
|
||||
question->setFocus();
|
||||
} else if (*error & Error::Options) {
|
||||
showError(tr::lng_polls_choose_answers(tr::now));
|
||||
options->focusFirst();
|
||||
} else if (*error & Error::Correct) {
|
||||
showError(tr::lng_polls_choose_correct(tr::now));
|
||||
} else if (!*error) {
|
||||
_submitRequests.fire({ collectResult(), sendOptions });
|
||||
}
|
||||
};
|
||||
const auto sendSilent = [=] {
|
||||
auto options = Api::SendOptions();
|
||||
|
|
@ -926,36 +967,6 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||
send),
|
||||
Ui::LayerOption::KeepOther);
|
||||
};
|
||||
const auto updateValid = [=] {
|
||||
valid->fire(isValidQuestion() && options->isValid());
|
||||
};
|
||||
connect(question, &Ui::InputField::changed, [=] {
|
||||
updateValid();
|
||||
});
|
||||
valid->events_starting_with(
|
||||
false
|
||||
) | rpl::distinct_until_changed(
|
||||
) | rpl::start_with_next([=](bool valid) {
|
||||
clearButtons();
|
||||
if (valid) {
|
||||
const auto submit = addButton(
|
||||
tr::lng_polls_create_button(),
|
||||
[=] { send({}); });
|
||||
if (_sendType == Api::SendType::Normal) {
|
||||
SetupSendMenuAndShortcuts(
|
||||
submit.data(),
|
||||
[=] { return SendMenuType::Scheduled; },
|
||||
sendSilent,
|
||||
sendScheduled);
|
||||
}
|
||||
}
|
||||
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
||||
}, lifetime());
|
||||
|
||||
options->isValidChanged(
|
||||
) | rpl::start_with_next([=] {
|
||||
updateValid();
|
||||
}, lifetime());
|
||||
|
||||
options->scrollToWidget(
|
||||
) | rpl::start_with_next([=](not_null<QWidget*> widget) {
|
||||
|
|
@ -967,6 +978,22 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||
FocusAtEnd(question);
|
||||
}, lifetime());
|
||||
|
||||
const auto submit = addButton(
|
||||
tr::lng_polls_create_button(),
|
||||
[=] { send({}); });
|
||||
if (_sendType == Api::SendType::Normal) {
|
||||
const auto sendMenuType = [=] {
|
||||
collectError();
|
||||
return *error ? SendMenuType::Disabled : SendMenuType::Scheduled;
|
||||
};
|
||||
SetupSendMenuAndShortcuts(
|
||||
submit.data(),
|
||||
sendMenuType,
|
||||
sendSilent,
|
||||
sendScheduled);
|
||||
}
|
||||
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "boxes/abstract_box.h"
|
||||
#include "api/api_common.h"
|
||||
#include "data/data_poll.h"
|
||||
#include "base/flags.h"
|
||||
|
||||
struct PollData;
|
||||
|
||||
|
|
@ -44,6 +45,15 @@ protected:
|
|||
void prepare() override;
|
||||
|
||||
private:
|
||||
enum class Error {
|
||||
Question = 0x01,
|
||||
Options = 0x02,
|
||||
Correct = 0x04,
|
||||
Other = 0x08,
|
||||
};
|
||||
friend constexpr inline bool is_flag_type(Error) { return true; }
|
||||
using Errors = base::flags<Error>;
|
||||
|
||||
object_ptr<Ui::RpWidget> setupContent();
|
||||
not_null<Ui::InputField*> setupQuestion(
|
||||
not_null<Ui::VerticalLayout*> container);
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#define TDESKTOP_ALPHA_VERSION (0ULL)
|
||||
#endif // TDESKTOP_ALLOW_CLOSED_ALPHA
|
||||
|
||||
constexpr auto AppVersion = 1009007;
|
||||
constexpr auto AppVersionStr = "1.9.7";
|
||||
constexpr auto AppVersion = 1009008;
|
||||
constexpr auto AppVersionStr = "1.9.8";
|
||||
constexpr auto AppBetaVersion = false;
|
||||
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
||||
constexpr auto AppKotatoVersion = 1001004;
|
||||
|
|
|
|||
|
|
@ -168,13 +168,12 @@ bool PollData::applyResultToAnswers(
|
|||
answer->chosen = voters.is_chosen();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!isMinResults || closed()) {
|
||||
if (answer->correct != voters.is_correct()) {
|
||||
answer->correct = voters.is_correct();
|
||||
changed = true;
|
||||
}
|
||||
} else if (const auto existing = answerByOption(option)) {
|
||||
answer->chosen = existing->chosen;
|
||||
answer->correct = existing->correct;
|
||||
}
|
||||
return changed;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1707,7 +1707,9 @@ void InnerWidget::repaintItem(const Element *view) {
|
|||
if (!view) {
|
||||
return;
|
||||
}
|
||||
update(0, itemTop(view), width(), view->height());
|
||||
const auto top = itemTop(view);
|
||||
const auto range = view->verticalRepaintRange();
|
||||
update(0, top + range.top, width(), range.height);
|
||||
}
|
||||
|
||||
void InnerWidget::resizeItem(not_null<Element*> view) {
|
||||
|
|
|
|||
|
|
@ -542,10 +542,10 @@ historyPollQuestionStyle: TextStyle(defaultTextStyle) {
|
|||
historyPollAnswerStyle: defaultTextStyle;
|
||||
historyPollQuestionTop: 7px;
|
||||
historyPollSubtitleSkip: 4px;
|
||||
historyPollAnswerPadding: margins(31px, 10px, 0px, 10px);
|
||||
historyPollAnswerPadding: margins(32px, 10px, 0px, 10px);
|
||||
historyPollAnswersSkip: 2px;
|
||||
historyPollPercentFont: semiboldFont;
|
||||
historyPollPercentSkip: 6px;
|
||||
historyPollPercentSkip: 5px;
|
||||
historyPollPercentTop: 0px;
|
||||
historyPollTotalVotesSkip: 5px;
|
||||
historyPollFillingMin: 4px;
|
||||
|
|
|
|||
|
|
@ -242,7 +242,8 @@ void HistoryInner::repaintItem(const Element *view) {
|
|||
}
|
||||
const auto top = itemTop(view);
|
||||
if (top >= 0) {
|
||||
update(0, top, width(), view->height());
|
||||
const auto range = view->verticalRepaintRange();
|
||||
update(0, top + range.top, width(), range.height);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -605,6 +605,13 @@ bool Element::hasVisibleText() const {
|
|||
return false;
|
||||
}
|
||||
|
||||
auto Element::verticalRepaintRange() const -> VerticalRepaintRange {
|
||||
return {
|
||||
.top = 0,
|
||||
.height = height()
|
||||
};
|
||||
}
|
||||
|
||||
void Element::unloadHeavyPart() {
|
||||
if (_media) {
|
||||
_media->unloadHeavyPart();
|
||||
|
|
|
|||
|
|
@ -264,6 +264,12 @@ public:
|
|||
virtual TimeId displayedEditDate() const;
|
||||
virtual bool hasVisibleText() const;
|
||||
|
||||
struct VerticalRepaintRange {
|
||||
int top = 0;
|
||||
int height = 0;
|
||||
};
|
||||
[[nodiscard]] virtual VerticalRepaintRange verticalRepaintRange() const;
|
||||
|
||||
virtual void unloadHeavyPart();
|
||||
|
||||
// Legacy blocks structure.
|
||||
|
|
|
|||
|
|
@ -2398,7 +2398,9 @@ void ListWidget::repaintItem(const Element *view) {
|
|||
if (!view) {
|
||||
return;
|
||||
}
|
||||
update(0, itemTop(view), width(), view->height());
|
||||
const auto top = itemTop(view);
|
||||
const auto range = view->verticalRepaintRange();
|
||||
update(0, top + range.top, width(), range.height);
|
||||
}
|
||||
|
||||
void ListWidget::repaintItem(FullMsgId itemId) {
|
||||
|
|
|
|||
|
|
@ -409,6 +409,15 @@ void Message::draw(
|
|||
|
||||
paintHighlight(p, g.height());
|
||||
|
||||
const auto roll = media ? media->bubbleRoll() : Media::BubbleRoll();
|
||||
if (roll) {
|
||||
p.save();
|
||||
p.translate(g.center());
|
||||
p.rotate(roll.rotate);
|
||||
p.scale(roll.scale, roll.scale);
|
||||
p.translate(-g.center());
|
||||
}
|
||||
|
||||
p.setTextPalette(selected
|
||||
? (outbg ? st::outTextPaletteSelected : st::inTextPaletteSelected)
|
||||
: (outbg ? st::outTextPalette : st::inTextPalette));
|
||||
|
|
@ -496,6 +505,10 @@ void Message::draw(
|
|||
const auto fastShareTop = g.top() + g.height() - fastShareSkip - st::historyFastShareSize;
|
||||
drawRightAction(p, fastShareLeft, fastShareTop, width());
|
||||
}
|
||||
|
||||
if (media) {
|
||||
media->paintBubbleFireworks(p, g, ms);
|
||||
}
|
||||
} else if (media && media->isDisplayed()) {
|
||||
p.translate(g.topLeft());
|
||||
media->draw(p, clip.translated(-g.topLeft()), skipTextSelection(selection), ms);
|
||||
|
|
@ -504,6 +517,10 @@ void Message::draw(
|
|||
|
||||
p.restoreTextPalette();
|
||||
|
||||
if (roll) {
|
||||
p.restore();
|
||||
}
|
||||
|
||||
const auto reply = item->Get<HistoryMessageReply>();
|
||||
if (reply && reply->isNameUpdated()) {
|
||||
const_cast<Message*>(this)->setPendingResize();
|
||||
|
|
@ -1280,6 +1297,15 @@ int Message::infoWidth() const {
|
|||
return result;
|
||||
}
|
||||
|
||||
auto Message::verticalRepaintRange() const -> VerticalRepaintRange {
|
||||
const auto media = this->media();
|
||||
const auto add = media ? media->bubbleRollRepaintMargins() : QMargins();
|
||||
return {
|
||||
.top = -add.top(),
|
||||
.height = height() + add.top() + add.bottom()
|
||||
};
|
||||
}
|
||||
|
||||
void Message::refreshDataIdHook() {
|
||||
if (base::take(_rightActionLink)) {
|
||||
_rightActionLink = rightActionLink();
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@ public:
|
|||
int infoWidth() const override;
|
||||
int plainMaxWidth() const override;
|
||||
|
||||
VerticalRepaintRange verticalRepaintRange() const override;
|
||||
|
||||
protected:
|
||||
void refreshDataIdHook() override;
|
||||
|
||||
|
|
|
|||
|
|
@ -233,6 +233,26 @@ public:
|
|||
return true;
|
||||
}
|
||||
|
||||
struct BubbleRoll {
|
||||
float64 rotate = 0.;
|
||||
float64 scale = 1.;
|
||||
|
||||
explicit operator bool() const {
|
||||
return (rotate != 0.) || (scale != 1.);
|
||||
}
|
||||
};
|
||||
[[nodiscard]] virtual BubbleRoll bubbleRoll() const {
|
||||
return BubbleRoll();
|
||||
}
|
||||
[[nodiscard]] virtual QMargins bubbleRollRepaintMargins() const {
|
||||
return QMargins();
|
||||
}
|
||||
virtual void paintBubbleFireworks(
|
||||
Painter &p,
|
||||
const QRect &bubble,
|
||||
crl::time ms) const {
|
||||
}
|
||||
|
||||
virtual void unloadHeavyPart() {
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/radial_animation.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/effects/fireworks_animation.h"
|
||||
#include "data/data_media_types.h"
|
||||
#include "data/data_poll.h"
|
||||
#include "data/data_user.h"
|
||||
|
|
@ -32,6 +33,11 @@ namespace HistoryView {
|
|||
namespace {
|
||||
|
||||
constexpr auto kShowRecentVotersCount = 3;
|
||||
constexpr auto kRotateSegments = 8;
|
||||
constexpr auto kRotateAmplitude = 3.;
|
||||
constexpr auto kScaleSegments = 2;
|
||||
constexpr auto kScaleAmplitude = 0.03;
|
||||
constexpr auto kRollDuration = crl::time(400);
|
||||
|
||||
struct PercentCounterItem {
|
||||
int index = 0;
|
||||
|
|
@ -344,6 +350,7 @@ void Poll::updateTexts() {
|
|||
_pollVersion = _poll->version;
|
||||
|
||||
const auto willStartAnimation = checkAnimationStart();
|
||||
const auto voted = _voted;
|
||||
|
||||
if (_question.toString() != _poll->question) {
|
||||
auto options = Ui::WebpageTextTitleOptions();
|
||||
|
|
@ -374,6 +381,30 @@ void Poll::updateTexts() {
|
|||
|
||||
if (willStartAnimation) {
|
||||
startAnswersAnimation();
|
||||
if (!voted) {
|
||||
checkQuizAnswered();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Poll::checkQuizAnswered() {
|
||||
if (!_voted || !_votedFromHere || !_poll->quiz() || anim::Disabled()) {
|
||||
return;
|
||||
}
|
||||
const auto i = ranges::find(_answers, true, &Answer::chosen);
|
||||
if (i == end(_answers)) {
|
||||
return;
|
||||
}
|
||||
if (i->correct) {
|
||||
_fireworksAnimation = std::make_unique<Ui::FireworksAnimation>(
|
||||
[=] { history()->owner().requestViewRepaint(_parent); });
|
||||
} else {
|
||||
_wrongAnswerAnimation.start(
|
||||
[=] { history()->owner().requestViewRepaint(_parent); },
|
||||
0.,
|
||||
1.,
|
||||
kRollDuration,
|
||||
anim::linear);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -426,6 +457,7 @@ ClickHandlerPtr Poll::createAnswerClickHandler(
|
|||
}));
|
||||
}
|
||||
return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
|
||||
_votedFromHere = true;
|
||||
history()->session().api().sendPollVotes(
|
||||
_parent->data()->fullId(),
|
||||
{ option });
|
||||
|
|
@ -465,9 +497,7 @@ void Poll::sendMultiOptions() {
|
|||
&Answer::option
|
||||
) | ranges::to_vector;
|
||||
if (!chosen.empty()) {
|
||||
for (auto &answer : _answers) {
|
||||
answer.selected = false;
|
||||
}
|
||||
_votedFromHere = true;
|
||||
history()->session().api().sendPollVotes(
|
||||
_parent->data()->fullId(),
|
||||
std::move(chosen));
|
||||
|
|
@ -481,7 +511,17 @@ void Poll::showResults() {
|
|||
}
|
||||
|
||||
void Poll::updateVotes() {
|
||||
_voted = _poll->voted();
|
||||
const auto voted = _poll->voted();
|
||||
if (_voted != voted) {
|
||||
_voted = voted;
|
||||
if (_voted) {
|
||||
for (auto &answer : _answers) {
|
||||
answer.selected = false;
|
||||
}
|
||||
} else {
|
||||
_votedFromHere = false;
|
||||
}
|
||||
}
|
||||
updateAnswerVotes();
|
||||
updateTotalVotes();
|
||||
}
|
||||
|
|
@ -961,6 +1001,8 @@ void Poll::paintFilling(
|
|||
|
||||
if (chosen && !correct) {
|
||||
p.setBrush(st::boxTextFgError);
|
||||
} else if (chosen && correct && _poll->quiz() && !outbg) {
|
||||
p.setBrush(st::boxTextFgGood);
|
||||
} else {
|
||||
const auto bar = outbg ? (selected ? st::msgWaveformOutActiveSelected : st::msgWaveformOutActive) : (selected ? st::msgWaveformInActiveSelected : st::msgWaveformInActive);
|
||||
p.setBrush(bar);
|
||||
|
|
@ -1119,6 +1161,49 @@ TextState Poll::textState(QPoint point, StateRequest request) const {
|
|||
return result;
|
||||
}
|
||||
|
||||
auto Poll::bubbleRoll() const -> BubbleRoll {
|
||||
const auto value = _wrongAnswerAnimation.value(1.);
|
||||
_wrongAnswerAnimated = (value < 1.);
|
||||
if (!_wrongAnswerAnimated) {
|
||||
return BubbleRoll();
|
||||
}
|
||||
const auto rotateFull = value * kRotateSegments;
|
||||
const auto progress = [](float64 full) {
|
||||
const auto lower = std::floor(full);
|
||||
const auto shift = (full - lower);
|
||||
switch (int(lower) % 4) {
|
||||
case 0: return -shift;
|
||||
case 1: return (shift - 1.);
|
||||
case 2: return shift;
|
||||
case 3: return (1. - shift);
|
||||
}
|
||||
Unexpected("Value in Poll::getBubbleRollDegrees.");
|
||||
};
|
||||
return {
|
||||
.rotate = progress(value * kRotateSegments) * kRotateAmplitude,
|
||||
.scale = 1. + progress(value * kScaleSegments) * kScaleAmplitude
|
||||
};
|
||||
}
|
||||
|
||||
QMargins Poll::bubbleRollRepaintMargins() const {
|
||||
if (!_wrongAnswerAnimated) {
|
||||
return QMargins();
|
||||
}
|
||||
static const auto kAdd = int(std::ceil(
|
||||
st::msgMaxWidth * std::sin(kRotateAmplitude * M_PI / 180.)));
|
||||
return QMargins(kAdd, kAdd, kAdd, kAdd);
|
||||
}
|
||||
|
||||
void Poll::paintBubbleFireworks(
|
||||
Painter &p,
|
||||
const QRect &bubble,
|
||||
crl::time ms) const {
|
||||
if (!_fireworksAnimation || _fireworksAnimation->paint(p, bubble)) {
|
||||
return;
|
||||
}
|
||||
_fireworksAnimation = nullptr;
|
||||
}
|
||||
|
||||
void Poll::clickHandlerPressedChanged(
|
||||
const ClickHandlerPtr &handler,
|
||||
bool pressed) {
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#pragma once
|
||||
|
||||
#include "history/view/media/history_view_media.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "data/data_poll.h"
|
||||
#include "base/weak_ptr.h"
|
||||
|
||||
namespace Ui {
|
||||
class RippleAnimation;
|
||||
class FireworksAnimation;
|
||||
} // namespace Ui
|
||||
|
||||
namespace HistoryView {
|
||||
|
|
@ -40,6 +42,13 @@ public:
|
|||
return false;
|
||||
}
|
||||
|
||||
BubbleRoll bubbleRoll() const override;
|
||||
QMargins bubbleRollRepaintMargins() const override;
|
||||
void paintBubbleFireworks(
|
||||
Painter &p,
|
||||
const QRect &bubble,
|
||||
crl::time ms) const override;
|
||||
|
||||
void clickHandlerPressedChanged(
|
||||
const ClickHandlerPtr &handler,
|
||||
bool pressed) override;
|
||||
|
|
@ -145,6 +154,7 @@ private:
|
|||
void toggleMultiOption(const QByteArray &option);
|
||||
void sendMultiOptions();
|
||||
void showResults();
|
||||
void checkQuizAnswered();
|
||||
|
||||
[[nodiscard]] int bottomButtonHeight() const;
|
||||
|
||||
|
|
@ -164,12 +174,17 @@ private:
|
|||
ClickHandlerPtr _showResultsLink;
|
||||
ClickHandlerPtr _sendVotesLink;
|
||||
mutable std::unique_ptr<Ui::RippleAnimation> _linkRipple;
|
||||
bool _hasSelected = false;
|
||||
|
||||
mutable std::unique_ptr<AnswersAnimation> _answersAnimation;
|
||||
mutable std::unique_ptr<SendingAnimation> _sendingAnimation;
|
||||
mutable std::unique_ptr<Ui::FireworksAnimation> _fireworksAnimation;
|
||||
Ui::Animations::Simple _wrongAnswerAnimation;
|
||||
mutable QPoint _lastLinkPoint;
|
||||
|
||||
bool _hasSelected = false;
|
||||
bool _votedFromHere = false;
|
||||
mutable bool _wrongAnswerAnimated = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace HistoryView
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#include "platform/linux/linux_desktop_environment.h"
|
||||
|
||||
#include "platform/linux/specific_linux.h"
|
||||
|
||||
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
|
||||
#include <QDBusInterface>
|
||||
#endif
|
||||
|
|
@ -26,7 +28,14 @@ Type Compute() {
|
|||
auto xdgCurrentDesktop = GetEnv("XDG_CURRENT_DESKTOP").toLower();
|
||||
auto list = xdgCurrentDesktop.split(':', QString::SkipEmptyParts);
|
||||
auto desktopSession = GetEnv("DESKTOP_SESSION").toLower();
|
||||
auto slash = desktopSession.lastIndexOf('/');
|
||||
auto kdeSession = GetEnv("KDE_SESSION_VERSION");
|
||||
|
||||
// DESKTOP_SESSION can contain a path
|
||||
if (slash != -1) {
|
||||
desktopSession = desktopSession.mid(slash + 1);
|
||||
}
|
||||
|
||||
if (!list.isEmpty()) {
|
||||
if (list.contains("unity")) {
|
||||
// gnome-fallback sessions set XDG_CURRENT_DESKTOP to Unity
|
||||
|
|
@ -35,8 +44,6 @@ Type Compute() {
|
|||
return Type::Gnome;
|
||||
}
|
||||
return Type::Unity;
|
||||
} else if (list.contains("xfce")) {
|
||||
return Type::XFCE;
|
||||
} else if (list.contains("pantheon")) {
|
||||
return Type::Pantheon;
|
||||
} else if (list.contains("gnome")) {
|
||||
|
|
@ -53,7 +60,7 @@ Type Compute() {
|
|||
}
|
||||
|
||||
if (!desktopSession.isEmpty()) {
|
||||
if (desktopSession == qstr("gnome") || desktopSession == qstr("mate")) {
|
||||
if (desktopSession == qstr("gnome")) {
|
||||
return Type::Gnome;
|
||||
} else if (desktopSession == qstr("kde4") || desktopSession == qstr("kde-plasma")) {
|
||||
return Type::KDE4;
|
||||
|
|
@ -63,10 +70,6 @@ Type Compute() {
|
|||
return Type::KDE4;
|
||||
}
|
||||
return Type::KDE3;
|
||||
} else if (desktopSession.indexOf(qstr("xfce")) >= 0 || desktopSession == qstr("xubuntu")) {
|
||||
return Type::XFCE;
|
||||
} else if (desktopSession == qstr("awesome")) {
|
||||
return Type::Awesome;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -95,9 +98,7 @@ Type ComputeAndLog() {
|
|||
case Type::KDE5: return "KDE5";
|
||||
case Type::Ubuntu: return "Ubuntu";
|
||||
case Type::Unity: return "Unity";
|
||||
case Type::XFCE: return "XFCE";
|
||||
case Type::Pantheon: return "Pantheon";
|
||||
case Type::Awesome: return "Awesome";
|
||||
}
|
||||
return QString::number(static_cast<int>(result));
|
||||
};
|
||||
|
|
@ -114,15 +115,17 @@ Type Get() {
|
|||
}
|
||||
|
||||
bool TryQtTrayIcon() {
|
||||
return !IsPantheon() && !IsAwesome();
|
||||
return !IsPantheon();
|
||||
}
|
||||
|
||||
bool PreferAppIndicatorTrayIcon() {
|
||||
return IsXFCE() || IsUnity() || IsUbuntu() ||
|
||||
return (InSandbox() && !IsKDE())
|
||||
|| IsUnity()
|
||||
|| IsUbuntu()
|
||||
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
|
||||
(IsGnome() && QDBusInterface("org.kde.StatusNotifierWatcher", "/").isValid());
|
||||
|| (IsGnome() && QDBusInterface("org.kde.StatusNotifierWatcher", "/").isValid());
|
||||
#else
|
||||
IsGnome();
|
||||
|| IsGnome();
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,9 +18,7 @@ enum class Type {
|
|||
KDE5,
|
||||
Ubuntu,
|
||||
Unity,
|
||||
XFCE,
|
||||
Pantheon,
|
||||
Awesome,
|
||||
};
|
||||
|
||||
Type Get();
|
||||
|
|
@ -53,18 +51,10 @@ inline bool IsUnity() {
|
|||
return Get() == Type::Unity;
|
||||
}
|
||||
|
||||
inline bool IsXFCE() {
|
||||
return Get() == Type::XFCE;
|
||||
}
|
||||
|
||||
inline bool IsPantheon() {
|
||||
return Get() == Type::Pantheon;
|
||||
}
|
||||
|
||||
inline bool IsAwesome() {
|
||||
return Get() == Type::Awesome;
|
||||
}
|
||||
|
||||
bool TryQtTrayIcon();
|
||||
bool PreferAppIndicatorTrayIcon();
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "lang/lang_keys.h"
|
||||
#include "facades.h"
|
||||
|
||||
#include <QtCore/QBuffer>
|
||||
|
||||
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
|
||||
#include <QtCore/QVersionNumber>
|
||||
#include <QtDBus/QDBusConnection>
|
||||
#include <QtDBus/QDBusReply>
|
||||
#include <QtDBus/QDBusMetaType>
|
||||
|
|
@ -101,7 +100,8 @@ NotificationData::NotificationData(
|
|||
if (capabilities.contains(qsl("body-markup"))) {
|
||||
_body = subtitle.isEmpty()
|
||||
? msg.toHtmlEscaped()
|
||||
: qsl("<b>%1</b>\n%2").arg(subtitle.toHtmlEscaped())
|
||||
: qsl("<b>%1</b>\n%2")
|
||||
.arg(subtitle.toHtmlEscaped())
|
||||
.arg(msg.toHtmlEscaped());
|
||||
} else {
|
||||
_body = subtitle.isEmpty()
|
||||
|
|
@ -351,7 +351,7 @@ void Manager::Private::showNotification(
|
|||
|
||||
const auto key = hideNameAndPhoto
|
||||
? InMemoryKey()
|
||||
:peer->userpicUniqueKey();
|
||||
: peer->userpicUniqueKey();
|
||||
notification->setImage(_cachedUserpics.get(key, peer));
|
||||
|
||||
auto i = _notifications.find(peer->id);
|
||||
|
|
|
|||
|
|
@ -384,6 +384,7 @@ bool OpenSystemSettings(SystemSettingsType type) {
|
|||
} else if (DesktopEnvironment::IsGnome()) {
|
||||
add("gnome-control-center sound");
|
||||
}
|
||||
add("pavucontrol-qt");
|
||||
add("pavucontrol");
|
||||
add("alsamixergui");
|
||||
return ranges::find_if(options, [](const QString &command) {
|
||||
|
|
|
|||
221
Telegram/SourceFiles/ui/effects/fireworks_animation.cpp
Normal file
221
Telegram/SourceFiles/ui/effects/fireworks_animation.cpp
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
/*
|
||||
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 "ui/effects/fireworks_animation.h"
|
||||
|
||||
#include "base/openssl_help.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kParticlesCount = 60;
|
||||
constexpr auto kFallCount = 30;
|
||||
constexpr auto kFirstUpdateTime = crl::time(16);
|
||||
constexpr auto kFireworkWidth = 480;
|
||||
constexpr auto kFireworkHeight = 320;
|
||||
|
||||
QBrush Brush(int color) {
|
||||
return QBrush{ QColor(
|
||||
color & 0xFF,
|
||||
(color >> 8) & 0xFF,
|
||||
(color >> 16) & 0xFF)
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<QBrush> PrepareBrushes() {
|
||||
return {
|
||||
Brush(0xff2CBCE8),
|
||||
Brush(0xff9E04D0),
|
||||
Brush(0xffFECB02),
|
||||
Brush(0xffFD2357),
|
||||
Brush(0xff278CFE),
|
||||
Brush(0xff59B86C),
|
||||
};
|
||||
}
|
||||
|
||||
int RandomInt(uint32 till) {
|
||||
return int(openssl::RandomValue<uint32>() % till);
|
||||
}
|
||||
|
||||
[[nodiscard]] float64 RandomFloat01() {
|
||||
return openssl::RandomValue<uint32>()
|
||||
/ float64(std::numeric_limits<uint32>::max());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FireworksAnimation::FireworksAnimation(Fn<void()> repaint)
|
||||
: _brushes(PrepareBrushes())
|
||||
, _animation([=](crl::time now) { update(now); })
|
||||
, _repaint(std::move(repaint)) {
|
||||
_smallSide = style::ConvertScale(2);
|
||||
_particles.reserve(kParticlesCount + kFallCount);
|
||||
for (auto i = 0; i != kParticlesCount; ++i) {
|
||||
initParticle(_particles.emplace_back(), false);
|
||||
}
|
||||
_animation.start();
|
||||
}
|
||||
|
||||
void FireworksAnimation::update(crl::time now) {
|
||||
const auto passed = _lastUpdate ? (now - _lastUpdate) : kFirstUpdateTime;
|
||||
_lastUpdate = now;
|
||||
auto allFinished = true;
|
||||
for (auto &particle : _particles) {
|
||||
updateParticle(particle, passed);
|
||||
if (!particle.finished) {
|
||||
allFinished = false;
|
||||
}
|
||||
}
|
||||
if (allFinished) {
|
||||
_animation.stop();
|
||||
} else if (_fallingDown >= kParticlesCount / 2 && _speedCoef > 0.2) {
|
||||
startFall();
|
||||
_speedCoef -= passed / 16.0 * 0.15;
|
||||
if (_speedCoef < 0.2) {
|
||||
_speedCoef = 0.2;
|
||||
}
|
||||
}
|
||||
_repaint();
|
||||
}
|
||||
|
||||
bool FireworksAnimation::paint(QPainter &p, const QRect &rect) {
|
||||
if (rect.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setClipRect(rect);
|
||||
for (auto &particle : _particles) {
|
||||
if (!particle.finished) {
|
||||
paintParticle(p, particle, rect);
|
||||
}
|
||||
}
|
||||
p.setClipping(false);
|
||||
return _animation.animating();
|
||||
}
|
||||
|
||||
void FireworksAnimation::paintParticle(
|
||||
QPainter &p,
|
||||
const Particle &particle,
|
||||
const QRect &rect) {
|
||||
const auto size = particle.size;
|
||||
const auto x = rect.x() + (particle.x * rect.width() / kFireworkWidth);
|
||||
const auto y = rect.y() + (particle.y * rect.height() / kFireworkHeight);
|
||||
p.setBrush(_brushes[particle.color]);
|
||||
if (particle.type == Particle::Type::Circle) {
|
||||
p.drawEllipse(x, y, size, size);
|
||||
} else {
|
||||
const auto rect = QRect(-size, -_smallSide, size, _smallSide);
|
||||
p.save();
|
||||
p.translate(x, y);
|
||||
p.rotate(particle.rotation);
|
||||
p.drawRoundedRect(rect, _smallSide, _smallSide);
|
||||
p.restore();
|
||||
}
|
||||
}
|
||||
|
||||
void FireworksAnimation::updateParticle(Particle &particle, crl::time dt) {
|
||||
if (particle.finished) {
|
||||
return;
|
||||
}
|
||||
const auto moveCoef = dt / 16.;
|
||||
particle.x += particle.moveX * moveCoef;
|
||||
particle.y += particle.moveY * moveCoef;
|
||||
if (particle.xFinished != 0) {
|
||||
const auto dp = 0.5;
|
||||
if (particle.xFinished == 1) {
|
||||
particle.moveX += dp * moveCoef * 0.05;
|
||||
if (particle.moveX >= dp) {
|
||||
particle.xFinished = 2;
|
||||
}
|
||||
} else {
|
||||
particle.moveX -= dp * moveCoef * 0.05f;
|
||||
if (particle.moveX <= -dp) {
|
||||
particle.xFinished = 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (particle.right) {
|
||||
if (particle.moveX < 0) {
|
||||
particle.moveX += moveCoef * 0.05f;
|
||||
if (particle.moveX >= 0) {
|
||||
particle.moveX = 0;
|
||||
particle.xFinished = particle.finishedStart;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (particle.moveX > 0) {
|
||||
particle.moveX -= moveCoef * 0.05f;
|
||||
if (particle.moveX <= 0) {
|
||||
particle.moveX = 0;
|
||||
particle.xFinished = particle.finishedStart;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto yEdge = -0.5;
|
||||
const auto wasNegative = (particle.moveY < yEdge);
|
||||
if (particle.moveY > yEdge) {
|
||||
particle.moveY += (1. / 3.) * moveCoef * _speedCoef;
|
||||
} else {
|
||||
particle.moveY += (1. / 3.) * moveCoef;
|
||||
}
|
||||
if (wasNegative && particle.moveY > yEdge) {
|
||||
++_fallingDown;
|
||||
}
|
||||
if (particle.type == Particle::Type::Rectangle) {
|
||||
particle.rotation += moveCoef * 10;
|
||||
if (particle.rotation > 360) {
|
||||
particle.rotation -= 360;
|
||||
}
|
||||
}
|
||||
if (particle.y >= kFireworkHeight) {
|
||||
particle.finished = true;
|
||||
}
|
||||
}
|
||||
|
||||
void FireworksAnimation::startFall() {
|
||||
if (_startedFall) {
|
||||
return;
|
||||
}
|
||||
_startedFall = true;
|
||||
for (auto i = 0; i != kFallCount; ++i) {
|
||||
initParticle(_particles.emplace_back(), true);
|
||||
}
|
||||
}
|
||||
|
||||
void FireworksAnimation::initParticle(Particle &particle, bool falling) {
|
||||
using Type = Particle::Type;
|
||||
particle.color = RandomInt(_brushes.size());
|
||||
particle.type = RandomInt(2) ? Type::Rectangle : Type::Circle;
|
||||
particle.right = (RandomInt(2) == 1);
|
||||
particle.finishedStart = 1 + RandomInt(2);
|
||||
if (particle.type == Type::Circle) {
|
||||
particle.size = style::ConvertScale(6 + RandomFloat01() * 3);
|
||||
} else {
|
||||
particle.size = style::ConvertScale(6 + RandomFloat01() * 6);
|
||||
particle.rotation = RandomInt(360);
|
||||
}
|
||||
if (falling) {
|
||||
particle.y = -RandomFloat01() * kFireworkHeight * 1.2f;
|
||||
particle.x = 5 + RandomInt(kFireworkWidth - 10);
|
||||
particle.xFinished = particle.finishedStart;
|
||||
} else {
|
||||
const auto xOffset = 4 + RandomInt(10);
|
||||
const auto yOffset = kFireworkHeight / 4;
|
||||
if (particle.right) {
|
||||
particle.x = kFireworkWidth + xOffset;
|
||||
} else {
|
||||
particle.x = -xOffset;
|
||||
}
|
||||
particle.moveX = (particle.right ? -1 : 1) * (1.2 + RandomFloat01() * 4);
|
||||
particle.moveY = -(4 + RandomFloat01() * 4);
|
||||
particle.y = yOffset / 2 + RandomInt(yOffset * 2);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
63
Telegram/SourceFiles/ui/effects/fireworks_animation.h
Normal file
63
Telegram/SourceFiles/ui/effects/fireworks_animation.h
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class FireworksAnimation final {
|
||||
public:
|
||||
explicit FireworksAnimation(Fn<void()> repaint);
|
||||
|
||||
bool paint(QPainter &p, const QRect &rect);
|
||||
|
||||
private:
|
||||
struct Particle {
|
||||
enum class Type : uchar {
|
||||
Circle,
|
||||
Rectangle
|
||||
};
|
||||
|
||||
float64 x = 0.;
|
||||
float64 y = 0.;
|
||||
float64 moveX = 0.;
|
||||
float64 moveY = 0.;
|
||||
uint16 rotation = 0;
|
||||
|
||||
Type type = Type::Circle;
|
||||
uchar color = 0;
|
||||
bool right = false;
|
||||
uchar size = 0;
|
||||
uchar xFinished = 0;
|
||||
uchar finishedStart = 0;
|
||||
bool finished = false;
|
||||
};
|
||||
|
||||
void update(crl::time now);
|
||||
void startFall();
|
||||
void paintParticle(
|
||||
QPainter &p,
|
||||
const Particle &particle,
|
||||
const QRect &rect);
|
||||
void initParticle(Particle &particle, bool falling);
|
||||
void updateParticle(Particle &particle, crl::time dt);
|
||||
|
||||
std::vector<Particle> _particles;
|
||||
std::vector<QBrush> _brushes;
|
||||
Ui::Animations::Basic _animation;
|
||||
Fn<void()> _repaint;
|
||||
crl::time _lastUpdate = 0;
|
||||
float64 _speedCoef = 1.;
|
||||
int _fallingDown = 0;
|
||||
int _smallSide = 0;
|
||||
bool _startedFall = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
2
Telegram/ThirdParty/rlottie
vendored
2
Telegram/ThirdParty/rlottie
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 0ee2e9c5843257ccd11672611829b9bb5d02aa98
|
||||
Subproject commit 75b31e49b3c69355c4971ee2029eff23a22fcb75
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
AppVersion 1009007
|
||||
AppVersion 1009008
|
||||
AppVersionStrMajor 1.9
|
||||
AppVersionStrSmall 1.9.7
|
||||
AppVersionStr 1.9.7
|
||||
AppVersionStrSmall 1.9.8
|
||||
AppVersionStr 1.9.8
|
||||
BetaChannel 0
|
||||
AlphaVersion 0
|
||||
AppVersionOriginal 1.9.7
|
||||
AppVersionOriginal 1.9.8
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
1.9.8 (24.01.20)
|
||||
|
||||
- Bug fixes and other minor improvements.
|
||||
|
||||
1.9.7 (23.01.20)
|
||||
|
||||
- Create three new kinds of polls.
|
||||
|
|
|
|||
2
cmake
2
cmake
|
|
@ -1 +1 @@
|
|||
Subproject commit 94bdb64c38fc3951fd8b2ae29ba8850484df044c
|
||||
Subproject commit b944efa1f33770fe88c1fb54295a84f3db4b4d26
|
||||
Loading…
Add table
Reference in a new issue