/* 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 "boxes/create_poll_box.h" #include "base/call_delayed.h" #include "base/event_filter.h" #include "base/random.h" #include "base/unique_qptr.h" #include "chat_helpers/emoji_suggestions_widget.h" #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" #include "lang/lang_keys.h" #include "main/main_app_config.h" #include "main/main_session.h" #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" #include "ui/widgets/buttons.h" #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" namespace { constexpr auto kQuestionLimit = 255; constexpr auto kMaxOptionsCount = PollData::kMaxOptions; constexpr auto kOptionLimit = 100; constexpr auto kWarnQuestionLimit = 80; constexpr auto kWarnOptionLimit = 30; 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, 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(); void enableChooseCorrect(bool enabled); [[nodiscard]] rpl::producer usedCount() const; [[nodiscard]] rpl::producer> scrollToWidget() const; [[nodiscard]] rpl::producer<> backspaceInFront() const; [[nodiscard]] rpl::producer<> tabbed() const; private: class Option { public: Option( not_null outer, not_null container, not_null session, int position, std::shared_ptr group, AttachCallback attachCallback); Option(const Option &other) = delete; Option &operator=(const Option &other) = delete; void toggleRemoveAlways(bool toggled); void enableChooseCorrect( std::shared_ptr group); void show(anim::type animated); void destroy(FnMut done); [[nodiscard]] bool hasShadow() const; void createShadow(); void destroyShadow(); [[nodiscard]] bool isEmpty() const; [[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(); void setPlaceholder() const; void removePlaceholder() const; [[nodiscard]] not_null field() const; [[nodiscard]] PollAnswer toPollAnswer(int index) const; [[nodiscard]] rpl::producer removeClicks() const; private: void createRemove(); void createAttach(); void createWarning(); void toggleCorrectSpace(bool visible); void updateFieldGeometry(); base::unique_qptr> _wrap; not_null _content; base::unique_qptr> _correct; Ui::Animations::Simple _correctShown; 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; }; [[nodiscard]] bool full() const; [[nodiscard]] bool correctShadows() const; void fixShadows(); void removeEmptyTail(); void addEmptyOption(); void checkLastOption(); void validateState(); void fixAfterErase(); void destroy(std::unique_ptr