Added stale media reupload guard before sending poll with expired files.

This commit is contained in:
23rd
2026-03-28 17:03:04 +03:00
parent c7ec23b872
commit 75632f7c86
9 changed files with 244 additions and 7 deletions
+4 -2
View File
@@ -34,7 +34,7 @@ void Polls::create(
const TextWithEntities &text,
SendAction action,
Fn<void()> done,
Fn<void()> fail) {
Fn<void(bool fileReferenceExpired)> fail) {
_session->api().sendAction(action);
const auto history = action.history;
@@ -133,7 +133,9 @@ void Polls::create(
monoforumPeerId,
UnixtimeFromMsgId(response.outerMsgId));
}
fail();
const auto expired = (error.code() == 400)
&& error.type().startsWith(u"FILE_REFERENCE_"_q);
fail(expired);
});
}
+1 -1
View File
@@ -32,7 +32,7 @@ public:
const TextWithEntities &text,
SendAction action,
Fn<void()> done,
Fn<void()> fail);
Fn<void(bool fileReferenceExpired)> fail);
void sendVotes(
FullMsgId itemId,
const std::vector<QByteArray> &options);
@@ -93,6 +93,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_polls.h"
#include "styles/style_settings.h"
#include <QtCore/QBuffer>
#include <QtCore/QMimeData>
namespace {
@@ -105,6 +106,7 @@ constexpr auto kWarnOptionLimit = 30;
constexpr auto kSolutionLimit = 200;
constexpr auto kWarnSolutionLimit = 60;
constexpr auto kErrorLimit = 99;
constexpr auto kMediaUploadMaxAge = 45 * 60 * crl::time(1000);
using PollMediaState = PollMediaUpload::PollMediaState;
using PollMediaButton = PollMediaUpload::PollMediaButton;
@@ -143,6 +145,7 @@ public:
[[nodiscard]] bool isValid() const;
[[nodiscard]] bool hasCorrect() const;
[[nodiscard]] bool hasUploadingMedia() const;
bool refreshStaleMedia(crl::time threshold);
[[nodiscard]] std::vector<PollAnswer> toPollAnswers() const;
void focusFirst();
@@ -187,6 +190,7 @@ private:
[[nodiscard]] bool isTooLong() const;
[[nodiscard]] bool isCorrect() const;
[[nodiscard]] bool uploadingMedia() const;
bool refreshMediaIfStale(crl::time threshold);
[[nodiscard]] bool hasFocus() const;
void setFocus() const;
void clearValue();
@@ -541,6 +545,18 @@ bool Options::Option::uploadingMedia() const {
return _media->uploading;
}
bool Options::Option::refreshMediaIfStale(crl::time threshold) {
if (_media->media
&& _media->uploadedAt > 0
&& (!threshold
|| (crl::now() - _media->uploadedAt > threshold))
&& _media->reupload) {
_media->reupload();
return true;
}
return false;
}
bool Options::Option::hasFocus() const {
return field()->hasFocus();
}
@@ -741,6 +757,16 @@ bool Options::hasUploadingMedia() const {
return ranges::any_of(_list, &Option::uploadingMedia);
}
bool Options::refreshStaleMedia(crl::time threshold) {
auto refreshed = false;
for (const auto &option : _list) {
if (option->refreshMediaIfStale(threshold)) {
refreshed = true;
}
}
return refreshed;
}
not_null<Ui::RpWidget*> Options::layoutWidget() const {
return _optionsLayout;
}
@@ -1179,6 +1205,13 @@ void CreatePollBox::submitFailed(const QString &error) {
showToast(error);
}
void CreatePollBox::submitMediaExpired() {
if (_refreshExpiredMedia) {
_refreshExpiredMedia();
showToast(tr::lng_polls_media_uploading_toast(tr::now));
}
}
not_null<Ui::InputField*> CreatePollBox::setupQuestion(
not_null<Ui::VerticalLayout*> container) {
using namespace Settings;
@@ -1397,6 +1430,9 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
QVector<MTPDocumentAttribute> attributes;
bool forceFile = true;
};
using StartUploadFn = Fn<void(
std::shared_ptr<PollMediaState>,
Ui::PreparedFile)>;
struct State final {
Errors error = Error::Question;
std::unique_ptr<Options> options;
@@ -1417,6 +1453,9 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
base::unique_qptr<Ui::PopupMenu> durationMenu;
base::unique_qptr<ChatHelpers::TabbedPanel> stickerPanel;
std::unique_ptr<TaskQueue> prepareQueue;
StartUploadFn startPhotoUpload;
StartUploadFn startDocumentUpload;
StartUploadFn startVideoUpload;
};
const auto state = lifetime().make_state<State>();
state->prepareQueue = std::make_unique<TaskQueue>();
@@ -1435,6 +1474,7 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
PollMedia value,
std::shared_ptr<Ui::DynamicImage> thumbnail,
bool rounded) {
const auto wasUploading = media->uploading;
media->token++;
media->media = value;
media->thumbnail = std::move(thumbnail);
@@ -1444,6 +1484,12 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
: 0.;
media->uploadDataId = 0;
media->uploading = false;
if (wasUploading && value) {
media->uploadedAt = crl::now();
} else {
media->uploadedAt = 0;
media->reupload = nullptr;
}
updateMedia(media);
};
struct UploadedMedia final {
@@ -1734,9 +1780,133 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
updateStickerPanelGeometry();
panel->toggleAnimated();
};
const auto asyncReupload = [=](
std::shared_ptr<PollMediaState> media,
Fn<Ui::PreparedList()> prepare,
Fn<bool(const Ui::PreparedList&)> validate,
const QString &name,
const StartUploadFn &startUpload) {
const auto reuploadToken = ++media->token;
media->media = PollMedia();
media->uploading = true;
media->progress = 0.;
media->uploadDataId = 0;
updateMedia(media);
const auto weak = QPointer<CreatePollBox>(this);
crl::async([=, prepare = std::move(prepare)] {
auto list = prepare();
crl::on_main([=, list = std::move(list)]() mutable {
if (!weak || media->token != reuploadToken) {
return;
}
if (list.error != Ui::PreparedList::Error::None
|| list.files.empty()
|| (validate && !validate(list))) {
setMedia(media, PollMedia(), nullptr, false);
showToast(tr::lng_attach_failed(tr::now));
return;
}
auto &f = list.files.front();
if (!name.isEmpty()) {
f.displayName = name;
}
startUpload(media, std::move(f));
});
});
};
const auto setFileReupload = [=](
std::shared_ptr<PollMediaState> media,
const QString &path,
const QString &name,
const StartUploadFn &startUpload) {
media->reupload = crl::guard(this, [=,
weak = std::weak_ptr(media)] {
const auto strong = weak.lock();
if (!strong) {
return;
}
if (path.isEmpty()) {
setMedia(strong, PollMedia(), nullptr, false);
showToast(tr::lng_attach_failed(tr::now));
return;
}
const auto premium = _controller->session().premium();
asyncReupload(
strong,
[=] {
return Storage::PrepareMediaList(
QStringList{ path },
st::sendMediaPreviewSize,
premium);
},
nullptr,
name,
startUpload);
});
};
const auto startPreparedPhotoUpload = [=](
std::shared_ptr<PollMediaState> media,
Ui::PreparedFile file) {
const auto sourceImage = (file.path.isEmpty()
&& file.content.isEmpty()
&& file.information)
? [&]() -> QImage {
const auto img = std::get_if<
Ui::PreparedFileInformation::Image>(
&file.information->media);
return (img && !img->data.isNull())
? img->data
: QImage();
}()
: QImage();
media->reupload = crl::guard(this, [=,
weak = std::weak_ptr(media),
path = file.path,
content = file.content,
name = file.displayName] {
const auto strong = weak.lock();
if (!strong) {
return;
}
const auto premium = _controller->session().premium();
asyncReupload(
strong,
[=] {
if (!path.isEmpty()) {
return Storage::PrepareMediaList(
QStringList{ path },
st::sendMediaPreviewSize,
premium);
}
if (!content.isEmpty()) {
auto image = QImage::fromData(content);
if (!image.isNull()) {
return Storage::PrepareMediaFromImage(
std::move(image),
QByteArray(content),
st::sendMediaPreviewSize);
}
} else if (!sourceImage.isNull()) {
auto bytes = QByteArray();
auto buffer = QBuffer(&bytes);
buffer.open(QIODevice::WriteOnly);
sourceImage.save(&buffer, "PNG");
return Storage::PrepareMediaFromImage(
QImage(sourceImage),
std::move(bytes),
st::sendMediaPreviewSize);
}
return Ui::PreparedList(
Ui::PreparedList::Error::EmptyFile,
QString());
},
[](const Ui::PreparedList &list) {
return list.files.front().type
== Ui::PreparedFile::Type::Photo;
},
name,
state->startPhotoUpload);
});
const auto token = ++media->token;
media->media = PollMedia();
media->thumbnail = std::make_shared<LocalImageThumbnail>(
@@ -1796,6 +1966,11 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
const auto displayName = file.displayName.isEmpty()
? QFileInfo(file.path).fileName()
: file.displayName;
setFileReupload(
media,
file.path,
displayName,
state->startDocumentUpload);
auto audioAttributes = PollMediaUpload::ExtractAudioAttributes(file);
const auto isAudio = !audioAttributes.isEmpty();
const auto token = ++media->token;
@@ -1859,6 +2034,11 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
const auto startPreparedVideoUpload = [=](
std::shared_ptr<PollMediaState> media,
Ui::PreparedFile file) {
setFileReupload(
media,
file.path,
file.displayName,
state->startVideoUpload);
const auto token = ++media->token;
media->media = PollMedia();
media->thumbnail = std::make_shared<LocalImageThumbnail>(
@@ -1919,6 +2099,9 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
prepared);
}));
};
state->startPhotoUpload = startPreparedPhotoUpload;
state->startDocumentUpload = startPreparedDocumentUpload;
state->startVideoUpload = startPreparedVideoUpload;
const auto applyPreparedPhotoList = [=](
std::shared_ptr<PollMediaState> media,
Ui::PreparedList &&list) {
@@ -2741,7 +2924,44 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
tr::phrase<> text) {
show->showToast(text(tr::now));
};
_refreshExpiredMedia = [=] {
const auto forceRefresh = [](
const std::shared_ptr<PollMediaState> &m) {
if (m->media && m->reupload) {
m->reupload();
}
};
forceRefresh(state->descriptionMedia);
forceRefresh(state->solutionMedia);
options->refreshStaleMedia(0);
};
const auto send = [=](Api::SendOptions sendOptions) {
const auto kStaleTimeout = kMediaUploadMaxAge;
auto refreshedAny = false;
const auto tryRefresh = [&](
const std::shared_ptr<PollMediaState> &m) {
if (m->media
&& m->uploadedAt > 0
&& (crl::now() - m->uploadedAt > kStaleTimeout)
&& m->reupload) {
m->reupload();
refreshedAny = true;
}
};
tryRefresh(state->descriptionMedia);
if (quiz->toggled()) {
tryRefresh(state->solutionMedia);
}
if (options->refreshStaleMedia(kStaleTimeout)) {
refreshedAny = true;
}
if (refreshedAny) {
collectError();
if (state->error & Error::Media) {
showError(tr::lng_polls_media_uploading_toast);
}
return;
}
collectError();
if (state->error & Error::Question) {
showError(tr::lng_polls_choose_question);
@@ -52,6 +52,7 @@ public:
[[nodiscard]] rpl::producer<Result> submitRequests() const;
void submitFailed(const QString &error);
void submitMediaExpired();
void setInnerFocus() override;
@@ -89,6 +90,7 @@ private:
rpl::variable<int> _starsRequired;
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
Fn<void()> _setInnerFocus;
Fn<void()> _refreshExpiredMedia;
Fn<rpl::producer<bool>()> _dataIsValidValue;
rpl::event_stream<Result> _submitRequests;
@@ -1675,7 +1675,7 @@ void AttachPollOptionTabs(
const auto direction = (index > 0)
? Ui::PopupMenu::SwitchDirection::LeftToRight
: Ui::PopupMenu::SwitchDirection::RightToLeft;
base::call_delayed(0, menu, [=] {
crl::on_main(menu, [=] {
menu->swapStashed(direction);
});
}, tabs->lifetime());
@@ -541,6 +541,7 @@ void PollMediaUploader::setMedia(
PollMedia value,
std::shared_ptr<Ui::DynamicImage> thumbnail,
bool rounded) {
const auto wasUploading = media->uploading;
media->token++;
media->media = value;
media->thumbnail = std::move(thumbnail);
@@ -550,6 +551,12 @@ void PollMediaUploader::setMedia(
: 0.;
media->uploadDataId = 0;
media->uploading = false;
if (wasUploading && value) {
media->uploadedAt = crl::now();
} else {
media->uploadedAt = 0;
media->reupload = nullptr;
}
updateMedia(media);
}
@@ -45,7 +45,9 @@ struct PollMediaState {
float64 progress = 0.;
uint64 uploadDataId = 0;
uint64 token = 0;
crl::time uploadedAt = 0;
Fn<void()> update;
Fn<void()> reupload;
};
class LocalImageThumbnail final : public Ui::DynamicImage {
@@ -2268,9 +2268,13 @@ void PeerMenuCreatePoll(
state->create = nullptr;
weak->closeBox();
}),
crl::guard(weak, [=] {
crl::guard(weak, [=](bool fileReferenceExpired) {
state->lock = false;
weak->submitFailed(tr::lng_attach_failed(tr::now));
if (fileReferenceExpired) {
weak->submitMediaExpired();
} else {
weak->submitFailed(tr::lng_attach_failed(tr::now));
}
}));
};
box->submitRequests(