diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index 931c45083f..a1f68a4110 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -34,7 +34,7 @@ void Polls::create( const TextWithEntities &text, SendAction action, Fn done, - Fn fail) { + Fn 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); }); } diff --git a/Telegram/SourceFiles/api/api_polls.h b/Telegram/SourceFiles/api/api_polls.h index c07341b6a5..26d46e09af 100644 --- a/Telegram/SourceFiles/api/api_polls.h +++ b/Telegram/SourceFiles/api/api_polls.h @@ -32,7 +32,7 @@ public: const TextWithEntities &text, SendAction action, Fn done, - Fn fail); + Fn fail); void sendVotes( FullMsgId itemId, const std::vector &options); diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp index a3641fb762..6ca576e608 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.cpp +++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp @@ -93,6 +93,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_polls.h" #include "styles/style_settings.h" +#include #include 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 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 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 CreatePollBox::setupQuestion( not_null container) { using namespace Settings; @@ -1397,6 +1430,9 @@ object_ptr CreatePollBox::setupContent() { QVector attributes; bool forceFile = true; }; + using StartUploadFn = Fn, + Ui::PreparedFile)>; struct State final { Errors error = Error::Question; std::unique_ptr options; @@ -1417,6 +1453,9 @@ object_ptr CreatePollBox::setupContent() { base::unique_qptr durationMenu; base::unique_qptr stickerPanel; std::unique_ptr prepareQueue; + StartUploadFn startPhotoUpload; + StartUploadFn startDocumentUpload; + StartUploadFn startVideoUpload; }; const auto state = lifetime().make_state(); state->prepareQueue = std::make_unique(); @@ -1435,6 +1474,7 @@ object_ptr CreatePollBox::setupContent() { PollMedia value, std::shared_ptr thumbnail, bool rounded) { + const auto wasUploading = media->uploading; media->token++; media->media = value; media->thumbnail = std::move(thumbnail); @@ -1444,6 +1484,12 @@ object_ptr 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 CreatePollBox::setupContent() { updateStickerPanelGeometry(); panel->toggleAnimated(); }; + const auto asyncReupload = [=]( + std::shared_ptr media, + Fn prepare, + Fn 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(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 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 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( @@ -1796,6 +1966,11 @@ object_ptr 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 CreatePollBox::setupContent() { const auto startPreparedVideoUpload = [=]( std::shared_ptr 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( @@ -1919,6 +2099,9 @@ object_ptr CreatePollBox::setupContent() { prepared); })); }; + state->startPhotoUpload = startPreparedPhotoUpload; + state->startDocumentUpload = startPreparedDocumentUpload; + state->startVideoUpload = startPreparedVideoUpload; const auto applyPreparedPhotoList = [=]( std::shared_ptr media, Ui::PreparedList &&list) { @@ -2741,7 +2924,44 @@ object_ptr CreatePollBox::setupContent() { tr::phrase<> text) { show->showToast(text(tr::now)); }; + _refreshExpiredMedia = [=] { + const auto forceRefresh = []( + const std::shared_ptr &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 &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); diff --git a/Telegram/SourceFiles/boxes/create_poll_box.h b/Telegram/SourceFiles/boxes/create_poll_box.h index b96e709794..6940101697 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.h +++ b/Telegram/SourceFiles/boxes/create_poll_box.h @@ -52,6 +52,7 @@ public: [[nodiscard]] rpl::producer submitRequests() const; void submitFailed(const QString &error); + void submitMediaExpired(); void setInnerFocus() override; @@ -89,6 +90,7 @@ private: rpl::variable _starsRequired; base::unique_qptr _emojiPanel; Fn _setInnerFocus; + Fn _refreshExpiredMedia; Fn()> _dataIsValidValue; rpl::event_stream _submitRequests; diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index d973d46abc..2f2775da28 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -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()); diff --git a/Telegram/SourceFiles/poll/poll_media_upload.cpp b/Telegram/SourceFiles/poll/poll_media_upload.cpp index b25f0d71b6..9c4f31d291 100644 --- a/Telegram/SourceFiles/poll/poll_media_upload.cpp +++ b/Telegram/SourceFiles/poll/poll_media_upload.cpp @@ -541,6 +541,7 @@ void PollMediaUploader::setMedia( PollMedia value, std::shared_ptr 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); } diff --git a/Telegram/SourceFiles/poll/poll_media_upload.h b/Telegram/SourceFiles/poll/poll_media_upload.h index 5a75f8e7bd..c5a3bb0354 100644 --- a/Telegram/SourceFiles/poll/poll_media_upload.h +++ b/Telegram/SourceFiles/poll/poll_media_upload.h @@ -45,7 +45,9 @@ struct PollMediaState { float64 progress = 0.; uint64 uploadDataId = 0; uint64 token = 0; + crl::time uploadedAt = 0; Fn update; + Fn reupload; }; class LocalImageThumbnail final : public Ui::DynamicImage { diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 9c38ed2056..d3ceb9094c 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -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( diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 07964a7b49..323db5bb83 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 07964a7b496d8da2f7f10ebe731f70f9ae2b6b05 +Subproject commit 323db5bb833ada7d77fc4ade91d2d293208bc438