[poll-view] Added initial inline add-option overlay with input field.

This commit is contained in:
23rd
2026-03-23 18:30:43 +03:00
parent db8878932f
commit 53dc6d45b9
20 changed files with 801 additions and 3 deletions
+2
View File
@@ -949,6 +949,8 @@ PRIVATE
history/view/history_view_cursor_state.h history/view/history_view_cursor_state.h
history/view/history_view_draw_to_reply.cpp history/view/history_view_draw_to_reply.cpp
history/view/history_view_draw_to_reply.h 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.cpp
history/view/history_view_element.h history/view/history_view_element.h
history/view/history_view_emoji_interactions.cpp history/view/history_view_emoji_interactions.cpp
+7
View File
@@ -6913,6 +6913,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_polls_votes_collapse" = "Collapse"; "lng_polls_votes_collapse" = "Collapse";
"lng_polls_vote_yesterday" = "yesterday"; "lng_polls_vote_yesterday" = "yesterday";
"lng_polls_option_added_by" = "Added by {user}"; "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" = "Checklist";
"lng_todo_title_group" = "Group Checklist"; "lng_todo_title_group" = "Group Checklist";
+41
View File
@@ -185,6 +185,47 @@ void Polls::sendVotes(
_pollVotesRequestIds.emplace(itemId, requestId); _pollVotesRequestIds.emplace(itemId, requestId);
} }
void Polls::addAnswer(
FullMsgId itemId,
const TextWithEntities &text,
Fn<void()> done,
Fn<void(QString)> 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<HistoryItem*> item) { void Polls::close(not_null<HistoryItem*> item) {
const auto itemId = item->fullId(); const auto itemId = item->fullId();
if (_pollCloseRequestIds.contains(itemId)) { if (_pollCloseRequestIds.contains(itemId)) {
+6
View File
@@ -35,6 +35,11 @@ public:
void sendVotes( void sendVotes(
FullMsgId itemId, FullMsgId itemId,
const std::vector<QByteArray> &options); const std::vector<QByteArray> &options);
void addAnswer(
FullMsgId itemId,
const TextWithEntities &text,
Fn<void()> done,
Fn<void(QString)> fail);
void close(not_null<HistoryItem*> item); void close(not_null<HistoryItem*> item);
void reloadResults(not_null<HistoryItem*> item); void reloadResults(not_null<HistoryItem*> item);
@@ -43,6 +48,7 @@ private:
MTP::Sender _api; MTP::Sender _api;
base::flat_map<FullMsgId, mtpRequestId> _pollVotesRequestIds; base::flat_map<FullMsgId, mtpRequestId> _pollVotesRequestIds;
base::flat_map<FullMsgId, mtpRequestId> _pollAddAnswerRequestIds;
base::flat_map<FullMsgId, mtpRequestId> _pollCloseRequestIds; base::flat_map<FullMsgId, mtpRequestId> _pollCloseRequestIds;
base::flat_map<FullMsgId, mtpRequestId> _pollReloadRequestIds; base::flat_map<FullMsgId, mtpRequestId> _pollReloadRequestIds;
+22 -1
View File
@@ -86,8 +86,29 @@ historyPollOutChosenSelected: icon {{ "poll_select_check", historyFileOutIconFgS
historyPollInChosen: icon {{ "poll_select_check", historyFileInIconFg }}; historyPollInChosen: icon {{ "poll_select_check", historyFileInIconFg }};
historyPollInChosenSelected: icon {{ "poll_select_check", historyFileInIconFgSelected }}; 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 }}; 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 }}; pollBoxOutlinePollAttachIcon: icon{{ "poll/general/outline_poll_attach", activeButtonFg }};
pollBoxMenuPollOrderIcon: icon{{ "poll/general/menu_poll_order-24x24", historyComposeIconFg }}; pollBoxMenuPollOrderIcon: icon{{ "poll/general/menu_poll_order-24x24", historyComposeIconFg }};
pollBoxFilledPollDeadlineIcon: icon{{ "poll/filled/filled_poll_deadline", activeButtonFg }}; pollBoxFilledPollDeadlineIcon: icon{{ "poll/filled/filled_poll_deadline", activeButtonFg }};
@@ -697,6 +697,16 @@ void InnerWidget::elementShowPollResults(
FullMsgId context) { FullMsgId context) {
} }
void InnerWidget::elementShowAddPollOption(
not_null<HistoryView::Element*> view,
not_null<PollData*> poll,
FullMsgId context,
QRect optionRect) {
}
void InnerWidget::elementSubmitAddPollOption(FullMsgId context) {
}
void InnerWidget::elementOpenPhoto( void InnerWidget::elementOpenPhoto(
not_null<PhotoData*> photo, not_null<PhotoData*> photo,
FullMsgId context) { FullMsgId context) {
@@ -109,6 +109,12 @@ public:
void elementShowPollResults( void elementShowPollResults(
not_null<PollData*> poll, not_null<PollData*> poll,
FullMsgId context) override; FullMsgId context) override;
void elementShowAddPollOption(
not_null<HistoryView::Element*> view,
not_null<PollData*> poll,
FullMsgId context,
QRect optionRect) override;
void elementSubmitAddPollOption(FullMsgId context) override;
void elementOpenPhoto( void elementOpenPhoto(
not_null<PhotoData*> photo, not_null<PhotoData*> photo,
FullMsgId context) override; FullMsgId context) override;
@@ -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_context_menu.h"
#include "history/view/history_view_reaction_preview.h" #include "history/view/history_view_reaction_preview.h"
#include "history/view/history_view_quick_action.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_emoji_interactions.h"
#include "history/view/history_view_top_peers_selector.h" #include "history/view/history_view_top_peers_selector.h"
#include "history/history_item_components.h" #include "history/history_item_components.h"
@@ -198,6 +199,20 @@ public:
_widget->elementShowPollResults(poll, context); _widget->elementShowPollResults(poll, context);
} }
} }
void elementShowAddPollOption(
not_null<Element*> view,
not_null<PollData*> 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( void elementOpenPhoto(
not_null<PhotoData*> photo, not_null<PhotoData*> photo,
FullMsgId context) override { FullMsgId context) override {
@@ -1870,6 +1885,15 @@ void HistoryInner::mousePressEvent(QMouseEvent *e) {
e->accept(); e->accept();
return; // ignore mouse press, that was hiding context menu 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()) { if (_middleClickAutoscroll.active()) {
_middleClickAutoscroll.stop(); _middleClickAutoscroll.stop();
e->accept(); e->accept();
@@ -2180,6 +2204,11 @@ void HistoryInner::itemRemoved(not_null<const HistoryItem*> item) {
} }
void HistoryInner::viewRemoved(not_null<const Element*> view) { void HistoryInner::viewRemoved(not_null<const Element*> view) {
if (_addPollOptionView == view) {
_addPollOptionWidget = nullptr;
_addPollOptionView = nullptr;
_addPollOptionContext = FullMsgId();
}
const auto refresh = [&](auto &saved) { const auto refresh = [&](auto &saved) {
if (saved == view) { if (saved == view) {
const auto now = viewByItem(view->data()); const auto now = viewByItem(view->data());
@@ -3944,6 +3973,8 @@ void HistoryInner::visibleAreaUpdated(int top, int bottom) {
_emojiInteractions->visibleAreaUpdated( _emojiInteractions->visibleAreaUpdated(
_visibleAreaTop, _visibleAreaTop,
_visibleAreaBottom); _visibleAreaBottom);
updateAddPollOptionPosition();
} }
bool HistoryInner::displayScrollDate() const { bool HistoryInner::displayScrollDate() const {
@@ -4101,6 +4132,7 @@ void HistoryInner::leaveEventHook(QEvent *e) {
} }
HistoryInner::~HistoryInner() { HistoryInner::~HistoryInner() {
hideAddPollOption();
_aboutView = nullptr; _aboutView = nullptr;
for (const auto &item : _animatedStickersPlayed) { for (const auto &item : _animatedStickersPlayed) {
if (const auto view = item->mainView()) { if (const auto view = item->mainView()) {
@@ -4272,6 +4304,98 @@ void HistoryInner::elementShowPollResults(
_controller->showPollResults(poll, context); _controller->showPollResults(poll, context);
} }
void HistoryInner::elementShowAddPollOption(
not_null<HistoryView::Element*> view,
not_null<PollData*> 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<Element*> view,
not_null<PollData*> 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( void HistoryInner::elementOpenPhoto(
not_null<PhotoData*> photo, not_null<PhotoData*> photo,
FullMsgId context) { FullMsgId context) {
@@ -37,6 +37,7 @@ struct StateRequest;
enum class CursorState : char; enum class CursorState : char;
enum class PointState : char; enum class PointState : char;
enum class ElementChatMode : char; enum class ElementChatMode : char;
class AddPollOptionWidget;
class EmptyPainter; class EmptyPainter;
class Element; class Element;
class TranslateTracker; class TranslateTracker;
@@ -155,6 +156,12 @@ public:
void elementShowPollResults( void elementShowPollResults(
not_null<PollData*> poll, not_null<PollData*> poll,
FullMsgId context); FullMsgId context);
void elementShowAddPollOption(
not_null<HistoryView::Element*> view,
not_null<PollData*> poll,
FullMsgId context,
QRect optionRect);
void elementSubmitAddPollOption(FullMsgId context);
void elementOpenPhoto( void elementOpenPhoto(
not_null<PhotoData*> photo, not_null<PhotoData*> photo,
FullMsgId context); FullMsgId context);
@@ -255,6 +262,13 @@ protected:
void contextMenuEvent(QContextMenuEvent *e) override; void contextMenuEvent(QContextMenuEvent *e) override;
private: private:
void showAddPollOption(
not_null<Element*> view,
not_null<PollData*> poll,
FullMsgId context);
void hideAddPollOption();
void updateAddPollOptionPosition();
void onTouchSelect(); void onTouchSelect();
void onTouchScrollTimer(); void onTouchScrollTimer();
void markReadMetricsStale(); void markReadMetricsStale();
@@ -596,6 +610,10 @@ private:
ClickHandlerPtr _scrollDateLink; ClickHandlerPtr _scrollDateLink;
ClickHandlerPtr _forumThreadBarLink; ClickHandlerPtr _forumThreadBarLink;
base::unique_qptr<HistoryView::AddPollOptionWidget> _addPollOptionWidget;
Element *_addPollOptionView = nullptr;
FullMsgId _addPollOptionContext;
}; };
[[nodiscard]] bool CanSendReply(not_null<const HistoryItem*> item); [[nodiscard]] bool CanSendReply(not_null<const HistoryItem*> item);
@@ -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<QWidget*> parent,
not_null<PollData*> poll,
FullMsgId itemId,
not_null<Main::Session*> session)
: RpWidget(parent)
, _poll(poll)
, _itemId(itemId)
, _session(session) {
setupField();
subscribeToPollUpdates();
}
void AddPollOptionWidget::setupField() {
_field = Ui::CreateChild<Ui::InputField>(
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<QEvent*> event) {
if (event->type() == QEvent::KeyPress) {
if (static_cast<QKeyEvent*>(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<PollData*> poll) {
return (poll == _poll);
}) | rpl::on_next([=](not_null<PollData*> poll) {
if (poll->closed()
|| (int(poll->answers.size())
>= _session->appConfig().pollOptionsLimit())) {
_cancelledEvents.fire({});
}
}, lifetime());
_session->data().itemRemoved(
) | rpl::filter([=](not_null<const HistoryItem*> 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
@@ -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<QWidget*> parent,
not_null<PollData*> poll,
FullMsgId itemId,
not_null<Main::Session*> 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<PollData*> _poll;
const FullMsgId _itemId;
const not_null<Main::Session*> _session;
Ui::InputField *_field = nullptr;
rpl::event_stream<> _submittedEvents;
rpl::event_stream<> _cancelledEvents;
};
} // namespace HistoryView
@@ -505,6 +505,16 @@ void DefaultElementDelegate::elementShowPollResults(
FullMsgId context) { FullMsgId context) {
} }
void DefaultElementDelegate::elementShowAddPollOption(
not_null<Element*> view,
not_null<PollData*> poll,
FullMsgId context,
QRect optionRect) {
}
void DefaultElementDelegate::elementSubmitAddPollOption(FullMsgId context) {
}
void DefaultElementDelegate::elementOpenPhoto( void DefaultElementDelegate::elementOpenPhoto(
not_null<PhotoData*> photo, not_null<PhotoData*> photo,
FullMsgId context) { FullMsgId context) {
@@ -2884,6 +2894,10 @@ QRect Element::effectIconGeometry() const {
return QRect(); return QRect();
} }
QPoint Element::mediaTopLeft() const {
return innerGeometry().topLeft();
}
Element::~Element() { Element::~Element() {
setReactions(nullptr); setReactions(nullptr);
@@ -107,6 +107,12 @@ public:
virtual void elementShowPollResults( virtual void elementShowPollResults(
not_null<PollData*> poll, not_null<PollData*> poll,
FullMsgId context) = 0; FullMsgId context) = 0;
virtual void elementShowAddPollOption(
not_null<Element*> view,
not_null<PollData*> poll,
FullMsgId context,
QRect optionRect) = 0;
virtual void elementSubmitAddPollOption(FullMsgId context) = 0;
virtual void elementOpenPhoto( virtual void elementOpenPhoto(
not_null<PhotoData*> photo, not_null<PhotoData*> photo,
FullMsgId context) = 0; FullMsgId context) = 0;
@@ -163,6 +169,12 @@ public:
void elementShowPollResults( void elementShowPollResults(
not_null<PollData*> poll, not_null<PollData*> poll,
FullMsgId context) override; FullMsgId context) override;
void elementShowAddPollOption(
not_null<Element*> view,
not_null<PollData*> poll,
FullMsgId context,
QRect optionRect) override;
void elementSubmitAddPollOption(FullMsgId context) override;
void elementOpenPhoto( void elementOpenPhoto(
not_null<PhotoData*> photo, not_null<PhotoData*> photo,
FullMsgId context) override; FullMsgId context) override;
@@ -637,6 +649,7 @@ public:
[[nodiscard]] virtual QRect effectIconGeometry() const; [[nodiscard]] virtual QRect effectIconGeometry() const;
[[nodiscard]] virtual QRect innerGeometry() const = 0; [[nodiscard]] virtual QRect innerGeometry() const = 0;
[[nodiscard]] virtual QPoint mediaTopLeft() const;
void customEmojiRepaint(); void customEmojiRepaint();
void prepareCustomEmojiPaint( void prepareCustomEmojiPaint(
@@ -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_cursor_state.h"
#include "history/view/history_view_translate_tracker.h" #include "history/view/history_view_translate_tracker.h"
#include "history/view/history_view_read_metrics_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_top_peers_selector.h"
#include "history/view/history_view_quick_action.h" #include "history/view/history_view_quick_action.h"
#include "chat_helpers/message_field.h" #include "chat_helpers/message_field.h"
@@ -1174,6 +1176,7 @@ void ListWidget::visibleTopBottomUpdated(
_applyUpdatedScrollState.call(); _applyUpdatedScrollState.call();
_emojiInteractions->visibleAreaUpdated(_visibleTop, _visibleBottom); _emojiInteractions->visibleAreaUpdated(_visibleTop, _visibleBottom);
updateAddPollOptionPosition();
} }
void ListWidget::applyUpdatedScrollState() { void ListWidget::applyUpdatedScrollState() {
@@ -1889,6 +1892,98 @@ void ListWidget::elementShowPollResults(
_delegate->listShowPollResults(poll, context); _delegate->listShowPollResults(poll, context);
} }
void ListWidget::elementShowAddPollOption(
not_null<Element*> view,
not_null<PollData*> 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<Element*> view,
not_null<PollData*> 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<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 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( void ListWidget::elementOpenPhoto(
not_null<PhotoData*> photo, not_null<PhotoData*> photo,
FullMsgId context) { FullMsgId context) {
@@ -3069,6 +3164,15 @@ void ListWidget::mousePressEvent(QMouseEvent *e) {
e->accept(); e->accept();
return; // ignore mouse press, that was hiding context menu 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()) { if (_middleClickAutoscroll.active()) {
_middleClickAutoscroll.stop(); _middleClickAutoscroll.stop();
e->accept(); e->accept();
@@ -4357,6 +4461,9 @@ void ListWidget::showItemHighlight(not_null<HistoryItem*> item) {
} }
void ListWidget::viewReplaced(not_null<const Element*> was, Element *now) { void ListWidget::viewReplaced(not_null<const Element*> was, Element *now) {
if (_addPollOptionView == was) {
hideAddPollOption();
}
if (_visibleTopItem == was) _visibleTopItem = now; if (_visibleTopItem == was) _visibleTopItem = now;
if (_scrollDateLastItem == was) _scrollDateLastItem = now; if (_scrollDateLastItem == was) _scrollDateLastItem = now;
if (_overElement == was) _overElement = now; if (_overElement == was) _overElement = now;
@@ -66,6 +66,7 @@ namespace HistoryView {
struct TextState; struct TextState;
struct StateRequest; struct StateRequest;
class AddPollOptionWidget;
class EmojiInteractions; class EmojiInteractions;
class TranslateTracker; class TranslateTracker;
class ReadMetricsTracker; class ReadMetricsTracker;
@@ -416,6 +417,12 @@ public:
void elementShowPollResults( void elementShowPollResults(
not_null<PollData*> poll, not_null<PollData*> poll,
FullMsgId context) override; FullMsgId context) override;
void elementShowAddPollOption(
not_null<Element*> view,
not_null<PollData*> poll,
FullMsgId context,
QRect optionRect) override;
void elementSubmitAddPollOption(FullMsgId context) override;
void elementOpenPhoto( void elementOpenPhoto(
not_null<PhotoData*> photo, not_null<PhotoData*> photo,
FullMsgId context) override; FullMsgId context) override;
@@ -540,6 +547,13 @@ private:
Deselecting, Deselecting,
}; };
void showAddPollOption(
not_null<Element*> view,
not_null<PollData*> poll,
FullMsgId context);
void hideAddPollOption();
void updateAddPollOptionPosition();
void onTouchSelect(); void onTouchSelect();
void onTouchScrollTimer(); void onTouchScrollTimer();
void markReadMetricsStale(); void markReadMetricsStale();
@@ -893,6 +907,10 @@ private:
rpl::event_stream<FullMsgId> _requestedToShowMessage; rpl::event_stream<FullMsgId> _requestedToShowMessage;
rpl::event_stream<not_null<QKeyEvent*>> _scrollKeyEvents; rpl::event_stream<not_null<QKeyEvent*>> _scrollKeyEvents;
base::unique_qptr<AddPollOptionWidget> _addPollOptionWidget;
Element *_addPollOptionView = nullptr;
FullMsgId _addPollOptionContext;
rpl::lifetime _viewerLifetime; rpl::lifetime _viewerLifetime;
}; };
@@ -1233,6 +1233,7 @@ void Message::draw(Painter &p, const PaintContext &context) const {
const auto maybeMediaHighlight = context.highlightPathCache const auto maybeMediaHighlight = context.highlightPathCache
&& context.highlightPathCache->isEmpty(); && context.highlightPathCache->isEmpty();
auto mediaPosition = QPoint(inner.left(), top); auto mediaPosition = QPoint(inner.left(), top);
_lastMediaPosition = mediaPosition;
p.translate(mediaPosition); p.translate(mediaPosition);
media->draw(p, context.translated( media->draw(p, context.translated(
-mediaPosition -mediaPosition
@@ -4815,6 +4816,10 @@ QRect Message::innerGeometry() const {
return result; return result;
} }
QPoint Message::mediaTopLeft() const {
return _lastMediaPosition;
}
bool Message::isCommentsRootView() const { bool Message::isCommentsRootView() const {
return context() == Context::Replies return context() == Context::Replies
&& data()->isDiscussionPost() && data()->isDiscussionPost()
@@ -189,6 +189,7 @@ public:
QRect effectIconGeometry() const override; QRect effectIconGeometry() const override;
QRect innerGeometry() const override; QRect innerGeometry() const override;
QPoint mediaTopLeft() const override;
[[nodiscard]] BottomRippleMask bottomRippleMask(int buttonHeight) const; [[nodiscard]] BottomRippleMask bottomRippleMask(int buttonHeight) const;
private: private:
@@ -398,6 +399,7 @@ private:
uint32 _postShowingAuthor : 1 = 0; uint32 _postShowingAuthor : 1 = 0;
BottomInfo _bottomInfo; BottomInfo _bottomInfo;
mutable QPoint _lastMediaPosition;
}; };
@@ -184,6 +184,12 @@ public:
virtual void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) { 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 { [[nodiscard]] virtual bool uploading() const {
return false; return false;
} }
@@ -49,6 +49,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/crc32hash.h" #include "base/crc32hash.h"
#include "base/unixtime.h" #include "base/unixtime.h"
#include "base/timer.h" #include "base/timer.h"
#include "main/main_app_config.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "api/api_polls.h" #include "api/api_polls.h"
@@ -470,6 +471,36 @@ Poll::Poll(
updateAnswerVotes(); updateAnswerVotes();
startAnswersAnimation(); startAnswersAnimation();
}))) })))
, _addOptionLink(
std::make_shared<LambdaClickHandler>(crl::guard(
this,
[=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
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<LambdaClickHandler>(crl::guard(
this,
[=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
if (const auto delegate = my.elementDelegate
? my.elementDelegate()
: nullptr) {
delegate->elementSubmitAddPollOption(
_parent->data()->fullId());
}
})))
, _closeTimer([=] { repaint(); }) { , _closeTimer([=] { repaint(); }) {
if (!consumed.text.isEmpty()) { if (!consumed.text.isEmpty()) {
updateDescription(); updateDescription();
@@ -510,6 +541,7 @@ QSize Poll::countOptimalSize() {
+ padding.bottom(); + padding.bottom();
}), 0); }), 0);
const auto addOptionHeight = canAddOption() ? this->addOptionHeight() : 0;
const auto bottomButtonHeight = inlineFooter() const auto bottomButtonHeight = inlineFooter()
? 0 ? 0
: st::historyPollBottomButtonSkip; : st::historyPollBottomButtonSkip;
@@ -521,6 +553,7 @@ QSize Poll::countOptimalSize() {
+ st::msgDateFont->height + st::msgDateFont->height
+ st::historyPollAnswersSkip + st::historyPollAnswersSkip
+ answersHeight + answersHeight
+ addOptionHeight
+ st::historyPollTotalVotesSkip + st::historyPollTotalVotesSkip
+ bottomButtonHeight + bottomButtonHeight
+ st::msgDateFont->height + st::msgDateFont->height
@@ -564,6 +597,45 @@ bool Poll::inlineFooter() const {
& (PollData::Flag::PublicVotes | PollData::Flag::MultiChoice)); & (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( int Poll::countAnswerTop(
const Answer &answer, const Answer &answer,
int innerWidth) const { int innerWidth) const {
@@ -624,6 +696,7 @@ QSize Poll::countCurrentSize(int newWidth) {
return countAnswerHeight(answer, innerWidth); return countAnswerHeight(answer, innerWidth);
}), 0); }), 0);
const auto addOptionHeight = canAddOption() ? this->addOptionHeight() : 0;
const auto bottomButtonHeight = inlineFooter() const auto bottomButtonHeight = inlineFooter()
? 0 ? 0
: st::historyPollBottomButtonSkip; : st::historyPollBottomButtonSkip;
@@ -634,6 +707,7 @@ QSize Poll::countCurrentSize(int newWidth) {
+ st::msgDateFont->height + st::msgDateFont->height
+ st::historyPollAnswersSkip + st::historyPollAnswersSkip
+ answersHeight + answersHeight
+ addOptionHeight
+ st::historyPollTotalVotesSkip + st::historyPollTotalVotesSkip
+ bottomButtonHeight + bottomButtonHeight
+ st::msgDateFont->height + st::msgDateFont->height
@@ -1636,6 +1710,10 @@ void Poll::draw(Painter &p, const PaintContext &context) const {
context); context);
tshift += height; tshift += height;
} }
if (canAddOption()) {
paintAddOption(p, padding.left(), tshift, paintw, context);
tshift += addOptionHeight();
}
if (!inlineFooter()) { if (!inlineFooter()) {
paintBottom(p, padding.left(), tshift, paintw, context); paintBottom(p, padding.left(), tshift, paintw, context);
} else if (!_totalVotesLabel.isEmpty()) { } 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( void Poll::paintBottom(
Painter &p, Painter &p,
int left, int left,
@@ -1703,6 +1830,19 @@ void Poll::paintBottom(
+ st::msgPadding.bottom() + st::msgPadding.bottom()
+ st::historyPollBottomButtonTop; + st::historyPollBottomButtonTop;
const auto stm = context.messageStyle(); 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()) { if (isAuthorNotVoted() && !_adminShowResults && !canSendVotes()) {
p.setPen(stm->msgFileThumbLinkFg); p.setPen(stm->msgFileThumbLinkFg);
const auto labelWidth = _adminVotesLabel.maxWidth(); const auto labelWidth = _adminVotesLabel.maxWidth();
@@ -2769,10 +2909,25 @@ TextState Poll::textState(QPoint point, StateRequest request) const {
} }
tshift += height; 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 linkHeight = bottomButtonHeight();
const auto linkTop = height() - linkHeight; 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)) { if (QRect(0, linkTop, width(), linkHeight).contains(point)) {
_lastLinkPoint = point; _lastLinkPoint = point;
result.link = _adminVotesLink; result.link = _adminVotesLink;
@@ -2872,10 +3027,13 @@ void Poll::clickHandlerPressedChanged(
if (canVote()) { if (canVote()) {
toggleRipple(*i, pressed); toggleRipple(*i, pressed);
} }
} else if (handler == _addOptionLink) {
toggleAddOptionRipple(pressed);
} else if (handler == _sendVotesLink } else if (handler == _sendVotesLink
|| handler == _showResultsLink || handler == _showResultsLink
|| handler == _adminVotesLink || handler == _adminVotesLink
|| handler == _adminBackVoteLink) { || handler == _adminBackVoteLink
|| handler == _saveOptionLink) {
toggleLinkRipple(pressed); toggleLinkRipple(pressed);
} }
if (_attachedMediaAttach) { if (_attachedMediaAttach) {
@@ -3002,6 +3160,29 @@ bool Poll::timerFooterMultiline(int paintw) const {
return (paintw + fullw) / 2 > paintw - skipw; 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<Ui::RippleAnimation>(
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) { void Poll::toggleLinkRipple(bool pressed) {
if (pressed) { if (pressed) {
const auto linkWidth = width(); const auto linkWidth = width();
@@ -71,6 +71,9 @@ public:
bool hasHeavyPart() const override; bool hasHeavyPart() const override;
void parentTextUpdated() override; void parentTextUpdated() override;
[[nodiscard]] QRect addOptionRect(int innerWidth) const override;
void setAddOptionActive(bool active) override;
private: private:
struct AnswerAnimation; struct AnswerAnimation;
struct AnswersAnimation; struct AnswersAnimation;
@@ -201,6 +204,7 @@ private:
void radialAnimationCallback() const; void radialAnimationCallback() const;
void toggleRipple(Answer &answer, bool pressed); void toggleRipple(Answer &answer, bool pressed);
void toggleAddOptionRipple(bool pressed);
void toggleLinkRipple(bool pressed); void toggleLinkRipple(bool pressed);
void toggleMultiOption(const QByteArray &option); void toggleMultiOption(const QByteArray &option);
void sendMultiOptions(); void sendMultiOptions();
@@ -229,6 +233,15 @@ private:
int paintw, int paintw,
const PaintContext &context) const; 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]] int bottomButtonHeight() const;
[[nodiscard]] QString closeTimerText() const; [[nodiscard]] QString closeTimerText() const;
[[nodiscard]] bool timerFooterMultiline(int paintw) const; [[nodiscard]] bool timerFooterMultiline(int paintw) const;
@@ -257,7 +270,10 @@ private:
ClickHandlerPtr _sendVotesLink; ClickHandlerPtr _sendVotesLink;
ClickHandlerPtr _adminVotesLink; ClickHandlerPtr _adminVotesLink;
ClickHandlerPtr _adminBackVoteLink; ClickHandlerPtr _adminBackVoteLink;
ClickHandlerPtr _addOptionLink;
ClickHandlerPtr _saveOptionLink;
mutable ClickHandlerPtr _showSolutionLink; mutable ClickHandlerPtr _showSolutionLink;
mutable std::unique_ptr<Ui::RippleAnimation> _addOptionRipple;
mutable std::unique_ptr<Ui::RippleAnimation> _linkRipple; mutable std::unique_ptr<Ui::RippleAnimation> _linkRipple;
mutable int _linkRippleShift = 0; mutable int _linkRippleShift = 0;
@@ -283,6 +299,7 @@ private:
bool _hasSelected = false; bool _hasSelected = false;
bool _anyAnswerHasMedia = false; bool _anyAnswerHasMedia = false;
bool _votedFromHere = false; bool _votedFromHere = false;
bool _addOptionActive = false;
mutable bool _wrongAnswerAnimated = false; mutable bool _wrongAnswerAnimated = false;
mutable bool _adminShowResults = false; mutable bool _adminShowResults = false;