From 57754be6f080a7e27fea51dd89758d3a1f2b72ad Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 23 Dec 2025 15:59:37 +0300 Subject: [PATCH] Added initial ability to summarize text of messages. --- Telegram/Resources/langs/lang.strings | 3 + Telegram/SourceFiles/api/api_transcribes.cpp | 62 ++++++++++ Telegram/SourceFiles/api/api_transcribes.h | 14 +++ .../history/view/history_view_element.cpp | 15 +++ .../history/view/history_view_message.cpp | 116 +++++++++++++++--- .../history/view/history_view_message.h | 11 ++ .../history/view/history_view_reply.cpp | 85 +++++++++++++ .../history/view/history_view_reply.h | 10 ++ .../view/history_view_transcribe_button.cpp | 55 ++++++++- .../view/history_view_transcribe_button.h | 9 +- 10 files changed, 356 insertions(+), 24 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 8eaa4b143a..b893ac30c8 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7392,6 +7392,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_bank_card_copy" = "Copy Card Number"; "lng_context_bank_card_copied" = "Card number copied to clipboard."; +"lng_summarize_header_title" = "AI summary"; +"lng_summarize_header_about" = "Click to see original text"; + // Wnd specific "lng_wnd_choose_program_menu" = "Choose Default Program..."; diff --git a/Telegram/SourceFiles/api/api_transcribes.cpp b/Telegram/SourceFiles/api/api_transcribes.cpp index a5c6ab96fb..688b1625b4 100644 --- a/Telegram/SourceFiles/api/api_transcribes.cpp +++ b/Telegram/SourceFiles/api/api_transcribes.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_transcribes.h" #include "apiwrap.h" +#include "api/api_text_entities.h" #include "data/data_channel.h" #include "data/data_document.h" #include "data/data_peer.h" @@ -114,6 +115,18 @@ void Transcribes::toggle(not_null item) { } } +void Transcribes::toggleSummary(not_null item) { + const auto id = item->fullId(); + auto i = _summaries.find(id); + if (i == _summaries.end()) { + summarize(item); + _session->data().requestItemResize(item); + } else if (!i->second.loading) { + i->second.shown = !i->second.shown; + _session->data().requestItemResize(item); + } +} + const Transcribes::Entry &Transcribes::entry( not_null item) const { static auto empty = Entry(); @@ -121,6 +134,12 @@ const Transcribes::Entry &Transcribes::entry( return (i != _map.end()) ? i->second : empty; } +const Transcribes::SummaryEntry &Transcribes::summary( + not_null item) const { + const auto i = _summaries.find(item->fullId()); + return (i != _summaries.end()) ? i->second : SummaryEntry(); +} + void Transcribes::apply(const MTPDupdateTranscribedAudio &update) { const auto id = update.vtranscription_id().v; const auto i = _ids.find(id); @@ -208,4 +227,47 @@ void Transcribes::load(not_null item) { entry.pending = false; } +void Transcribes::summarize(not_null item) { + if (!item->isHistoryEntry() || item->isLocal()) { + return; + } + const auto id = item->fullId(); + const auto requestId = _api.request(MTPmessages_TranslateText( + MTP_flags(MTPmessages_TranslateText::Flag::f_peer + | MTPmessages_TranslateText::Flag::f_id), + item->history()->peer->input(), + MTP_vector(1, MTP_int(item->id)), + MTPVector(), + MTP_string("sum") + )).done([=](const MTPmessages_TranslatedText &result) { + const auto &list = result.data().vresult().v; + if (!list.isEmpty()) { + const auto &tl = list.front().data(); + auto &entry = _summaries[id]; + entry.requestId = 0; + entry.loading = false; + entry.result = TextWithEntities( + qs(tl.vtext()), + Api::EntitiesFromMTP(_session, tl.ventities().v)); + if (const auto item = _session->data().message(id)) { + _session->data().requestItemResize(item); + } + } + }).fail([=] { + auto &entry = _summaries[id]; + entry.requestId = 0; + entry.loading = false; + if (const auto item = _session->data().message(id)) { + _session->data().requestItemResize(item); + } + }).send(); + auto &entry = _summaries.emplace(id).first->second; + entry.requestId = requestId; + entry.shown = true; + entry.loading = true; + if (const auto item = _session->data().message(id)) { + _session->data().requestItemResize(item); + } +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_transcribes.h b/Telegram/SourceFiles/api/api_transcribes.h index 93b42401df..f77c34c72f 100644 --- a/Telegram/SourceFiles/api/api_transcribes.h +++ b/Telegram/SourceFiles/api/api_transcribes.h @@ -31,9 +31,20 @@ public: mtpRequestId requestId = 0; }; + struct SummaryEntry { + TextWithEntities result; + bool shown = false; + bool loading = false; + mtpRequestId requestId = 0; + }; + void toggle(not_null item); [[nodiscard]] const Entry &entry(not_null item) const; + void toggleSummary(not_null item); + [[nodiscard]] const SummaryEntry &summary( + not_null item) const; + void apply(const MTPDupdateTranscribedAudio &update); [[nodiscard]] bool freeFor(not_null item) const; @@ -47,6 +58,7 @@ public: private: void load(not_null item); + void summarize(not_null item); const not_null _session; MTP::Sender _api; @@ -58,6 +70,8 @@ private: base::flat_map _map; base::flat_map _ids; + base::flat_map _summaries; + }; } // namespace Api diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index b810bf57ae..b97650a7ed 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/history_view_element.h" +#include "apiwrap.h" +#include "api/api_transcribes.h" #include "history/view/history_view_service_message.h" #include "history/view/history_view_message.h" #include "history/view/media/history_view_media_generic.h" @@ -1668,6 +1670,19 @@ void Element::validateText() { return; } } + { + const auto &summary + = item->history()->session().api().transcribes().summary(item); + if (!summary.result.empty() && summary.shown) { + _textItem = item; + setTextWithLinks(summary.result); + return; + } + if (!summary.result.empty() && !summary.shown) { + _textItem = item; + setTextWithLinks(item->originalText()); + } + } // Albums may show text of a different item than the parent one. _textItem = _media ? _media->itemForText() : item.get(); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index c49ac38088..fda908fbb8 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/history_view_message.h" +#include "api/api_transcribes.h" #include "api/api_suggest_post.h" #include "base/qt/qt_key_modifiers.h" #include "base/unixtime.h" @@ -22,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/reactions/history_view_reactions_button.h" #include "history/view/history_view_group_call_bar.h" // UserpicInRow. #include "history/view/history_view_reply.h" +#include "history/view/history_view_transcribe_button.h" #include "history/view/history_view_view_button.h" // ViewButton. #include "history/history.h" #include "boxes/premium_preview_box.h" @@ -466,7 +468,11 @@ QSize Message::performCountOptimalSize() { const auto item = data(); const auto replyData = item->Get(); - if (replyData && !_hideReply) { + const auto &transcribes = item->history()->session().api().transcribes(); + const auto &summary = transcribes.summary(item); + const auto showSummaryReply = !summary.result.empty() && summary.shown; + + if ((replyData && !_hideReply) || showSummaryReply) { AddComponents(Reply::Bit()); } else { RemoveComponents(Reply::Bit()); @@ -510,6 +516,13 @@ QSize Message::performCountOptimalSize() { return embedReactionsInBubble() ? 0 : 1; }; const auto oldKey = reactionsKey(); + if (_summarize) { + const auto &summary + = history()->session().api().transcribes().summary(item); + if (_summarize->loading() != summary.loading) { + _summarize->setLoading(summary.loading, [this] { repaint(); }); + } + } validateText(); validateInlineKeyboard(markup); updateViewButtonExistence(); @@ -547,7 +560,11 @@ QSize Message::performCountOptimalSize() { const auto reply = Get(); if (reply) { - reply->update(this, replyData); + if (showSummaryReply && !replyData) { + reply->updateForSummary(this); + } else { + reply->update(this, replyData); + } } if (drawBubble()) { @@ -1194,24 +1211,46 @@ void Message::draw(Painter &p, const PaintContext &context) const { p.setOpacity(o); } } - if (const auto size = rightActionSize()) { - const auto fastShareSkip = std::clamp( - (g.height() - size->height()) / 2, - 0, - st::historyFastShareBottom); + ensureSummarizeButton(); + if (const auto size = rightActionSize(); size || _summarize) { + const auto rightActionWidth = size + ? size->width() + : _summarize->size().width(); + const auto fastShareSkip = size + ? std::clamp( + (g.height() - size->height()) / 2, + 0, + st::historyFastShareBottom) + : st::historyFastShareBottom; const auto fastShareLeft = hasRightLayout() - ? (g.left() - size->width() - st::historyFastShareLeft) + ? (g.left() - rightActionWidth - st::historyFastShareLeft) : (g.left() + g.width() + st::historyFastShareLeft); - const auto fastShareTop = data()->isSponsored() - ? g.top() + fastShareSkip - : g.top() + g.height() - fastShareSkip - size->height(); - const auto o = p.opacity(); - if (selectionModeResult.progress > 0) { - p.setOpacity(1. - selectionModeResult.progress); + const auto fastShareTop = g.top() + (data()->isSponsored() + ? fastShareSkip + : g.height() - fastShareSkip - (size ? size->height() : 0)); + if (size) { + const auto o = p.opacity(); + if (selectionModeResult.progress > 0) { + p.setOpacity(1. - selectionModeResult.progress); + } + drawRightAction( + p, + context, + fastShareLeft, + fastShareTop, + width()); + if (selectionModeResult.progress > 0) { + p.setOpacity(o); + } } - drawRightAction(p, context, fastShareLeft, fastShareTop, width()); - if (selectionModeResult.progress > 0) { - p.setOpacity(o); + if (_summarize) { + paintSummarize( + p, + fastShareLeft, + fastShareTop, + !context.outbg, + context, + g); } } @@ -1985,6 +2024,12 @@ void Message::clickHandlerPressedChanged( } else if (const auto reply = Get() ; reply && (handler == reply->link())) { toggleReplyRipple(pressed); + } else if (_summarize && (handler == _summarize->link())) { + if (pressed) { + _summarize->addRipple([=] { repaint(); }); + } else { + _summarize->stopRipple(); + } } } @@ -2477,6 +2522,9 @@ TextState Message::textState( - QPoint(fastShareLeft, fastShareTop)); } } + if (_summarize && _summarize->contains(point)) { + result.link = _summarize->link(); + } } else if (media && media->isDisplayed()) { result = media->textState(point - g.topLeft(), request); if (request.onlyMessageText) { @@ -4526,4 +4574,38 @@ const HistoryMessageEdited *Message::displayedEditBadge() const { return data()->Get(); } +void Message::ensureSummarizeButton() const { + const auto item = data(); + if (item->isPost()/* && item->history()->session().premium()*/) { + if (!_summarize) { + _summarize + = std::make_unique(item, false, true); + } + } else { + _summarize = nullptr; + } +} + +void Message::paintSummarize( + Painter &p, + int x, + int y, + bool right, + const PaintContext &context, + QRect g) const { + if (!_summarize) { + return; + } + const auto s = _summarize->size(); + const auto buttonY = y - s.height() - st::msgDateImgDelta; + if (buttonY < g.top()) { + return; + } + _summarize->paint( + p, + x - (right ? 0 : s.width()), + buttonY, + context); +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index 08d6be246f..5045b490ec 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -31,6 +31,7 @@ namespace HistoryView { class ViewButton; class WebPage; +class TranscribeButton; namespace Reactions { class InlineList; @@ -292,6 +293,15 @@ private: void refreshInfoSkipBlock(HistoryItem *textItem); [[nodiscard]] int monospaceMaxWidth() const; + void ensureSummarizeButton() const; + void paintSummarize( + Painter &p, + int x, + int y, + bool right, + const PaintContext &context, + QRect g) const; + void updateViewButtonExistence(); [[nodiscard]] int viewButtonHeight() const; @@ -311,6 +321,7 @@ private: mutable std::unique_ptr _viewButton; std::unique_ptr _topicButton; mutable std::unique_ptr _comments; + mutable std::unique_ptr _summarize; mutable Ui::Text::String _fromName; mutable std::unique_ptr _fromNameStatus; diff --git a/Telegram/SourceFiles/history/view/history_view_reply.cpp b/Telegram/SourceFiles/history/view/history_view_reply.cpp index f7af04bdf8..94161ba2fd 100644 --- a/Telegram/SourceFiles/history/view/history_view_reply.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reply.cpp @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/history_view_reply.h" +#include "apiwrap.h" +#include "api/api_transcribes.h" #include "core/click_handler_types.h" #include "core/ui_integration.h" #include "data/stickers/data_custom_emoji.h" @@ -411,6 +413,63 @@ void Reply::update( } } +void Reply::updateForSummary(not_null view) { + const auto item = view->data(); + + _externalSender = nullptr; + _colorPeer = nullptr; + _hiddenSenderColorIndexPlusOne = 1; + _hasPreview = 0; + _displaying = 1; + _multiline = 1; + _replyToStory = 0; + _hasQuoteIcon = 0; + _spoiler = nullptr; + + using namespace Ui; + _summarize = std::make_unique( + StarParticles(StarParticles::Type::Right, 15, st::lineWidth * 8)); + _summarize->particles.setSpeed(0.05); + + const auto repaint = [=] { item->customEmojiRepaint(); }; + auto helper = Ui::Text::CustomEmojiHelper(Core::TextContext({ + .session = &view->history()->session(), + .repaint = repaint, + })); + + _text.setMarkedText( + st::defaultTextStyle, + tr::lng_summarize_header_about(tr::now, tr::rich), + Ui::ItemTextDefaultOptions(), + helper.context()); + + _name.setMarkedText( + st::msgNameStyle, + tr::lng_summarize_header_title(tr::now, tr::rich), + Ui::NameTextOptions()); + _nameVersion = 0; + // updateName(view, data); + _nameVersion = 1; + _maxWidth = st::historyReplyPadding.left() + + 200 + + st::historyReplyPadding.right(); + _minHeight = st::historyReplyPadding.top() + + st::msgServiceNameFont->height * 2 + // + optimalTextSize.height() + + st::historyReplyPadding.bottom(); + + _link = std::make_shared([=](ClickContext context) { + const auto my = context.other.value(); + const auto controller = my.sessionWindow.get(); + if (!controller) { + return; + } + if (const auto i = controller->session().data().message(my.itemId)) { + controller->session().api().transcribes().toggleSummary(i); + } + }); +} + bool Reply::expand() { if (!_expandable || _expanded) { return false; @@ -818,6 +877,32 @@ void Reply::paint( cache->bg = rippleColor; } + if (_summarize) { + const auto size = QSize(w, _height); + if (_summarize->cachedSize != size) { + _summarize->path = QPainterPath(); + _summarize->path.addRoundedRect( + QRect(0, 0, w, _height), + quoteSt.radius, + quoteSt.radius); + _summarize->cachedSize = size; + } + p.translate(x, y); + p.setClipPath(_summarize->path); + const auto nameColor = !inBubble + ? st->msgImgReplyBarColor()->c + : (colorCollectible || colorIndexPlusOne) + ? FromNameFg( + context, + colorIndexPlusOne - 1, + colorCollectible) + : stm->msgServiceFg->c; + _summarize->particles.setColor(nameColor); + _summarize->particles.paint(p, QRect(0, 0, w, _height), context.now); + p.setClipping(false); + p.translate(-x, -y); + } + if (_ripple.animation) { _ripple.animation->paint(p, x, y, w, &rippleColor); if (_ripple.animation->empty()) { diff --git a/Telegram/SourceFiles/history/view/history_view_reply.h b/Telegram/SourceFiles/history/view/history_view_reply.h index 297e4cc9d1..5d8e734c79 100644 --- a/Telegram/SourceFiles/history/view/history_view_reply.h +++ b/Telegram/SourceFiles/history/view/history_view_reply.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "history/view/history_view_element.h" +#include "ui/effects/ministar_particles.h" namespace Data { class Session; @@ -71,6 +72,8 @@ public: not_null view, not_null data); + void updateForSummary(not_null view); + [[nodiscard]] bool isNameUpdated( not_null view, not_null data) const; @@ -113,6 +116,12 @@ public: const FullReplyTo &replyTo); private: + struct SummarizeAnimation { + Ui::StarParticles particles; + QPainterPath path; + QSize cachedSize; + }; + [[nodiscard]] Ui::Text::GeometryDescriptor textGeometry( int available, int firstLineSkip, @@ -136,6 +145,7 @@ private: ClickHandlerPtr _link; std::unique_ptr _spoiler; + std::unique_ptr _summarize; mutable PeerData *_externalSender = nullptr; mutable PeerData *_colorPeer = nullptr; mutable struct { diff --git a/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp b/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp index 1688b8f6c1..3d20f27b69 100644 --- a/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp +++ b/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp @@ -19,11 +19,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/chat_style.h" #include "ui/effects/radial_animation.h" #include "ui/effects/ripple_animation.h" +#include "ui/effects/ministar_particles.h" #include "ui/painter.h" #include "ui/rect.h" +#include "ui/ui_utility.h" #include "api/api_transcribes.h" #include "apiwrap.h" #include "styles/style_chat.h" +#include "styles/style_media_view.h" #include "window/window_session_controller.h" namespace HistoryView { @@ -51,10 +54,12 @@ void ClipPainterForLock(QPainter &p, bool roundview, const QRect &r) { TranscribeButton::TranscribeButton( not_null item, - bool roundview) + bool roundview, + bool summarize) : _item(item) , _roundview(roundview) -, _size(!roundview +, _summarize(summarize) +, _size(!roundview && !_summarize ? st::historyTranscribeSize : QSize(st::historyFastShareSize, st::historyFastShareSize)) { } @@ -80,6 +85,10 @@ void TranscribeButton::setLoading(bool loading, Fn update) { } } +bool TranscribeButton::loading() const { + return _loading; +} + void TranscribeButton::paint( QPainter &p, int x, @@ -88,7 +97,7 @@ void TranscribeButton::paint( auto hq = PainterHighQualityEnabler(p); const auto opened = _openedAnimation.value(_opened ? 1. : 0.); const auto stm = context.messageStyle(); - if (_roundview) { + if (_roundview || _summarize) { _lastPaintedPoint = { x, y }; const auto r = QRect(QPoint(x, y), size()); @@ -119,7 +128,34 @@ void TranscribeButton::paint( r.topLeft() + st::historyFastTranscribeLockPos, r.width()); } else { - context.st->historyFastTranscribeIcon().paintInCenter(p, r); + if (_summarize) { + if (!_particles) { + _particles = std::make_unique( + Ui::StarParticles::Type::RadialInside, + 7, + st::lineWidth * 4); + _particles->setColor(st::msgServiceFg->c); + _particles->setSpeed(0.2); + } + p.setClipRegion(QRegion(r, QRegion::Ellipse)); + _particles->paint(p, r, crl::now()); + p.setClipping(false); + const auto session = &_item->history()->session(); + Ui::PostponeCall(session, [=, itemId = _item->fullId()] { + if (const auto item = session->data().message(itemId)) { + session->data().requestItemRepaint(item); + } + }); + (_item->history()->session().api().transcribes().summary( + _item).shown + ? st::mediaviewFullScreenButton.icon + : st::mediaviewFullScreenOutIcon).paintInCenter( + p, + r, + st::msgServiceFg->c); + } else { + context.st->historyFastTranscribeIcon().paintInCenter(p, r); + } } const auto state = _animation @@ -259,13 +295,17 @@ ClickHandlerPtr TranscribeButton::link() { } const auto session = &_item->history()->session(); const auto id = _item->fullId(); + const auto summarize = _summarize; _link = std::make_shared([=](ClickContext context) { const auto item = session->data().message(id); if (!item) { return; } if (session->premium()) { - return session->api().transcribes().toggle(item); + auto &transcribes = session->api().transcribes(); + return summarize + ? transcribes.toggleSummary(item) + : transcribes.toggle(item); } const auto my = context.other.value(); if (hasLock()) { @@ -288,7 +328,10 @@ ClickHandlerPtr TranscribeButton::link() { } } } - session->api().transcribes().toggle(item); + auto &transcribes = session->api().transcribes(); + summarize + ? transcribes.toggleSummary(item) + : transcribes.toggle(item); } }); return _link; diff --git a/Telegram/SourceFiles/history/view/history_view_transcribe_button.h b/Telegram/SourceFiles/history/view/history_view_transcribe_button.h index db318eb5a2..8373e6e278 100644 --- a/Telegram/SourceFiles/history/view/history_view_transcribe_button.h +++ b/Telegram/SourceFiles/history/view/history_view_transcribe_button.h @@ -13,6 +13,7 @@ namespace Ui { struct ChatPaintContext; class InfiniteRadialAnimation; class RippleAnimation; +class StarParticles; } // namespace Ui namespace HistoryView { @@ -21,13 +22,17 @@ using PaintContext = Ui::ChatPaintContext; class TranscribeButton final { public: - explicit TranscribeButton(not_null item, bool roundview); + explicit TranscribeButton( + not_null item, + bool roundview, + bool summarize = false); ~TranscribeButton(); [[nodiscard]] QSize size() const; void setOpened(bool opened, Fn update); void setLoading(bool loading, Fn update); + [[nodiscard]] bool loading() const; void paint(QPainter &p, int x, int y, const PaintContext &context); void addRipple(Fn callback); void stopRipple() const; @@ -40,9 +45,11 @@ private: const not_null _item; const bool _roundview = false; + const bool _summarize = false; const QSize _size; mutable std::unique_ptr _animation; + mutable std::unique_ptr _particles; std::unique_ptr _ripple; ClickHandlerPtr _link; QString _text;