From b6ccc35ad7c9c17a50bbd7691b8a06feddefccf5 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 22 May 2026 17:14:00 +0300 Subject: [PATCH] [poll-create] Added poll answer link entry and preview. --- Telegram/Resources/langs/lang.strings | 4 + .../SourceFiles/boxes/create_poll_box.cpp | 142 ++++++++++++++++++ Telegram/SourceFiles/boxes/polls.style | 9 ++ .../SourceFiles/poll/poll_media_upload.cpp | 39 +++-- 4 files changed, 185 insertions(+), 9 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 58f0513c5f..2989e48e29 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7171,6 +7171,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_polls_create_description_placeholder" = "Add Description (optional)"; "lng_polls_create_options" = "Poll options"; "lng_polls_create_option_add" = "Add an option..."; +"lng_polls_create_option_link" = "Link"; +"lng_polls_create_option_link_title" = "Add link"; +"lng_polls_create_option_link_description" = "Attach a link to this option."; +"lng_polls_create_option_link_placeholder" = "URL"; "lng_polls_create_limit#one" = "You can add {count} more option."; "lng_polls_create_limit#other" = "You can add {count} more options."; "lng_polls_create_maximum" = "You have added the maximum number of options."; diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp index 74093297b1..bf0b7b20fa 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.cpp +++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/create_poll_box.h" +#include "poll/poll_link_thumbnail.h" #include "poll/poll_media_upload.h" #include "base/call_delayed.h" #include "base/qt/qt_key_modifiers.h" @@ -36,7 +37,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo.h" #include "data/data_session.h" #include "data/data_user.h" +#include "data/data_web_page.h" #include "data/stickers/data_custom_emoji.h" +#include "history/view/controls/history_view_webpage_processor.h" #include "history/view/media/menu/history_view_poll_menu.h" #include "history/view/history_view_schedule_box.h" #include "info/channel_statistics/boosts/giveaway/select_countries_box.h" @@ -75,6 +78,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/fields/input_field.h" #include "ui/widgets/labels.h" #include "ui/boxes/choose_date_time.h" +#include "ui/layers/generic_box.h" #include "ui/text/format_values.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/scroll_area.h" @@ -340,6 +344,51 @@ void FocusAtEnd(not_null field) { field->ensureCursorVisible(); } +void AddPollOptionLinkBox( + not_null box, + const QString &initial, + Fn callback) { + box->setTitle(tr::lng_polls_create_option_link_title()); + + const auto content = box->verticalLayout(); + content->add( + object_ptr( + content, + tr::lng_polls_create_option_link_description(), + st::defaultFlatLabel), + st::boxRowPadding); + Ui::AddSkip(content); + Ui::AddSkip(content); + + const auto field = content->add( + object_ptr( + content, + st::createPollLinkUrlField, + Ui::InputField::Mode::SingleLine, + tr::lng_polls_create_option_link_placeholder(), + initial), + st::boxRowPadding); + + const auto submit = [=] { + const auto url = field->getLastText().trimmed(); + if (url.isEmpty()) { + field->showError(); + return; + } + const auto weak = base::make_weak(box); + callback(url); + if (weak) { + box->closeBox(); + } + }; + field->submits( + ) | rpl::on_next(submit, field->lifetime()); + + box->addButton(tr::lng_box_done(), submit); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + box->setFocusCallback([=] { field->setFocusFast(); }); +} + [[nodiscard]] QStringList ParsePastedList(const QString &text) { auto list = QStringView(text).split('\n'); for (auto i = list.begin(); i != list.end();) { @@ -1570,6 +1619,9 @@ object_ptr CreatePollBox::setupContent() { = std::make_shared(); std::shared_ptr solutionMedia = std::make_shared(); + std::shared_ptr + webpageResolver; + base::flat_map webPageLifetimes; std::weak_ptr stickerTarget; base::flat_map uploads; base::unique_qptr mediaMenu; @@ -1582,6 +1634,8 @@ object_ptr CreatePollBox::setupContent() { }; const auto state = lifetime().make_state(); state->prepareQueue = std::make_unique(); + state->webpageResolver = std::make_shared< + HistoryView::Controls::WebpageResolver>(&_controller->session()); auto result = object_ptr(this); const auto container = result.data(); @@ -2461,6 +2515,89 @@ object_ptr CreatePollBox::setupContent() { FileDialog::AllFilesFilter(), callback); }; + const auto applyResolvedWebPage = [=]( + std::shared_ptr media, + not_null page) { + auto pollMedia = PollMedia(); + pollMedia.webpage = page; + pollMedia.url = page->url.isEmpty() ? media->media.url : page->url; + auto thumbnail = page->photo + ? Ui::MakePhotoThumbnailCenterCrop(page->photo, FullMsgId()) + : Poll::MakeLinkThumbnail(); + const auto rounded = (page->photo != nullptr); + setMedia(media, pollMedia, std::move(thumbnail), rounded); + }; + const auto subscribeToWebPageUpdates = [=]( + std::shared_ptr media, + not_null page) { + const auto raw = media.get(); + const auto weak = std::weak_ptr(media); + _controller->session().data().webPageUpdates( + ) | rpl::filter([=](not_null updated) { + const auto locked = weak.lock(); + return locked + && (updated == page) + && (locked->media.webpage == page); + }) | rpl::on_next([=] { + if (const auto locked = weak.lock()) { + applyResolvedWebPage(locked, page); + } + }, state->webPageLifetimes[raw]); + }; + const auto resolveLink = [=]( + std::shared_ptr media, + QString url) { + const auto raw = media.get(); + const auto weak = std::weak_ptr(media); + state->webPageLifetimes[raw].destroy(); + const auto token = media->token; + const auto apply = [=](const QString &resolvedUrl) { + const auto locked = weak.lock(); + if (!locked || locked->token != token || resolvedUrl != url) { + return; + } + const auto cached = state->webpageResolver->lookup(url); + if (!cached || !*cached) { + return; + } + const auto page = *cached; + applyResolvedWebPage(locked, page); + subscribeToWebPageUpdates(locked, page); + }; + if (const auto cached = state->webpageResolver->lookup(url)) { + if (*cached) { + applyResolvedWebPage(media, *cached); + subscribeToWebPageUpdates(media, *cached); + } + return; + } + state->webPageLifetimes[raw] + = state->webpageResolver->resolved( + ) | rpl::filter([=](const QString &resolvedUrl) { + const auto locked = weak.lock(); + return locked + && (resolvedUrl == url) + && (locked->token == token); + }) | rpl::take(1) | rpl::on_next(apply); + state->webpageResolver->request(url); + }; + const auto chooseLink = [=](std::shared_ptr media) { + const auto initial = media->media.url; + const auto callback = crl::guard(this, [=](QString url) { + auto pollMedia = PollMedia(); + pollMedia.url = url; + setMedia( + media, + pollMedia, + Poll::MakeLinkThumbnail(), + false); + resolveLink(media, url); + }); + _controller->show(Box( + AddPollOptionLinkBox, + initial, + callback)); + }; const auto clearMedia = [=](std::shared_ptr media) { auto toCancel = std::vector(); for (auto i = state->uploads.begin(); i != state->uploads.end();) { @@ -2474,6 +2611,7 @@ object_ptr CreatePollBox::setupContent() { for (const auto &id : toCancel) { _controller->session().uploader().cancel(id); } + state->webPageLifetimes.remove(media.get()); setMedia(media, PollMedia(), nullptr, false); }; const auto chooseLocation = [=]( @@ -2580,6 +2718,10 @@ object_ptr CreatePollBox::setupContent() { [=] { showStickerPanel(button, media); }, &st::menuIconStickers); } + state->mediaMenu->addAction( + tr::lng_polls_create_option_link(tr::now), + [=] { chooseLink(media); }, + &st::menuIconLink); if (media->media || media->uploading) { state->mediaMenu->addAction( tr::lng_box_remove(tr::now), diff --git a/Telegram/SourceFiles/boxes/polls.style b/Telegram/SourceFiles/boxes/polls.style index c5022fc7fd..b6e8599e9f 100644 --- a/Telegram/SourceFiles/boxes/polls.style +++ b/Telegram/SourceFiles/boxes/polls.style @@ -10,6 +10,7 @@ using "ui/basic.style"; using "ui/widgets/widgets.style"; using "boxes/boxes.style"; using "chat_helpers/chat_helpers.style"; +using "dialogs/dialogs.style"; using "ui/chat/chat.style"; historyPollQuestionFont: semiboldFont; @@ -151,6 +152,7 @@ historyPollAnswerThumbRadius: 9px; pollAttachProgressMargin: 4px; pollAttachCancel: icon {{ "history_audio_cancel", historyFileThumbIconFg }}; pollAttachView: icon {{ "mediaview/views", historyFileThumbIconFg }}; +pollAttachLink: icon {{ "menu/link", historyFileThumbIconFg }}; createPollField: InputField(defaultInputField) { textMargins: margins(0px, 4px, 0px, 4px); @@ -216,6 +218,13 @@ createPollWarningPosition: point(16px, 6px); createPollCheckboxMargin: margins(22px, 10px, 22px, 10px); createPollFieldTitlePadding: margins(22px, 7px, 10px, 6px); +createPollLinkUrlField: InputField(dialogsFilter) { + textBgActive: filterInputInactiveBg; + textMargins: margins(14px, 8px, 14px, 5px); + placeholderMargins: margins(0px, 0px, 2px, 0px); + heightMin: 36px; +} + createTodoOptionField: InputField(createPollOptionField) { textMargins: margins(22px, 11px, 68px, 11px); } diff --git a/Telegram/SourceFiles/poll/poll_media_upload.cpp b/Telegram/SourceFiles/poll/poll_media_upload.cpp index 9c4f31d291..3a118a45bc 100644 --- a/Telegram/SourceFiles/poll/poll_media_upload.cpp +++ b/Telegram/SourceFiles/poll/poll_media_upload.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer.h" #include "data/data_photo.h" #include "data/data_session.h" +#include "data/data_web_page.h" #include "lang/lang_keys.h" #include "layout/layout_document_generic_preview.h" #include "main/main_session.h" @@ -72,8 +73,8 @@ QImage GenerateDocumentFilePreview( p.setBrush(color); p.drawRoundedRect( QRect(0, 0, size, size), - st::roundRadiusSmall, - st::roundRadiusSmall); + st::pollAttachRadius, + st::pollAttachRadius); if (!ext.isEmpty()) { const auto refSize = st::overviewFileLayout.fileThumbSize; @@ -200,6 +201,8 @@ void PollMediaButton::paintEvent(QPaintEvent *e) { p, _st.rippleAreaPosition, _rippleColorOverride ? &*_rippleColorOverride : nullptr); + const auto isLinkWithPhoto = _state->media.webpage + && _state->media.webpage->photo; if (_state->thumbnail) { const auto target = rippleRect(); paintCover( @@ -208,6 +211,23 @@ void PollMediaButton::paintEvent(QPaintEvent *e) { _state->thumbnail->image( std::max(target.width(), target.height())), _state->rounded); + if (isLinkWithPhoto) { + p.save(); + auto hq = PainterHighQualityEnabler(p); + auto path = QPainterPath(); + path.addRoundedRect( + target, + st::pollAttachRadius, + st::pollAttachRadius); + p.setClipPath(path); + p.fillRect(target, st::songCoverOverlayFg); + const auto center = QPointF(rect::center(target)); + p.translate(center); + p.rotate(-45.); + p.translate(-center); + st::pollAttachLink.paintInCenter(p, target); + p.restore(); + } } else if (_iconColorOverride) { const auto &icon = (isOver() && !_st.iconOver.empty()) ? _st.iconOver @@ -226,7 +246,7 @@ void PollMediaButton::paintEvent(QPaintEvent *e) { target, image->image(std::max(target.width(), target.height()))); } - if (_state->thumbnail && !_state->uploading) { + if (_state->thumbnail && !_state->uploading && !isLinkWithPhoto) { const auto viewOpacity = _viewShown.value( (isOver() || isDown()) ? 1. : 0.); if (viewOpacity > 0.) { @@ -235,8 +255,8 @@ void PollMediaButton::paintEvent(QPaintEvent *e) { auto path = QPainterPath(); path.addRoundedRect( rippleRect(), - st::roundRadiusSmall, - st::roundRadiusSmall); + st::pollAttachRadius, + st::pollAttachRadius); p.setClipPath(path); p.fillRect(rippleRect(), st::songCoverOverlayFg); st::pollAttachView.paintInCenter(p, rippleRect()); @@ -252,8 +272,8 @@ void PollMediaButton::paintEvent(QPaintEvent *e) { auto path = QPainterPath(); path.addRoundedRect( rippleRect(), - st::roundRadiusSmall, - st::roundRadiusSmall); + st::pollAttachRadius, + st::pollAttachRadius); p.setClipPath(path); p.fillRect(rippleRect(), st::songCoverOverlayFg); p.restore(); @@ -338,12 +358,13 @@ void PollMediaButton::paintCover( size.width(), size.height()); p.save(); + auto hq = PainterHighQualityEnabler(p); if (rounded) { auto path = QPainterPath(); path.addRoundedRect( target, - st::roundRadiusSmall, - st::roundRadiusSmall); + st::pollAttachRadius, + st::pollAttachRadius); p.setClipPath(path); } p.drawImage(geometry, image, source);