/* 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 "boxes/premium_limits_box.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/mime_type.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_reaction_preview.h" #include "history/view/history_view_schedule_box.h" #include "lang/lang_keys.h" #include "layout/layout_document_generic_preview.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/dropdown_menu.h" #include "ui/widgets/menu/menu_action.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/section_widget.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_overview.h" #include "styles/style_polls.h" #include "styles/style_settings.h" #include 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; [[nodiscard]] Ui::PreparedList PhotoListFromMimeData( not_null data, bool premium) { using Error = Ui::PreparedList::Error; const auto urls = Core::ReadMimeUrls(data); auto result = !urls.isEmpty() ? Storage::PrepareMediaList( urls.mid(0, 1), st::sendMediaPreviewSize, premium) : Ui::PreparedList(Error::EmptyFile, QString()); if (result.error == Error::None) { return result; } else if (auto read = Core::ReadMimeImage(data)) { return Storage::PrepareMediaFromImage( std::move(read.image), std::move(read.content), st::sendMediaPreviewSize); } return result; } 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) : _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; }; [[nodiscard]] 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; } 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 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 onStateChanged(State was, StateChangeSource source) override { RippleButton::onStateChanged(was, source); if (!_state->thumbnail) { update(); } if (_state->uploading) { const auto over = isOver() || isDown(); const auto wasOver = (was & StateFlag::Over) || (was & StateFlag::Down); if (over != wasOver) { _cancelShown.start( [=] { update(); }, over ? 0. : 1., over ? 1. : 0., st::universalDuration); } } } 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; Ui::Animations::Simple _cancelShown; }; 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)>; using FieldDropCallback = Fn, std::shared_ptr)>; using WidgetDropCallback = Fn, std::shared_ptr)>; Options( not_null box, not_null container, not_null controller, ChatHelpers::TabbedPanel *emojiPanel, bool chooseCorrectEnabled, AttachCallback attachCallback, FieldDropCallback fieldDropCallback, WidgetDropCallback widgetDropCallback); [[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, FieldDropCallback fieldDropCallback, WidgetDropCallback widgetDropCallback); 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; FieldDropCallback _fieldDropCallback; WidgetDropCallback _widgetDropCallback; 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