diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 414d59ca41..3f53d1f488 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -6875,6 +6875,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_polls_solution_title" = "Explanation"; "lng_polls_solution_placeholder" = "Add a Comment (Optional)"; "lng_polls_solution_about" = "Users will see this comment after choosing a wrong answer, good for educational purposes."; +"lng_polls_media_uploading" = "Please wait until media upload is complete."; "lng_polls_poll_results_title" = "Poll results"; "lng_polls_quiz_results_title" = "Quiz results"; diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp index a7283d8962..56b3d45185 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.cpp +++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp @@ -15,9 +15,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/message_field.h" #include "chat_helpers/tabbed_panel.h" #include "chat_helpers/tabbed_selector.h" +#include "core/file_utilities.h" #include "core/application.h" #include "core/core_settings.h" +#include "data/data_document.h" #include "data/data_poll.h" +#include "data/data_peer.h" +#include "data/data_photo.h" +#include "data/data_session.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" #include "history/view/history_view_schedule_box.h" @@ -27,8 +32,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "menu/menu_send.h" #include "settings/detailed_settings_button.h" #include "settings/settings_common.h" +#include "storage/file_upload.h" +#include "storage/localimageloader.h" +#include "storage/storage_media_prepare.h" #include "ui/controls/emoji_button.h" #include "ui/controls/emoji_button_factory.h" +#include "ui/chat/attach/attach_prepare.h" +#include "ui/dynamic_image.h" +#include "ui/dynamic_thumbnails.h" +#include "ui/effects/radial_animation.h" +#include "ui/effects/ripple_animation.h" +#include "ui/painter.h" +#include "ui/rect.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/vertical_list.h" @@ -36,15 +51,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/checkbox.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/labels.h" +#include "ui/widgets/popup_menu.h" #include "ui/widgets/shadow.h" #include "ui/wrap/fade_wrap.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/ui_utility.h" #include "window/window_session_controller.h" +#include "apiwrap.h" #include "styles/style_boxes.h" +#include "styles/style_chat.h" #include "styles/style_chat_helpers.h" // defaultComposeFiles. #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" #include "styles/style_polls.h" #include "styles/style_settings.h" @@ -59,18 +78,281 @@ constexpr auto kSolutionLimit = 200; constexpr auto kWarnSolutionLimit = 60; constexpr auto kErrorLimit = 99; +struct PollMediaState { + std::optional 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) + : _original(std::move(original)) { + } + + std::shared_ptr clone() override { + return std::make_shared(_original); + } + + QImage image(int size) override { + return _original; + } + + void subscribeToUpdates(Fn callback) override { + } + +private: + QImage _original; + +}; + +class PollMediaButton final : public Ui::RippleButton { +public: + 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() override { + if (_subscribed) { + _subscribed->subscribeToUpdates(nullptr); + } + } + +protected: + void paintEvent(QPaintEvent *e) override { + 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->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 line = float64(st::lineWidth * 2); + const auto margin = float64(st::pollAttachProgressMargin); + const auto arc = QRectF(rippleRect()) - Margins(margin); + _radial.draw(p, arc, line, st::historyFileThumbRadialFg); + } + } + + void onStateChanged(State was, StateChangeSource source) override { + RippleButton::onStateChanged(was, source); + if (!_state->thumbnail) { + update(); + } + } + + QImage prepareRippleMask() const override { + return Ui::RippleAnimation::EllipseMask(QSize( + _st.rippleAreaSize, + _st.rippleAreaSize)); + } + + QPoint prepareRippleStartPosition() const override { + auto result = mapFromGlobal(QCursor::pos()) + - _st.rippleAreaPosition; + const auto rect = QRect( + QPoint(), + QSize(_st.rippleAreaSize, _st.rippleAreaSize)); + return rect.contains(result) + ? result + : DisabledRippleStartPosition(); + } + +private: + void 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 radialAnimationCallback(crl::time now) { + const auto updated = _radial.update( + _state->progress, + !_state->uploading, + now); + if (!anim::Disabled() || updated || _radial.animating()) { + update(rippleRect()); + } + } + + [[nodiscard]] QRect rippleRect() const { + return QRect( + _st.rippleAreaPosition, + QSize(_st.rippleAreaSize, _st.rippleAreaSize)); + } + + [[nodiscard]] QRect 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 currentAttachThumbnail() const { + return (isOver() || isDown()) ? _attachOver : _attach; + } + + void 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(); + } + }); + } + + 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; + +}; + +class PreparePollMediaTask final : public Task { +public: + PreparePollMediaTask( + FileLoadTask::Args &&args, + Fn)> done) + : _task(std::move(args)) + , _done(std::move(done)) { + } + + void process() override { + _task.process({ .generateGoodThumbnail = false }); + } + + void finish() override { + _done(_task.peekResult()); + } + +private: + FileLoadTask _task; + Fn)> _done; + +}; + class Options { public: + using AttachCallback = Fn, + std::shared_ptr)>; + Options( not_null box, not_null container, not_null controller, ChatHelpers::TabbedPanel *emojiPanel, - bool chooseCorrectEnabled); + bool chooseCorrectEnabled, + AttachCallback attachCallback); [[nodiscard]] bool hasOptions() const; [[nodiscard]] bool isValid() const; [[nodiscard]] bool hasCorrect() const; + [[nodiscard]] bool hasUploadingMedia() const; [[nodiscard]] std::vector toPollAnswers() const; void focusFirst(); @@ -89,7 +371,8 @@ private: not_null container, not_null session, int position, - std::shared_ptr group); + std::shared_ptr group, + AttachCallback attachCallback); Option(const Option &other) = delete; Option &operator=(const Option &other) = delete; @@ -109,6 +392,7 @@ private: [[nodiscard]] bool isGood() const; [[nodiscard]] bool isTooLong() const; [[nodiscard]] bool isCorrect() const; + [[nodiscard]] bool uploadingMedia() const; [[nodiscard]] bool hasFocus() const; void setFocus() const; void clearValue(); @@ -124,6 +408,7 @@ private: private: void createRemove(); + void createAttach(); void createWarning(); void toggleCorrectSpace(bool visible); void updateFieldGeometry(); @@ -135,8 +420,11 @@ private: bool _hasCorrect = false; Ui::InputField *_field = nullptr; base::unique_qptr _shadow; + base::unique_qptr _attach; base::unique_qptr _remove; rpl::variable *_removeAlways = nullptr; + AttachCallback _attachCallback; + std::shared_ptr _media; }; @@ -158,6 +446,7 @@ private: not_null _container; const not_null _controller; ChatHelpers::TabbedPanel * const _emojiPanel; + const AttachCallback _attachCallback; std::shared_ptr _chooseCorrectGroup; int _position = 0; std::vector> _list; @@ -247,7 +536,8 @@ Options::Option::Option( not_null container, not_null session, int position, - std::shared_ptr group) + std::shared_ptr group, + AttachCallback attachCallback) : _wrap(container->insert( position, object_ptr>( @@ -261,7 +551,9 @@ Options::Option::Option( ? st::createPollOptionFieldPremium : st::createPollOptionField, Ui::InputField::Mode::NoNewlines, - tr::lng_polls_create_option_add())) { + tr::lng_polls_create_option_add())) +, _attachCallback(std::move(attachCallback)) +, _media(std::make_shared()) { InitField(outer, _field, session); _field->setMaxLength(kOptionLimit + kErrorLimit); _field->show(); @@ -289,6 +581,7 @@ Options::Option::Option( createShadow(); createRemove(); + createAttach(); createWarning(); enableChooseCorrect(group); _correctShown.stop(); @@ -356,8 +649,9 @@ void Options::Option::createRemove() { field->widthValue( ) | rpl::on_next([=](int width) { + const auto attachSkip = st::pollAttach.width + st::lineWidth * 4; remove->moveToRight( - st::createPollOptionRemovePosition.x(), + st::createPollOptionRemovePosition.x() + attachSkip, st::createPollOptionRemovePosition.y(), width); }, remove->lifetime()); @@ -365,6 +659,32 @@ void Options::Option::createRemove() { _remove.reset(remove); } +void Options::Option::createAttach() { + const auto field = Option::field(); + const auto attach = Ui::CreateChild( + field.get(), + st::pollAttach, + _media); + attach->show(); + field->sizeValue( + ) | rpl::on_next([=](QSize size) { + attach->moveToRight( + st::createPollOptionRemovePosition.x(), + st::createPollOptionRemovePosition.y() - st::lineWidth * 2, + size.width()); + }, attach->lifetime()); + attach->clicks( + ) | rpl::on_next([=](Qt::MouseButton button) { + if (button != Qt::LeftButton) { + return; + } + if (_attachCallback) { + _attachCallback(not_null(attach), _media); + } + }, attach->lifetime()); + _attach.reset(attach); +} + void Options::Option::createWarning() { using namespace rpl::mappers; @@ -405,6 +725,10 @@ bool Options::Option::isCorrect() const { return isGood() && _correct && _correct->entity()->Checkbox::checked(); } +bool Options::Option::uploadingMedia() const { + return _media->uploading; +} + bool Options::Option::hasFocus() const { return field()->hasFocus(); } @@ -502,6 +826,7 @@ PollAnswer Options::Option::toPollAnswer(int index) const { }, QByteArray(1, ('0' + index)), }; + result.media = _media->media; TextUtilities::Trim(result.text); result.correct = _correct ? _correct->entity()->Checkbox::checked() : false; return result; @@ -516,11 +841,13 @@ Options::Options( not_null container, not_null controller, ChatHelpers::TabbedPanel *emojiPanel, - bool chooseCorrectEnabled) + bool chooseCorrectEnabled, + AttachCallback attachCallback) : _box(box) , _container(container) , _controller(controller) , _emojiPanel(emojiPanel) +, _attachCallback(std::move(attachCallback)) , _chooseCorrectGroup(chooseCorrectEnabled ? createChooseCorrectGroup() : nullptr) @@ -545,6 +872,10 @@ bool Options::hasCorrect() const { return _hasCorrect; } +bool Options::hasUploadingMedia() const { + return ranges::any_of(_list, &Option::uploadingMedia); +} + rpl::producer Options::usedCount() const { return _usedCount.value(); } @@ -695,7 +1026,8 @@ void Options::addEmptyOption() { _container, &_controller->session(), _position + _list.size() + _destroyed.size(), - _chooseCorrectGroup)); + _chooseCorrectGroup, + _attachCallback)); const auto field = _list.back()->field(); if (const auto emojiPanel = _emojiPanel) { const auto emojiToggle = Ui::AddEmojiToggleToField( @@ -836,12 +1168,14 @@ void Options::checkLastOption() { CreatePollBox::CreatePollBox( QWidget*, not_null controller, + not_null peer, PollData::Flags chosen, PollData::Flags disabled, rpl::producer starsRequired, Api::SendType sendType, SendMenu::Details sendMenuDetails) : _controller(controller) +, _peer(peer) , _chosen(chosen) , _disabled(disabled) , _sendType(sendType) @@ -980,7 +1314,7 @@ not_null CreatePollBox::setupSolution( const auto solution = inner->add( object_ptr( inner, - st::createPollSolutionField, + st::pollMediaField, Ui::InputField::Mode::MultiLine, tr::lng_polls_solution_placeholder()), st::createPollFieldPadding); @@ -1039,19 +1373,310 @@ object_ptr CreatePollBox::setupContent() { using namespace Settings; const auto id = base::RandomValue(); + struct UploadContext { + std::weak_ptr media; + uint64 token = 0; + }; struct State final { Errors error = Error::Question; std::unique_ptr options; rpl::event_stream multipleForceOff; rpl::event_stream quizForceOff; + std::shared_ptr descriptionMedia + = std::make_shared(); + std::shared_ptr solutionMedia + = std::make_shared(); + base::flat_map uploads; + base::unique_qptr mediaMenu; + std::unique_ptr prepareQueue; }; const auto state = lifetime().make_state(); + state->prepareQueue = std::make_unique(); auto result = object_ptr(this); const auto container = result.data(); + const auto updateMedia = [=]( + const std::shared_ptr &media) { + if (media->update) { + media->update(); + } + }; + const auto setMedia = [=]( + const std::shared_ptr &media, + std::optional value, + std::shared_ptr thumbnail, + bool rounded) { + media->token++; + media->media = std::move(value); + media->thumbnail = std::move(thumbnail); + media->rounded = rounded; + media->progress = (media->uploading && media->media.has_value()) + ? 1. + : 0.; + media->uploadDataId = 0; + media->uploading = false; + updateMedia(media); + }; + struct UploadedMedia final { + std::optional input; + std::shared_ptr thumbnail; + }; + const auto parseUploaded = [=]( + const MTPMessageMedia &result, + FullMsgId fullId) { + auto parsed = UploadedMedia{ + .input = PollMediaToInputMedia(result), + }; + result.match([&](const MTPDmessageMediaPhoto &media) { + if (const auto photo = media.vphoto()) { + photo->match([&](const MTPDphoto &) { + parsed.thumbnail = Ui::MakePhotoThumbnail( + _controller->session().data().processPhoto( + *photo), + fullId); + }, [](const auto &) { + }); + } + }, [&](const MTPDmessageMediaDocument &media) { + if (const auto document = media.vdocument()) { + document->match([&](const MTPDdocument &) { + parsed.thumbnail = Ui::MakeDocumentThumbnail( + _controller->session().data().processDocument(*document), + fullId); + }, [](const auto &) { + }); + } + }, [](const auto &) { + }); + return parsed; + }; + const auto 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()); + _controller->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, std::nullopt, nullptr, false); + showToast(tr::lng_attach_failed(tr::now)); + return; + } + setMedia( + media, + std::move(parsed.input), + media->thumbnail + ? media->thumbnail + : std::move(parsed.thumbnail), + true); + }).fail([=](const MTP::Error &) { + if (media->token != token) { + return; + } + setMedia(media, std::nullopt, nullptr, false); + showToast(tr::lng_attach_failed(tr::now)); + }).send(); + }; + _controller->session().uploader().photoReady( + ) | rpl::on_next([=](const Storage::UploadedMedia &data) { + const auto context = state->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()); + _controller->session().uploader().photoProgress( + ) | rpl::on_next([=](const FullMsgId &id) { + const auto i = state->uploads.find(id); + if (i == state->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 = _controller->session().data().photo( + media->uploadDataId)->progress(); + updateMedia(media); + }, lifetime()); + _controller->session().uploader().photoFailed( + ) | rpl::on_next([=](const FullMsgId &id) { + const auto context = state->uploads.take(id); + if (!context) { + return; + } + const auto media = context->media.lock(); + if (!media || (media->token != context->token)) { + return; + } + setMedia(media, std::nullopt, nullptr, false); + showToast(tr::lng_attach_failed(tr::now)); + }, lifetime()); + const auto choosePhoto = [=](std::shared_ptr media) { + const auto callback = crl::guard(this, [=]( + FileDialog::OpenResult &&result) { + const auto checkResult = [&](const Ui::PreparedList &list) { + using namespace Ui; + return (list.files.size() == 1) + && (list.files.front().type == PreparedFile::Type::Photo); + }; + const auto showError = [=](tr::phrase<> text) { + showToast(text(tr::now)); + }; + auto list = Storage::PreparedFileFromFilesDialog( + std::move(result), + checkResult, + showError, + st::sendMediaPreviewSize, + _controller->session().premium()); + if (!list) { + return; + } + auto file = std::move(list->files.front()); + const auto token = ++media->token; + media->media = std::nullopt; + media->thumbnail = std::make_shared( + std::move(file.preview)); + media->rounded = true; + media->uploading = true; + media->progress = 0.; + media->uploadDataId = 0; + updateMedia(media); + using PreparePoll = PreparePollMediaTask; + state->prepareQueue->addTask(std::make_unique( + FileLoadTask::Args{ + .session = &_controller->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, std::nullopt, nullptr, false); + showToast(tr::lng_attach_failed(tr::now)); + } + return; + } + const auto uploadId = FullMsgId( + _peer->id, + _controller->session().data().nextLocalMessageId()); + state->uploads.emplace(uploadId, UploadContext{ + .media = media, + .token = token, + }); + media->uploadDataId = prepared->id; + _controller->session().uploader().upload( + uploadId, + prepared); + })); + }); + FileDialog::GetOpenPath( + this, + tr::lng_attach_photo(tr::now), + FileDialog::ImagesFilter(), + callback); + }; + const auto clearMedia = [=](std::shared_ptr media) { + auto toCancel = std::vector(); + for (auto i = state->uploads.begin(); i != state->uploads.end();) { + if (i->second.media.lock() == media) { + toCancel.push_back(i->first); + i = state->uploads.erase(i); + } else { + ++i; + } + } + for (const auto &id : toCancel) { + _controller->session().uploader().cancel(id); + } + setMedia(media, std::nullopt, nullptr, false); + }; + const auto showMediaMenu = [=]( + not_null button, + std::shared_ptr media) { + state->mediaMenu = base::make_unique_q( + button, + st::popupMenuWithIcons); + state->mediaMenu->setForcedOrigin( + Ui::PanelAnimation::Origin::TopRight); + state->mediaMenu->addAction( + tr::lng_attach_photo(tr::now), + [=] { choosePhoto(media); }, + &st::menuIconPhoto); + if (media->media || media->uploading) { + state->mediaMenu->addAction( + tr::lng_box_remove(tr::now), + [=] { clearMedia(media); }, + &st::menuIconDelete); + } + state->mediaMenu->popup(QCursor::pos()); + }; + const auto addMediaButton = [=]( + not_null field, + std::shared_ptr media) { + const auto button = Ui::CreateChild( + field, + st::pollAttach, + media); + button->show(); + field->sizeValue( + ) | rpl::on_next([=](QSize size) { + button->moveToRight( + st::createPollOptionRemovePosition.x() + st::pollAttachShift.x(), + ((size.height() - button->height()) / 2) + + st::pollAttachShift.y(), + size.width()); + }, button->lifetime()); + button->clicks( + ) | rpl::on_next([=](Qt::MouseButton buttonType) { + if (buttonType != Qt::LeftButton) { + return; + } + showMediaMenu(not_null(button), media); + }, button->lifetime()); + }; + const auto question = setupQuestion(container); const auto description = setupDescription(container); + addMediaButton(description, state->descriptionMedia); Ui::AddDivider(container); Ui::AddSkip(container); container->add( @@ -1065,7 +1690,8 @@ object_ptr CreatePollBox::setupContent() { container, _controller, _emojiPanel ? _emojiPanel.get() : nullptr, - (_chosen & PollData::Flag::Quiz)); + (_chosen & PollData::Flag::Quiz), + showMediaMenu); const auto options = state->options.get(); auto limit = options->usedCount() | rpl::after_next([=](int count) { setCloseByEscape(!count); @@ -1115,21 +1741,17 @@ object_ptr CreatePollBox::setupContent() { rpl::single(!!(_chosen & PollData::Flag::PublicVotes)), st::detailedSettingsButtonStyle) : nullptr; - const auto hasMultiple = !(_chosen & PollData::Flag::Quiz) - || !(_disabled & PollData::Flag::Quiz); - const auto multiple = hasMultiple - ? AddPollToggleButton( - container, - tr::lng_polls_create_allow_multiple_answers(), - tr::lng_polls_create_allow_multiple_answers_about(), - { - .icon = &st::pollBoxFilledPollMultipleIcon, - .background = &st::settingsIconBg3, - }, - rpl::single(!!(_chosen & PollData::Flag::MultiChoice)) - | rpl::then(state->multipleForceOff.events()), - st::detailedSettingsButtonStyle) - : nullptr; + const auto multiple = AddPollToggleButton( + container, + tr::lng_polls_create_allow_multiple_answers(), + tr::lng_polls_create_allow_multiple_answers_about(), + { + .icon = &st::pollBoxFilledPollMultipleIcon, + .background = &st::settingsIconBg3, + }, + rpl::single(!!(_chosen & PollData::Flag::MultiChoice)) + | rpl::then(state->multipleForceOff.events()), + st::detailedSettingsButtonStyle); const auto addOptions = (!(_disabled & PollData::Flag::OpenAnswers)) ? AddPollToggleButton( container, @@ -1177,6 +1799,7 @@ object_ptr CreatePollBox::setupContent() { const auto solution = setupSolution( container, rpl::single(quiz->toggled()) | rpl::then(quiz->toggledChanges())); + addMediaButton(solution, state->solutionMedia); options->tabbed( ) | rpl::on_next([=] { @@ -1199,25 +1822,7 @@ object_ptr CreatePollBox::setupContent() { } revoting->setToggleLocked(_disabled & PollData::Flag::RevotingDisabled); shuffle->setToggleLocked(_disabled & PollData::Flag::ShuffleAnswers); - if (multiple) { - multiple->setToggleLocked((_disabled & PollData::Flag::MultiChoice) - || (_chosen & PollData::Flag::Quiz)); - multiple->clickAreaEvents( - ) | rpl::filter([=](not_null e) { - return (e->type() == QEvent::MouseButtonPress) - && quiz->toggled(); - }) | rpl::on_next([show = uiShow()] { - show->showToast(tr::lng_polls_create_one_answer(tr::now)); - }, multiple->lifetime()); - multiple->toggledChanges( - ) | rpl::on_next([=](bool checked) { - if (checked - && (quiz->toggled() - || (_disabled & PollData::Flag::MultiChoice))) { - state->multipleForceOff.fire(false); - } - }, multiple->lifetime()); - } + multiple->setToggleLocked(_disabled & PollData::Flag::MultiChoice); using namespace rpl::mappers; quiz->toggledChanges( @@ -1226,13 +1831,6 @@ object_ptr CreatePollBox::setupContent() { state->quizForceOff.fire(false); return; } - if (multiple) { - if (checked && multiple->toggled()) { - state->multipleForceOff.fire(false); - } - multiple->setToggleLocked(checked - || (_disabled & PollData::Flag::MultiChoice)); - } options->enableChooseCorrect(checked); }, quiz->lifetime()); @@ -1273,8 +1871,12 @@ object_ptr CreatePollBox::setupContent() { solutionWithTags.text, TextUtilities::ConvertTextTagsToEntities(solutionWithTags.tags) }; + result.attachedMedia = state->descriptionMedia->media; + if (quiz->toggled()) { + result.solutionMedia = state->solutionMedia->media; + } const auto publicVotes = (showWhoVoted && showWhoVoted->toggled()); - const auto multiChoice = (multiple && multiple->toggled()); + const auto multiChoice = multiple->toggled(); result.setFlags(Flag(0) | (publicVotes ? Flag::PublicVotes : Flag(0)) | (multiChoice ? Flag::MultiChoice : Flag(0)) @@ -1318,6 +1920,13 @@ object_ptr CreatePollBox::setupContent() { } else { state->error &= ~Error::Solution; } + if (state->descriptionMedia->uploading + || (quiz->toggled() && state->solutionMedia->uploading) + || options->hasUploadingMedia()) { + state->error |= Error::Media; + } else { + state->error &= ~Error::Media; + } }; const auto showError = [show = uiShow()]( tr::phrase<> text) { @@ -1335,6 +1944,8 @@ object_ptr CreatePollBox::setupContent() { showError(tr::lng_polls_choose_correct); } else if (state->error & Error::Solution) { solution->showError(); + } else if (state->error & Error::Media) { + showError(tr::lng_polls_media_uploading); } else if (!state->error) { auto result = collectResult(); result.options = sendOptions; diff --git a/Telegram/SourceFiles/boxes/create_poll_box.h b/Telegram/SourceFiles/boxes/create_poll_box.h index 42f07259b0..22cd771671 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.h +++ b/Telegram/SourceFiles/boxes/create_poll_box.h @@ -30,6 +30,8 @@ namespace SendMenu { struct Details; } // namespace SendMenu +class PeerData; + class CreatePollBox : public Ui::BoxContent { public: struct Result { @@ -41,6 +43,7 @@ public: CreatePollBox( QWidget*, not_null controller, + not_null peer, PollData::Flags chosen, PollData::Flags disabled, rpl::producer starsRequired, @@ -62,6 +65,7 @@ private: Correct = 0x04, Other = 0x08, Solution = 0x10, + Media = 0x20, }; friend constexpr inline bool is_flag_type(Error) { return true; } using Errors = base::flags; @@ -76,6 +80,7 @@ private: rpl::producer shown); const not_null _controller; + const not_null _peer; const PollData::Flags _chosen = PollData::Flags(); const PollData::Flags _disabled = PollData::Flags(); const Api::SendType _sendType = Api::SendType(); diff --git a/Telegram/SourceFiles/boxes/polls.style b/Telegram/SourceFiles/boxes/polls.style index b4f9f57140..a8b8730c16 100644 --- a/Telegram/SourceFiles/boxes/polls.style +++ b/Telegram/SourceFiles/boxes/polls.style @@ -26,6 +26,8 @@ pollBoxFilledChatlistMentionIcon: icon{{ "poll/filled/filled_chatlist_mention", pollBoxFilledPollMultipleIcon: icon{{ "poll/filled/filled_poll_multiple", activeButtonFg }}; pollAttachTextSkip: 28px; +pollAttachProgressMargin: 4px; +pollAttachShift: point(-11px, -2px); pollDescriptionFieldPadding: margins(22px, 5px, 11px, 5px); pollDescriptionField: InputField(createPollField) { @@ -33,3 +35,19 @@ pollDescriptionField: InputField(createPollField) { border: 0px; borderActive: 0px; } + +pollAttach: IconButton(defaultIconButton) { + width: pollAttachTextSkip; + height: pollAttachTextSkip; + + icon: icon {{ "poll/general/outline_poll_attach-24x24", historyComposeIconFg }}; + iconOver: icon {{ "poll/general/outline_poll_attach-24x24", historyComposeIconFgOver }}; + + rippleAreaPosition: point(0px, 0px); + rippleAreaSize: pollAttachTextSkip; + ripple: defaultRippleAnimationBgOver; +} + +pollMediaField: InputField(createPollSolutionField) { + textMargins: margins(0px, 4px, pollAttachTextSkip, 4px); +} diff --git a/Telegram/SourceFiles/data/data_poll.cpp b/Telegram/SourceFiles/data/data_poll.cpp index 13297cc465..f369ba66db 100644 --- a/Telegram/SourceFiles/data/data_poll.cpp +++ b/Telegram/SourceFiles/data/data_poll.cpp @@ -79,7 +79,10 @@ bool PollData::applyChanges(const MTPDpoll &poll) { | (poll.is_quiz() ? Flag::Quiz : Flag(0)) | (poll.is_shuffle_answers() ? Flag::ShuffleAnswers : Flag(0)) | (poll.is_revoting_disabled() ? Flag::RevotingDisabled : Flag(0)) - | (poll.is_open_answers() ? Flag::OpenAnswers : Flag(0)); + | (poll.is_open_answers() ? Flag::OpenAnswers : Flag(0)) + | (poll.is_hide_results_until_close() + ? Flag::HideResultsUntilClose + : Flag(0)); const auto newCloseDate = poll.vclose_date().value_or_empty(); const auto newClosePeriod = poll.vclose_period().value_or_empty(); auto newAnswers = ranges::views::all( @@ -91,6 +94,18 @@ bool PollData::applyChanges(const MTPDpoll &poll) { result.text = Api::ParseTextWithEntities( &session(), answer.vtext()); + if (const auto media = answer.vmedia()) { + result.media = PollMediaToInputMedia(*media); + } + return result; + }, [&](const MTPDinputPollAnswer &answer) { + auto result = PollAnswer(); + result.text = Api::ParseTextWithEntities( + &session(), + answer.vtext()); + if (const auto media = answer.vmedia()) { + result.media = *media; + } return result; }, [](const auto &) { return PollAnswer(); @@ -174,6 +189,16 @@ bool PollData::applyResults(const MTPPollResults &results) { changed = true; } } + if (const auto media = results.vsolution_media()) { + const auto parsed = PollMediaToInputMedia(*media); + const auto changedMedia = !parsed + ? solutionMedia.has_value() + : (!solutionMedia || (solutionMedia->type() != parsed->type())); + solutionMedia = parsed; + if (changedMedia) { + changed = true; + } + } if (!changed) { return false; } @@ -211,12 +236,12 @@ bool PollData::applyResultToAnswers( if (!answer) { return false; } - const auto newVotes = voters.vvoters() - ? voters.vvoters()->v - : 0; - auto changed = (answer->votes != newVotes); - if (changed) { - answer->votes = newVotes; + auto changed = false; + if (const auto count = voters.vvoters()) { + if (answer->votes != count->v) { + answer->votes = count->v; + changed = true; + } } if (!isMinResults) { if (answer->chosen != voters.is_chosen()) { @@ -275,17 +300,68 @@ bool PollData::openAnswers() const { return (_flags & Flag::OpenAnswers); } +bool PollData::hideResultsUntilClose() const { + return (_flags & Flag::HideResultsUntilClose); +} + +std::optional PollMediaToInputMedia( + const MTPMessageMedia &media) { + return media.match([&](const MTPDmessageMediaPhoto &media) { + const auto photo = media.vphoto(); + if (!photo || photo->type() != mtpc_photo) { + return std::optional(); + } + const auto &fields = photo->c_photo(); + using Flag = MTPDinputMediaPhoto::Flag; + const auto flags = media.vttl_seconds() + ? Flag::f_ttl_seconds + : Flag(0); + return std::optional(MTP_inputMediaPhoto( + MTP_flags(flags), + MTP_inputPhoto( + fields.vid(), + fields.vaccess_hash(), + fields.vfile_reference()), + MTP_int(media.vttl_seconds().value_or_empty()), + MTPInputDocument())); + }, [&](const MTPDmessageMediaDocument &media) { + const auto document = media.vdocument(); + if (!document || document->type() != mtpc_document) { + return std::optional(); + } + const auto &fields = document->c_document(); + using Flag = MTPDinputMediaDocument::Flag; + const auto flags = Flag() + | (media.vttl_seconds() ? Flag::f_ttl_seconds : Flag()) + | (media.vvideo_timestamp() + ? Flag::f_video_timestamp + : Flag()); + return std::optional(MTP_inputMediaDocument( + MTP_flags(flags), + MTP_inputDocument( + fields.vid(), + fields.vaccess_hash(), + fields.vfile_reference()), + MTPInputPhoto(), + MTP_int(media.vvideo_timestamp().value_or_empty()), + MTP_int(media.vttl_seconds().value_or_empty()), + MTPstring())); + }, [](const auto &) { + return std::optional(); + }); +} + MTPPoll PollDataToMTP(not_null poll, bool close) { const auto convert = [&](const PollAnswer &answer) { - return MTP_pollAnswer( - MTP_flags(0), + const auto flags = answer.media + ? MTPDinputPollAnswer::Flag::f_media + : MTPDinputPollAnswer::Flag(0); + return MTP_inputPollAnswer( + MTP_flags(flags), MTP_textWithEntities( MTP_string(answer.text.text), Api::EntitiesToMTP(&poll->session(), answer.text.entities)), - MTP_bytes(answer.option), - MTPMessageMedia(), // media - MTPPeer(), // added_by - MTPint()); // date + answer.media ? *answer.media : MTPInputMedia()); }; auto answers = QVector(); answers.reserve(poll->answers.size()); @@ -301,6 +377,9 @@ MTPPoll PollDataToMTP(not_null poll, bool close) { | (poll->shuffleAnswers() ? Flag::f_shuffle_answers : Flag(0)) | (poll->revotingDisabled() ? Flag::f_revoting_disabled : Flag(0)) | (poll->openAnswers() ? Flag::f_open_answers : Flag(0)) + | (poll->hideResultsUntilClose() + ? Flag::f_hide_results_until_close + : Flag(0)) | (poll->closePeriod > 0 ? Flag::f_close_period : Flag(0)) | (poll->closeDate > 0 ? Flag::f_close_date : Flag(0)); return MTP_poll( @@ -343,12 +422,18 @@ MTPInputMedia PollDataToInputMedia( if (!sentEntities.v.isEmpty()) { inputFlags |= MTPDinputMediaPoll::Flag::f_solution_entities; } + if (poll->attachedMedia) { + inputFlags |= MTPDinputMediaPoll::Flag::f_attached_media; + } + if (poll->solutionMedia) { + inputFlags |= MTPDinputMediaPoll::Flag::f_solution_media; + } return MTP_inputMediaPoll( MTP_flags(inputFlags), PollDataToMTP(poll, close), MTP_vector(correct), - MTPInputMedia(), // attached_media + poll->attachedMedia ? *poll->attachedMedia : MTPInputMedia(), MTP_string(solution.text), sentEntities, - MTPInputMedia()); // solution_media + poll->solutionMedia ? *poll->solutionMedia : MTPInputMedia()); } diff --git a/Telegram/SourceFiles/data/data_poll.h b/Telegram/SourceFiles/data/data_poll.h index 0d020295a0..1994e00c0d 100644 --- a/Telegram/SourceFiles/data/data_poll.h +++ b/Telegram/SourceFiles/data/data_poll.h @@ -18,6 +18,7 @@ class Session; struct PollAnswer { TextWithEntities text; QByteArray option; + std::optional media; int votes = 0; bool chosen = false; bool correct = false; @@ -39,13 +40,14 @@ struct PollData { [[nodiscard]] Main::Session &session() const; enum class Flag { - Closed = 0x01, - PublicVotes = 0x02, - MultiChoice = 0x04, - Quiz = 0x08, - ShuffleAnswers = 0x10, - RevotingDisabled = 0x20, - OpenAnswers = 0x40, + Closed = 0x01, + PublicVotes = 0x02, + MultiChoice = 0x04, + Quiz = 0x08, + ShuffleAnswers = 0x10, + RevotingDisabled = 0x20, + OpenAnswers = 0x40, + HideResultsUntilClose = 0x80, }; friend inline constexpr bool is_flag_type(Flag) { return true; }; using Flags = base::flags; @@ -69,6 +71,7 @@ struct PollData { [[nodiscard]] bool shuffleAnswers() const; [[nodiscard]] bool revotingDisabled() const; [[nodiscard]] bool openAnswers() const; + [[nodiscard]] bool hideResultsUntilClose() const; PollId id = 0; TextWithEntities question; @@ -76,6 +79,8 @@ struct PollData { std::vector> recentVoters; std::vector sendingVotes; TextWithEntities solution; + std::optional attachedMedia; + std::optional solutionMedia; TimeId closePeriod = 0; TimeId closeDate = 0; int totalVoters = 0; @@ -100,3 +105,5 @@ private: [[nodiscard]] MTPInputMedia PollDataToInputMedia( not_null poll, bool close = false); +[[nodiscard]] std::optional PollMediaToInputMedia( + const MTPMessageMedia &media); diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index f7743be32e..29196e4043 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -4358,6 +4358,18 @@ not_null Session::processPoll(const MTPPoll &data) { not_null Session::processPoll(const MTPDmessageMediaPoll &data) { const auto result = processPoll(data.vpoll()); + const auto media = data.vattached_media() + ? PollMediaToInputMedia(*data.vattached_media()) + : std::optional(); + const auto changedMedia = !media + ? result->attachedMedia.has_value() + : (!result->attachedMedia + || (result->attachedMedia->type() != media->type())); + result->attachedMedia = media; + if (changedMedia) { + ++result->version; + notifyPollUpdateDelayed(result); + } const auto changed = result->applyResults(data.vresults()); if (changed) { notifyPollUpdateDelayed(result); diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp index 05aea8d3e9..bee271cd0a 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp @@ -302,6 +302,9 @@ QSize Poll::countOptimalSize() { } bool Poll::showVotes() const { + if (_flags & PollData::Flag::HideResultsUntilClose) { + return (_flags & PollData::Flag::Closed) || _parent->data()->out(); + } return _voted || (_flags & PollData::Flag::Closed); } diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 0dc7d4477c..9c38ed2056 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -2214,6 +2214,7 @@ void PeerMenuCreatePoll( }); auto box = Box( controller, + peer, chosen, disabled, std::move(starsRequired),