diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 2956a33f25..cc82bb6cb7 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -949,6 +949,8 @@ PRIVATE history/view/history_view_cursor_state.h history/view/history_view_draw_to_reply.cpp history/view/history_view_draw_to_reply.h + history/view/history_view_add_poll_option.cpp + history/view/history_view_add_poll_option.h history/view/history_view_element.cpp history/view/history_view_element.h history/view/history_view_emoji_interactions.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 70bc122edc..439500f5fc 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -6913,6 +6913,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_polls_votes_collapse" = "Collapse"; "lng_polls_vote_yesterday" = "yesterday"; "lng_polls_option_added_by" = "Added by {user}"; +"lng_polls_add_option" = "Add an Option"; +"lng_polls_add_option_placeholder" = "Option text..."; +"lng_polls_max_options_reached" = "Maximum number of options reached."; +"lng_polls_add_option_duplicate" = "This option already exists."; +"lng_polls_add_option_closed" = "This poll has been closed."; +"lng_polls_add_option_error" = "Could not add the option. Please try again."; +"lng_polls_add_option_save" = "Save"; "lng_todo_title" = "Checklist"; "lng_todo_title_group" = "Group Checklist"; diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index 874f792907..794e86a18d 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -185,6 +185,47 @@ void Polls::sendVotes( _pollVotesRequestIds.emplace(itemId, requestId); } +void Polls::addAnswer( + FullMsgId itemId, + const TextWithEntities &text, + Fn done, + Fn fail) { + if (_pollAddAnswerRequestIds.contains(itemId)) { + return; + } + const auto item = _session->data().message(itemId); + if (!item) { + return; + } + const auto sentEntities = Api::EntitiesToMTP( + _session, + text.entities, + Api::ConvertOption::SkipLocal); + const auto answer = MTP_inputPollAnswer( + MTP_flags(MTPDinputPollAnswer::Flags(0)), + MTP_textWithEntities( + MTP_string(text.text), + sentEntities), + MTPInputMedia()); + const auto requestId = _api.request(MTPmessages_AddPollAnswer( + item->history()->peer->input(), + MTP_int(item->id), + answer + )).done([=](const MTPUpdates &result) { + _pollAddAnswerRequestIds.erase(itemId); + _session->updates().applyUpdates(result); + if (done) { + done(); + } + }).fail([=](const MTP::Error &error) { + _pollAddAnswerRequestIds.erase(itemId); + if (fail) { + fail(error.type()); + } + }).send(); + _pollAddAnswerRequestIds.emplace(itemId, requestId); +} + void Polls::close(not_null item) { const auto itemId = item->fullId(); if (_pollCloseRequestIds.contains(itemId)) { diff --git a/Telegram/SourceFiles/api/api_polls.h b/Telegram/SourceFiles/api/api_polls.h index e5b1f2487a..57d8568d4a 100644 --- a/Telegram/SourceFiles/api/api_polls.h +++ b/Telegram/SourceFiles/api/api_polls.h @@ -35,6 +35,11 @@ public: void sendVotes( FullMsgId itemId, const std::vector &options); + void addAnswer( + FullMsgId itemId, + const TextWithEntities &text, + Fn done, + Fn fail); void close(not_null item); void reloadResults(not_null item); @@ -43,6 +48,7 @@ private: MTP::Sender _api; base::flat_map _pollVotesRequestIds; + base::flat_map _pollAddAnswerRequestIds; base::flat_map _pollCloseRequestIds; base::flat_map _pollReloadRequestIds; diff --git a/Telegram/SourceFiles/boxes/polls.style b/Telegram/SourceFiles/boxes/polls.style index 00dbc68b9d..ae73d195f4 100644 --- a/Telegram/SourceFiles/boxes/polls.style +++ b/Telegram/SourceFiles/boxes/polls.style @@ -86,8 +86,29 @@ historyPollOutChosenSelected: icon {{ "poll_select_check", historyFileOutIconFgS historyPollInChosen: icon {{ "poll_select_check", historyFileInIconFg }}; historyPollInChosenSelected: icon {{ "poll_select_check", historyFileInIconFgSelected }}; +historyPollAddOptionTop: 3px; +historyPollAddOptionHeight: 32px; +historyPollAddOptionField: InputField(defaultInputField) { + textBg: transparent; + textFg: windowFg; + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + textMargins: margins(8px, 7px, 28px, 4px); + heightMin: 32px; + heightMax: 32px; + style: TextStyle(defaultTextStyle) { + font: font(13px); + } + placeholderMargins: margins(0px, 1px, 0px, 0px); + placeholderFont: font(12px); + placeholderScale: 0.; + border: 0px; + borderActive: 0px; +} + pollBoxOutlinePollEmojiIcon: icon{{ "poll/general/outline_poll_emoji", activeButtonFg }}; -pollBoxOutlinePollAddIcon: icon{{ "poll/general/outline_poll_add", activeButtonFg }}; +pollBoxOutlinePollAddIcon: icon{{ "poll/general/outline_poll_add-18x18", activeButtonFg }}; pollBoxOutlinePollAttachIcon: icon{{ "poll/general/outline_poll_attach", activeButtonFg }}; pollBoxMenuPollOrderIcon: icon{{ "poll/general/menu_poll_order-24x24", historyComposeIconFg }}; pollBoxFilledPollDeadlineIcon: icon{{ "poll/filled/filled_poll_deadline", activeButtonFg }}; diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp index 515e15947d..c733b9a301 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -697,6 +697,16 @@ void InnerWidget::elementShowPollResults( FullMsgId context) { } +void InnerWidget::elementShowAddPollOption( + not_null view, + not_null poll, + FullMsgId context, + QRect optionRect) { +} + +void InnerWidget::elementSubmitAddPollOption(FullMsgId context) { +} + void InnerWidget::elementOpenPhoto( not_null photo, FullMsgId context) { diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h index 028b05c9c2..7912104838 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h @@ -109,6 +109,12 @@ public: void elementShowPollResults( not_null poll, FullMsgId context) override; + void elementShowAddPollOption( + not_null view, + not_null poll, + FullMsgId context, + QRect optionRect) override; + void elementSubmitAddPollOption(FullMsgId context) override; void elementOpenPhoto( not_null photo, FullMsgId context) override; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 17b2edac25..fc5fdc1082 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_context_menu.h" #include "history/view/history_view_reaction_preview.h" #include "history/view/history_view_quick_action.h" +#include "history/view/history_view_add_poll_option.h" #include "history/view/history_view_emoji_interactions.h" #include "history/view/history_view_top_peers_selector.h" #include "history/history_item_components.h" @@ -198,6 +199,20 @@ public: _widget->elementShowPollResults(poll, context); } } + void elementShowAddPollOption( + not_null view, + not_null poll, + FullMsgId context, + QRect optionRect) override { + if (_widget) { + _widget->elementShowAddPollOption(view, poll, context, optionRect); + } + } + void elementSubmitAddPollOption(FullMsgId context) override { + if (_widget) { + _widget->elementSubmitAddPollOption(context); + } + } void elementOpenPhoto( not_null photo, FullMsgId context) override { @@ -1870,6 +1885,15 @@ void HistoryInner::mousePressEvent(QMouseEvent *e) { e->accept(); return; // ignore mouse press, that was hiding context menu } + if (_addPollOptionWidget && _addPollOptionView) { + const auto top = itemTop(_addPollOptionView); + const auto viewRect = (top >= 0) + ? QRect(0, top, width(), _addPollOptionView->height()) + : QRect(); + if (!viewRect.contains(e->pos())) { + hideAddPollOption(); + } + } if (_middleClickAutoscroll.active()) { _middleClickAutoscroll.stop(); e->accept(); @@ -2180,6 +2204,11 @@ void HistoryInner::itemRemoved(not_null item) { } void HistoryInner::viewRemoved(not_null view) { + if (_addPollOptionView == view) { + _addPollOptionWidget = nullptr; + _addPollOptionView = nullptr; + _addPollOptionContext = FullMsgId(); + } const auto refresh = [&](auto &saved) { if (saved == view) { const auto now = viewByItem(view->data()); @@ -3944,6 +3973,8 @@ void HistoryInner::visibleAreaUpdated(int top, int bottom) { _emojiInteractions->visibleAreaUpdated( _visibleAreaTop, _visibleAreaBottom); + + updateAddPollOptionPosition(); } bool HistoryInner::displayScrollDate() const { @@ -4101,6 +4132,7 @@ void HistoryInner::leaveEventHook(QEvent *e) { } HistoryInner::~HistoryInner() { + hideAddPollOption(); _aboutView = nullptr; for (const auto &item : _animatedStickersPlayed) { if (const auto view = item->mainView()) { @@ -4272,6 +4304,98 @@ void HistoryInner::elementShowPollResults( _controller->showPollResults(poll, context); } +void HistoryInner::elementShowAddPollOption( + not_null view, + not_null poll, + FullMsgId context, + QRect optionRect) { + showAddPollOption(view, poll, context); +} + +void HistoryInner::elementSubmitAddPollOption(FullMsgId context) { + if (_addPollOptionWidget && _addPollOptionContext == context) { + _addPollOptionWidget->triggerSubmit(); + } +} + +void HistoryInner::showAddPollOption( + not_null view, + not_null poll, + FullMsgId context) { + if (_addPollOptionWidget && _addPollOptionContext == context) { + hideAddPollOption(); + return; + } + hideAddPollOption(); + + _addPollOptionView = view; + _addPollOptionContext = context; + + if (const auto media = view->media()) { + media->setAddOptionActive(true); + } + + _addPollOptionWidget = base::make_unique_q< + HistoryView::AddPollOptionWidget>( + this, + poll, + context, + &poll->session()); + + _addPollOptionWidget->submitted( + ) | rpl::on_next([=] { + hideAddPollOption(); + }, _addPollOptionWidget->lifetime()); + + _addPollOptionWidget->cancelled( + ) | rpl::on_next([=] { + hideAddPollOption(); + }, _addPollOptionWidget->lifetime()); + + updateAddPollOptionPosition(); + _addPollOptionWidget->show(); +} + +void HistoryInner::hideAddPollOption() { + if (!_addPollOptionWidget) { + return; + } + if (_addPollOptionView) { + if (const auto media = _addPollOptionView->media()) { + media->setAddOptionActive(false); + } + } + _addPollOptionWidget = nullptr; + _addPollOptionView = nullptr; + _addPollOptionContext = FullMsgId(); +} + +void HistoryInner::updateAddPollOptionPosition() { + if (!_addPollOptionWidget || !_addPollOptionView) { + return; + } + const auto view = _addPollOptionView; + const auto top = itemTop(view); + if (top < 0) { + return; + } + const auto media = view->media(); + if (!media) { + hideAddPollOption(); + return; + } + const auto mediaPos = view->mediaTopLeft(); + const auto innerWidth = view->innerGeometry().width() + - st::msgPadding.left() + - st::msgPadding.right(); + const auto rect = media->addOptionRect(innerWidth); + _addPollOptionWidget->updatePosition( + QPoint( + mediaPos.x() + rect.x(), + top + mediaPos.y() + rect.y()), + rect.width()); +} + void HistoryInner::elementOpenPhoto( not_null photo, FullMsgId context) { diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 60781aaa05..05d3968781 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -37,6 +37,7 @@ struct StateRequest; enum class CursorState : char; enum class PointState : char; enum class ElementChatMode : char; +class AddPollOptionWidget; class EmptyPainter; class Element; class TranslateTracker; @@ -155,6 +156,12 @@ public: void elementShowPollResults( not_null poll, FullMsgId context); + void elementShowAddPollOption( + not_null view, + not_null poll, + FullMsgId context, + QRect optionRect); + void elementSubmitAddPollOption(FullMsgId context); void elementOpenPhoto( not_null photo, FullMsgId context); @@ -255,6 +262,13 @@ protected: void contextMenuEvent(QContextMenuEvent *e) override; private: + void showAddPollOption( + not_null view, + not_null poll, + FullMsgId context); + void hideAddPollOption(); + void updateAddPollOptionPosition(); + void onTouchSelect(); void onTouchScrollTimer(); void markReadMetricsStale(); @@ -596,6 +610,10 @@ private: ClickHandlerPtr _scrollDateLink; ClickHandlerPtr _forumThreadBarLink; + base::unique_qptr _addPollOptionWidget; + Element *_addPollOptionView = nullptr; + FullMsgId _addPollOptionContext; + }; [[nodiscard]] bool CanSendReply(not_null item); diff --git a/Telegram/SourceFiles/history/view/history_view_add_poll_option.cpp b/Telegram/SourceFiles/history/view/history_view_add_poll_option.cpp new file mode 100644 index 0000000000..24d95313ac --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_add_poll_option.cpp @@ -0,0 +1,146 @@ +/* +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 "history/view/history_view_add_poll_option.h" + +#include "api/api_polls.h" +#include "apiwrap.h" +#include "base/event_filter.h" +#include "data/data_poll.h" +#include "data/data_session.h" +#include "history/history_item.h" +#include "lang/lang_keys.h" +#include "main/main_app_config.h" +#include "main/main_session.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/toast/toast.h" +#include "styles/style_chat.h" +#include "styles/style_polls.h" + +namespace HistoryView { + +AddPollOptionWidget::AddPollOptionWidget( + not_null parent, + not_null poll, + FullMsgId itemId, + not_null session) +: RpWidget(parent) +, _poll(poll) +, _itemId(itemId) +, _session(session) { + setupField(); + subscribeToPollUpdates(); +} + +void AddPollOptionWidget::setupField() { + _field = Ui::CreateChild( + this, + st::historyPollAddOptionField, + Ui::InputField::Mode::SingleLine, + tr::lng_polls_add_option_placeholder()); + + _field->setMaxLength(100); + + const auto field = _field; + sizeValue( + ) | rpl::on_next([field](QSize size) { + field->setGeometry(0, 0, size.width(), size.height()); + }, _field->lifetime()); + + _field->submits( + ) | rpl::on_next([=] { + triggerSubmit(); + }, _field->lifetime()); + + base::install_event_filter(_field, [=](not_null event) { + if (event->type() == QEvent::KeyPress) { + if (static_cast(event.get())->key() == Qt::Key_Escape) { + _cancelledEvents.fire({}); + return base::EventFilterResult::Cancel; + } + } + return base::EventFilterResult::Continue; + }); + + _field->setFocusFast(); +} + +void AddPollOptionWidget::subscribeToPollUpdates() { + _session->data().pollUpdates( + ) | rpl::filter([=](not_null poll) { + return (poll == _poll); + }) | rpl::on_next([=](not_null poll) { + if (poll->closed() + || (int(poll->answers.size()) + >= _session->appConfig().pollOptionsLimit())) { + _cancelledEvents.fire({}); + } + }, lifetime()); + + _session->data().itemRemoved( + ) | rpl::filter([=](not_null item) { + return (item->fullId() == _itemId); + }) | rpl::on_next([=] { + _cancelledEvents.fire({}); + }, lifetime()); +} + +void AddPollOptionWidget::triggerSubmit() { + const auto text = _field->getLastText().trimmed(); + if (text.isEmpty()) { + return; + } + if (int(_poll->answers.size()) + >= _session->appConfig().pollOptionsLimit()) { + Ui::Toast::Show( + parentWidget(), + tr::lng_polls_max_options_reached(tr::now)); + return; + } + + _field->setEnabled(false); + + _session->api().polls().addAnswer( + _itemId, + { text }, + [=] { _submittedEvents.fire({}); }, + [=](QString error) { + _field->setEnabled(true); + Ui::Toast::Show( + parentWidget(), + mapErrorToText(error)); + }); +} + +QString AddPollOptionWidget::mapErrorToText(const QString &error) { + if (error == u"POLL_ANSWERS_TOO_MUCH"_q) { + return tr::lng_polls_max_options_reached(tr::now); + } else if (error == u"POLL_ANSWER_DUPLICATE"_q) { + return tr::lng_polls_add_option_duplicate(tr::now); + } else if (error.startsWith(u"POLL"_q) && error.contains(u"CLOSE"_q)) { + return tr::lng_polls_add_option_closed(tr::now); + } + return tr::lng_polls_add_option_error(tr::now); +} + +void AddPollOptionWidget::updatePosition(QPoint topLeft, int w) { + setGeometry( + topLeft.x(), + topLeft.y() + st::historyPollAddOptionTop, + w, + st::historyPollAddOptionField.heightMin); +} + +rpl::producer<> AddPollOptionWidget::submitted() const { + return _submittedEvents.events(); +} + +rpl::producer<> AddPollOptionWidget::cancelled() const { + return _cancelledEvents.events(); +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_add_poll_option.h b/Telegram/SourceFiles/history/view/history_view_add_poll_option.h new file mode 100644 index 0000000000..7e117df2f4 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_add_poll_option.h @@ -0,0 +1,54 @@ +/* +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 "ui/rp_widget.h" + +struct PollData; + +namespace Ui { +class InputField; +} // namespace Ui + +namespace Main { +class Session; +} // namespace Main + +namespace HistoryView { + +class AddPollOptionWidget final : public Ui::RpWidget { +public: + AddPollOptionWidget( + not_null parent, + not_null poll, + FullMsgId itemId, + not_null session); + + void updatePosition(QPoint topLeft, int width); + void triggerSubmit(); + + [[nodiscard]] rpl::producer<> submitted() const; + [[nodiscard]] rpl::producer<> cancelled() const; + +private: + void setupField(); + void subscribeToPollUpdates(); + [[nodiscard]] static QString mapErrorToText(const QString &error); + + const not_null _poll; + const FullMsgId _itemId; + const not_null _session; + + Ui::InputField *_field = nullptr; + + rpl::event_stream<> _submittedEvents; + rpl::event_stream<> _cancelledEvents; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index eb69ee0409..a56b578fdd 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -505,6 +505,16 @@ void DefaultElementDelegate::elementShowPollResults( FullMsgId context) { } +void DefaultElementDelegate::elementShowAddPollOption( + not_null view, + not_null poll, + FullMsgId context, + QRect optionRect) { +} + +void DefaultElementDelegate::elementSubmitAddPollOption(FullMsgId context) { +} + void DefaultElementDelegate::elementOpenPhoto( not_null photo, FullMsgId context) { @@ -2884,6 +2894,10 @@ QRect Element::effectIconGeometry() const { return QRect(); } +QPoint Element::mediaTopLeft() const { + return innerGeometry().topLeft(); +} + Element::~Element() { setReactions(nullptr); diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 6b1a96157e..b0307e71fc 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -107,6 +107,12 @@ public: virtual void elementShowPollResults( not_null poll, FullMsgId context) = 0; + virtual void elementShowAddPollOption( + not_null view, + not_null poll, + FullMsgId context, + QRect optionRect) = 0; + virtual void elementSubmitAddPollOption(FullMsgId context) = 0; virtual void elementOpenPhoto( not_null photo, FullMsgId context) = 0; @@ -163,6 +169,12 @@ public: void elementShowPollResults( not_null poll, FullMsgId context) override; + void elementShowAddPollOption( + not_null view, + not_null poll, + FullMsgId context, + QRect optionRect) override; + void elementSubmitAddPollOption(FullMsgId context) override; void elementOpenPhoto( not_null photo, FullMsgId context) override; @@ -637,6 +649,7 @@ public: [[nodiscard]] virtual QRect effectIconGeometry() const; [[nodiscard]] virtual QRect innerGeometry() const = 0; + [[nodiscard]] virtual QPoint mediaTopLeft() const; void customEmojiRepaint(); void prepareCustomEmojiPaint( diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 5783f734ef..f9be9b0a7b 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -29,6 +29,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_cursor_state.h" #include "history/view/history_view_translate_tracker.h" #include "history/view/history_view_read_metrics_tracker.h" +#include "history/view/history_view_add_poll_option.h" +#include "data/data_poll.h" #include "history/view/history_view_top_peers_selector.h" #include "history/view/history_view_quick_action.h" #include "chat_helpers/message_field.h" @@ -1174,6 +1176,7 @@ void ListWidget::visibleTopBottomUpdated( _applyUpdatedScrollState.call(); _emojiInteractions->visibleAreaUpdated(_visibleTop, _visibleBottom); + updateAddPollOptionPosition(); } void ListWidget::applyUpdatedScrollState() { @@ -1889,6 +1892,98 @@ void ListWidget::elementShowPollResults( _delegate->listShowPollResults(poll, context); } +void ListWidget::elementShowAddPollOption( + not_null view, + not_null poll, + FullMsgId context, + QRect optionRect) { + showAddPollOption(view, poll, context); +} + +void ListWidget::elementSubmitAddPollOption(FullMsgId context) { + if (_addPollOptionWidget && _addPollOptionContext == context) { + _addPollOptionWidget->triggerSubmit(); + } +} + +void ListWidget::showAddPollOption( + not_null view, + not_null poll, + FullMsgId context) { + if (_addPollOptionWidget && _addPollOptionContext == context) { + hideAddPollOption(); + return; + } + hideAddPollOption(); + + _addPollOptionView = view; + _addPollOptionContext = context; + + if (const auto media = view->media()) { + media->setAddOptionActive(true); + } + + _addPollOptionWidget = base::make_unique_q( + this, + poll, + context, + &poll->session()); + + _addPollOptionWidget->submitted( + ) | rpl::on_next([=] { + hideAddPollOption(); + }, _addPollOptionWidget->lifetime()); + + _addPollOptionWidget->cancelled( + ) | rpl::on_next([=] { + hideAddPollOption(); + }, _addPollOptionWidget->lifetime()); + + updateAddPollOptionPosition(); + _addPollOptionWidget->show(); +} + +void ListWidget::hideAddPollOption() { + if (!_addPollOptionWidget) { + return; + } + if (_addPollOptionView) { + if (const auto media = _addPollOptionView->media()) { + media->setAddOptionActive(false); + } + } + _addPollOptionWidget = nullptr; + _addPollOptionView = nullptr; + _addPollOptionContext = FullMsgId(); + _delegate->listWindowSetInnerFocus(); +} + +void ListWidget::updateAddPollOptionPosition() { + if (!_addPollOptionWidget || !_addPollOptionView) { + return; + } + const auto view = _addPollOptionView; + const auto top = itemTop(view); + if (top < 0) { + return; + } + const auto media = view->media(); + if (!media) { + hideAddPollOption(); + return; + } + const auto mediaPos = view->mediaTopLeft(); + const auto innerWidth = view->innerGeometry().width() + - st::msgPadding.left() + - st::msgPadding.right(); + const auto rect = media->addOptionRect(innerWidth); + _addPollOptionWidget->updatePosition( + QPoint( + mediaPos.x() + rect.x(), + top + mediaPos.y() + rect.y()), + rect.width()); +} + void ListWidget::elementOpenPhoto( not_null photo, FullMsgId context) { @@ -3069,6 +3164,15 @@ void ListWidget::mousePressEvent(QMouseEvent *e) { e->accept(); return; // ignore mouse press, that was hiding context menu } + if (_addPollOptionWidget && _addPollOptionView) { + const auto top = itemTop(_addPollOptionView); + const auto viewRect = (top >= 0) + ? QRect(0, top, width(), _addPollOptionView->height()) + : QRect(); + if (!viewRect.contains(e->pos())) { + hideAddPollOption(); + } + } if (_middleClickAutoscroll.active()) { _middleClickAutoscroll.stop(); e->accept(); @@ -4357,6 +4461,9 @@ void ListWidget::showItemHighlight(not_null item) { } void ListWidget::viewReplaced(not_null was, Element *now) { + if (_addPollOptionView == was) { + hideAddPollOption(); + } if (_visibleTopItem == was) _visibleTopItem = now; if (_scrollDateLastItem == was) _scrollDateLastItem = now; if (_overElement == was) _overElement = now; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index c8a4834194..650a286af7 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -66,6 +66,7 @@ namespace HistoryView { struct TextState; struct StateRequest; +class AddPollOptionWidget; class EmojiInteractions; class TranslateTracker; class ReadMetricsTracker; @@ -416,6 +417,12 @@ public: void elementShowPollResults( not_null poll, FullMsgId context) override; + void elementShowAddPollOption( + not_null view, + not_null poll, + FullMsgId context, + QRect optionRect) override; + void elementSubmitAddPollOption(FullMsgId context) override; void elementOpenPhoto( not_null photo, FullMsgId context) override; @@ -540,6 +547,13 @@ private: Deselecting, }; + void showAddPollOption( + not_null view, + not_null poll, + FullMsgId context); + void hideAddPollOption(); + void updateAddPollOptionPosition(); + void onTouchSelect(); void onTouchScrollTimer(); void markReadMetricsStale(); @@ -893,6 +907,10 @@ private: rpl::event_stream _requestedToShowMessage; rpl::event_stream> _scrollKeyEvents; + base::unique_qptr _addPollOptionWidget; + Element *_addPollOptionView = nullptr; + FullMsgId _addPollOptionContext; + rpl::lifetime _viewerLifetime; }; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 1b725fc0ef..76e02ec631 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -1233,6 +1233,7 @@ void Message::draw(Painter &p, const PaintContext &context) const { const auto maybeMediaHighlight = context.highlightPathCache && context.highlightPathCache->isEmpty(); auto mediaPosition = QPoint(inner.left(), top); + _lastMediaPosition = mediaPosition; p.translate(mediaPosition); media->draw(p, context.translated( -mediaPosition @@ -4815,6 +4816,10 @@ QRect Message::innerGeometry() const { return result; } +QPoint Message::mediaTopLeft() const { + return _lastMediaPosition; +} + bool Message::isCommentsRootView() const { return context() == Context::Replies && data()->isDiscussionPost() diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index 6ebe83cce4..24ba34a0cb 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -189,6 +189,7 @@ public: QRect effectIconGeometry() const override; QRect innerGeometry() const override; + QPoint mediaTopLeft() const override; [[nodiscard]] BottomRippleMask bottomRippleMask(int buttonHeight) const; private: @@ -398,6 +399,7 @@ private: uint32 _postShowingAuthor : 1 = 0; BottomInfo _bottomInfo; + mutable QPoint _lastMediaPosition; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.h b/Telegram/SourceFiles/history/view/media/history_view_media.h index 310a5ce7ce..3ab27ced75 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media.h @@ -184,6 +184,12 @@ public: virtual void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) { } + [[nodiscard]] virtual QRect addOptionRect(int innerWidth) const { + return {}; + } + virtual void setAddOptionActive(bool active) { + } + [[nodiscard]] virtual bool uploading() const { return false; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp index a128715102..9c92eaf915 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp @@ -49,6 +49,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/crc32hash.h" #include "base/unixtime.h" #include "base/timer.h" +#include "main/main_app_config.h" #include "main/main_session.h" #include "apiwrap.h" #include "api/api_polls.h" @@ -470,6 +471,36 @@ Poll::Poll( updateAnswerVotes(); startAnswersAnimation(); }))) +, _addOptionLink( + std::make_shared(crl::guard( + this, + [=](ClickContext context) { + const auto my = context.other.value(); + if (const auto delegate = my.elementDelegate + ? my.elementDelegate() + : nullptr) { + const auto innerWidth = width() + - st::msgPadding.left() + - st::msgPadding.right(); + delegate->elementShowAddPollOption( + _parent, + _poll, + _parent->data()->fullId(), + addOptionRect(innerWidth)); + } + }))) +, _saveOptionLink( + std::make_shared(crl::guard( + this, + [=](ClickContext context) { + const auto my = context.other.value(); + if (const auto delegate = my.elementDelegate + ? my.elementDelegate() + : nullptr) { + delegate->elementSubmitAddPollOption( + _parent->data()->fullId()); + } + }))) , _closeTimer([=] { repaint(); }) { if (!consumed.text.isEmpty()) { updateDescription(); @@ -510,6 +541,7 @@ QSize Poll::countOptimalSize() { + padding.bottom(); }), 0); + const auto addOptionHeight = canAddOption() ? this->addOptionHeight() : 0; const auto bottomButtonHeight = inlineFooter() ? 0 : st::historyPollBottomButtonSkip; @@ -521,6 +553,7 @@ QSize Poll::countOptimalSize() { + st::msgDateFont->height + st::historyPollAnswersSkip + answersHeight + + addOptionHeight + st::historyPollTotalVotesSkip + bottomButtonHeight + st::msgDateFont->height @@ -564,6 +597,45 @@ bool Poll::inlineFooter() const { & (PollData::Flag::PublicVotes | PollData::Flag::MultiChoice)); } +bool Poll::canAddOption() const { + return (_flags & PollData::Flag::OpenAnswers) + && !(_flags & PollData::Flag::Closed) + && (int(_poll->answers.size()) + < _poll->session().appConfig().pollOptionsLimit()); +} + +int Poll::addOptionHeight() const { + return st::historyPollAnswerPaddingNoMedia.top() + + st::msgDateFont->height + + st::historyPollAnswerPaddingNoMedia.bottom(); +} + +QRect Poll::addOptionRect(int innerWidth) const { + const auto answersHeight = ranges::accumulate(ranges::views::all( + _answers + ) | ranges::views::transform([&](const Answer &answer) { + return countAnswerHeight(answer, innerWidth); + }), 0); + const auto top = countQuestionTop(innerWidth) + + _question.countHeight(innerWidth) + + st::historyPollSubtitleSkip + + st::msgDateFont->height + + st::historyPollAnswersSkip + + answersHeight; + return QRect( + st::msgPadding.left(), + top, + innerWidth, + addOptionHeight()); +} + +void Poll::setAddOptionActive(bool active) { + if (_addOptionActive != active) { + _addOptionActive = active; + history()->owner().requestViewResize(_parent); + } +} + int Poll::countAnswerTop( const Answer &answer, int innerWidth) const { @@ -624,6 +696,7 @@ QSize Poll::countCurrentSize(int newWidth) { return countAnswerHeight(answer, innerWidth); }), 0); + const auto addOptionHeight = canAddOption() ? this->addOptionHeight() : 0; const auto bottomButtonHeight = inlineFooter() ? 0 : st::historyPollBottomButtonSkip; @@ -634,6 +707,7 @@ QSize Poll::countCurrentSize(int newWidth) { + st::msgDateFont->height + st::historyPollAnswersSkip + answersHeight + + addOptionHeight + st::historyPollTotalVotesSkip + bottomButtonHeight + st::msgDateFont->height @@ -1636,6 +1710,10 @@ void Poll::draw(Painter &p, const PaintContext &context) const { context); tshift += height; } + if (canAddOption()) { + paintAddOption(p, padding.left(), tshift, paintw, context); + tshift += addOptionHeight(); + } if (!inlineFooter()) { paintBottom(p, padding.left(), tshift, paintw, context); } else if (!_totalVotesLabel.isEmpty()) { @@ -1693,6 +1771,55 @@ void Poll::paintInlineFooter( } } +void Poll::paintAddOption( + Painter &p, + int left, + int top, + int paintw, + const PaintContext &context) const { + if (_addOptionActive) { + return; + } + const auto stm = context.messageStyle(); + const auto height = addOptionHeight(); + const auto &padding = st::historyPollAnswerPaddingNoMedia; + const auto textTop = top + padding.top(); + + if (_addOptionRipple) { + p.setOpacity(st::historyPollRippleOpacity); + _addOptionRipple->paint( + p, + left - st::msgPadding.left(), + top, + width(), + &stm->msgWaveformInactive->c); + if (_addOptionRipple->empty()) { + _addOptionRipple.reset(); + } + p.setOpacity(1.); + } + + const auto color = stm->msgDateFg->c; + const auto &icon = st::pollBoxOutlinePollAddIcon; + const auto &radio = st::historyPollRadio; + const auto iconLeft = left + + (radio.diameter - icon.width()) / 2; + const auto iconTop = textTop + + (st::msgDateFont->height - icon.height()) / 2; + icon.paint(p, iconLeft, iconTop, width(), color); + + p.setFont(st::normalFont); + p.setPen(stm->msgDateFg); + const auto text = tr::lng_polls_add_option(tr::now); + const auto textw = st::normalFont->width(text); + p.drawTextLeft( + left + st::historyPollAnswerPadding.left(), + textTop, + width(), + text, + textw); +} + void Poll::paintBottom( Painter &p, int left, @@ -1703,6 +1830,19 @@ void Poll::paintBottom( + st::msgPadding.bottom() + st::historyPollBottomButtonTop; const auto stm = context.messageStyle(); + if (_addOptionActive) { + p.setFont(st::semiboldFont); + p.setPen(stm->msgFileThumbLinkFg); + const auto text = tr::lng_polls_add_option_save(tr::now); + const auto textw = st::semiboldFont->width(text); + p.drawTextLeft( + left + (paintw - textw) / 2, + stringtop, + width(), + text, + textw); + return; + } if (isAuthorNotVoted() && !_adminShowResults && !canSendVotes()) { p.setPen(stm->msgFileThumbLinkFg); const auto labelWidth = _adminVotesLabel.maxWidth(); @@ -2769,10 +2909,25 @@ TextState Poll::textState(QPoint point, StateRequest request) const { } tshift += height; } + if (canAddOption()) { + const auto optHeight = addOptionHeight(); + if (QRect(0, tshift, width(), optHeight).contains(point)) { + _lastLinkPoint = point; + result.link = _addOptionLink; + return result; + } + tshift += optHeight; + } { const auto linkHeight = bottomButtonHeight(); const auto linkTop = height() - linkHeight; - if (isAuthorNotVoted() && !_adminShowResults && !canSendVotes()) { + if (_addOptionActive) { + if (QRect(0, linkTop, width(), linkHeight).contains(point)) { + _lastLinkPoint = point; + result.link = _saveOptionLink; + return result; + } + } else if (isAuthorNotVoted() && !_adminShowResults && !canSendVotes()) { if (QRect(0, linkTop, width(), linkHeight).contains(point)) { _lastLinkPoint = point; result.link = _adminVotesLink; @@ -2872,10 +3027,13 @@ void Poll::clickHandlerPressedChanged( if (canVote()) { toggleRipple(*i, pressed); } + } else if (handler == _addOptionLink) { + toggleAddOptionRipple(pressed); } else if (handler == _sendVotesLink || handler == _showResultsLink || handler == _adminVotesLink - || handler == _adminBackVoteLink) { + || handler == _adminBackVoteLink + || handler == _saveOptionLink) { toggleLinkRipple(pressed); } if (_attachedMediaAttach) { @@ -3002,6 +3160,29 @@ bool Poll::timerFooterMultiline(int paintw) const { return (paintw + fullw) / 2 > paintw - skipw; } +void Poll::toggleAddOptionRipple(bool pressed) { + if (pressed) { + const auto optWidth = width(); + const auto optHeight = addOptionHeight(); + if (!_addOptionRipple) { + auto mask = Ui::RippleAnimation::RectMask( + QSize(optWidth, optHeight)); + _addOptionRipple = std::make_unique( + st::defaultRippleAnimation, + std::move(mask), + [=] { repaint(); }); + } + const auto innerWidth = optWidth + - st::msgPadding.left() + - st::msgPadding.right(); + const auto rect = addOptionRect(innerWidth); + _addOptionRipple->add(_lastLinkPoint + - QPoint(0, rect.y())); + } else if (_addOptionRipple) { + _addOptionRipple->lastStop(); + } +} + void Poll::toggleLinkRipple(bool pressed) { if (pressed) { const auto linkWidth = width(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.h b/Telegram/SourceFiles/history/view/media/history_view_poll.h index 99862e97f1..01c63de533 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_poll.h +++ b/Telegram/SourceFiles/history/view/media/history_view_poll.h @@ -71,6 +71,9 @@ public: bool hasHeavyPart() const override; void parentTextUpdated() override; + [[nodiscard]] QRect addOptionRect(int innerWidth) const override; + void setAddOptionActive(bool active) override; + private: struct AnswerAnimation; struct AnswersAnimation; @@ -201,6 +204,7 @@ private: void radialAnimationCallback() const; void toggleRipple(Answer &answer, bool pressed); + void toggleAddOptionRipple(bool pressed); void toggleLinkRipple(bool pressed); void toggleMultiOption(const QByteArray &option); void sendMultiOptions(); @@ -229,6 +233,15 @@ private: int paintw, const PaintContext &context) const; + [[nodiscard]] bool canAddOption() const; + [[nodiscard]] int addOptionHeight() const; + void paintAddOption( + Painter &p, + int left, + int top, + int paintw, + const PaintContext &context) const; + [[nodiscard]] int bottomButtonHeight() const; [[nodiscard]] QString closeTimerText() const; [[nodiscard]] bool timerFooterMultiline(int paintw) const; @@ -257,7 +270,10 @@ private: ClickHandlerPtr _sendVotesLink; ClickHandlerPtr _adminVotesLink; ClickHandlerPtr _adminBackVoteLink; + ClickHandlerPtr _addOptionLink; + ClickHandlerPtr _saveOptionLink; mutable ClickHandlerPtr _showSolutionLink; + mutable std::unique_ptr _addOptionRipple; mutable std::unique_ptr _linkRipple; mutable int _linkRippleShift = 0; @@ -283,6 +299,7 @@ private: bool _hasSelected = false; bool _anyAnswerHasMedia = false; bool _votedFromHere = false; + bool _addOptionActive = false; mutable bool _wrongAnswerAnimated = false; mutable bool _adminShowResults = false;