diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index cc82bb6cb7..80878834a9 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1441,6 +1441,8 @@ PRIVATE overview/overview_layout.cpp overview/overview_layout.h overview/overview_layout_delegate.h + poll/poll_media_upload.cpp + poll/poll_media_upload.h passport/passport_encryption.cpp passport/passport_encryption.h passport/passport_form_controller.cpp diff --git a/Telegram/SourceFiles/poll/poll_media_upload.cpp b/Telegram/SourceFiles/poll/poll_media_upload.cpp new file mode 100644 index 0000000000..17ce7e25eb --- /dev/null +++ b/Telegram/SourceFiles/poll/poll_media_upload.cpp @@ -0,0 +1,986 @@ +/* +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 "poll/poll_media_upload.h" + +#include "api/api_sending.h" +#include "apiwrap.h" +#include "base/event_filter.h" +#include "core/file_utilities.h" +#include "core/mime_type.h" +#include "data/data_document.h" +#include "data/data_peer.h" +#include "data/data_photo.h" +#include "data/data_session.h" +#include "lang/lang_keys.h" +#include "layout/layout_document_generic_preview.h" +#include "main/main_session.h" +#include "platform/platform_file_utilities.h" +#include "storage/file_upload.h" +#include "storage/localimageloader.h" +#include "storage/storage_media_prepare.h" +#include "ui/chat/attach/attach_prepare.h" +#include "ui/dynamic_thumbnails.h" +#include "ui/effects/ripple_animation.h" +#include "ui/painter.h" +#include "ui/rect.h" +#include "ui/widgets/fields/input_field.h" +#include "styles/style_boxes.h" +#include "styles/style_overview.h" +#include "styles/style_polls.h" + +#include + +namespace PollMediaUpload { + +LocalImageThumbnail::LocalImageThumbnail(QImage original) +: _original(std::move(original)) { +} + +std::shared_ptr LocalImageThumbnail::clone() { + return std::make_shared(_original); +} + +QImage LocalImageThumbnail::image(int size) { + return _original; +} + +void LocalImageThumbnail::subscribeToUpdates(Fn callback) { +} + +QImage GenerateDocumentFilePreview( + const QString &filename, + int size) { + const auto preview = Layout::DocumentGenericPreview::Create(filename); + const auto &color = preview.color; + const auto &ext = preview.ext; + + const auto ratio = style::DevicePixelRatio(); + auto result = QImage( + QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + result.fill(Qt::transparent); + result.setDevicePixelRatio(ratio); + + auto p = QPainter(&result); + PainterHighQualityEnabler hq(p); + p.setPen(Qt::NoPen); + p.setBrush(color); + p.drawRoundedRect( + QRect(0, 0, size, size), + st::roundRadiusSmall, + st::roundRadiusSmall); + + if (!ext.isEmpty()) { + const auto refSize = st::overviewFileLayout.fileThumbSize; + const auto fontSize = std::max( + size * st::overviewFileExtFont->f.pixelSize() / refSize, + 8); + const auto font = style::font( + fontSize, + st::overviewFileExtFont->flags(), + st::overviewFileExtFont->family()); + const auto padding = size * st::overviewFileExtPadding / refSize; + const auto maxw = size - padding * 2; + auto extStr = ext; + auto extw = font->width(extStr); + if (extw > maxw) { + extStr = font->elided(extStr, maxw, Qt::ElideMiddle); + extw = font->width(extStr); + } + p.setFont(font); + p.setPen(st::overviewFileExtFg); + p.drawText( + (size - extw) / 2, + (size - font->height) / 2 + font->ascent, + extStr); + } + p.end(); + return result; +} + +bool ValidateFileDragData(not_null data) { + if (data->hasImage()) { + return true; + } + const auto urls = Core::ReadMimeUrls(data); + return (urls.size() == 1) && urls.front().isLocalFile(); +} + +Ui::PreparedList FileListFromMimeData( + not_null data, + bool premium) { + using Error = Ui::PreparedList::Error; + const auto urls = Core::ReadMimeUrls(data); + if (!urls.isEmpty()) { + return Storage::PrepareMediaList( + urls.mid(0, 1), + st::sendMediaPreviewSize, + premium); + } else if (auto read = Core::ReadMimeImage(data)) { + return Storage::PrepareMediaFromImage( + std::move(read.image), + std::move(read.content), + st::sendMediaPreviewSize); + } + return Ui::PreparedList(Error::EmptyFile, QString()); +} + +PollMediaButton::PollMediaButton( + not_null parent, + const style::IconButton &st, + std::shared_ptr state) +: Ui::RippleButton(parent, st.ripple) +, _st(st) +, _state(std::move(state)) +, _attach(Ui::MakeIconThumbnail(_st.icon)) +, _attachOver(_st.iconOver.empty() + ? _attach + : Ui::MakeIconThumbnail(_st.iconOver)) +, _radial([=](crl::time now) { radialAnimationCallback(now); }) { + const auto weak = QPointer(this); + _state->update = [=] { + if (weak) { + weak->updateMediaSubscription(); + weak->update(); + } + }; + resize(_st.width, _st.height); + setPointerCursor(true); + updateMediaSubscription(); +} + +PollMediaButton::~PollMediaButton() { + if (_subscribed) { + _subscribed->subscribeToUpdates(nullptr); + } +} + +void PollMediaButton::paintEvent(QPaintEvent *e) { + auto p = Painter(this); + paintRipple(p, _st.rippleAreaPosition); + if (const auto image = _state->thumbnail + ? _state->thumbnail + : currentAttachThumbnail()) { + const auto target = _state->thumbnail + ? rippleRect() + : iconRect(); + if (_state->thumbnail) { + paintCover( + p, + target, + image->image(std::max(target.width(), target.height())), + _state->rounded); + } else { + p.drawImage( + target, + image->image(std::max(target.width(), target.height()))); + } + } + if (_state->thumbnail && !_state->uploading) { + const auto viewOpacity = _viewShown.value( + (isOver() || isDown()) ? 1. : 0.); + if (viewOpacity > 0.) { + p.save(); + p.setOpacity(viewOpacity); + auto path = QPainterPath(); + path.addRoundedRect( + rippleRect(), + st::roundRadiusSmall, + st::roundRadiusSmall); + p.setClipPath(path); + p.fillRect(rippleRect(), st::songCoverOverlayFg); + st::pollAttachView.paintInCenter(p, rippleRect()); + p.restore(); + } + } + if (_state->uploading && !_radial.animating()) { + _radial.start(_state->progress); + } + if (_state->uploading || _radial.animating()) { + if (_state->thumbnail) { + p.save(); + auto path = QPainterPath(); + path.addRoundedRect( + rippleRect(), + st::roundRadiusSmall, + st::roundRadiusSmall); + p.setClipPath(path); + p.fillRect(rippleRect(), st::songCoverOverlayFg); + p.restore(); + } + const auto cancelOpacity = _state->uploading + ? _cancelShown.value( + (isOver() || isDown()) ? 1. : 0.) + : 0.; + const auto line = float64(st::lineWidth * 2); + const auto margin = float64(st::pollAttachProgressMargin); + const auto arc = QRectF(rippleRect()) - Margins(margin); + if (cancelOpacity > 0.) { + p.setOpacity(cancelOpacity); + st::pollAttachCancel.paintInCenter(p, rippleRect()); + p.setOpacity(1.); + } + _radial.draw(p, arc, line, st::historyFileThumbRadialFg); + } +} + +void PollMediaButton::onStateChanged(State was, StateChangeSource source) { + RippleButton::onStateChanged(was, source); + if (!_state->thumbnail) { + update(); + } + const auto over = isOver() || isDown(); + const auto wasOver = (was & StateFlag::Over) + || (was & StateFlag::Down); + if (over != wasOver) { + if (_state->uploading) { + _cancelShown.start( + [=] { update(); }, + over ? 0. : 1., + over ? 1. : 0., + st::universalDuration); + } + if (_state->thumbnail && !_state->uploading) { + _viewShown.start( + [=] { update(); }, + over ? 0. : 1., + over ? 1. : 0., + st::universalDuration); + } + } +} + +QImage PollMediaButton::prepareRippleMask() const { + return Ui::RippleAnimation::EllipseMask(QSize( + _st.rippleAreaSize, + _st.rippleAreaSize)); +} + +QPoint PollMediaButton::prepareRippleStartPosition() const { + auto result = mapFromGlobal(QCursor::pos()) + - _st.rippleAreaPosition; + const auto rect = QRect( + QPoint(), + QSize(_st.rippleAreaSize, _st.rippleAreaSize)); + return rect.contains(result) + ? result + : DisabledRippleStartPosition(); +} + +void PollMediaButton::paintCover( + Painter &p, + QRect target, + QImage image, + bool rounded) const { + if (image.isNull() || target.isEmpty()) { + return; + } + const auto source = QRectF(0, 0, image.width(), image.height()); + const auto kx = target.width() / source.width(); + const auto ky = target.height() / source.height(); + const auto scale = std::max(kx, ky); + const auto size = QSizeF( + source.width() * scale, + source.height() * scale); + const auto geometry = QRectF( + target.x() + (target.width() - size.width()) / 2., + target.y() + (target.height() - size.height()) / 2., + size.width(), + size.height()); + p.save(); + if (rounded) { + auto path = QPainterPath(); + path.addRoundedRect( + target, + st::roundRadiusSmall, + st::roundRadiusSmall); + p.setClipPath(path); + } + p.drawImage(geometry, image, source); + p.restore(); +} + +void PollMediaButton::radialAnimationCallback(crl::time now) { + const auto updated = _radial.update( + _state->progress, + !_state->uploading, + now); + if (!anim::Disabled() || updated || _radial.animating()) { + update(rippleRect()); + } +} + +QRect PollMediaButton::rippleRect() const { + return QRect( + _st.rippleAreaPosition, + QSize(_st.rippleAreaSize, _st.rippleAreaSize)); +} + +QRect PollMediaButton::iconRect() const { + const auto over = isOver() || isDown(); + const auto &icon = over && !_st.iconOver.empty() + ? _st.iconOver + : _st.icon; + auto position = _st.iconPosition; + if (position.x() < 0) { + position.setX((width() - icon.width()) / 2); + } + if (position.y() < 0) { + position.setY((height() - icon.height()) / 2); + } + return QRect(position, QSize(icon.width(), icon.height())); +} + +std::shared_ptr +PollMediaButton::currentAttachThumbnail() const { + return (isOver() || isDown()) ? _attachOver : _attach; +} + +void PollMediaButton::updateMediaSubscription() { + if (_subscribed == _state->thumbnail) { + return; + } + if (_subscribed) { + _subscribed->subscribeToUpdates(nullptr); + } + _subscribed = _state->thumbnail; + if (!_subscribed) { + return; + } + const auto weak = QPointer(this); + _subscribed->subscribeToUpdates([=] { + if (weak) { + weak->update(); + } + }); +} + +PreparePollMediaTask::PreparePollMediaTask( + FileLoadTask::Args &&args, + Fn)> done) +: _task(std::move(args)) +, _done(std::move(done)) { +} + +void PreparePollMediaTask::process() { + _task.process({ .generateGoodThumbnail = false }); +} + +void PreparePollMediaTask::finish() { + _done(_task.peekResult()); +} + +PollMediaUploader::PollMediaUploader(Args &&args) +: _session(args.session) +, _peer(args.peer) +, _showError(std::move(args.showError)) +, _prepareQueue(std::make_unique()) { + subscribeToUploader(); +} + +PollMediaUploader::~PollMediaUploader() = default; + +void PollMediaUploader::subscribeToUploader() { + auto &uploader = _session->uploader(); + + uploader.photoReady( + ) | rpl::on_next([=](const Storage::UploadedMedia &data) { + const auto context = _uploads.take(data.fullId); + if (!context) { + return; + } + const auto media = context->media.lock(); + if (!media || (media->token != context->token)) { + return; + } + applyUploaded(media, context->token, data.fullId, data.info.file); + }, _lifetime); + + uploader.photoProgress( + ) | rpl::on_next([=](const FullMsgId &id) { + const auto i = _uploads.find(id); + if (i == _uploads.end()) { + return; + } + const auto &context = i->second; + const auto media = context.media.lock(); + if (!media + || (media->token != context.token) + || !media->uploadDataId) { + return; + } + media->progress = _session->data().photo( + media->uploadDataId)->progress(); + updateMedia(media); + }, _lifetime); + + uploader.photoFailed( + ) | rpl::on_next([=](const FullMsgId &id) { + const auto context = _uploads.take(id); + if (!context) { + return; + } + const auto media = context->media.lock(); + if (!media || (media->token != context->token)) { + return; + } + setMedia(media, PollMedia(), nullptr, false); + _showError(tr::lng_attach_failed(tr::now)); + }, _lifetime); + + uploader.documentReady( + ) | rpl::on_next([=](const Storage::UploadedMedia &data) { + const auto context = _uploads.take(data.fullId); + if (!context) { + return; + } + const auto media = context->media.lock(); + if (!media || (media->token != context->token)) { + return; + } + applyUploadedDocument( + media, + context->token, + data.fullId, + data.info, + *context); + }, _lifetime); + + uploader.documentProgress( + ) | rpl::on_next([=](const FullMsgId &id) { + const auto i = _uploads.find(id); + if (i == _uploads.end()) { + return; + } + const auto &context = i->second; + const auto media = context.media.lock(); + if (!media + || (media->token != context.token) + || !media->uploadDataId) { + return; + } + media->progress = _session->data().document( + media->uploadDataId)->progress(); + updateMedia(media); + }, _lifetime); + + uploader.documentFailed( + ) | rpl::on_next([=](const FullMsgId &id) { + const auto context = _uploads.take(id); + if (!context) { + return; + } + const auto media = context->media.lock(); + if (!media || (media->token != context->token)) { + return; + } + setMedia(media, PollMedia(), nullptr, false); + _showError(tr::lng_attach_failed(tr::now)); + }, _lifetime); +} + +void PollMediaUploader::updateMedia( + const std::shared_ptr &media) { + if (media->update) { + media->update(); + } +} + +void PollMediaUploader::setMedia( + const std::shared_ptr &media, + PollMedia value, + std::shared_ptr thumbnail, + bool rounded) { + media->token++; + media->media = value; + media->thumbnail = std::move(thumbnail); + media->rounded = rounded; + media->progress = (media->uploading && media->media) + ? 1. + : 0.; + media->uploadDataId = 0; + media->uploading = false; + updateMedia(media); +} + +auto PollMediaUploader::parseUploaded( + const MTPMessageMedia &result, + FullMsgId fullId) -> UploadedMedia { + auto parsed = UploadedMedia(); + auto &owner = _session->data(); + result.match([&](const MTPDmessageMediaPhoto &media) { + if (const auto photo = media.vphoto()) { + photo->match([&](const MTPDphoto &) { + parsed.input.photo = owner.processPhoto(*photo); + parsed.thumbnail = Ui::MakePhotoThumbnail( + parsed.input.photo, + fullId); + }, [](const auto &) { + }); + } + }, [&](const MTPDmessageMediaDocument &media) { + if (const auto document = media.vdocument()) { + document->match([&](const MTPDdocument &) { + parsed.input.document = owner.processDocument( + *document); + parsed.thumbnail + = Ui::MakeDocumentFilePreviewThumbnail( + parsed.input.document, + fullId); + }, [](const auto &) { + }); + } + }, [](const auto &) { + }); + return parsed; +} + +void PollMediaUploader::applyUploaded( + const std::shared_ptr &media, + uint64 token, + FullMsgId fullId, + const MTPInputFile &file) { + const auto uploaded = MTP_inputMediaUploadedPhoto( + MTP_flags(0), + file, + MTP_vector(QVector()), + MTPint(), + MTPInputDocument()); + _session->api().request(MTPmessages_UploadMedia( + MTP_flags(0), + MTPstring(), + _peer->input(), + uploaded + )).done([=](const MTPMessageMedia &result) { + if (media->token != token) { + return; + } + auto parsed = parseUploaded(result, fullId); + if (!parsed.input) { + setMedia(media, PollMedia(), nullptr, false); + _showError(tr::lng_attach_failed(tr::now)); + return; + } + setMedia( + media, + parsed.input, + media->thumbnail + ? media->thumbnail + : std::move(parsed.thumbnail), + true); + }).fail([=](const MTP::Error &) { + if (media->token != token) { + return; + } + setMedia(media, PollMedia(), nullptr, false); + _showError(tr::lng_attach_failed(tr::now)); + }).send(); +} + +void PollMediaUploader::applyUploadedDocument( + const std::shared_ptr &media, + uint64 token, + FullMsgId fullId, + const Api::RemoteFileInfo &info, + const UploadContext &context) { + using Flag = MTPDinputMediaUploadedDocument::Flag; + const auto flags = (context.forceFile ? Flag::f_force_file : Flag()) + | (info.thumb ? Flag::f_thumb : Flag()); + auto attributes = !context.attributes.isEmpty() + ? context.attributes + : QVector{ + MTP_documentAttributeFilename( + MTP_string(context.filename)), + }; + const auto uploaded = MTP_inputMediaUploadedDocument( + MTP_flags(flags), + info.file, + info.thumb.value_or(MTPInputFile()), + MTP_string(context.filemime), + MTP_vector(std::move(attributes)), + MTP_vector(), + MTPInputPhoto(), + MTP_int(0), + MTP_int(0)); + _session->api().request(MTPmessages_UploadMedia( + MTP_flags(0), + MTPstring(), + _peer->input(), + uploaded + )).done([=](const MTPMessageMedia &result) { + if (media->token != token) { + return; + } + auto parsed = parseUploaded(result, fullId); + if (!parsed.input) { + setMedia(media, PollMedia(), nullptr, false); + _showError(tr::lng_attach_failed(tr::now)); + return; + } + const auto isVideo = parsed.input.document + && parsed.input.document->isVideoFile(); + setMedia( + media, + parsed.input, + isVideo + ? (media->thumbnail + ? media->thumbnail + : std::move(parsed.thumbnail)) + : std::move(parsed.thumbnail), + isVideo); + }).fail([=](const MTP::Error &) { + if (media->token != token) { + return; + } + setMedia(media, PollMedia(), nullptr, false); + _showError(tr::lng_attach_failed(tr::now)); + }).send(); +} + +void PollMediaUploader::startPhotoUpload( + std::shared_ptr media, + Ui::PreparedFile file) { + const auto token = ++media->token; + media->media = PollMedia(); + media->thumbnail = std::make_shared( + std::move(file.preview)); + media->rounded = true; + media->uploading = true; + media->progress = 0.; + media->uploadDataId = 0; + updateMedia(media); + _prepareQueue->addTask(std::make_unique( + FileLoadTask::Args{ + .session = _session, + .filepath = file.path, + .content = file.content, + .information = std::move(file.information), + .videoCover = nullptr, + .type = SendMediaType::Photo, + .to = FileLoadTo( + _peer->id, + Api::SendOptions(), + FullReplyTo(), + MsgId()), + .caption = TextWithTags(), + .spoiler = false, + .album = nullptr, + .forceFile = false, + .idOverride = 0, + .displayName = file.displayName, + }, + [=](std::shared_ptr prepared) { + if ((media->token != token) + || !prepared + || (prepared->type != SendMediaType::Photo)) { + if (media->token == token) { + setMedia(media, PollMedia(), nullptr, false); + _showError(tr::lng_attach_failed(tr::now)); + } + return; + } + const auto uploadId = FullMsgId( + _peer->id, + _session->data().nextLocalMessageId()); + _uploads.emplace(uploadId, UploadContext{ + .media = media, + .token = token, + }); + media->uploadDataId = prepared->id; + _session->uploader().upload(uploadId, prepared); + })); +} + +void PollMediaUploader::startDocumentUpload( + std::shared_ptr media, + Ui::PreparedFile file) { + const auto displayName = file.displayName.isEmpty() + ? QFileInfo(file.path).fileName() + : file.displayName; + const auto token = ++media->token; + media->media = PollMedia(); + media->thumbnail = std::make_shared( + GenerateDocumentFilePreview( + displayName, + st::pollAttach.rippleAreaSize)); + media->rounded = false; + media->uploading = true; + media->progress = 0.; + media->uploadDataId = 0; + updateMedia(media); + _prepareQueue->addTask(std::make_unique( + FileLoadTask::Args{ + .session = _session, + .filepath = file.path, + .content = file.content, + .information = std::move(file.information), + .videoCover = nullptr, + .type = SendMediaType::File, + .to = FileLoadTo( + _peer->id, + Api::SendOptions(), + FullReplyTo(), + MsgId()), + .caption = TextWithTags(), + .spoiler = false, + .album = nullptr, + .forceFile = true, + .idOverride = 0, + .displayName = displayName, + }, + [=](std::shared_ptr prepared) { + if ((media->token != token) || !prepared) { + if (media->token == token) { + setMedia(media, PollMedia(), nullptr, false); + _showError(tr::lng_attach_failed(tr::now)); + } + return; + } + const auto uploadId = FullMsgId( + _peer->id, + _session->data().nextLocalMessageId()); + _uploads.emplace(uploadId, UploadContext{ + .media = media, + .token = token, + .filename = prepared->filename, + .filemime = prepared->filemime, + }); + media->uploadDataId = prepared->id; + _session->uploader().upload(uploadId, prepared); + })); +} + +void PollMediaUploader::startVideoUpload( + std::shared_ptr media, + Ui::PreparedFile file) { + const auto token = ++media->token; + media->media = PollMedia(); + media->thumbnail = std::make_shared( + std::move(file.preview)); + media->rounded = true; + media->uploading = true; + media->progress = 0.; + media->uploadDataId = 0; + updateMedia(media); + _prepareQueue->addTask(std::make_unique( + FileLoadTask::Args{ + .session = _session, + .filepath = file.path, + .content = file.content, + .information = std::move(file.information), + .videoCover = nullptr, + .type = SendMediaType::File, + .to = FileLoadTo( + _peer->id, + Api::SendOptions(), + FullReplyTo(), + MsgId()), + .caption = TextWithTags(), + .spoiler = false, + .album = nullptr, + .forceFile = false, + .idOverride = 0, + .displayName = file.displayName, + }, + [=](std::shared_ptr prepared) { + if ((media->token != token) || !prepared) { + if (media->token == token) { + setMedia(media, PollMedia(), nullptr, false); + _showError(tr::lng_attach_failed(tr::now)); + } + return; + } + auto attributes = QVector(); + prepared->document.match([&](const MTPDdocument &data) { + attributes = data.vattributes().v; + }, [](const auto &) { + }); + const auto uploadId = FullMsgId( + _peer->id, + _session->data().nextLocalMessageId()); + _uploads.emplace(uploadId, UploadContext{ + .media = media, + .token = token, + .filename = prepared->filename, + .filemime = prepared->filemime, + .attributes = std::move(attributes), + .forceFile = false, + }); + media->uploadDataId = prepared->id; + _session->uploader().upload(uploadId, prepared); + })); +} + +bool PollMediaUploader::applyPreparedPhotoList( + std::shared_ptr media, + Ui::PreparedList &&list) { + if (list.error != Ui::PreparedList::Error::None + || (list.files.size() != 1) + || (list.files.front().type != Ui::PreparedFile::Type::Photo)) { + return false; + } + startPhotoUpload(media, std::move(list.files.front())); + return true; +} + +void PollMediaUploader::clearMedia(std::shared_ptr media) { + auto toCancel = std::vector(); + for (auto i = _uploads.begin(); i != _uploads.end();) { + if (i->second.media.lock() == media) { + toCancel.push_back(i->first); + i = _uploads.erase(i); + } else { + ++i; + } + } + for (const auto &id : toCancel) { + _session->uploader().cancel(id); + } + setMedia(media, PollMedia(), nullptr, false); +} + +void PollMediaUploader::choosePhotoOrVideo( + not_null parent, + std::shared_ptr media) { + const auto weak = QPointer(parent.get()); + const auto callback = crl::guard(parent.get(), [=]( + FileDialog::OpenResult &&result) { + const auto checkResult = [&](const Ui::PreparedList &list) { + using namespace Ui; + if (list.files.size() != 1) { + return false; + } + const auto type = list.files.front().type; + return (type == PreparedFile::Type::Photo) + || (type == PreparedFile::Type::Video); + }; + const auto showError = [=](tr::phrase<> text) { + _showError(text(tr::now)); + }; + auto list = Storage::PreparedFileFromFilesDialog( + std::move(result), + checkResult, + showError, + st::sendMediaPreviewSize, + _session->premium()); + if (!list) { + return; + } + auto &file = list->files.front(); + if (file.type == Ui::PreparedFile::Type::Photo) { + applyPreparedPhotoList(media, std::move(*list)); + } else { + startVideoUpload(media, std::move(file)); + } + }); + FileDialog::GetOpenPath( + QPointer(parent.get()), + tr::lng_attach_photo_or_video(tr::now), + FileDialog::PhotoVideoFilesFilter(), + callback); +} + +void PollMediaUploader::chooseDocument( + not_null parent, + std::shared_ptr media) { + const auto callback = crl::guard(parent.get(), [=]( + FileDialog::OpenResult &&result) { + if (result.paths.isEmpty()) { + return; + } + auto list = Storage::PrepareMediaList( + result.paths.mid(0, 1), + st::sendMediaPreviewSize, + _session->premium()); + if (list.error != Ui::PreparedList::Error::None + || list.files.empty()) { + return; + } + startDocumentUpload( + media, + std::move(list.files.front())); + }); + FileDialog::GetOpenPath( + QPointer(parent.get()), + tr::lng_attach_file(tr::now), + FileDialog::AllFilesFilter(), + callback); +} + +void PollMediaUploader::installDropToWidget( + not_null widget, + std::shared_ptr media, + bool allowDocuments) { + widget->setAcceptDrops(true); + base::install_event_filter(widget, [=](not_null event) { + const auto type = event->type(); + if (type != QEvent::DragEnter + && type != QEvent::DragMove + && type != QEvent::Drop) { + return base::EventFilterResult::Continue; + } + const auto drop = static_cast(event.get()); + const auto data = drop->mimeData(); + if (!data || !ValidateFileDragData(data)) { + return base::EventFilterResult::Continue; + } + if (type == QEvent::Drop) { + auto list = FileListFromMimeData(data, _session->premium()); + if (list.error != Ui::PreparedList::Error::None + || list.files.empty()) { + return base::EventFilterResult::Continue; + } + auto &file = list.files.front(); + if (file.type == Ui::PreparedFile::Type::Photo) { + startPhotoUpload(media, std::move(file)); + } else if (file.type == Ui::PreparedFile::Type::Video) { + startVideoUpload(media, std::move(file)); + } else if (allowDocuments) { + startDocumentUpload(media, std::move(file)); + } else { + return base::EventFilterResult::Continue; + } + } + drop->acceptProposedAction(); + return base::EventFilterResult::Cancel; + }); +} + +void PollMediaUploader::installDropToField( + not_null field, + std::shared_ptr media, + bool allowDocuments) { + field->setMimeDataHook([=]( + not_null data, + Ui::InputField::MimeAction action) { + if (action == Ui::InputField::MimeAction::Check) { + return ValidateFileDragData(data); + } else if (action == Ui::InputField::MimeAction::Insert) { + auto list = FileListFromMimeData(data, _session->premium()); + if (list.error != Ui::PreparedList::Error::None + || list.files.empty()) { + return false; + } + auto &file = list.files.front(); + if (file.type == Ui::PreparedFile::Type::Photo) { + startPhotoUpload(media, std::move(file)); + return true; + } else if (file.type == Ui::PreparedFile::Type::Video) { + startVideoUpload(media, std::move(file)); + return true; + } else if (allowDocuments) { + startDocumentUpload(media, std::move(file)); + return true; + } + return false; + } + Unexpected("Polls: action in MimeData hook."); + }); +} + +} // namespace PollMediaUpload diff --git a/Telegram/SourceFiles/poll/poll_media_upload.h b/Telegram/SourceFiles/poll/poll_media_upload.h new file mode 100644 index 0000000000..f2cd4ddbaa --- /dev/null +++ b/Telegram/SourceFiles/poll/poll_media_upload.h @@ -0,0 +1,214 @@ +/* +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 "data/data_poll.h" +#include "storage/localimageloader.h" +#include "ui/dynamic_image.h" +#include "ui/effects/radial_animation.h" +#include "ui/widgets/buttons.h" + +struct FilePrepareResult; + +namespace Api { +struct RemoteFileInfo; +} // namespace Api + +namespace Main { +class Session; +} // namespace Main + +namespace Storage { +struct UploadedMedia; +} // namespace Storage + +namespace Ui { +class InputField; +struct PreparedFile; +struct PreparedList; +} // namespace Ui + +class PeerData; + +namespace PollMediaUpload { + +struct PollMediaState { + PollMedia media; + std::shared_ptr thumbnail; + bool rounded = false; + bool uploading = false; + float64 progress = 0.; + uint64 uploadDataId = 0; + uint64 token = 0; + Fn update; +}; + +class LocalImageThumbnail final : public Ui::DynamicImage { +public: + explicit LocalImageThumbnail(QImage original); + + std::shared_ptr clone() override; + QImage image(int size) override; + void subscribeToUpdates(Fn callback) override; + +private: + QImage _original; + +}; + +[[nodiscard]] QImage GenerateDocumentFilePreview( + const QString &filename, + int size); + +[[nodiscard]] bool ValidateFileDragData(not_null data); + +[[nodiscard]] Ui::PreparedList FileListFromMimeData( + not_null data, + bool premium); + +class PollMediaButton final : public Ui::RippleButton { +public: + PollMediaButton( + not_null parent, + const style::IconButton &st, + std::shared_ptr state); + ~PollMediaButton() override; + +protected: + void paintEvent(QPaintEvent *e) override; + void onStateChanged(State was, StateChangeSource source) override; + QImage prepareRippleMask() const override; + QPoint prepareRippleStartPosition() const override; + +private: + void paintCover( + Painter &p, + QRect target, + QImage image, + bool rounded) const; + void radialAnimationCallback(crl::time now); + [[nodiscard]] QRect rippleRect() const; + [[nodiscard]] QRect iconRect() const; + std::shared_ptr currentAttachThumbnail() const; + void updateMediaSubscription(); + + const style::IconButton &_st; + const std::shared_ptr _state; + const std::shared_ptr _attach; + const std::shared_ptr _attachOver; + std::shared_ptr _subscribed; + Ui::RadialAnimation _radial; + Ui::Animations::Simple _cancelShown; + Ui::Animations::Simple _viewShown; + +}; + +class PreparePollMediaTask final : public Task { +public: + PreparePollMediaTask( + FileLoadTask::Args &&args, + Fn)> done); + + void process() override; + void finish() override; + +private: + FileLoadTask _task; + Fn)> _done; + +}; + +struct UploadContext { + std::weak_ptr media; + uint64 token = 0; + QString filename; + QString filemime; + QVector attributes; + bool forceFile = true; +}; + +class PollMediaUploader final { +public: + struct Args { + not_null session; + not_null peer; + Fn showError; + }; + + explicit PollMediaUploader(Args &&args); + ~PollMediaUploader(); + + void startPhotoUpload( + std::shared_ptr media, + Ui::PreparedFile file); + void startVideoUpload( + std::shared_ptr media, + Ui::PreparedFile file); + void startDocumentUpload( + std::shared_ptr media, + Ui::PreparedFile file); + bool applyPreparedPhotoList( + std::shared_ptr media, + Ui::PreparedList &&list); + + void setMedia( + const std::shared_ptr &media, + PollMedia value, + std::shared_ptr thumbnail, + bool rounded); + void clearMedia(std::shared_ptr media); + + void choosePhotoOrVideo( + not_null parent, + std::shared_ptr media); + void chooseDocument( + not_null parent, + std::shared_ptr media); + + void installDropToWidget( + not_null widget, + std::shared_ptr media, + bool allowDocuments); + void installDropToField( + not_null field, + std::shared_ptr media, + bool allowDocuments); + +private: + struct UploadedMedia { + PollMedia input; + std::shared_ptr thumbnail; + }; + + void updateMedia(const std::shared_ptr &media); + [[nodiscard]] UploadedMedia parseUploaded( + const MTPMessageMedia &result, + FullMsgId fullId); + void applyUploaded( + const std::shared_ptr &media, + uint64 token, + FullMsgId fullId, + const MTPInputFile &file); + void applyUploadedDocument( + const std::shared_ptr &media, + uint64 token, + FullMsgId fullId, + const Api::RemoteFileInfo &info, + const UploadContext &context); + void subscribeToUploader(); + + const not_null _session; + const not_null _peer; + const Fn _showError; + std::unique_ptr _prepareQueue; + base::flat_map _uploads; + rpl::lifetime _lifetime; + +}; + +} // namespace PollMediaUpload