From 04ccdc4b30f39541fab749831bbd95aa627401da Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sun, 1 Mar 2026 08:52:54 +0300 Subject: [PATCH 001/415] Added initial support of system accent color. --- Telegram/Resources/langs/lang.strings | 1 + Telegram/SourceFiles/core/core_settings.cpp | 12 ++- Telegram/SourceFiles/core/core_settings.h | 10 +++ .../settings/sections/settings_chat.cpp | 89 +++++++++++++++++-- .../window/themes/window_theme.cpp | 22 +++++ .../window/themes/window_themes_embedded.cpp | 21 ++++- .../window/themes/window_themes_embedded.h | 1 + 7 files changed, 148 insertions(+), 8 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index b643c1e348..ee568a35ef 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -829,6 +829,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_theme_tinted" = "Tinted"; "lng_settings_theme_night" = "Night"; "lng_settings_theme_accent_title" = "Choose accent color"; +"lng_settings_theme_system_accent_color" = "System accent color"; "lng_settings_data_storage" = "Data and storage"; "lng_settings_information" = "Edit profile"; "lng_settings_my_account" = "My Account"; diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index 2c94567b59..6e4b91d035 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -246,7 +246,7 @@ QByteArray Settings::serialize() const { + sizeof(ushort) + sizeof(qint32) // _notificationsDisplayChecksum + Serialize::bytearraySize(callPanelPosition) - + sizeof(qint32); + + sizeof(qint32) * 2; // _cornerReply + _systemAccentColorEnabled auto result = QByteArray(); result.reserve(size); @@ -412,7 +412,8 @@ QByteArray Settings::serialize() const { << _notificationsVolume << _notificationsDisplayChecksum << callPanelPosition - << qint32(_cornerReply.current() ? 1 : 0); + << qint32(_cornerReply.current() ? 1 : 0) + << qint32(_systemAccentColorEnabled ? 1 : 0); } Ensures(result.size() == size); @@ -546,6 +547,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) { quint32 chatFiltersHorizontal = _chatFiltersHorizontal.current() ? 1 : 0; quint32 quickDialogAction = quint32(_quickDialogAction); ushort notificationsVolume = _notificationsVolume; + qint32 systemAccentColorEnabled = _systemAccentColorEnabled + ? 1 + : 0; stream >> themesAccentColors; if (!stream.atEnd()) { @@ -889,6 +893,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) { if (!stream.atEnd()) { stream >> cornerReply; } + if (!stream.atEnd()) { + stream >> systemAccentColorEnabled; + } if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for Core::Settings::constructFromSerialized()")); @@ -929,6 +936,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { case ScreenCorner::BottomLeft: _notificationsCorner = uncheckedNotificationsCorner; break; } _notificationsDisplayChecksum = notificationsDisplayChecksum; + _systemAccentColorEnabled = (systemAccentColorEnabled == 1); _includeMutedCounter = (includeMutedCounter == 1); _includeMutedCounterFolders = (includeMutedCounterFolders == 1); _countUnreadMessages = (countUnreadMessages == 1); diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index b6fc5e01f5..9525364652 100644 --- a/Telegram/SourceFiles/core/core_settings.h +++ b/Telegram/SourceFiles/core/core_settings.h @@ -390,6 +390,9 @@ public: [[nodiscard]] Window::Theme::AccentColors &themesAccentColors() { return _themesAccentColors; } + [[nodiscard]] const Window::Theme::AccentColors &themesAccentColors() const { + return _themesAccentColors; + } void setThemesAccentColors(Window::Theme::AccentColors &&colors) { _themesAccentColors = std::move(colors); } @@ -703,6 +706,12 @@ public: [[nodiscard]] rpl::producer systemDarkModeEnabledChanges() const { return _systemDarkModeEnabled.changes(); } + void setSystemAccentColorEnabled(bool value) { + _systemAccentColorEnabled = value; + } + [[nodiscard]] bool systemAccentColorEnabled() const { + return _systemAccentColorEnabled; + } [[nodiscard]] WindowTitleContent windowTitleContent() const { return _windowTitleContent.current(); } @@ -1068,6 +1077,7 @@ private: rpl::variable _nativeWindowFrame = false; rpl::variable> _systemDarkMode = std::nullopt; rpl::variable _systemDarkModeEnabled = true; + bool _systemAccentColorEnabled = false; rpl::variable _windowTitleContent; WindowPosition _windowPosition; // per-window bool _disableOpenGL = false; diff --git a/Telegram/SourceFiles/settings/sections/settings_chat.cpp b/Telegram/SourceFiles/settings/sections/settings_chat.cpp index 812bb4aa0b..2cac29431a 100644 --- a/Telegram/SourceFiles/settings/sections/settings_chat.cpp +++ b/Telegram/SourceFiles/settings/sections/settings_chat.cpp @@ -98,6 +98,14 @@ using namespace Builder; const auto kSchemesList = Window::Theme::EmbeddedThemes(); constexpr auto kCustomColorButtonParts = 7; +[[nodiscard]] bool IsSystemAccentColorSupported() { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + return true; +#else + return !Platform::IsWindows() || !Platform::IsWindows8OrGreater(); +#endif +} + class ColorsPalette final { public: using Type = Window::Theme::EmbeddedType; @@ -277,8 +285,12 @@ void ColorsPalette::show(Type type) { return; } list.insert(list.begin(), scheme->accentColor); - const auto color = Core::App().settings().themesAccentColors().get(type); - const auto current = color.value_or(scheme->accentColor); + const auto &settings = Core::App().settings(); + const auto color = settings.themesAccentColors().get(type); + const auto current = (settings.systemAccentColorEnabled() + ? Window::Theme::SystemAccentColor() + : std::optional()).value_or( + color.value_or(scheme->accentColor)); const auto i = ranges::find(list, current); if (i == end(list)) { list.back() = current; @@ -804,6 +816,22 @@ void BuildThemeOptionsSection(SectionBuilder &builder) { .keywords = { u"accent"_q, u"color"_q, u"customize"_q }, }; }); + + if (IsSystemAccentColorSupported()) { + builder.add(nullptr, [] { + return SearchEntry{ + .id = u"chat/themes-system-accent"_q, + .title = tr::lng_settings_theme_system_accent_color(tr::now), + .keywords = { + u"system"_q, + u"accent"_q, + u"color"_q, + u"theme"_q, + u"os"_q, + }, + }; + }); + } } void BuildThemeSettingsSection(SectionBuilder &builder) { @@ -2320,6 +2348,16 @@ void SetupDefaultThemes( const auto palette = Ui::CreateChild( container.get(), container.get()); + const auto systemAccentWrap = container->add( + object_ptr>( + container, + object_ptr( + container, + tr::lng_settings_theme_system_accent_color(tr::now), + Core::App().settings().systemAccentColorEnabled(), + st::settingsCheckbox)), + st::settingsCheckboxPadding); + systemAccentWrap->setDuration(0); const auto chosen = [] { const auto &object = Background()->themeObject(); @@ -2395,14 +2433,17 @@ void SetupDefaultThemes( palette->show(type); } - const auto &colors = Core::App().settings().themesAccentColors(); + const auto &settings = Core::App().settings(); const auto i = checks.find(type); const auto scheme = ranges::find(kSchemesList, type, &Scheme::type); if (scheme == end(kSchemesList)) { return; } + const auto color = settings.systemAccentColorEnabled() + ? Window::Theme::SystemAccentColor() + : settings.themesAccentColors().get(type); if (i != end(checks)) { - if (const auto color = colors.get(type)) { + if (color) { const auto colorizer = ColorizerFrom(*scheme, *color); i->second->setColors(ColorsFromScheme(*scheme, colorizer)); } else { @@ -2410,6 +2451,11 @@ void SetupDefaultThemes( } } }; + const auto refreshSystemAccentVisibility = [=](Type type) { + systemAccentWrap->toggle( + IsSystemAccentColorSupported() && (type != Type(-1)), + anim::type::instant); + }; group->setChangedCallback([=](Type type) { const auto scheme = ranges::find( kSchemesList, @@ -2424,6 +2470,22 @@ void SetupDefaultThemes( for (const auto &scheme : kSchemesList) { refreshColorizer(scheme.type); } + refreshSystemAccentVisibility(chosen()); + systemAccentWrap->entity()->checkedChanges( + ) | rpl::on_next([=](bool checked) { + auto &settings = Core::App().settings(); + if (settings.systemAccentColorEnabled() == checked) { + return; + } + settings.setSystemAccentColorEnabled(checked); + Local::writeSettings(); + + const auto type = chosen(); + const auto scheme = ranges::find(kSchemesList, type, &Scheme::type); + if (scheme != end(kSchemesList)) { + apply(*scheme); + } + }, container->lifetime()); if (highlights) { const auto add = st::roundRadiusSmall; @@ -2434,6 +2496,12 @@ void SetupDefaultThemes( .shape = HighlightShape::Ellipse, }, } }); + if (IsSystemAccentColorSupported()) { + highlights->push_back({ u"chat/themes-system-accent"_q, { + systemAccentWrap->entity(), + { .radius = st::boxRadius } + } }); + } } Background()->updates( @@ -2443,6 +2511,7 @@ void SetupDefaultThemes( return chosen(); }) | rpl::on_next([=](Type type) { refreshColorizer(type); + refreshSystemAccentVisibility(type); group->setValue(type); }, container->lifetime()); @@ -2492,9 +2561,19 @@ void SetupDefaultThemes( if (scheme == end(kSchemesList)) { return; } - auto &colors = Core::App().settings().themesAccentColors(); + auto &settings = Core::App().settings(); + auto changed = false; + if (settings.systemAccentColorEnabled()) { + settings.setSystemAccentColorEnabled(false); + systemAccentWrap->entity()->setChecked(false); + changed = true; + } + auto &colors = settings.themesAccentColors(); if (colors.get(type) != color) { colors.set(type, color); + changed = true; + } + if (changed) { Local::writeSettings(); } apply(*scheme); diff --git a/Telegram/SourceFiles/window/themes/window_theme.cpp b/Telegram/SourceFiles/window/themes/window_theme.cpp index f51fd766a8..f6d7d8ace0 100644 --- a/Telegram/SourceFiles/window/themes/window_theme.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme.cpp @@ -566,6 +566,28 @@ void ChatBackground::start() { ) | rpl::on_next([](bool dark) { Core::App().settings().setSystemDarkMode(dark); }, _lifetime); + + rpl::single( + QGuiApplication::palette() + ) | rpl::then( + base::qt_signal_producer( + qApp, + &QGuiApplication::paletteChanged + ) + ) | rpl::on_next([=] { + const auto &settings = Core::App().settings(); + if (!settings.systemAccentColorEnabled() + || _themeObject.cloud.id + || editingTheme()) { + return; + } + const auto path = _themeObject.pathAbsolute; + if (!IsEmbeddedTheme(path)) { + return; + } + ApplyDefaultWithPath(path); + KeepApplied(); + }, _lifetime); } void ChatBackground::refreshThemeWatcher() { diff --git a/Telegram/SourceFiles/window/themes/window_themes_embedded.cpp b/Telegram/SourceFiles/window/themes/window_themes_embedded.cpp index 541534662d..58ebea3240 100644 --- a/Telegram/SourceFiles/window/themes/window_themes_embedded.cpp +++ b/Telegram/SourceFiles/window/themes/window_themes_embedded.cpp @@ -14,6 +14,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/core_settings.h" #include "ui/style/style_palette_colorizer.h" +#include +#include + namespace Window { namespace Theme { namespace { @@ -175,6 +178,16 @@ style::colorizer ColorizerFrom( return result; } +std::optional SystemAccentColor() { +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + constexpr auto kAccentRole = QPalette::ColorRole::Accent; +#else + constexpr auto kAccentRole = QPalette::ColorRole::Highlight; +#endif + const auto accent = QGuiApplication::palette().color(kAccentRole); + return accent.isValid() ? std::make_optional(accent) : std::nullopt; +} + style::colorizer ColorizerForTheme(const QString &absolutePath) { if (!IsEmbeddedTheme(absolutePath)) { return {}; @@ -187,7 +200,13 @@ style::colorizer ColorizerForTheme(const QString &absolutePath) { if (i == end(schemes)) { return {}; } - const auto &colors = Core::App().settings().themesAccentColors(); + const auto &settings = Core::App().settings(); + if (settings.systemAccentColorEnabled()) { + if (const auto accent = SystemAccentColor()) { + return ColorizerFrom(*i, *accent); + } + } + const auto &colors = settings.themesAccentColors(); if (const auto accent = colors.get(i->type)) { return ColorizerFrom(*i, *accent); } diff --git a/Telegram/SourceFiles/window/themes/window_themes_embedded.h b/Telegram/SourceFiles/window/themes/window_themes_embedded.h index cbef706e09..fd0a9ba6f6 100644 --- a/Telegram/SourceFiles/window/themes/window_themes_embedded.h +++ b/Telegram/SourceFiles/window/themes/window_themes_embedded.h @@ -50,6 +50,7 @@ private: [[nodiscard]] style::colorizer ColorizerFrom( const EmbeddedScheme &scheme, const QColor &color); +[[nodiscard]] std::optional SystemAccentColor(); [[nodiscard]] style::colorizer ColorizerForTheme(const QString &absolutePath); void Colorize( From 47beb2e7e1eea4095d9544353c7dc4cb49b2aa4a Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 27 Feb 2026 20:57:48 +0300 Subject: [PATCH 002/415] Added initial support of simple captions to attach prepared files. --- Telegram/SourceFiles/boxes/send_files_box.cpp | 25 ++++- Telegram/SourceFiles/boxes/send_files_box.h | 8 ++ .../attach_abstract_single_file_preview.cpp | 100 ++++++++++++++++-- .../attach_abstract_single_file_preview.h | 16 ++- .../ui/chat/attach/attach_album_preview.cpp | 58 ++++++++-- .../ui/chat/attach/attach_album_preview.h | 5 + .../ui/chat/attach/attach_album_thumbnail.cpp | 82 +++++++++++++- .../ui/chat/attach/attach_album_thumbnail.h | 8 ++ .../attach_item_single_file_preview.cpp | 4 +- .../ui/chat/attach/attach_prepare.h | 1 + .../attach/attach_single_file_preview.cpp | 28 ++++- .../chat/attach/attach_single_file_preview.h | 7 ++ Telegram/SourceFiles/ui/chat/chat.style | 2 + 13 files changed, 310 insertions(+), 34 deletions(-) diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index e2aafdc72d..b658e8fd89 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -290,6 +290,7 @@ SendFilesBox::Block::Block( not_null*> items, int from, int till, + const Ui::Text::MarkedContext &captionContext, Fn gifPaused, SendFilesWay way, Fn actionAllowed) @@ -309,6 +310,7 @@ SendFilesBox::Block::Block( parent.get(), st, my, + captionContext, way, [=](int index, Ui::AttachActionType type) { return actionAllowed((*_items)[from + index], type); @@ -332,7 +334,8 @@ SendFilesBox::Block::Block( _preview.reset(Ui::CreateChild( parent.get(), st, - first)); + first, + captionContext)); } } _preview->show(); @@ -530,6 +533,22 @@ bool SendFilesBox::Block::setSingleFileDisplayName( return true; } +bool SendFilesBox::Block::setSingleFileCaption( + int index, + const TextWithTags &caption) { + if (_isSingleMedia || index < _from || index >= _till) { + return false; + } + if (_isAlbum) { + const auto album = static_cast(_preview.get()); + album->setCaption(index - _from, caption); + return true; + } + const auto single = static_cast(_preview.get()); + single->setCaption(caption); + return true; +} + SendFilesBox::SendFilesBox( QWidget*, not_null controller, @@ -1138,12 +1157,16 @@ void SendFilesBox::pushBlock(int from, int till) { const auto gifPaused = [show = _show] { return show->paused(Window::GifPauseReason::Layer); }; + const auto captionContext = Core::TextContext({ + .session = &_show->session(), + }); _blocks.emplace_back( _inner.data(), _st, &_list.files, from, till, + captionContext, gifPaused, _sendWay.current(), [=](const Ui::PreparedFile &file, Ui::AttachActionType type) { diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index 5582f6ea2c..18d1fec84f 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -16,6 +16,10 @@ namespace style { struct ComposeControls; } // namespace style +namespace Ui::Text { +struct MarkedContext; +} // namespace Ui::Text + namespace Window { class SessionController; } // namespace Window @@ -153,6 +157,7 @@ private: not_null*> items, int from, int till, + const Ui::Text::MarkedContext &captionContext, Fn gifPaused, Ui::SendFilesWay way, Fn _preview; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp index 5ad8e15d18..862b6a4f4f 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp @@ -22,12 +22,26 @@ namespace Ui { AbstractSingleFilePreview::AbstractSingleFilePreview( QWidget *parent, const style::ComposeControls &st, - AttachControls::Type type) + AttachControls::Type type, + const Text::MarkedContext &captionContext) : AbstractSinglePreview(parent) , _st(st) , _type(type) +, _captionContext(captionContext) , _editMedia(this, _st.files.buttonFile) , _deleteMedia(this, _st.files.buttonFile) { + const auto repaint = _captionContext.repaint; + _captionContext.repaint = [=] { + if (repaint) { + repaint(); + } + const auto rect = captionRect(); + if (rect.isEmpty()) { + update(); + } else { + update(rect); + } + }; _editMedia->setIconOverride(&_st.files.buttonFileEdit); _deleteMedia->setIconOverride(&_st.files.buttonFileDelete); @@ -69,9 +83,25 @@ rpl::producer<> AbstractSingleFilePreview::clearCoverRequests() const { } void AbstractSingleFilePreview::setDisplayName(const QString &displayName) { - auto data = _data; - data.name = displayName; - setData(data); + _data.name = displayName; + updateTextWidthFor(_data); + updateDataGeometry(); + update(); +} + +void AbstractSingleFilePreview::setCaption(const TextWithTags &caption) { + auto marked = TextWithEntities{ + caption.text, + TextUtilities::ConvertTextTagsToEntities(caption.tags), + }; + marked = TextUtilities::SingleLine(marked); + _data.caption.setMarkedText( + st::defaultTextStyle, + marked, + kMarkupTextOptions, + _captionContext); + updateTextWidthFor(_data); + updateDataGeometry(); update(); } @@ -151,6 +181,23 @@ void AbstractSingleFilePreview::paintEvent(QPaintEvent *e) { width(), _data.statusText, _data.statusWidth); + if (!_data.caption.isEmpty()) { + p.setPen(_st.files.nameFg); + const auto captionTop = y + + st.thumbSize + + st::attachPreviewCaptionTopOffset; + _data.caption.draw(p, { + .position = { + x, + captionTop, + }, + .outerWidth = width(), + .availableWidth = _data.captionAvailableWidth, + .align = style::al_left, + .elisionLines = 1, + .elisionBreakEverywhere = true, + }); + } } void AbstractSingleFilePreview::resizeEvent(QResizeEvent *e) { @@ -167,7 +214,7 @@ void AbstractSingleFilePreview::resizeEvent(QResizeEvent *e) { _editMedia->moveToRight(right, top); } -bool AbstractSingleFilePreview::isThumbedLayout(Data &data) const { +bool AbstractSingleFilePreview::isThumbedLayout(const Data &data) const { return (!data.fileThumb.isNull() && !data.fileIsAudio); } @@ -187,6 +234,10 @@ void AbstractSingleFilePreview::updateTextWidthFor(Data &data) { - _st.files.buttonFile.width * buttonsCount - st::sendBoxAlbumGroupEditInternalSkip * buttonsCount - st::sendBoxAlbumGroupSkipRight; + const auto availableCaptionWidth = st::sendMediaPreviewSize + - _st.files.buttonFile.width * buttonsCount + - st::sendBoxAlbumGroupEditInternalSkip * buttonsCount + - st::sendBoxAlbumGroupSkipRight; data.nameWidth = st::semiboldFont->width(data.name); if (data.nameWidth > availableFileWidth) { data.name = st::semiboldFont->elided( @@ -196,17 +247,44 @@ void AbstractSingleFilePreview::updateTextWidthFor(Data &data) { data.nameWidth = st::semiboldFont->width(data.name); } data.statusWidth = st::normalFont->width(data.statusText); + data.captionAvailableWidth = availableCaptionWidth; } -void AbstractSingleFilePreview::setData(const Data &data) { - _data = data; - - updateTextWidthFor(_data); - +void AbstractSingleFilePreview::updateDataGeometry() { const auto &st = !isThumbedLayout(_data) ? st::attachPreviewLayout : st::attachPreviewThumbLayout; - resize(width(), st.thumbSize); + const auto height = st.thumbSize + (_data.caption.isEmpty() + ? 0 + : (st::attachPreviewCaptionTopOffset + _data.caption.lineHeight())); + resize(width(), height); +} + +QRect AbstractSingleFilePreview::captionRect() const { + if (_data.caption.isEmpty()) { + return {}; + } + const auto w = width() + - st::boxPhotoPadding.left() + - st::boxPhotoPadding.right(); + const auto &st = !isThumbedLayout(_data) + ? st::attachPreviewLayout + : st::attachPreviewThumbLayout; + const auto x = (width() - w) / 2; + const auto captionLineHeight = _data.caption.lineHeight(); + const auto top = st.thumbSize + + st::attachPreviewCaptionTopOffset; + return QRect( + x, + top, + _data.captionAvailableWidth, + captionLineHeight) + st::attachPreviewCaptionRepaintMargin; +} + +void AbstractSingleFilePreview::setData(Data data) { + _data = std::move(data); + updateTextWidthFor(_data); + updateDataGeometry(); } } // namespace Ui diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.h b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.h index 5fd830725a..aaae3e44a7 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.h @@ -24,7 +24,8 @@ public: AbstractSingleFilePreview( QWidget *parent, const style::ComposeControls &st, - AttachControls::Type type); + AttachControls::Type type, + const Text::MarkedContext &captionContext); ~AbstractSingleFilePreview(); [[nodiscard]] rpl::producer<> deleteRequests() const override; @@ -33,31 +34,40 @@ public: [[nodiscard]] rpl::producer<> editCoverRequests() const override; [[nodiscard]] rpl::producer<> clearCoverRequests() const override; virtual void setDisplayName(const QString &displayName); + virtual void setCaption(const TextWithTags &caption); protected: struct Data { QPixmap fileThumb; QString name; QString statusText; + Text::String caption; int nameWidth = 0; int statusWidth = 0; + int captionAvailableWidth = 0; bool fileIsAudio = false; bool fileIsImage = false; }; void prepareThumbFor(Data &data, const QImage &preview); - bool isThumbedLayout(Data &data) const; + bool isThumbedLayout(const Data &data) const; + [[nodiscard]] const Text::MarkedContext &captionContext() const { + return _captionContext; + } - void setData(const Data &data); + void setData(Data data); private: void paintEvent(QPaintEvent *e) override; void resizeEvent(QResizeEvent *e) override; void updateTextWidthFor(Data &data); + void updateDataGeometry(); + [[nodiscard]] QRect captionRect() const; const style::ComposeControls &_st; const AttachControls::Type _type; + Text::MarkedContext _captionContext; Data _data; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp index 28431418ad..176e30556a 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp @@ -37,10 +37,12 @@ AlbumPreview::AlbumPreview( QWidget *parent, const style::ComposeControls &st, gsl::span items, + const Text::MarkedContext &captionContext, SendFilesWay way, Fn actionAllowed) : RpWidget(parent) , _st(st) +, _captionContext(captionContext) , _sendWay(way) , _actionAllowed(std::move(actionAllowed)) , _dragTimer([=] { switchToDrag(); }) { @@ -62,6 +64,37 @@ void AlbumPreview::setSendWay(SendFilesWay way) { update(); } +void AlbumPreview::setCaption(int index, const TextWithTags &caption) { + if (index < 0 || index >= _thumbs.size()) { + return; + } + const auto realIndex = _order[index]; + const auto oldHeight = _thumbs[realIndex]->fileHeight(); + _thumbs[realIndex]->setCaption(caption); + const auto newHeight = _thumbs[realIndex]->fileHeight(); + if (oldHeight == newHeight) { + return; + } + const auto firstFileHeight = _thumbs.front()->fileHeight(); + _hasMixedFileHeights = ranges::any_of( + _thumbs, + [=](const auto &thumb) { + return thumb->fileHeight() != firstFileHeight; + }); + _filesHeight = ranges::accumulate(ranges::views::all( + _thumbs + ) | ranges::views::transform([](const auto &thumb) { + return thumb->fileHeight(); + }), 0) + (int(_thumbs.size()) - 1) * st::sendMediaRowSkip; + updateSize(); + updateFileRows(); +} + +int AlbumPreview::indexFromPoint(QPoint position) const { + const auto thumb = findThumb(position); + return thumb ? orderIndex(thumb) : -1; +} + void AlbumPreview::updateFileRows() { Expects(_order.size() == _thumbs.size()); @@ -148,9 +181,11 @@ void AlbumPreview::prepareThumbs(gsl::span items) { _thumbs.push_back(std::make_unique( _st, items[i], + _captionContext, layout[i], this, [=] { update(); }, + [=](QRect rect) { update(rect); }, [=] { changeThumbByIndex(orderIndex(thumbUnderCursor())); }, [=] { deleteThumbByIndex(orderIndex(thumbUnderCursor())); })); if (_thumbs.back()->isCompressedSticker()) { @@ -164,16 +199,17 @@ void AlbumPreview::prepareThumbs(gsl::span items) { return thumb->photoHeight(); }), 0) + (count - 1) * st::sendMediaRowSkip; - if (!_hasMixedFileHeights) { - _filesHeight = count * _thumbs.front()->fileHeight() - + (count - 1) * st::sendMediaRowSkip; - } else { - _filesHeight = ranges::accumulate(ranges::views::all( - _thumbs - ) | ranges::views::transform([](const auto &thumb) { - return thumb->fileHeight(); - }), 0) + (count - 1) * st::sendMediaRowSkip; - } + const auto firstFileHeight = _thumbs.front()->fileHeight(); + _hasMixedFileHeights = _hasMixedFileHeights || ranges::any_of( + _thumbs, + [=](const auto &thumb) { + return thumb->fileHeight() != firstFileHeight; + }); + _filesHeight = ranges::accumulate(ranges::views::all( + _thumbs + ) | ranges::views::transform([](const auto &thumb) { + return thumb->fileHeight(); + }), 0) + (count - 1) * st::sendMediaRowSkip; } int AlbumPreview::contentLeft() const { @@ -375,7 +411,7 @@ void AlbumPreview::paintFiles(Painter &p, QRect clip) const { const auto left = (st::boxWideWidth - st::sendMediaPreviewSize) / 2; const auto outerWidth = width(); if (!_hasMixedFileHeights) { - const auto fileHeight = st::attachPreviewThumbLayout.thumbSize + const auto fileHeight = _thumbs.front()->fileHeight() + st::sendMediaRowSkip; const auto bottom = clip.y() + clip.height(); const auto from = std::clamp( diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h index 9a740eb5bf..a7b26ad73c 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/rp_widget.h" #include "ui/chat/attach/attach_send_files_way.h" +#include "ui/text/text.h" #include "base/timer.h" namespace style { @@ -28,11 +29,14 @@ public: QWidget *parent, const style::ComposeControls &st, gsl::span items, + const Text::MarkedContext &captionContext, SendFilesWay way, Fn actionAllowed); ~AlbumPreview(); void setSendWay(SendFilesWay way); + void setCaption(int index, const TextWithTags &caption); + [[nodiscard]] int indexFromPoint(QPoint position) const; [[nodiscard]] base::flat_set collectSpoileredIndices(); [[nodiscard]] bool canHaveSpoiler(int index) const; @@ -103,6 +107,7 @@ private: void showContextMenu(not_null thumb, QPoint position); const style::ComposeControls &_st; + const Text::MarkedContext _captionContext; SendFilesWay _sendWay; Fn _actionAllowed; style::cursor _cursor = style::cur_default; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp index 562ae2d442..df101b8cc1 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/attach/attach_prepare.h" #include "ui/image/image_prepare.h" #include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" #include "ui/effects/spoiler_mess.h" #include "ui/ui_utility.h" @@ -28,9 +29,11 @@ namespace Ui { AlbumThumbnail::AlbumThumbnail( const style::ComposeControls &st, const PreparedFile &file, + const Text::MarkedContext &captionContext, const GroupMediaLayout &layout, QWidget *parent, Fn repaint, + Fn repaintRect, Fn editCallback, Fn deleteCallback) : _st(st) @@ -40,7 +43,8 @@ AlbumThumbnail::AlbumThumbnail( , _isPhoto(file.type == PreparedFile::Type::Photo) , _isVideo(file.type == PreparedFile::Type::Video) , _isCompressedSticker(Core::IsMimeSticker(file.information->filemime)) -, _repaint(std::move(repaint)) { +, _repaint(std::move(repaint)) +, _repaintRect(std::move(repaintRect)) { Expects(!_fullPreview.isNull()); moveToLayout(layout); @@ -82,13 +86,22 @@ AlbumThumbnail::AlbumThumbnail( - st::sendBoxAlbumGroupButtonFile.width * 2 - st::sendBoxAlbumGroupEditInternalSkip * 2 - st::sendBoxAlbumGroupSkipRight; + const auto availableCaptionWidth = st::sendMediaPreviewSize + - st::sendBoxAlbumGroupButtonFile.width * 2 + - st::sendBoxAlbumGroupEditInternalSkip * 2 + - st::sendBoxAlbumGroupSkipRight; + _captionAvailableWidth = availableCaptionWidth; const auto filepath = file.path; if (filepath.isEmpty()) { - _name = "image.png"; + _name = file.displayName.isEmpty() + ? "image.png" + : file.displayName; _status = FormatImageSizeText(file.originalDimensions); } else { auto fileinfo = QFileInfo(filepath); - _name = fileinfo.fileName(); + _name = file.displayName.isEmpty() + ? fileinfo.fileName() + : file.displayName; _status = FormatSizeText(fileinfo.size()); } _nameWidth = st::semiboldFont->width(_name); @@ -100,6 +113,29 @@ AlbumThumbnail::AlbumThumbnail( _nameWidth = st::semiboldFont->width(_name); } _statusWidth = st::normalFont->width(_status); + auto caption = TextWithEntities{ + file.caption.text, + TextUtilities::ConvertTextTagsToEntities(file.caption.tags), + }; + caption = TextUtilities::SingleLine(caption); + auto context = captionContext; + const auto repaintCaption = context.repaint; + context.repaint = [=] { + if (repaintCaption) { + repaintCaption(); + } + if (!_lastRectOfCaption.isEmpty() && _repaintRect) { + _repaintRect(_lastRectOfCaption); + } else { + _repaint(); + } + }; + _captionContext = context; + _caption.setMarkedText( + st::defaultTextStyle, + caption, + kMarkupTextOptions, + _captionContext); _editMedia.create(parent, _st.files.buttonFile); _deleteMedia.create(parent, _st.files.buttonFile); @@ -126,6 +162,20 @@ void AlbumThumbnail::setSpoiler(bool spoiler) { _repaint(); } +void AlbumThumbnail::setCaption(const TextWithTags &caption) { + auto marked = TextWithEntities{ + caption.text, + TextUtilities::ConvertTextTagsToEntities(caption.tags), + }; + marked = TextUtilities::SingleLine(marked); + _caption.setMarkedText( + st::defaultTextStyle, + marked, + kMarkupTextOptions, + _captionContext); + _repaint(); +} + bool AlbumThumbnail::hasSpoiler() const { return _spoiler != nullptr; } @@ -186,7 +236,9 @@ int AlbumThumbnail::photoHeight() const { int AlbumThumbnail::fileHeight() const { return _isCompressedSticker ? photoHeight() - : st::attachPreviewThumbLayout.thumbSize; + : st::attachPreviewThumbLayout.thumbSize + (_caption.isEmpty() + ? 0 + : (st::attachPreviewCaptionTopOffset + _caption.lineHeight())); } bool AlbumThumbnail::isCompressedSticker() const { @@ -497,6 +549,28 @@ void AlbumThumbnail::paintFile( outerWidth, _status, _statusWidth); + if (!_caption.isEmpty()) { + p.setPen(_st.files.nameFg); + const auto captionLineHeight = _caption.lineHeight(); + const auto captionTop = top + + st.thumbSize + + st::attachPreviewCaptionTopOffset; + _lastRectOfCaption = QRect( + left, + captionTop, + _captionAvailableWidth, + captionLineHeight) + st::attachPreviewCaptionRepaintMargin; + _caption.draw(p, { + .position = { left, captionTop }, + .outerWidth = outerWidth, + .availableWidth = _captionAvailableWidth, + .align = style::al_left, + .elisionLines = 1, + .elisionBreakEverywhere = true, + }); + } else { + _lastRectOfCaption = {}; + } _lastRectOfModify = QRect( QPoint(left, top), diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h b/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h index 209f864cde..d10b77723c 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h @@ -29,9 +29,11 @@ public: AlbumThumbnail( const style::ComposeControls &st, const PreparedFile &file, + const Text::MarkedContext &captionContext, const GroupMediaLayout &layout, QWidget *parent, Fn repaint, + Fn repaintRect, Fn editCallback, Fn deleteCallback); @@ -40,6 +42,7 @@ public: void resetLayoutAnimation(); void setSpoiler(bool spoiler); + void setCaption(const TextWithTags &caption); [[nodiscard]] bool hasSpoiler() const; [[nodiscard]] int photoHeight() const; @@ -101,8 +104,11 @@ private: QPixmap _fileThumb; QString _name; QString _status; + Text::String _caption; + Text::MarkedContext _captionContext; int _nameWidth = 0; int _statusWidth = 0; + int _captionAvailableWidth = 0; float64 _suggestedMove = 0.; Animations::Simple _suggestedMoveAnimation; int _lastShrinkValue = 0; @@ -112,9 +118,11 @@ private: std::unique_ptr _spoiler; QImage _cornerCache; Fn _repaint; + Fn _repaintRect; QRect _lastRectOfModify; QRect _lastRectOfButtons; + QRect _lastRectOfCaption; object_ptr _editMedia = { nullptr }; object_ptr _deleteMedia = { nullptr }; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_item_single_file_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_item_single_file_preview.cpp index 0dfff6b52c..8e60e74bfe 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_item_single_file_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_item_single_file_preview.cpp @@ -40,7 +40,7 @@ ItemSingleFilePreview::ItemSingleFilePreview( const style::ComposeControls &st, not_null item, AttachControls::Type type) -: AbstractSingleFilePreview(parent, st, CheckControlsType(item, type)) { +: AbstractSingleFilePreview(parent, st, CheckControlsType(item, type), {}) { const auto media = item->media(); Assert(media != nullptr); const auto document = media->document(); @@ -109,7 +109,7 @@ void ItemSingleFilePreview::preparePreview(not_null document) { } data.statusText = FormatSizeText(document->size); - setData(data); + setData(std::move(data)); } } // namespace Ui diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h index 5eddcf8ad1..bee70e6421 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h @@ -80,6 +80,7 @@ struct PreparedFile { QString path; QString displayName; + TextWithTags caption; QByteArray content; int64 size = 0; std::unique_ptr information; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.cpp index c7ad3d0bef..0eef6b959a 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/attach/attach_prepare.h" #include "ui/text/format_song_name.h" #include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" #include "ui/ui_utility.h" #include "core/mime_type.h" #include "styles/style_chat.h" @@ -22,15 +23,28 @@ SingleFilePreview::SingleFilePreview( QWidget *parent, const style::ComposeControls &st, const PreparedFile &file, + const Text::MarkedContext &captionContext, AttachControls::Type type) -: AbstractSingleFilePreview(parent, st, type) { +: AbstractSingleFilePreview(parent, st, type, captionContext) { preparePreview(file); } +SingleFilePreview::SingleFilePreview( + QWidget *parent, + const style::ComposeControls &st, + const PreparedFile &file, + AttachControls::Type type) +: SingleFilePreview(parent, st, file, {}, type) { +} + void SingleFilePreview::setDisplayName(const QString &displayName) { AbstractSingleFilePreview::setDisplayName(displayName); } +void SingleFilePreview::setCaption(const TextWithTags &caption) { + AbstractSingleFilePreview::setCaption(caption); +} + void SingleFilePreview::preparePreview(const PreparedFile &file) { AbstractSingleFilePreview::Data data; @@ -82,8 +96,18 @@ void SingleFilePreview::preparePreview(const PreparedFile &file) { .string(); data.statusText = FormatSizeText(fileinfo.size()); } + auto caption = TextWithEntities{ + file.caption.text, + TextUtilities::ConvertTextTagsToEntities(file.caption.tags), + }; + caption = TextUtilities::SingleLine(caption); + data.caption.setMarkedText( + st::defaultTextStyle, + caption, + kMarkupTextOptions, + captionContext()); - setData(data); + setData(std::move(data)); } } // namespace Ui diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.h b/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.h index 4fc1f3b53a..a388c0473e 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.h @@ -15,12 +15,19 @@ struct PreparedFile; class SingleFilePreview final : public AbstractSingleFilePreview { public: + SingleFilePreview( + QWidget *parent, + const style::ComposeControls &st, + const PreparedFile &file, + const Text::MarkedContext &captionContext, + AttachControls::Type type = AttachControls::Type::Full); SingleFilePreview( QWidget *parent, const style::ComposeControls &st, const PreparedFile &file, AttachControls::Type type = AttachControls::Type::Full); void setDisplayName(const QString &displayName) override; + void setCaption(const TextWithTags &caption) override; private: void preparePreview(const PreparedFile &file); diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 6a42ed7b89..e3ff85c83f 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -534,6 +534,8 @@ attachPreviewThumbLayout: HistoryFileLayout { thumbSize: 64px; thumbSkip: 10px; } +attachPreviewCaptionTopOffset: 6px; +attachPreviewCaptionRepaintMargin: margins(0px, 2px, 0px, 2px); msgFileMinWidth: 268px; msgFileTopMinus: 6px; From fe4f460a3b6d3be1110b4893cd6154b5a1a977cf Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 27 Feb 2026 20:59:29 +0300 Subject: [PATCH 003/415] Added logic to process captions from attach prepared files. --- Telegram/SourceFiles/apiwrap.cpp | 17 +++- Telegram/SourceFiles/boxes/send_files_box.cpp | 88 +++++++++++++++++-- Telegram/SourceFiles/boxes/send_files_box.h | 8 ++ 3 files changed, 100 insertions(+), 13 deletions(-) diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 589b842a13..eff5a0107e 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3838,7 +3838,8 @@ void ApiWrap::sendFiles( TextWithTags &&caption, std::shared_ptr album, const SendAction &action) { - const auto haveCaption = !caption.text.isEmpty(); + auto sharedCaption = std::move(caption); + const auto haveCaption = !sharedCaption.text.isEmpty(); const auto captionAttached = !haveCaption ? false : (list.files.size() == 1) @@ -3856,10 +3857,12 @@ void ApiWrap::sendFiles( false); if (haveCaption && !captionAttached) { auto message = MessageToSend(action); - message.textWithTags = base::take(caption); + message.textWithTags = base::take(sharedCaption); message.action.clearDraft = false; sendMessage(std::move(message)); } + auto attachSharedCaption = haveCaption && captionAttached; + auto sharedCaptionConsumed = false; const auto to = FileLoadTaskOptions(action); if (album) { @@ -3868,6 +3871,13 @@ void ApiWrap::sendFiles( auto tasks = std::vector>(); tasks.reserve(list.files.size()); for (auto &file : list.files) { + auto fileCaption = std::move(file.caption); + if (attachSharedCaption && !sharedCaptionConsumed) { + sharedCaptionConsumed = true; + if (fileCaption.text.isEmpty()) { + fileCaption = base::take(sharedCaption); + } + } const auto uploadWithType = !album ? type : (file.type == Ui::PreparedFile::Type::Photo @@ -3899,14 +3909,13 @@ void ApiWrap::sendFiles( : nullptr), .type = uploadWithType, .to = to, - .caption = caption, + .caption = std::move(fileCaption), .spoiler = file.spoiler, .album = album, .forceFile = forceFile, .idOverride = 0, .displayName = file.displayName, })); - caption = TextWithTags(); } if (album) { _sendingAlbums.emplace(album->groupId, album); diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index b658e8fd89..ccd4d0536c 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -781,6 +781,65 @@ bool SendFilesBox::setDisplayNameInSingleFilePreview( return false; } +bool SendFilesBox::setCaptionInSingleFilePreview( + int fileIndex, + const TextWithTags &caption) { + for (auto &block : _blocks) { + if (fileIndex < block.fromIndex() || fileIndex >= block.tillIndex()) { + continue; + } + return block.setSingleFileCaption(fileIndex, caption); + } + return false; +} + +bool SendFilesBox::mainCaptionWillBeAttached() const { + const auto way = _sendWay.current(); + const auto slowmode = (_limits & SendFilesAllow::OnlyOne) + && (_list.files.size() > 1); + return Ui::CaptionWillBeAttached(_list, way, slowmode); +} + +void SendFilesBox::applyMainCaptionToFirstFile() { + if (!_caption + || _caption->isHidden() + || (_list.files.size() <= 1) + || _list.files.empty()) { + if (_mainCaptionAttachedToFirstFile && !_list.files.empty()) { + _list.files.front().caption = _firstFileCaptionBackup.value_or( + TextWithTags()); + if (!setCaptionInSingleFilePreview( + 0, + _list.files.front().caption)) { + } + } + _mainCaptionAttachedToFirstFile = false; + _firstFileCaptionBackup = std::nullopt; + return; + } + if (mainCaptionWillBeAttached()) { + if (!_mainCaptionAttachedToFirstFile) { + _firstFileCaptionBackup = _list.files.front().caption; + } + auto text = _caption->getTextWithAppliedMarkdown(); + _list.files.front().caption = text; + _mainCaptionAttachedToFirstFile = true; + if (!setCaptionInSingleFilePreview( + 0, + text)) { + } + } else if (_mainCaptionAttachedToFirstFile) { + _list.files.front().caption = _firstFileCaptionBackup.value_or( + TextWithTags()); + _mainCaptionAttachedToFirstFile = false; + _firstFileCaptionBackup = std::nullopt; + if (!setCaptionInSingleFilePreview( + 0, + _list.files.front().caption)) { + } + } +} + void SendFilesBox::openDialogToAddFileToAlbum() { const auto show = uiShow(); const auto checkResult = [=](const Ui::PreparedList &list) { @@ -810,10 +869,7 @@ void SendFilesBox::openDialogToAddFileToAlbum() { } void SendFilesBox::refreshMessagesCount() { - const auto way = _sendWay.current(); - const auto slowmode = (_limits & SendFilesAllow::OnlyOne) - && (_list.files.size() > 1); - const auto withCaption = Ui::CaptionWillBeAttached(_list, way, slowmode); + const auto withCaption = mainCaptionWillBeAttached(); const auto withComment = !withCaption && _caption && !_caption->isHidden() @@ -1423,6 +1479,7 @@ void SendFilesBox::refreshControls(bool initial) { refreshTitleText(); updateSendWayControls(); updateCaptionPlaceholder(); + applyMainCaptionToFirstFile(); } void SendFilesBox::setupSendWayControls() { @@ -1613,6 +1670,7 @@ void SendFilesBox::setupCaption() { _caption->changes() ) | rpl::on_next([=] { checkCharsLimitation(); + applyMainCaptionToFirstFile(); refreshMessagesCount(); }, _caption->lifetime()); } @@ -2028,14 +2086,20 @@ void SendFilesBox::saveSendWaySettings() { } bool SendFilesBox::validateLength(const QString &text) const { + const auto way = _sendWay.current(); + if (!_list.canAddCaption( + way.groupFiles() && way.sendImagesAsPhotos(), + way.sendImagesAsPhotos())) { + return true; + } + return validateSingleCaptionLength(text); +} + +bool SendFilesBox::validateSingleCaptionLength(const QString &text) const { const auto session = &_show->session(); const auto limit = Data::PremiumLimits(session).captionLengthCurrent(); const auto remove = int(text.size()) - limit; - const auto way = _sendWay.current(); - if (remove <= 0 - || !_list.canAddCaption( - way.groupFiles() && way.sendImagesAsPhotos(), - way.sendImagesAsPhotos())) { + if (remove <= 0) { return true; } _show->showBox( @@ -2074,6 +2138,7 @@ void SendFilesBox::send( applyBlockChanges(); Storage::ApplyModifications(_list); + applyMainCaptionToFirstFile(); _confirmed = true; if (_confirmedCallback) { @@ -2083,6 +2148,11 @@ void SendFilesBox::send( if (!validateLength(caption.text)) { return; } + for (const auto &file : _list.files) { + if (!validateSingleCaptionLength(file.caption.text)) { + return; + } + } options.invertCaption = _invertCaption; options.price = hasPrice() ? _price.current() : 0; if (options.price > 0) { diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index 18d1fec84f..cf0c9490f0 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -256,6 +256,9 @@ private: [[nodiscard]] bool setDisplayNameInSingleFilePreview( int fileIndex, const QString &displayName); + [[nodiscard]] bool setCaptionInSingleFilePreview( + int fileIndex, + const TextWithTags &caption); void enqueueNextPrepare(); void addPreparedAsyncFile(Ui::PreparedFile &&file); @@ -264,6 +267,9 @@ private: void refreshMessagesCount(); void requestToTakeTextWithTags() const; + bool validateSingleCaptionLength(const QString &text) const; + bool mainCaptionWillBeAttached() const; + void applyMainCaptionToFirstFile(); [[nodiscard]] Fn prepareSendMenuDetails( const SendFilesBoxDescriptor &descriptor); @@ -294,6 +300,8 @@ private: QImage _priceTagBg; bool _confirmed = false; bool _invertCaption = false; + bool _mainCaptionAttachedToFirstFile = false; + std::optional _firstFileCaptionBackup; object_ptr _caption = { nullptr }; std::unique_ptr _autocomplete; From 8c7d38ea25efa1e46990c9118e4ff7543f2786f8 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 27 Feb 2026 20:59:55 +0300 Subject: [PATCH 004/415] Added ability to change caption to every file in SendFilesBox. --- Telegram/SourceFiles/boxes/send_files_box.cpp | 139 +++++++++++++-- .../boxes/send_gif_with_caption_box.cpp | 167 ++++++++++-------- .../boxes/send_gif_with_caption_box.h | 14 ++ 3 files changed, 229 insertions(+), 91 deletions(-) diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index ccd4d0536c..487a33007b 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -32,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/call_delayed.h" #include "boxes/premium_limits_box.h" #include "boxes/premium_preview_box.h" +#include "boxes/send_gif_with_caption_box.h" #include "boxes/send_credits_box.h" #include "ui/effects/scroll_content_shadow.h" #include "ui/widgets/fields/number_input.h" @@ -57,6 +58,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/stickers/data_stickers.h" #include "data/stickers/data_custom_emoji.h" #include "window/window_session_controller.h" +#include "window/window_controller.h" #include "core/application.h" #include "core/core_settings.h" #include "styles/style_boxes.h" @@ -164,6 +166,58 @@ void RenameFileBox( }); } +void EditFileCaptionBox( + not_null box, + const style::ComposeControls &st, + PeerData *captionToPeer, + TextWithTags currentCaption, + Fn apply) { + box->setTitle(tr::lng_context_upload_edit_caption()); + const auto wrap = box->addRow( + object_ptr(box), + st::boxRowPadding); + const auto field = Ui::CreateChild( + wrap, + st.files.caption, + Ui::InputField::Mode::MultiLine, + tr::lng_photo_caption()); + field->setMaxLength(kMaxMessageLength); + field->setSubmitSettings(Core::App().settings().sendSubmitWay()); + Ui::ResizeFitChild(wrap, field); + if (const auto window = Core::App().findWindow(box)) { + const auto controller = window->sessionController(); + Ui::SetupCaptionFieldInBox( + box, + controller, + field, + captionToPeer, + [=](not_null emoji) { + return captionToPeer + && Data::AllowEmojiWithoutPremium(captionToPeer, emoji); + }, + PremiumFeature::EmojiStatus); + } + field->setTextWithTags(std::move(currentCaption)); + + box->setFocusCallback([=] { + field->setFocusFast(); + }); + const auto save = [=] { + const auto text = field->getTextWithAppliedMarkdown(); + if (!apply(text)) { + return; + } + box->closeBox(); + }; + field->submits() | rpl::on_next([=] { + save(); + }, box->lifetime()); + box->addButton(tr::lng_settings_save(), save); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + void EditPriceBox( not_null box, not_null session, @@ -1449,23 +1503,80 @@ void SendFilesBox::pushBlock(int from, int till) { if (from >= till || from >= _list.files.size()) { return base::EventFilterResult::Continue; } - const auto fileIndex = from; + auto fileIndex = from; + if (const auto album = dynamic_cast(widget)) { + const auto indexInBlock = album->indexFromPoint(mouse->pos()); + if (indexInBlock < 0) { + return base::EventFilterResult::Continue; + } + fileIndex += indexInBlock; + } + if (fileIndex >= till || fileIndex >= _list.files.size()) { + return base::EventFilterResult::Continue; + } state->menu = base::make_unique_q( widget, _st.tabbed.menu); - state->menu->addAction(tr::lng_rename_file(tr::now), [=] { - auto &file = _list.files[fileIndex]; - _show->show(Box(RenameFileBox, file.displayName, [=]( - QString newName) { - const auto displayName = std::move(newName); - _list.files[fileIndex].displayName = displayName; - if (!setDisplayNameInSingleFilePreview( - fileIndex, - displayName)) { - refreshAllAfterChanges(from); - } - })); - }, &st::menuIconEdit); + const auto &file = _list.files[fileIndex]; + const auto canEditFileData + = !_sendWay.current().sendImagesAsPhotos() + || (file.type != Ui::PreparedFile::Type::Photo + && file.type != Ui::PreparedFile::Type::Video); + if (canEditFileData) { + state->menu->addAction(tr::lng_rename_file(tr::now), [=] { + auto &file = _list.files[fileIndex]; + _show->show(Box(RenameFileBox, file.displayName, [=]( + QString newName) { + const auto displayName = std::move(newName); + _list.files[fileIndex].displayName = displayName; + if (!setDisplayNameInSingleFilePreview( + fileIndex, + displayName)) { + refreshAllAfterChanges(from); + } + })); + }, &st::menuIconEdit); + state->menu->addAction( + tr::lng_context_upload_edit_caption(tr::now), + [=] { + auto &file = _list.files[fileIndex]; + _show->show(Box( + EditFileCaptionBox, + _st, + _captionToPeer, + file.caption, + [=](TextWithTags text) { + if (!validateSingleCaptionLength(text.text)) { + return false; + } + auto updated = text; + const auto syncMainCaption = (fileIndex == 0) + && _caption + && !_caption->isHidden() + && mainCaptionWillBeAttached(); + if (fileIndex == 0) { + _mainCaptionAttachedToFirstFile = false; + _firstFileCaptionBackup = text; + } + _list.files[fileIndex].caption + = std::move(text); + if (syncMainCaption) { + _caption->setTextWithTags(updated); + } + if (!setCaptionInSingleFilePreview( + fileIndex, + updated)) { + refreshAllAfterChanges(from); + } + return true; + })); + }, + &st::menuIconCaptionShow); + } + if (state->menu->empty()) { + state->menu = nullptr; + return base::EventFilterResult::Continue; + } state->menu->popup(mouse->globalPos()); return base::EventFilterResult::Cancel; } diff --git a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp index 33635b45ba..3c4a6ca823 100644 --- a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp @@ -53,6 +53,89 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_menu_icons.h" namespace Ui { + +void SetupCaptionFieldInBox( + not_null box, + not_null controller, + not_null field, + PeerData *panelPeer, + Fn)> allowWithoutPremium, + PremiumFeature premiumFeature) { + using Limit = HistoryView::Controls::CharactersLimitLabel; + struct State final { + base::unique_qptr emojiPanel; + base::unique_qptr charsLimitation; + }; + const auto state = box->lifetime().make_state(); + const auto container = box->getDelegate()->outerContainer(); + using Selector = ChatHelpers::TabbedSelector; + state->emojiPanel = base::make_unique_q( + container, + controller, + object_ptr( + nullptr, + controller->uiShow(), + Window::GifPauseReason::Layer, + Selector::Mode::EmojiOnly)); + const auto emojiPanel = state->emojiPanel.get(); + emojiPanel->setDesiredHeightValues( + 1., + st::emojiPanMinHeight / 2, + st::emojiPanMinHeight); + emojiPanel->hide(); + emojiPanel->selector()->setCurrentPeer( + panelPeer ? panelPeer : controller->session().user()); + emojiPanel->selector()->emojiChosen( + ) | rpl::on_next([=](ChatHelpers::EmojiChosen data) { + Ui::InsertEmojiAtCursor(field->textCursor(), data.emoji); + }, field->lifetime()); + emojiPanel->selector()->customEmojiChosen( + ) | rpl::on_next([=](ChatHelpers::FileChosen data) { + const auto info = data.document->sticker(); + if (info + && info->setType == Data::StickersType::Emoji + && !allowWithoutPremium(data.document) + && !controller->session().premium()) { + ShowPremiumPreviewBox(controller, premiumFeature); + } else { + Data::InsertCustomEmoji(field, data.document); + } + }, field->lifetime()); + + const auto emojiButton = Ui::AddEmojiToggleToField( + field, + box, + controller, + emojiPanel, + st::sendGifWithCaptionEmojiPosition); + emojiButton->show(); + + const auto session = &controller->session(); + const auto checkCharsLimitation = [=](auto repeat) -> void { + const auto remove = Ui::ComputeFieldCharacterCount(field) + - Data::PremiumLimits(session).captionLengthCurrent(); + if (remove > 0) { + if (!state->charsLimitation) { + state->charsLimitation = base::make_unique_q( + field, + emojiButton, + style::al_top); + state->charsLimitation->show(); + Data::AmPremiumValue(session) | rpl::on_next([=] { + repeat(repeat); + }, state->charsLimitation->lifetime()); + } + state->charsLimitation->setLeft(remove); + state->charsLimitation->show(); + } else { + state->charsLimitation = nullptr; + } + }; + field->changes() | rpl::on_next([=] { + checkCharsLimitation(checkCharsLimitation); + }, field->lifetime()); +} + namespace { struct State final { @@ -205,8 +288,6 @@ struct State final { [[nodiscard]] not_null AddInputField( not_null box, not_null controller) { - using Limit = HistoryView::Controls::CharactersLimitLabel; - const auto bottomContainer = box->setPinnedToBottomContent( object_ptr(box)); const auto wrap = bottomContainer->add( @@ -218,83 +299,15 @@ struct State final { Ui::InputField::Mode::MultiLine, tr::lng_photo_caption()); Ui::ResizeFitChild(wrap, input); - - struct State final { - base::unique_qptr emojiPanel; - base::unique_qptr charsLimitation; - }; - const auto state = box->lifetime().make_state(); - - { - const auto container = box->getDelegate()->outerContainer(); - using Selector = ChatHelpers::TabbedSelector; - state->emojiPanel = base::make_unique_q( - container, - controller, - object_ptr( - nullptr, - controller->uiShow(), - Window::GifPauseReason::Layer, - Selector::Mode::EmojiOnly)); - const auto emojiPanel = state->emojiPanel.get(); - emojiPanel->setDesiredHeightValues( - 1., - st::emojiPanMinHeight / 2, - st::emojiPanMinHeight); - emojiPanel->hide(); - emojiPanel->selector()->setCurrentPeer(controller->session().user()); - emojiPanel->selector()->emojiChosen( - ) | rpl::on_next([=](ChatHelpers::EmojiChosen data) { - Ui::InsertEmojiAtCursor(input->textCursor(), data.emoji); - }, input->lifetime()); - emojiPanel->selector()->customEmojiChosen( - ) | rpl::on_next([=](ChatHelpers::FileChosen data) { - const auto info = data.document->sticker(); - if (info - && info->setType == Data::StickersType::Emoji - && !controller->session().premium()) { - ShowPremiumPreviewBox( - controller, - PremiumFeature::AnimatedEmoji); - } else { - Data::InsertCustomEmoji(input, data.document); - } - }, input->lifetime()); - } - - const auto emojiButton = Ui::AddEmojiToggleToField( - input, + SetupCaptionFieldInBox( box, controller, - state->emojiPanel.get(), - st::sendGifWithCaptionEmojiPosition); - emojiButton->show(); - - const auto session = &controller->session(); - const auto checkCharsLimitation = [=](auto repeat) -> void { - const auto remove = Ui::ComputeFieldCharacterCount(input) - - Data::PremiumLimits(session).captionLengthCurrent(); - if (remove > 0) { - if (!state->charsLimitation) { - state->charsLimitation = base::make_unique_q( - input, - emojiButton, - style::al_top); - state->charsLimitation->show(); - Data::AmPremiumValue(session) | rpl::on_next([=] { - repeat(repeat); - }, state->charsLimitation->lifetime()); - } - state->charsLimitation->setLeft(remove); - state->charsLimitation->show(); - } else { - state->charsLimitation = nullptr; - } - }; - - input->changes() | rpl::on_next([=] { - checkCharsLimitation(checkCharsLimitation); - }, input->lifetime()); + input, + controller->session().user(), + [](not_null) { + return false; + }, + PremiumFeature::AnimatedEmoji); return input; } diff --git a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.h b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.h index 1730c86714..db3c68ac14 100644 --- a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.h +++ b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class PeerData; class DocumentData; +enum class PremiumFeature; namespace Api { struct SendOptions; @@ -22,9 +23,22 @@ namespace HistoryView { class Element; } // namespace HistoryView +namespace Window { +class SessionController; +} // namespace Window + namespace Ui { class GenericBox; +class InputField; + +void SetupCaptionFieldInBox( + not_null box, + not_null controller, + not_null field, + PeerData *panelPeer, + Fn)> allowWithoutPremium, + PremiumFeature premiumFeature); void EditCaptionBox( not_null box, From 8a9409005fc24269c8e183f4cd34d88e817601cd Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 3 Mar 2026 13:03:53 +0300 Subject: [PATCH 005/415] Split translate box to api and td_ui parts. --- Telegram/SourceFiles/boxes/translate_box.cpp | 251 +++--------------- .../boxes/translate_box_content.cpp | 212 +++++++++++++++ .../SourceFiles/boxes/translate_box_content.h | 33 +++ Telegram/cmake/td_ui.cmake | 2 + 4 files changed, 289 insertions(+), 209 deletions(-) create mode 100644 Telegram/SourceFiles/boxes/translate_box_content.cpp create mode 100644 Telegram/SourceFiles/boxes/translate_box_content.h diff --git a/Telegram/SourceFiles/boxes/translate_box.cpp b/Telegram/SourceFiles/boxes/translate_box.cpp index a5b3456caa..ed103018d6 100644 --- a/Telegram/SourceFiles/boxes/translate_box.cpp +++ b/Telegram/SourceFiles/boxes/translate_box.cpp @@ -6,6 +6,7 @@ For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/translate_box.h" +#include "boxes/translate_box_content.h" #include "api/api_text_entities.h" // Api::EntitiesToMTP / EntitiesFromMTP. #include "core/application.h" @@ -20,76 +21,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mtproto/sender.h" #include "spellcheck/platform/platform_language.h" #include "ui/boxes/choose_language_box.h" -#include "ui/effects/loading_element.h" #include "ui/layers/generic_box.h" -#include "ui/text/text_utilities.h" -#include "ui/vertical_list.h" -#include "ui/painter.h" -#include "ui/power_saving.h" -#include "ui/widgets/buttons.h" -#include "ui/widgets/labels.h" #include "ui/widgets/multi_select.h" -#include "ui/wrap/fade_wrap.h" -#include "ui/wrap/slide_wrap.h" -#include "styles/style_boxes.h" -#include "styles/style_chat_helpers.h" -#include "styles/style_info.h" // inviteLinkListItem. -#include "styles/style_layers.h" - -#include +#include "ui/text/text_utilities.h" namespace Ui { namespace { constexpr auto kSkipAtLeastOneDuration = 3 * crl::time(1000); -class ShowButton final : public RpWidget { -public: - ShowButton(not_null parent); - - [[nodiscard]] rpl::producer clicks() const; - -protected: - void paintEvent(QPaintEvent *e) override; - -private: - LinkButton _button; - -}; - -ShowButton::ShowButton(not_null parent) -: RpWidget(parent) -, _button(this, tr::lng_usernames_activate_confirm(tr::now)) { - _button.sizeValue( - ) | rpl::on_next([=](const QSize &s) { - resize( - s.width() + st::defaultEmojiSuggestions.fadeRight.width(), - s.height()); - _button.moveToRight(0, 0); - }, lifetime()); - _button.show(); -} - -void ShowButton::paintEvent(QPaintEvent *e) { - auto p = QPainter(this); - const auto clip = e->rect(); - - const auto &icon = st::defaultEmojiSuggestions.fadeRight; - const auto fade = QRect(0, 0, icon.width(), height()); - if (fade.intersects(clip)) { - icon.fill(p, fade); - } - const auto fill = clip.intersected( - { icon.width(), 0, width() - icon.width(), height() }); - if (!fill.isEmpty()) { - p.fillRect(fill, st::boxBg); - } -} - -rpl::producer ShowButton::clicks() const { - return _button.clicks(); -} - } // namespace void TranslateBox( @@ -98,10 +38,6 @@ void TranslateBox( MsgId msgId, TextWithEntities text, bool hasCopyRestriction) { - box->setWidth(st::boxWideWidth); - box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); }); - const auto container = box->verticalLayout(); - struct State { State(not_null session) : api(&session->mtp()) { } @@ -122,150 +58,47 @@ void TranslateBox( : !text.text.isEmpty() ? Flag::f_text : Flag(0); + const auto requestText = text; - const auto &stLabel = st::aboutLabel; - const auto lineHeight = stLabel.style.lineHeight; - - Ui::AddSkip(container); - // Ui::AddSubsectionTitle( - // container, - // tr::lng_translate_box_original()); - - const auto animationsPaused = [] { - using Which = FlatLabel::WhichAnimationsPaused; - const auto emoji = On(PowerSaving::kEmojiChat); - const auto spoiler = On(PowerSaving::kChatSpoiler); - return emoji - ? (spoiler ? Which::All : Which::CustomEmoji) - : (spoiler ? Which::Spoiler : Which::None); - }; - const auto original = box->addRow(object_ptr>( - box, - object_ptr(box, stLabel))); - { - if (hasCopyRestriction) { - original->entity()->setContextMenuHook([](auto&&) { - }); - } - original->entity()->setAnimationsPausedCallback(animationsPaused); - original->entity()->setMarkedText( - text, - Core::TextContext({ .session = &peer->session() })); - original->setMinimalHeight(lineHeight); - original->hide(anim::type::instant); - - const auto show = Ui::CreateChild>( - container.get(), - object_ptr(container)); - show->hide(anim::type::instant); - rpl::combine( - container->widthValue(), - original->geometryValue() - ) | rpl::on_next([=](int width, const QRect &rect) { - show->moveToLeft( - width - show->width() - st::boxRowPadding.right(), - rect.y() + std::abs(lineHeight - show->height()) / 2); - }, show->lifetime()); - original->entity()->heightValue( - ) | rpl::filter([](int height) { - return height > 0; - }) | rpl::take(1) | rpl::on_next([=](int height) { - if (height > lineHeight) { - show->show(anim::type::instant); - } - }, show->lifetime()); - show->toggleOn(show->entity()->clicks() | rpl::map_to(false)); - original->toggleOn(show->entity()->clicks() | rpl::map_to(true)); - } - Ui::AddSkip(container); - Ui::AddSkip(container); - Ui::AddDivider(container); - Ui::AddSkip(container); - - { - const auto padding = st::defaultSubsectionTitlePadding; - const auto subtitle = Ui::AddSubsectionTitle( - container, - state->to.value() | rpl::map(LanguageName)); - - // Workaround. - state->to.value() | rpl::on_next([=] { - subtitle->resizeToWidth(container->width() - - padding.left() - - padding.right()); - }, subtitle->lifetime()); - } - - const auto translated = box->addRow(object_ptr>( - box, - object_ptr(box, stLabel))); - translated->entity()->setSelectable(!hasCopyRestriction); - translated->entity()->setAnimationsPausedCallback(animationsPaused); - - constexpr auto kMaxLines = 3; - container->resizeToWidth(box->width()); - const auto loading = box->addRow(object_ptr>( - box, - CreateLoadingTextWidget( - box, - st::aboutLabel.style, - std::min(original->entity()->height() / lineHeight, kMaxLines), - state->to.value() | rpl::map([=](LanguageId id) { - return id.locale().textDirection() == Qt::RightToLeft; - })))); - - const auto showText = [=](TextWithEntities text) { - const auto label = translated->entity(); - label->setMarkedText( - text, - Core::TextContext({ .session = &peer->session() })); - translated->show(anim::type::instant); - loading->hide(anim::type::instant); - }; - - const auto send = [=](LanguageId to) { - loading->show(anim::type::instant); - translated->hide(anim::type::instant); - state->api.request(MTPmessages_TranslateText( - MTP_flags(flags), - msgId ? peer->input() : MTP_inputPeerEmpty(), - (msgId - ? MTP_vector(1, MTP_int(msgId)) - : MTPVector()), - (msgId - ? MTPVector() - : MTP_vector(1, MTP_textWithEntities( - MTP_string(text.text), - Api::EntitiesToMTP( - &peer->session(), - text.entities, - Api::ConvertOption::SkipLocal)))), - MTP_string(to.twoLetterCode()) - )).done([=](const MTPmessages_TranslatedText &result) { - const auto &data = result.data(); - const auto &list = data.vresult().v; - if (list.isEmpty()) { - showText( - tr::italic(tr::lng_translate_box_error(tr::now))); - } else { - showText(Api::ParseTextWithEntities( - &peer->session(), - list.front())); - } - }).fail([=](const MTP::Error &error) { - showText( - tr::italic(tr::lng_translate_box_error(tr::now))); - }).send(); - }; - state->to.value() | rpl::on_next(send, box->lifetime()); - - box->addLeftButton(tr::lng_settings_language(), [=] { - if (loading->toggled()) { - return; - } - box->uiShow()->showBox(ChooseTranslateToBox( - state->to.current(), - crl::guard(box, [=](LanguageId id) { state->to = id; }))); + TranslateBoxContent(box, { + .text = std::move(text), + .hasCopyRestriction = hasCopyRestriction, + .textContext = Core::TextContext({ .session = &peer->session() }), + .to = state->to.value(), + .chooseTo = [=] { + box->uiShow()->showBox(ChooseTranslateToBox( + state->to.current(), + crl::guard(box, [=](LanguageId id) { state->to = id; }))); + }, + .request = [=](LanguageId to, Fn)> done) { + const auto callback = std::make_shared< + Fn)>>(std::move(done)); + state->api.request(MTPmessages_TranslateText( + MTP_flags(flags), + msgId ? peer->input() : MTP_inputPeerEmpty(), + (msgId + ? MTP_vector(1, MTP_int(msgId)) + : MTPVector()), + (msgId + ? MTPVector() + : MTP_vector(1, MTP_textWithEntities( + MTP_string(requestText.text), + Api::EntitiesToMTP( + &peer->session(), + requestText.entities, + Api::ConvertOption::SkipLocal)))), + MTP_string(to.twoLetterCode()) + )).done([=](const MTPmessages_TranslatedText &result) { + const auto &data = result.data(); + const auto &list = data.vresult().v; + (*callback)(list.isEmpty() + ? std::optional() + : std::optional( + Api::ParseTextWithEntities(&peer->session(), list.front()))); + }).fail([=](const MTP::Error &) { + (*callback)(std::nullopt); + }).send(); + }, }); } diff --git a/Telegram/SourceFiles/boxes/translate_box_content.cpp b/Telegram/SourceFiles/boxes/translate_box_content.cpp new file mode 100644 index 0000000000..c467ad759c --- /dev/null +++ b/Telegram/SourceFiles/boxes/translate_box_content.cpp @@ -0,0 +1,212 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "boxes/translate_box_content.h" + +#include "lang/lang_keys.h" +#include "ui/boxes/choose_language_box.h" +#include "ui/effects/loading_element.h" +#include "ui/layers/generic_box.h" +#include "ui/painter.h" +#include "ui/power_saving.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/wrap/fade_wrap.h" +#include "ui/wrap/slide_wrap.h" +#include "styles/style_boxes.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_layers.h" + +namespace Ui { +namespace { + +class ShowButton final : public RpWidget { +public: + ShowButton(not_null parent); + + [[nodiscard]] rpl::producer clicks() const; + +protected: + void paintEvent(QPaintEvent *e) override; + +private: + LinkButton _button; + +}; + +ShowButton::ShowButton(not_null parent) +: RpWidget(parent) +, _button(this, tr::lng_usernames_activate_confirm(tr::now)) { + _button.sizeValue( + ) | rpl::on_next([=](const QSize &s) { + resize( + s.width() + st::defaultEmojiSuggestions.fadeRight.width(), + s.height()); + _button.moveToRight(0, 0); + }, lifetime()); + _button.show(); +} + +void ShowButton::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + const auto clip = e->rect(); + + const auto &icon = st::defaultEmojiSuggestions.fadeRight; + const auto fade = QRect(0, 0, icon.width(), height()); + if (fade.intersects(clip)) { + icon.fill(p, fade); + } + const auto fill = clip.intersected( + { icon.width(), 0, width() - icon.width(), height() }); + if (!fill.isEmpty()) { + p.fillRect(fill, st::boxBg); + } +} + +rpl::producer ShowButton::clicks() const { + return _button.clicks(); +} + +} // namespace + +void TranslateBoxContent( + not_null box, + TranslateBoxContentArgs &&args) { + box->setWidth(st::boxWideWidth); + box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); }); + const auto container = box->verticalLayout(); + + const auto text = std::move(args.text); + const auto hasCopyRestriction = args.hasCopyRestriction; + const auto textContext = std::move(args.textContext); + const auto chooseTo = std::make_shared>(std::move(args.chooseTo)); + const auto request = std::make_shared< + Fn)>)>>( + std::move(args.request)); + + auto to = std::move(args.to) | rpl::start_spawning(box->lifetime()); + const auto toTitle = rpl::duplicate(to) | rpl::map(LanguageName); + const auto toDirection = rpl::duplicate(to) | rpl::map([=](LanguageId id) { + return id.locale().textDirection() == Qt::RightToLeft; + }); + + const auto &stLabel = st::aboutLabel; + const auto lineHeight = stLabel.style.lineHeight; + + Ui::AddSkip(container); + + const auto animationsPaused = [] { + using Which = FlatLabel::WhichAnimationsPaused; + const auto emoji = On(PowerSaving::kEmojiChat); + const auto spoiler = On(PowerSaving::kChatSpoiler); + return emoji + ? (spoiler ? Which::All : Which::CustomEmoji) + : (spoiler ? Which::Spoiler : Which::None); + }; + const auto original = box->addRow(object_ptr>( + box, + object_ptr(box, stLabel))); + { + if (hasCopyRestriction) { + original->entity()->setContextMenuHook([](auto&&) { + }); + } + original->entity()->setAnimationsPausedCallback(animationsPaused); + original->entity()->setMarkedText(text, textContext); + original->setMinimalHeight(lineHeight); + original->hide(anim::type::instant); + + const auto show = Ui::CreateChild>( + container.get(), + object_ptr(container)); + show->hide(anim::type::instant); + rpl::combine( + container->widthValue(), + original->geometryValue() + ) | rpl::on_next([=](int width, const QRect &rect) { + show->moveToLeft( + width - show->width() - st::boxRowPadding.right(), + rect.y() + std::abs(lineHeight - show->height()) / 2); + }, show->lifetime()); + original->entity()->heightValue( + ) | rpl::filter([](int height) { + return height > 0; + }) | rpl::take(1) | rpl::on_next([=](int height) { + if (height > lineHeight) { + show->show(anim::type::instant); + } + }, show->lifetime()); + show->toggleOn(show->entity()->clicks() | rpl::map_to(false)); + original->toggleOn(show->entity()->clicks() | rpl::map_to(true)); + } + Ui::AddSkip(container); + Ui::AddSkip(container); + Ui::AddDivider(container); + Ui::AddSkip(container); + + { + const auto padding = st::defaultSubsectionTitlePadding; + const auto subtitle = Ui::AddSubsectionTitle(container, std::move(toTitle)); + + rpl::duplicate(to) | rpl::on_next([=] { + subtitle->resizeToWidth(container->width() + - padding.left() + - padding.right()); + }, subtitle->lifetime()); + } + + const auto translated = box->addRow(object_ptr>( + box, + object_ptr(box, stLabel))); + translated->entity()->setSelectable(!hasCopyRestriction); + translated->entity()->setAnimationsPausedCallback(animationsPaused); + + constexpr auto kMaxLines = 3; + container->resizeToWidth(box->width()); + const auto loading = box->addRow(object_ptr>( + box, + CreateLoadingTextWidget( + box, + st::aboutLabel.style, + std::min(original->entity()->height() / lineHeight, kMaxLines), + std::move(toDirection)))); + + struct State { + int requestId = 0; + }; + const auto state = box->lifetime().make_state(); + + const auto showText = [=](std::optional translatedText) { + auto value = translatedText.value_or( + tr::italic(tr::lng_translate_box_error(tr::now))); + translated->entity()->setMarkedText(value, textContext); + translated->show(anim::type::instant); + loading->hide(anim::type::instant); + }; + const auto send = [=](LanguageId id) { + const auto requestId = ++state->requestId; + loading->show(anim::type::instant); + translated->hide(anim::type::instant); + (*request)(id, [=](std::optional translatedText) { + if (state->requestId != requestId) { + return; + } + showText(std::move(translatedText)); + }); + }; + std::move(to) | rpl::on_next(send, box->lifetime()); + + box->addLeftButton(tr::lng_settings_language(), [=] { + if (loading->toggled()) { + return; + } + (*chooseTo)(); + }); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/translate_box_content.h b/Telegram/SourceFiles/boxes/translate_box_content.h new file mode 100644 index 0000000000..e2d982bd8b --- /dev/null +++ b/Telegram/SourceFiles/boxes/translate_box_content.h @@ -0,0 +1,33 @@ +/* +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 "spellcheck/platform/platform_language.h" + +namespace Ui::Text { +struct MarkedContext; +} // namespace Ui::Text + +namespace Ui { + +class GenericBox; + +struct TranslateBoxContentArgs { + TextWithEntities text; + bool hasCopyRestriction = false; + Text::MarkedContext textContext; + rpl::producer to; + Fn chooseTo; + Fn)>)> request; +}; + +void TranslateBoxContent( + not_null box, + TranslateBoxContentArgs &&args); + +} // namespace Ui diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 2aa6bbc804..9ee7210042 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -65,6 +65,8 @@ PRIVATE boxes/url_auth_box_content.cpp boxes/url_auth_box_content.h + boxes/translate_box_content.cpp + boxes/translate_box_content.h calls/group/ui/calls_group_recording_box.cpp calls/group/ui/calls_group_recording_box.h From cec580a6451ebb40197e3d0481b2b88f530558d6 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 3 Mar 2026 13:04:23 +0300 Subject: [PATCH 006/415] Moved out api translations as separated provider. --- Telegram/CMakeLists.txt | 2 + Telegram/SourceFiles/boxes/translate_box.cpp | 56 ++------ .../SourceFiles/lang/translate_provider.cpp | 121 ++++++++++++++++++ .../SourceFiles/lang/translate_provider.h | 46 +++++++ 4 files changed, 181 insertions(+), 44 deletions(-) create mode 100644 Telegram/SourceFiles/lang/translate_provider.cpp create mode 100644 Telegram/SourceFiles/lang/translate_provider.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 5608edf262..060290eceb 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1230,6 +1230,8 @@ PRIVATE lang/lang_numbers_animation.h lang/lang_translator.cpp lang/lang_translator.h + lang/translate_provider.cpp + lang/translate_provider.h layout/layout_document_generic_preview.cpp layout/layout_document_generic_preview.h layout/layout_item_base.cpp diff --git a/Telegram/SourceFiles/boxes/translate_box.cpp b/Telegram/SourceFiles/boxes/translate_box.cpp index ed103018d6..d07a87f74a 100644 --- a/Telegram/SourceFiles/boxes/translate_box.cpp +++ b/Telegram/SourceFiles/boxes/translate_box.cpp @@ -7,8 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/translate_box.h" #include "boxes/translate_box_content.h" +#include "lang/translate_provider.h" -#include "api/api_text_entities.h" // Api::EntitiesToMTP / EntitiesFromMTP. #include "core/application.h" #include "core/core_settings.h" #include "core/ui_integration.h" @@ -18,7 +18,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_instance.h" #include "lang/lang_keys.h" #include "main/main_session.h" -#include "mtproto/sender.h" #include "spellcheck/platform/platform_language.h" #include "ui/boxes/choose_language_box.h" #include "ui/layers/generic_box.h" @@ -39,29 +38,24 @@ void TranslateBox( TextWithEntities text, bool hasCopyRestriction) { struct State { - State(not_null session) : api(&session->mtp()) { + State(not_null session) + : provider(CreateTranslateProvider(session)) { } - MTP::Sender api; + std::unique_ptr provider; rpl::variable to; }; const auto state = box->lifetime().make_state(&peer->session()); state->to = ChooseTranslateTo(peer->owner().history(peer)); - - if (!IsServerMsgId(msgId)) { - msgId = 0; - } - - using Flag = MTPmessages_TranslateText::Flag; - const auto flags = msgId - ? (Flag::f_peer | Flag::f_id) - : !text.text.isEmpty() - ? Flag::f_text - : Flag(0); - const auto requestText = text; + const auto request = std::make_shared( + PrepareTranslateProviderRequest( + state->provider.get(), + peer, + msgId, + std::move(text))); TranslateBoxContent(box, { - .text = std::move(text), + .text = request->text, .hasCopyRestriction = hasCopyRestriction, .textContext = Core::TextContext({ .session = &peer->session() }), .to = state->to.value(), @@ -71,33 +65,7 @@ void TranslateBox( crl::guard(box, [=](LanguageId id) { state->to = id; }))); }, .request = [=](LanguageId to, Fn)> done) { - const auto callback = std::make_shared< - Fn)>>(std::move(done)); - state->api.request(MTPmessages_TranslateText( - MTP_flags(flags), - msgId ? peer->input() : MTP_inputPeerEmpty(), - (msgId - ? MTP_vector(1, MTP_int(msgId)) - : MTPVector()), - (msgId - ? MTPVector() - : MTP_vector(1, MTP_textWithEntities( - MTP_string(requestText.text), - Api::EntitiesToMTP( - &peer->session(), - requestText.entities, - Api::ConvertOption::SkipLocal)))), - MTP_string(to.twoLetterCode()) - )).done([=](const MTPmessages_TranslatedText &result) { - const auto &data = result.data(); - const auto &list = data.vresult().v; - (*callback)(list.isEmpty() - ? std::optional() - : std::optional( - Api::ParseTextWithEntities(&peer->session(), list.front()))); - }).fail([=](const MTP::Error &) { - (*callback)(std::nullopt); - }).send(); + state->provider->request(*request, to, std::move(done)); }, }); } diff --git a/Telegram/SourceFiles/lang/translate_provider.cpp b/Telegram/SourceFiles/lang/translate_provider.cpp new file mode 100644 index 0000000000..e56ea74184 --- /dev/null +++ b/Telegram/SourceFiles/lang/translate_provider.cpp @@ -0,0 +1,121 @@ +/* +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 "lang/translate_provider.h" + +#include "api/api_text_entities.h" +#include "data/data_msg_id.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "history/history_item.h" +#include "main/main_session.h" +#include "mtproto/sender.h" + +namespace Ui { +namespace { + +enum class TranslateProviderKind { + MTProtoServer, +}; + +[[nodiscard]] TranslateProviderKind CurrentTranslateProviderKind() { + return TranslateProviderKind::MTProtoServer; +} + +class MTProtoTranslateProvider final : public TranslateProvider { +public: + explicit MTProtoTranslateProvider(not_null session) + : _api(&session->mtp()) { + } + + [[nodiscard]] bool supportsMessageId() const override { + return true; + } + + void request( + TranslateProviderRequest request, + LanguageId to, + Fn)> done) override { + using Flag = MTPmessages_TranslateText::Flag; + const auto flags = request.msgId + ? (Flag::f_peer | Flag::f_id) + : !request.text.text.isEmpty() + ? Flag::f_text + : Flag(0); + if (!flags) { + done(std::nullopt); + return; + } + const auto callback = std::make_shared< + Fn)>>(std::move(done)); + _api.request(MTPmessages_TranslateText( + MTP_flags(flags), + request.msgId ? request.peer->input() : MTP_inputPeerEmpty(), + (request.msgId + ? MTP_vector(1, MTP_int(request.msgId)) + : MTPVector()), + (request.msgId + ? MTPVector() + : MTP_vector(1, MTP_textWithEntities( + MTP_string(request.text.text), + Api::EntitiesToMTP( + &request.peer->session(), + request.text.entities, + Api::ConvertOption::SkipLocal)))), + MTP_string(to.twoLetterCode()) + )).done([=](const MTPmessages_TranslatedText &result) { + const auto &data = result.data(); + const auto &list = data.vresult().v; + (*callback)(list.isEmpty() + ? std::optional() + : std::optional(Api::ParseTextWithEntities( + &request.peer->session(), + list.front()))); + }).fail([=](const MTP::Error &) { + (*callback)(std::nullopt); + }).send(); + } + +private: + MTP::Sender _api; + +}; + +} // namespace + +std::unique_ptr CreateTranslateProvider( + not_null session) { + switch (CurrentTranslateProviderKind()) { + case TranslateProviderKind::MTProtoServer: + return std::make_unique(session); + } + return std::make_unique(session); +} + +TranslateProviderRequest PrepareTranslateProviderRequest( + not_null provider, + not_null peer, + MsgId msgId, + TextWithEntities text) { + auto result = TranslateProviderRequest{ + .peer = peer, + .msgId = IsServerMsgId(msgId) ? msgId : MsgId(), + .text = std::move(text), + }; + if (provider->supportsMessageId()) { + return result; + } + if (result.msgId) { + if (const auto item = peer->owner().message(peer, result.msgId)) { + result.text = item->originalText(); + } + result.msgId = 0; + } + return result; +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/lang/translate_provider.h b/Telegram/SourceFiles/lang/translate_provider.h new file mode 100644 index 0000000000..8da0ca0950 --- /dev/null +++ b/Telegram/SourceFiles/lang/translate_provider.h @@ -0,0 +1,46 @@ +/* +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 "spellcheck/platform/platform_language.h" +#include "ui/text/text_entity.h" + +class PeerData; + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { + +struct TranslateProviderRequest { + not_null peer; + MsgId msgId = 0; + TextWithEntities text; +}; + +class TranslateProvider { +public: + virtual ~TranslateProvider() = default; + [[nodiscard]] virtual bool supportsMessageId() const = 0; + virtual void request( + TranslateProviderRequest request, + LanguageId to, + Fn)> done) = 0; +}; + +[[nodiscard]] std::unique_ptr CreateTranslateProvider( + not_null session); + +[[nodiscard]] TranslateProviderRequest PrepareTranslateProviderRequest( + not_null provider, + not_null peer, + MsgId msgId, + TextWithEntities text); + +} // namespace Ui From d7e81993658b5133e7a848fe2dea343f55b5c415 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 2 Mar 2026 16:56:43 +0300 Subject: [PATCH 007/415] Added platform provider of translations. --- .gitmodules | 3 + Telegram/CMakeLists.txt | 14 ++- .../lang/translate_mtproto_provider.cpp | 82 ++++++++++++++++ .../lang/translate_mtproto_provider.h | 21 ++++ .../SourceFiles/lang/translate_provider.cpp | 81 +-------------- .../SourceFiles/lang/translate_provider.h | 21 +--- .../platform/linux/translate_provider_linux.h | 22 +++++ .../platform/mac/translate_provider_mac.h | 18 ++++ .../platform/mac/translate_provider_mac.mm | 98 +++++++++++++++++++ .../platform/platform_translate_provider.h | 25 +++++ .../platform/win/translate_provider_win.h | 22 +++++ .../cmake/telegram_apple_swift_runtime.cmake | 27 +++++ Telegram/lib_translate | 1 + 13 files changed, 336 insertions(+), 99 deletions(-) create mode 100644 Telegram/SourceFiles/lang/translate_mtproto_provider.cpp create mode 100644 Telegram/SourceFiles/lang/translate_mtproto_provider.h create mode 100644 Telegram/SourceFiles/platform/linux/translate_provider_linux.h create mode 100644 Telegram/SourceFiles/platform/mac/translate_provider_mac.h create mode 100644 Telegram/SourceFiles/platform/mac/translate_provider_mac.mm create mode 100644 Telegram/SourceFiles/platform/platform_translate_provider.h create mode 100644 Telegram/SourceFiles/platform/win/translate_provider_win.h create mode 100644 Telegram/cmake/telegram_apple_swift_runtime.cmake create mode 160000 Telegram/lib_translate diff --git a/.gitmodules b/.gitmodules index b8ed3d659f..2d65da9935 100644 --- a/.gitmodules +++ b/.gitmodules @@ -91,3 +91,6 @@ [submodule "Telegram/ThirdParty/xdg-desktop-portal"] path = Telegram/ThirdParty/xdg-desktop-portal url = https://github.com/flatpak/xdg-desktop-portal.git +[submodule "Telegram/lib_translate"] + path = Telegram/lib_translate + url = https://github.com/desktop-app/lib_translate diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 060290eceb..a1ad8e4912 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -16,6 +16,7 @@ add_subdirectory(lib_spellcheck) add_subdirectory(lib_storage) add_subdirectory(lib_lottie) add_subdirectory(lib_qr) +add_subdirectory(lib_translate) add_subdirectory(lib_webrtc) add_subdirectory(lib_webview) add_subdirectory(codegen) @@ -35,6 +36,7 @@ include(cmake/td_mtproto.cmake) include(cmake/td_scheme.cmake) include(cmake/td_tde2e.cmake) include(cmake/td_ui.cmake) +include(cmake/telegram_apple_swift_runtime.cmake) include(cmake/generate_appstream_changelog.cmake) if (DESKTOP_APP_TEST_APPS) @@ -78,6 +80,7 @@ PRIVATE desktop-app::lib_storage desktop-app::lib_lottie desktop-app::lib_qr + desktop-app::lib_translate desktop-app::lib_webview desktop-app::lib_ffmpeg desktop-app::lib_stripe @@ -94,6 +97,8 @@ PRIVATE desktop-app::external_xxhash ) +telegram_add_apple_swift_runtime(Telegram) + target_precompile_headers(Telegram PRIVATE $<$:${src_loc}/stdafx.h>) nice_target_sources(Telegram ${src_loc} PRIVATE @@ -1230,6 +1235,8 @@ PRIVATE lang/lang_numbers_animation.h lang/lang_translator.cpp lang/lang_translator.h + lang/translate_mtproto_provider.cpp + lang/translate_mtproto_provider.h lang/translate_provider.cpp lang/translate_provider.h layout/layout_document_generic_preview.cpp @@ -1444,6 +1451,7 @@ PRIVATE platform/linux/overlay_widget_linux.h platform/linux/specific_linux.cpp platform/linux/specific_linux.h + platform/linux/translate_provider_linux.h platform/linux/tray_linux.cpp platform/linux/tray_linux.h platform/linux/webauthn_linux.cpp @@ -1464,8 +1472,10 @@ PRIVATE platform/mac/specific_mac.h platform/mac/specific_mac_p.mm platform/mac/specific_mac_p.h - platform/mac/tray_mac.mm + platform/mac/translate_provider_mac.h + platform/mac/translate_provider_mac.mm platform/mac/tray_mac.h + platform/mac/tray_mac.mm platform/mac/webauthn_mac.mm platform/mac/window_title_mac.mm platform/mac/touchbar/items/mac_formatter_item.h @@ -1499,6 +1509,7 @@ PRIVATE platform/win/overlay_widget_win.h platform/win/specific_win.cpp platform/win/specific_win.h + platform/win/translate_provider_win.h platform/win/tray_win.cpp platform/win/tray_win.h platform/win/webauthn_win.cpp @@ -1519,6 +1530,7 @@ PRIVATE platform/platform_overlay_widget.cpp platform/platform_overlay_widget.h platform/platform_specific.h + platform/platform_translate_provider.h platform/platform_tray.h platform/platform_webauthn.h platform/platform_window_title.h diff --git a/Telegram/SourceFiles/lang/translate_mtproto_provider.cpp b/Telegram/SourceFiles/lang/translate_mtproto_provider.cpp new file mode 100644 index 0000000000..eab649f289 --- /dev/null +++ b/Telegram/SourceFiles/lang/translate_mtproto_provider.cpp @@ -0,0 +1,82 @@ +/* +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 "lang/translate_mtproto_provider.h" + +#include "api/api_text_entities.h" +#include "data/data_peer.h" +#include "main/main_session.h" +#include "mtproto/sender.h" + +namespace Ui { +namespace { + +class MTProtoTranslateProvider final : public TranslateProvider { +public: + explicit MTProtoTranslateProvider(not_null session) + : _api(&session->mtp()) { + } + + [[nodiscard]] bool supportsMessageId() const override { + return true; + } + + void request( + TranslateProviderRequest request, + LanguageId to, + Fn)> done) override { + using Flag = MTPmessages_TranslateText::Flag; + const auto flags = request.msgId + ? (Flag::f_peer | Flag::f_id) + : !request.text.text.isEmpty() + ? Flag::f_text + : Flag(0); + if (!flags) { + done(std::nullopt); + return; + } + _api.request(MTPmessages_TranslateText( + MTP_flags(flags), + request.msgId ? request.peer->input() : MTP_inputPeerEmpty(), + (request.msgId + ? MTP_vector(1, MTP_int(request.msgId)) + : MTPVector()), + (request.msgId + ? MTPVector() + : MTP_vector(1, MTP_textWithEntities( + MTP_string(request.text.text), + Api::EntitiesToMTP( + &request.peer->session(), + request.text.entities, + Api::ConvertOption::SkipLocal)))), + MTP_string(to.twoLetterCode()) + )).done([=](const MTPmessages_TranslatedText &result) { + const auto &data = result.data(); + const auto &list = data.vresult().v; + done(list.isEmpty() + ? std::optional() + : std::optional(Api::ParseTextWithEntities( + &request.peer->session(), + list.front()))); + }).fail([=](const MTP::Error &) { + done(std::nullopt); + }).send(); + } + +private: + MTP::Sender _api; + +}; + +} // namespace + +std::unique_ptr CreateMTProtoTranslateProvider( + not_null session) { + return std::make_unique(session); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/lang/translate_mtproto_provider.h b/Telegram/SourceFiles/lang/translate_mtproto_provider.h new file mode 100644 index 0000000000..9121450ecc --- /dev/null +++ b/Telegram/SourceFiles/lang/translate_mtproto_provider.h @@ -0,0 +1,21 @@ +/* +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 "translate_provider.h" + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { + +[[nodiscard]] std::unique_ptr CreateMTProtoTranslateProvider( + not_null session); + +} // namespace Ui diff --git a/Telegram/SourceFiles/lang/translate_provider.cpp b/Telegram/SourceFiles/lang/translate_provider.cpp index e56ea74184..ef318a3702 100644 --- a/Telegram/SourceFiles/lang/translate_provider.cpp +++ b/Telegram/SourceFiles/lang/translate_provider.cpp @@ -7,93 +7,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "lang/translate_provider.h" -#include "api/api_text_entities.h" #include "data/data_msg_id.h" #include "data/data_peer.h" #include "data/data_session.h" #include "history/history_item.h" -#include "main/main_session.h" -#include "mtproto/sender.h" +#include "lang/translate_mtproto_provider.h" +#include "platform/platform_translate_provider.h" namespace Ui { -namespace { - -enum class TranslateProviderKind { - MTProtoServer, -}; - -[[nodiscard]] TranslateProviderKind CurrentTranslateProviderKind() { - return TranslateProviderKind::MTProtoServer; -} - -class MTProtoTranslateProvider final : public TranslateProvider { -public: - explicit MTProtoTranslateProvider(not_null session) - : _api(&session->mtp()) { - } - - [[nodiscard]] bool supportsMessageId() const override { - return true; - } - - void request( - TranslateProviderRequest request, - LanguageId to, - Fn)> done) override { - using Flag = MTPmessages_TranslateText::Flag; - const auto flags = request.msgId - ? (Flag::f_peer | Flag::f_id) - : !request.text.text.isEmpty() - ? Flag::f_text - : Flag(0); - if (!flags) { - done(std::nullopt); - return; - } - const auto callback = std::make_shared< - Fn)>>(std::move(done)); - _api.request(MTPmessages_TranslateText( - MTP_flags(flags), - request.msgId ? request.peer->input() : MTP_inputPeerEmpty(), - (request.msgId - ? MTP_vector(1, MTP_int(request.msgId)) - : MTPVector()), - (request.msgId - ? MTPVector() - : MTP_vector(1, MTP_textWithEntities( - MTP_string(request.text.text), - Api::EntitiesToMTP( - &request.peer->session(), - request.text.entities, - Api::ConvertOption::SkipLocal)))), - MTP_string(to.twoLetterCode()) - )).done([=](const MTPmessages_TranslatedText &result) { - const auto &data = result.data(); - const auto &list = data.vresult().v; - (*callback)(list.isEmpty() - ? std::optional() - : std::optional(Api::ParseTextWithEntities( - &request.peer->session(), - list.front()))); - }).fail([=](const MTP::Error &) { - (*callback)(std::nullopt); - }).send(); - } - -private: - MTP::Sender _api; - -}; - -} // namespace std::unique_ptr CreateTranslateProvider( not_null session) { - switch (CurrentTranslateProviderKind()) { - case TranslateProviderKind::MTProtoServer: - return std::make_unique(session); - } - return std::make_unique(session); + return CreateMTProtoTranslateProvider(session); } TranslateProviderRequest PrepareTranslateProviderRequest( diff --git a/Telegram/SourceFiles/lang/translate_provider.h b/Telegram/SourceFiles/lang/translate_provider.h index 8da0ca0950..2902207656 100644 --- a/Telegram/SourceFiles/lang/translate_provider.h +++ b/Telegram/SourceFiles/lang/translate_provider.h @@ -7,10 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "spellcheck/platform/platform_language.h" -#include "ui/text/text_entity.h" - -class PeerData; +#include namespace Main { class Session; @@ -18,22 +15,6 @@ class Session; namespace Ui { -struct TranslateProviderRequest { - not_null peer; - MsgId msgId = 0; - TextWithEntities text; -}; - -class TranslateProvider { -public: - virtual ~TranslateProvider() = default; - [[nodiscard]] virtual bool supportsMessageId() const = 0; - virtual void request( - TranslateProviderRequest request, - LanguageId to, - Fn)> done) = 0; -}; - [[nodiscard]] std::unique_ptr CreateTranslateProvider( not_null session); diff --git a/Telegram/SourceFiles/platform/linux/translate_provider_linux.h b/Telegram/SourceFiles/platform/linux/translate_provider_linux.h new file mode 100644 index 0000000000..8fed093cb4 --- /dev/null +++ b/Telegram/SourceFiles/platform/linux/translate_provider_linux.h @@ -0,0 +1,22 @@ +/* +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 "platform/platform_translate_provider.h" + +namespace Platform { + +inline bool IsTranslateProviderAvailable() { + return false; +} + +inline std::unique_ptr CreateTranslateProvider() { + return nullptr; +} + +} // namespace Platform diff --git a/Telegram/SourceFiles/platform/mac/translate_provider_mac.h b/Telegram/SourceFiles/platform/mac/translate_provider_mac.h new file mode 100644 index 0000000000..996f5d484f --- /dev/null +++ b/Telegram/SourceFiles/platform/mac/translate_provider_mac.h @@ -0,0 +1,18 @@ +// 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 "translate_provider.h" + +namespace Platform { + +[[nodiscard]] std::unique_ptr +CreateTranslateProvider(); + +[[nodiscard]] bool IsTranslateProviderAvailable(); + +} // namespace Platform diff --git a/Telegram/SourceFiles/platform/mac/translate_provider_mac.mm b/Telegram/SourceFiles/platform/mac/translate_provider_mac.mm new file mode 100644 index 0000000000..d5717d2e03 --- /dev/null +++ b/Telegram/SourceFiles/platform/mac/translate_provider_mac.mm @@ -0,0 +1,98 @@ +/* +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 "platform/mac/translate_provider_mac.h" + +#include "base/weak_ptr.h" +#include "translate_provider_mac_swift_bridge.h" + +#include +#include +#include + +namespace Platform { +namespace { + +class TranslateProvider final : public Ui::TranslateProvider +, public base::has_weak_ptr { +public: + [[nodiscard]] bool supportsMessageId() const override { + return false; + } + + void request( + Ui::TranslateProviderRequest request, + LanguageId to, + Fn)> done) override { + if (request.text.text.isEmpty()) { + done(std::nullopt); + return; + } + const auto text = request.text.text.toUtf8(); + const auto target = to.twoLetterCode().toUtf8(); + if (target.isEmpty()) { + done(std::nullopt); + return; + } + struct CallbackContext { + base::weak_ptr provider; + Fn)> done; + }; + auto ownedContext = std::make_unique(CallbackContext{ + .provider = base::make_weak(this), + .done = std::move(done), + }); + TranslateProviderMacSwiftTranslate( + text.constData(), + target.constData(), + ownedContext.release(), + [](void *context, const char *resultUtf8, const char *errorUtf8) { + auto guard = std::unique_ptr( + static_cast(context)); + auto done = std::move(guard->done); + const auto isAlive = (guard->provider.get() != nullptr); + auto translatedText = QString(); + auto hasError = (resultUtf8 == nullptr); + if (resultUtf8 != nullptr) { + translatedText = QString::fromUtf8(resultUtf8); + std::free(const_cast(resultUtf8)); + } + if (errorUtf8 != nullptr) { + hasError = true; + std::free(const_cast(errorUtf8)); + } + if (!isAlive) { + return; + } + crl::on_main([=, + done = std::move(done), + translatedText = std::move(translatedText)] { + done(hasError + ? std::optional() + : std::optional(TextWithEntities{ + .text = std::move(translatedText), + })); + }); + }); + } + +}; + +} // namespace + +std::unique_ptr CreateTranslateProvider() { + if (TranslateProviderMacSwiftIsAvailable()) { + return std::make_unique(); + } + return nullptr; +} + +bool IsTranslateProviderAvailable() { + return TranslateProviderMacSwiftIsAvailable(); +} + +} // namespace Platform diff --git a/Telegram/SourceFiles/platform/platform_translate_provider.h b/Telegram/SourceFiles/platform/platform_translate_provider.h new file mode 100644 index 0000000000..a15b7a8e5c --- /dev/null +++ b/Telegram/SourceFiles/platform/platform_translate_provider.h @@ -0,0 +1,25 @@ +/* +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 "translate_provider.h" + +namespace Platform { + +[[nodiscard]] bool IsTranslateProviderAvailable(); +[[nodiscard]] std::unique_ptr CreateTranslateProvider(); + +} // namespace Platform + +#if defined Q_OS_WINRT || defined Q_OS_WIN +#include "platform/win/translate_provider_win.h" +#elif defined Q_OS_MAC +#include "platform/mac/translate_provider_mac.h" +#else +#include "platform/linux/translate_provider_linux.h" +#endif diff --git a/Telegram/SourceFiles/platform/win/translate_provider_win.h b/Telegram/SourceFiles/platform/win/translate_provider_win.h new file mode 100644 index 0000000000..8fed093cb4 --- /dev/null +++ b/Telegram/SourceFiles/platform/win/translate_provider_win.h @@ -0,0 +1,22 @@ +/* +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 "platform/platform_translate_provider.h" + +namespace Platform { + +inline bool IsTranslateProviderAvailable() { + return false; +} + +inline std::unique_ptr CreateTranslateProvider() { + return nullptr; +} + +} // namespace Platform diff --git a/Telegram/cmake/telegram_apple_swift_runtime.cmake b/Telegram/cmake/telegram_apple_swift_runtime.cmake new file mode 100644 index 0000000000..36125c26f9 --- /dev/null +++ b/Telegram/cmake/telegram_apple_swift_runtime.cmake @@ -0,0 +1,27 @@ +# 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 + +function(telegram_add_apple_swift_runtime target_name) + if (NOT APPLE) + return() + endif() + + target_link_options(${target_name} + PRIVATE + "-Wl,-rpath,/usr/lib/swift" + "-Wl,-rpath,@executable_path/../Frameworks" + ) + + add_custom_command(TARGET ${target_name} POST_BUILD + COMMAND mkdir -p $/../Frameworks + COMMAND xcrun swift-stdlib-tool + --copy + --platform macosx + --scan-executable $ + --destination $/../Frameworks + VERBATIM + ) +endfunction() diff --git a/Telegram/lib_translate b/Telegram/lib_translate new file mode 160000 index 0000000000..48d9e3ca92 --- /dev/null +++ b/Telegram/lib_translate @@ -0,0 +1 @@ +Subproject commit 48d9e3ca92fbee00d636ace5a2c71f9972b5ac27 From 1e0a16a214a4ac6f3713989e49a62ce411f93b4a Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 2 Mar 2026 14:37:01 +0300 Subject: [PATCH 008/415] Added ability to use platform provider of translations on macOS. --- Telegram/Resources/langs/lang.strings | 2 ++ Telegram/SourceFiles/boxes/language_box.cpp | 32 +++++++++++++++++++ Telegram/SourceFiles/core/core_settings.cpp | 18 +++++++++-- Telegram/SourceFiles/core/core_settings.h | 3 ++ .../SourceFiles/lang/translate_provider.cpp | 6 ++++ 5 files changed, 59 insertions(+), 2 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index ee568a35ef..cdef1a3b23 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -6777,6 +6777,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_translate_settings_subtitle" = "Translate Messages"; "lng_translate_settings_show" = "Show Translate Button"; +"lng_translate_settings_use_platform_mac" = "Use Apple Translations"; +"lng_translate_settings_use_platform_mac_about" = "Translation on macOS won't work until you download local language packs in System Settings."; "lng_translate_settings_chat" = "Translate Entire Chats"; "lng_translate_settings_choose" = "Do Not Translate"; "lng_translate_settings_about" = "The 'Translate' button will appear in the context menu of messages containing text."; diff --git a/Telegram/SourceFiles/boxes/language_box.cpp b/Telegram/SourceFiles/boxes/language_box.cpp index c55423eca5..1a3e9935b8 100644 --- a/Telegram/SourceFiles/boxes/language_box.cpp +++ b/Telegram/SourceFiles/boxes/language_box.cpp @@ -35,8 +35,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwidget.h" #include "mainwindow.h" #include "core/application.h" +#include "base/platform/base_platform_info.h" #include "lang/lang_instance.h" #include "lang/lang_cloud_manager.h" +#include "platform/platform_translate_provider.h" #include "settings/settings_common.h" #include "spellcheck/spellcheck_types.h" #include "window/window_controller.h" @@ -1217,6 +1219,36 @@ void LanguageBox::setupTop(not_null container) { Core::App().saveSettingsDelayed(); }, translateEnabled->lifetime()); + if (Platform::IsMac() && Platform::IsTranslateProviderAvailable()) { + const auto platformTranslateWrap = container->add( + object_ptr>( + container, + object_ptr(container))); + platformTranslateWrap->toggle( + translateEnabled->toggled(), + anim::type::instant); + platformTranslateWrap->toggleOn(translateEnabled->toggledValue()); + const auto platformTranslateEnabled = platformTranslateWrap->entity()->add( + object_ptr( + platformTranslateWrap->entity(), + tr::lng_translate_settings_use_platform_mac(), + st::settingsButtonNoIcon))->toggleOn( + rpl::single( + Core::App().settings().usePlatformTranslation())); + platformTranslateEnabled->toggledValue( + ) | rpl::filter([](bool checked) { + return (checked + != Core::App().settings().usePlatformTranslation()); + }) | rpl::on_next([=](bool checked) { + Core::App().settings().setUsePlatformTranslation(checked); + Core::App().saveSettingsDelayed(); + }, platformTranslateEnabled->lifetime()); + Ui::AddSkip(platformTranslateWrap->entity()); + Ui::AddDividerText( + platformTranslateWrap->entity(), + tr::lng_translate_settings_use_platform_mac_about()); + } + using namespace rpl::mappers; auto premium = Data::AmPremiumValue(&_controller->session()); const auto translateChat = container->add(object_ptr( diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index 6e4b91d035..000ef24776 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -246,7 +246,7 @@ QByteArray Settings::serialize() const { + sizeof(ushort) + sizeof(qint32) // _notificationsDisplayChecksum + Serialize::bytearraySize(callPanelPosition) - + sizeof(qint32) * 2; // _cornerReply + _systemAccentColorEnabled + + sizeof(qint32) * 3; // _cornerReply + _systemAccentColorEnabled + _usePlatformTranslation auto result = QByteArray(); result.reserve(size); @@ -413,7 +413,8 @@ QByteArray Settings::serialize() const { << _notificationsDisplayChecksum << callPanelPosition << qint32(_cornerReply.current() ? 1 : 0) - << qint32(_systemAccentColorEnabled ? 1 : 0); + << qint32(_systemAccentColorEnabled ? 1 : 0) + << qint32(_usePlatformTranslation ? 1 : 0); } Ensures(result.size() == size); @@ -550,6 +551,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { qint32 systemAccentColorEnabled = _systemAccentColorEnabled ? 1 : 0; + qint32 usePlatformTranslation = _usePlatformTranslation ? 1 : 0; stream >> themesAccentColors; if (!stream.atEnd()) { @@ -896,6 +898,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) { if (!stream.atEnd()) { stream >> systemAccentColorEnabled; } + if (!stream.atEnd()) { + stream >> usePlatformTranslation; + } if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for Core::Settings::constructFromSerialized()")); @@ -937,6 +942,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { } _notificationsDisplayChecksum = notificationsDisplayChecksum; _systemAccentColorEnabled = (systemAccentColorEnabled == 1); + _usePlatformTranslation = (usePlatformTranslation == 1); _includeMutedCounter = (includeMutedCounter == 1); _includeMutedCounterFolders = (includeMutedCounterFolders == 1); _countUnreadMessages = (countUnreadMessages == 1); @@ -1591,6 +1597,14 @@ bool Settings::translateButtonEnabled() const { return _translateButtonEnabled; } +void Settings::setUsePlatformTranslation(bool value) { + _usePlatformTranslation = value; +} + +bool Settings::usePlatformTranslation() const { + return _usePlatformTranslation; +} + void Settings::setTranslateChatEnabled(bool value) { _translateChatEnabled = value; } diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index 9525364652..272b9a1eee 100644 --- a/Telegram/SourceFiles/core/core_settings.h +++ b/Telegram/SourceFiles/core/core_settings.h @@ -857,6 +857,8 @@ public: void setTranslateButtonEnabled(bool value); [[nodiscard]] bool translateButtonEnabled() const; + void setUsePlatformTranslation(bool value); + [[nodiscard]] bool usePlatformTranslation() const; void setTranslateChatEnabled(bool value); [[nodiscard]] bool translateChatEnabled() const; [[nodiscard]] rpl::producer translateChatEnabledValue() const; @@ -1098,6 +1100,7 @@ private: HistoryView::DoubleClickQuickAction _chatQuickAction = HistoryView::DoubleClickQuickAction(); bool _translateButtonEnabled = false; + bool _usePlatformTranslation = false; rpl::variable _translateChatEnabled = true; rpl::variable _translateToRaw = 0; rpl::variable> _skipTranslationLanguages; diff --git a/Telegram/SourceFiles/lang/translate_provider.cpp b/Telegram/SourceFiles/lang/translate_provider.cpp index ef318a3702..15b79c5b99 100644 --- a/Telegram/SourceFiles/lang/translate_provider.cpp +++ b/Telegram/SourceFiles/lang/translate_provider.cpp @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "lang/translate_provider.h" +#include "core/application.h" +#include "core/core_settings.h" #include "data/data_msg_id.h" #include "data/data_peer.h" #include "data/data_session.h" @@ -18,6 +20,10 @@ namespace Ui { std::unique_ptr CreateTranslateProvider( not_null session) { + if (Core::App().settings().usePlatformTranslation() + && Platform::IsTranslateProviderAvailable()) { + return Platform::CreateTranslateProvider(); + } return CreateMTProtoTranslateProvider(session); } From ccce8756ed9a8d869495543d7ba70616c8b19186 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 2 Mar 2026 16:20:30 +0300 Subject: [PATCH 009/415] Added simple error handler for translations. --- Telegram/Resources/langs/lang.strings | 1 + Telegram/SourceFiles/boxes/translate_box.cpp | 21 +++++++++- .../boxes/translate_box_content.cpp | 15 +++++--- .../SourceFiles/boxes/translate_box_content.h | 13 ++++++- .../lang/translate_mtproto_provider.cpp | 22 +++++++---- .../platform/mac/translate_provider_mac.mm | 38 ++++++++++++------- Telegram/lib_translate | 2 +- 7 files changed, 81 insertions(+), 31 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index cdef1a3b23..931ea862bd 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -6774,6 +6774,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_language_not_ready_link" = "translation platform"; "lng_translate_box_error" = "Translate failed."; +"lng_translate_box_error_language_pack_not_installed" = "Translation requires a local language pack. Download it in System Settings."; "lng_translate_settings_subtitle" = "Translate Messages"; "lng_translate_settings_show" = "Show Translate Button"; diff --git a/Telegram/SourceFiles/boxes/translate_box.cpp b/Telegram/SourceFiles/boxes/translate_box.cpp index d07a87f74a..e91a5f4956 100644 --- a/Telegram/SourceFiles/boxes/translate_box.cpp +++ b/Telegram/SourceFiles/boxes/translate_box.cpp @@ -64,8 +64,25 @@ void TranslateBox( state->to.current(), crl::guard(box, [=](LanguageId id) { state->to = id; }))); }, - .request = [=](LanguageId to, Fn)> done) { - state->provider->request(*request, to, std::move(done)); + .request = [=]( + LanguageId to, + Fn done) { + state->provider->request( + *request, + to, + [done = std::move(done)](TranslateProviderResult result) { + using ProviderError = TranslateProviderError; + using UiError = TranslateBoxContentError; + done(TranslateBoxContentResult{ + .text = std::move(result.text), + .error = (result.error + == ProviderError::LocalLanguagePackMissing) + ? UiError::LocalLanguagePackMissing + : (result.error == ProviderError::None) + ? UiError::None + : UiError::Unknown, + }); + }); }, }); } diff --git a/Telegram/SourceFiles/boxes/translate_box_content.cpp b/Telegram/SourceFiles/boxes/translate_box_content.cpp index c467ad759c..daf204eee3 100644 --- a/Telegram/SourceFiles/boxes/translate_box_content.cpp +++ b/Telegram/SourceFiles/boxes/translate_box_content.cpp @@ -86,7 +86,7 @@ void TranslateBoxContent( const auto textContext = std::move(args.textContext); const auto chooseTo = std::make_shared>(std::move(args.chooseTo)); const auto request = std::make_shared< - Fn)>)>>( + Fn)>>( std::move(args.request)); auto to = std::move(args.to) | rpl::start_spawning(box->lifetime()); @@ -181,9 +181,12 @@ void TranslateBoxContent( }; const auto state = box->lifetime().make_state(); - const auto showText = [=](std::optional translatedText) { - auto value = translatedText.value_or( - tr::italic(tr::lng_translate_box_error(tr::now))); + const auto showText = [=](TranslateBoxContentResult result) { + using UiError = TranslateBoxContentError; + auto value = result.text.value_or( + tr::italic(((result.error == UiError::LocalLanguagePackMissing) + ? tr::lng_translate_box_error_language_pack_not_installed + : tr::lng_translate_box_error)(tr::now))); translated->entity()->setMarkedText(value, textContext); translated->show(anim::type::instant); loading->hide(anim::type::instant); @@ -192,11 +195,11 @@ void TranslateBoxContent( const auto requestId = ++state->requestId; loading->show(anim::type::instant); translated->hide(anim::type::instant); - (*request)(id, [=](std::optional translatedText) { + (*request)(id, [=](TranslateBoxContentResult result) { if (state->requestId != requestId) { return; } - showText(std::move(translatedText)); + showText(std::move(result)); }); }; std::move(to) | rpl::on_next(send, box->lifetime()); diff --git a/Telegram/SourceFiles/boxes/translate_box_content.h b/Telegram/SourceFiles/boxes/translate_box_content.h index e2d982bd8b..38e68804b7 100644 --- a/Telegram/SourceFiles/boxes/translate_box_content.h +++ b/Telegram/SourceFiles/boxes/translate_box_content.h @@ -15,6 +15,17 @@ struct MarkedContext; namespace Ui { +enum class TranslateBoxContentError { + None = 0, + Unknown, + LocalLanguagePackMissing, +}; + +struct TranslateBoxContentResult { + std::optional text; + TranslateBoxContentError error = TranslateBoxContentError::None; +}; + class GenericBox; struct TranslateBoxContentArgs { @@ -23,7 +34,7 @@ struct TranslateBoxContentArgs { Text::MarkedContext textContext; rpl::producer to; Fn chooseTo; - Fn)>)> request; + Fn)> request; }; void TranslateBoxContent( diff --git a/Telegram/SourceFiles/lang/translate_mtproto_provider.cpp b/Telegram/SourceFiles/lang/translate_mtproto_provider.cpp index eab649f289..1c487411df 100644 --- a/Telegram/SourceFiles/lang/translate_mtproto_provider.cpp +++ b/Telegram/SourceFiles/lang/translate_mtproto_provider.cpp @@ -28,7 +28,7 @@ public: void request( TranslateProviderRequest request, LanguageId to, - Fn)> done) override { + Fn done) override { using Flag = MTPmessages_TranslateText::Flag; const auto flags = request.msgId ? (Flag::f_peer | Flag::f_id) @@ -36,7 +36,9 @@ public: ? Flag::f_text : Flag(0); if (!flags) { - done(std::nullopt); + done(TranslateProviderResult{ + .error = TranslateProviderError::Unknown, + }); return; } _api.request(MTPmessages_TranslateText( @@ -58,12 +60,18 @@ public: const auto &data = result.data(); const auto &list = data.vresult().v; done(list.isEmpty() - ? std::optional() - : std::optional(Api::ParseTextWithEntities( - &request.peer->session(), - list.front()))); + ? TranslateProviderResult{ + .error = TranslateProviderError::Unknown, + } + : TranslateProviderResult{ + .text = Api::ParseTextWithEntities( + &request.peer->session(), + list.front()), + }); }).fail([=](const MTP::Error &) { - done(std::nullopt); + done(TranslateProviderResult{ + .error = TranslateProviderError::Unknown, + }); }).send(); } diff --git a/Telegram/SourceFiles/platform/mac/translate_provider_mac.mm b/Telegram/SourceFiles/platform/mac/translate_provider_mac.mm index d5717d2e03..7e9f7f779a 100644 --- a/Telegram/SourceFiles/platform/mac/translate_provider_mac.mm +++ b/Telegram/SourceFiles/platform/mac/translate_provider_mac.mm @@ -17,6 +17,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Platform { namespace { +[[nodiscard]] Ui::TranslateProviderError ParseErrorCode( + const char *errorUtf8) { + return !std::strcmp(errorUtf8, "local-language-pack-missing") + ? Ui::TranslateProviderError::LocalLanguagePackMissing + : Ui::TranslateProviderError::Unknown; +} + class TranslateProvider final : public Ui::TranslateProvider , public base::has_weak_ptr { public: @@ -27,20 +34,24 @@ public: void request( Ui::TranslateProviderRequest request, LanguageId to, - Fn)> done) override { + Fn done) override { if (request.text.text.isEmpty()) { - done(std::nullopt); + done(Ui::TranslateProviderResult{ + .error = Ui::TranslateProviderError::Unknown, + }); return; } const auto text = request.text.text.toUtf8(); const auto target = to.twoLetterCode().toUtf8(); if (target.isEmpty()) { - done(std::nullopt); + done(Ui::TranslateProviderResult{ + .error = Ui::TranslateProviderError::Unknown, + }); return; } struct CallbackContext { base::weak_ptr provider; - Fn)> done; + Fn done; }; auto ownedContext = std::make_unique(CallbackContext{ .provider = base::make_weak(this), @@ -55,27 +66,26 @@ public: static_cast(context)); auto done = std::move(guard->done); const auto isAlive = (guard->provider.get() != nullptr); - auto translatedText = QString(); - auto hasError = (resultUtf8 == nullptr); + auto result = Ui::TranslateProviderResult(); if (resultUtf8 != nullptr) { - translatedText = QString::fromUtf8(resultUtf8); + result.text = TextWithEntities{ + .text = QString::fromUtf8(resultUtf8), + }; std::free(const_cast(resultUtf8)); } if (errorUtf8 != nullptr) { - hasError = true; + result.error = ParseErrorCode(errorUtf8); std::free(const_cast(errorUtf8)); + } else if (!result.text.has_value()) { + result.error = Ui::TranslateProviderError::Unknown; } if (!isAlive) { return; } crl::on_main([=, done = std::move(done), - translatedText = std::move(translatedText)] { - done(hasError - ? std::optional() - : std::optional(TextWithEntities{ - .text = std::move(translatedText), - })); + result = std::move(result)] { + done(std::move(result)); }); }); } diff --git a/Telegram/lib_translate b/Telegram/lib_translate index 48d9e3ca92..ed5bbec209 160000 --- a/Telegram/lib_translate +++ b/Telegram/lib_translate @@ -1 +1 @@ -Subproject commit 48d9e3ca92fbee00d636ace5a2c71f9972b5ac27 +Subproject commit ed5bbec209451bd56237783e783fdadfb3c1fdd2 From df1a8346e0fd940e4035ad10162391d2eb76eea8 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 2 Mar 2026 17:53:01 +0300 Subject: [PATCH 010/415] Added new translate provider that uses url template from experimental. --- Telegram/CMakeLists.txt | 2 + .../SourceFiles/lang/translate_provider.cpp | 18 ++ .../lang/translate_url_provider.cpp | 247 ++++++++++++++++++ .../SourceFiles/lang/translate_url_provider.h | 17 ++ 4 files changed, 284 insertions(+) create mode 100644 Telegram/SourceFiles/lang/translate_url_provider.cpp create mode 100644 Telegram/SourceFiles/lang/translate_url_provider.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index a1ad8e4912..b5b4c7436a 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1239,6 +1239,8 @@ PRIVATE lang/translate_mtproto_provider.h lang/translate_provider.cpp lang/translate_provider.h + lang/translate_url_provider.cpp + lang/translate_url_provider.h layout/layout_document_generic_preview.cpp layout/layout_document_generic_preview.h layout/layout_item_base.cpp diff --git a/Telegram/SourceFiles/lang/translate_provider.cpp b/Telegram/SourceFiles/lang/translate_provider.cpp index 15b79c5b99..454c54aad9 100644 --- a/Telegram/SourceFiles/lang/translate_provider.cpp +++ b/Telegram/SourceFiles/lang/translate_provider.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "lang/translate_provider.h" +#include "base/options.h" #include "core/application.h" #include "core/core_settings.h" #include "data/data_msg_id.h" @@ -14,12 +15,29 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "history/history_item.h" #include "lang/translate_mtproto_provider.h" +#include "lang/translate_url_provider.h" #include "platform/platform_translate_provider.h" +namespace { + +base::options::option OptionTranslateUrlTemplate({ + .id = "translate-url-template", + .name = "Translate URL template", + .description = "Template URL for custom translation provider." + " Supports %q text, %f source language and %t target language.", +}); + +} // namespace + namespace Ui { std::unique_ptr CreateTranslateProvider( not_null session) { + const auto urlTemplate = OptionTranslateUrlTemplate.value(); + if (!urlTemplate.isEmpty() + && urlTemplate.contains(u"%q"_q)) { + return CreateUrlTranslateProvider(urlTemplate); + } if (Core::App().settings().usePlatformTranslation() && Platform::IsTranslateProviderAvailable()) { return Platform::CreateTranslateProvider(); diff --git a/Telegram/SourceFiles/lang/translate_url_provider.cpp b/Telegram/SourceFiles/lang/translate_url_provider.cpp new file mode 100644 index 0000000000..7d4c137caa --- /dev/null +++ b/Telegram/SourceFiles/lang/translate_url_provider.cpp @@ -0,0 +1,247 @@ +/* +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 "lang/translate_url_provider.h" + +#include "spellcheck/platform/platform_language.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Ui { +namespace { + +[[nodiscard]] bool SkipJsonKey(const QString &key) { + return (key.compare(u"code"_q, Qt::CaseInsensitive) == 0); +} + +[[nodiscard]] QString DetectFromLanguage(const QString &text) { +#ifndef TDESKTOP_DISABLE_SPELLCHECK + const auto result = Platform::Language::Recognize(text); + return result.known() ? result.twoLetterCode() : u"auto"_q; +#else // TDESKTOP_DISABLE_SPELLCHECK + return u"auto"_q; +#endif // !TDESKTOP_DISABLE_SPELLCHECK +} + +[[nodiscard]] QString JsonValueToText(const QJsonValue &v) { + switch (v.type()) { + case QJsonValue::Null: return u"null"_q; + case QJsonValue::Bool: return v.toBool() + ? u"true"_q + : u"false"_q; + case QJsonValue::Double: return QString::number(v.toDouble(), 'g', 15); + case QJsonValue::String: return v.toString().trimmed(); + case QJsonValue::Array: return QString::fromUtf8( + QJsonDocument(v.toArray()).toJson(QJsonDocument::Compact)); + case QJsonValue::Object: return QString::fromUtf8( + QJsonDocument(v.toObject()).toJson(QJsonDocument::Compact)); + case QJsonValue::Undefined: return QString(); + } + return QString(); +} + +[[nodiscard]] std::optional ParseSegmentedArrayResponse( + const QJsonDocument &parsed) { + if (!parsed.isArray()) { + return std::nullopt; + } + const auto root = parsed.array(); + if (root.isEmpty() || !root[0].isArray()) { + return std::nullopt; + } + const auto segments = root[0].toArray(); + auto translated = QString(); + for (const auto &segmentValue : segments) { + if (!segmentValue.isArray()) { + return std::nullopt; + } + const auto segment = segmentValue.toArray(); + if (segment.isEmpty()) { + return std::nullopt; + } + if (!segment[0].isString()) { + return std::nullopt; + } + translated += segment[0].toString(); + } + if (translated.trimmed().isEmpty()) { + return std::nullopt; + } + return translated; +} + +struct JsonLine { + QString name; + QString value; + int length = 0; +}; + +void CollectJsonLines( + const QString &name, + const QJsonValue &value, + std::vector &lines) { + if (value.isObject()) { + const auto object = value.toObject(); + for (auto i = object.constBegin(); i != object.constEnd(); ++i) { + if (SkipJsonKey(i.key())) { + continue; + } + CollectJsonLines( + name.isEmpty() ? i.key() : (name + '.' + i.key()), + i.value(), + lines); + } + return; + } + if (value.isArray()) { + const auto array = value.toArray(); + for (auto i = 0; i != array.size(); ++i) { + CollectJsonLines( + u"%1[%2]"_q.arg(name).arg(i), + array.at(i), + lines); + } + return; + } + const auto text = JsonValueToText(value); + if (text.isEmpty()) { + return; + } + lines.push_back(JsonLine{ + .name = name, + .value = text, + .length = int(text.size()), + }); +} + +[[nodiscard]] std::optional FormatJsonResponse( + const QByteArray &body) { + auto error = QJsonParseError(); + const auto parsed = QJsonDocument::fromJson(body, &error); + if (error.error != QJsonParseError::NoError) { + return std::nullopt; + } + if (const auto parsedArray = ParseSegmentedArrayResponse(parsed)) { + return parsedArray; + } + auto lines = std::vector(); + if (parsed.isObject()) { + const auto object = parsed.object(); + for (auto i = object.constBegin(); i != object.constEnd(); ++i) { + if (SkipJsonKey(i.key())) { + continue; + } + CollectJsonLines(i.key(), i.value(), lines); + } + } else if (parsed.isArray()) { + const auto array = parsed.array(); + for (auto i = 0; i != array.size(); ++i) { + CollectJsonLines(u"[%1]"_q.arg(i), array.at(i), lines); + } + } + if (lines.empty()) { + return QString::fromUtf8(body); + } + ranges::sort(lines, [](const JsonLine &a, const JsonLine &b) { + return (a.length != b.length) + ? (a.length > b.length) + : (a.name < b.name); + }); + auto result = QString(); + result.reserve(lines.size() * 16); + for (auto i = 0; i != int(lines.size()); ++i) { + const auto &line = lines[i]; + if (!line.name.isEmpty()) { + result += line.name; + result += '\n'; + } + result += line.value; + if (i + 1 != int(lines.size())) { + result += "\n\n"; + } + } + return result; +} + +class UrlTranslateProvider final : public TranslateProvider { +public: + explicit UrlTranslateProvider(QString urlTemplate) + : _urlTemplate(std::move(urlTemplate)) { + } + + [[nodiscard]] bool supportsMessageId() const override { + return false; + } + + void request( + TranslateProviderRequest request, + LanguageId to, + Fn done) override { + if (request.text.text.isEmpty()) { + done(TranslateProviderResult{ + .error = TranslateProviderError::Unknown, + }); + return; + } + auto url = _urlTemplate; + const auto from = DetectFromLanguage(request.text.text); + const auto toCode = to.twoLetterCode(); + url.replace( + u"%q"_q, + QString::fromLatin1( + QUrl::toPercentEncoding(request.text.text.toHtmlEscaped()))); + url.replace( + u"%f"_q, + QString::fromLatin1(QUrl::toPercentEncoding(from))); + url.replace( + u"%t"_q, + QString::fromLatin1(QUrl::toPercentEncoding(toCode))); + const auto requestUrl = QUrl(url); + if (!requestUrl.isValid()) { + done(TranslateProviderResult{ + .error = TranslateProviderError::Unknown, + }); + return; + } + auto networkRequest = QNetworkRequest(requestUrl); + const auto reply = _network.get(networkRequest); + QObject::connect(reply, &QNetworkReply::finished, [=] { + auto result = TranslateProviderResult(); + if (reply->error() != QNetworkReply::NoError) { + result.error = TranslateProviderError::Unknown; + } else { + const auto body = reply->readAll(); + const auto formatted = FormatJsonResponse(body).value_or( + QString::fromUtf8(body)); + result.text = TextWithEntities{ formatted }; + } + done(std::move(result)); + reply->deleteLater(); + }); + } + +private: + const QString _urlTemplate; + QNetworkAccessManager _network; + +}; + +} // namespace + +std::unique_ptr CreateUrlTranslateProvider( + QString urlTemplate) { + return std::make_unique(std::move(urlTemplate)); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/lang/translate_url_provider.h b/Telegram/SourceFiles/lang/translate_url_provider.h new file mode 100644 index 0000000000..8bb1efbb03 --- /dev/null +++ b/Telegram/SourceFiles/lang/translate_url_provider.h @@ -0,0 +1,17 @@ +/* +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 "translate_provider.h" + +namespace Ui { + +[[nodiscard]] std::unique_ptr CreateUrlTranslateProvider( + QString urlTemplate); + +} // namespace Ui From d4a5bb788c72c931f0c3d16b69733f4dd67931ee Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 3 Mar 2026 14:25:34 +0300 Subject: [PATCH 011/415] Moved out some classes from lib_translate. --- .../lang/translate_mtproto_provider.cpp | 27 ++++++++++++------- .../SourceFiles/lang/translate_provider.cpp | 8 +++--- .../SourceFiles/lang/translate_provider.h | 3 +++ .../platform/mac/translate_provider_mac.mm | 5 +--- Telegram/lib_translate | 2 +- 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/Telegram/SourceFiles/lang/translate_mtproto_provider.cpp b/Telegram/SourceFiles/lang/translate_mtproto_provider.cpp index 1c487411df..4525425ed8 100644 --- a/Telegram/SourceFiles/lang/translate_mtproto_provider.cpp +++ b/Telegram/SourceFiles/lang/translate_mtproto_provider.cpp @@ -9,8 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_text_entities.h" #include "data/data_peer.h" +#include "data/data_session.h" #include "main/main_session.h" #include "mtproto/sender.h" +#include "spellcheck/platform/platform_language.h" namespace Ui { namespace { @@ -18,7 +20,8 @@ namespace { class MTProtoTranslateProvider final : public TranslateProvider { public: explicit MTProtoTranslateProvider(not_null session) - : _api(&session->mtp()) { + : _session(session) + , _api(&session->mtp()) { } [[nodiscard]] bool supportsMessageId() const override { @@ -29,13 +32,18 @@ public: TranslateProviderRequest request, LanguageId to, Fn done) override { + const auto msgId = MsgId(request.msgId); + const auto peerId = PeerId(PeerIdHelper(request.peerId)); + const auto peer = msgId + ? _session->data().peerLoaded(peerId) + : nullptr; using Flag = MTPmessages_TranslateText::Flag; - const auto flags = request.msgId + const auto flags = msgId ? (Flag::f_peer | Flag::f_id) : !request.text.text.isEmpty() ? Flag::f_text : Flag(0); - if (!flags) { + if (!flags || (msgId && !peer)) { done(TranslateProviderResult{ .error = TranslateProviderError::Unknown, }); @@ -43,16 +51,16 @@ public: } _api.request(MTPmessages_TranslateText( MTP_flags(flags), - request.msgId ? request.peer->input() : MTP_inputPeerEmpty(), - (request.msgId - ? MTP_vector(1, MTP_int(request.msgId)) + msgId ? peer->input() : MTP_inputPeerEmpty(), + (msgId + ? MTP_vector(1, MTP_int(msgId.bare)) : MTPVector()), - (request.msgId + (msgId ? MTPVector() : MTP_vector(1, MTP_textWithEntities( MTP_string(request.text.text), Api::EntitiesToMTP( - &request.peer->session(), + _session, request.text.entities, Api::ConvertOption::SkipLocal)))), MTP_string(to.twoLetterCode()) @@ -65,7 +73,7 @@ public: } : TranslateProviderResult{ .text = Api::ParseTextWithEntities( - &request.peer->session(), + _session, list.front()), }); }).fail([=](const MTP::Error &) { @@ -76,6 +84,7 @@ public: } private: + const not_null _session; MTP::Sender _api; }; diff --git a/Telegram/SourceFiles/lang/translate_provider.cpp b/Telegram/SourceFiles/lang/translate_provider.cpp index 454c54aad9..5f4bba6b6f 100644 --- a/Telegram/SourceFiles/lang/translate_provider.cpp +++ b/Telegram/SourceFiles/lang/translate_provider.cpp @@ -51,16 +51,16 @@ TranslateProviderRequest PrepareTranslateProviderRequest( MsgId msgId, TextWithEntities text) { auto result = TranslateProviderRequest{ - .peer = peer, - .msgId = IsServerMsgId(msgId) ? msgId : MsgId(), + .peerId = int64(peer->id.value), + .msgId = IsServerMsgId(msgId) ? msgId.bare : 0, .text = std::move(text), }; if (provider->supportsMessageId()) { return result; } if (result.msgId) { - if (const auto item = peer->owner().message(peer, result.msgId)) { - result.text = item->originalText(); + if (const auto i = peer->owner().message(peer, MsgId(result.msgId))) { + result.text = i->originalText(); } result.msgId = 0; } diff --git a/Telegram/SourceFiles/lang/translate_provider.h b/Telegram/SourceFiles/lang/translate_provider.h index 2902207656..e420c260af 100644 --- a/Telegram/SourceFiles/lang/translate_provider.h +++ b/Telegram/SourceFiles/lang/translate_provider.h @@ -9,6 +9,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include +class PeerData; +struct MsgId; + namespace Main { class Session; } // namespace Main diff --git a/Telegram/SourceFiles/platform/mac/translate_provider_mac.mm b/Telegram/SourceFiles/platform/mac/translate_provider_mac.mm index 7e9f7f779a..f686dc7771 100644 --- a/Telegram/SourceFiles/platform/mac/translate_provider_mac.mm +++ b/Telegram/SourceFiles/platform/mac/translate_provider_mac.mm @@ -8,12 +8,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "platform/mac/translate_provider_mac.h" #include "base/weak_ptr.h" +#include "spellcheck/platform/platform_language.h" #include "translate_provider_mac_swift_bridge.h" -#include -#include -#include - namespace Platform { namespace { diff --git a/Telegram/lib_translate b/Telegram/lib_translate index ed5bbec209..e65fc00f72 160000 --- a/Telegram/lib_translate +++ b/Telegram/lib_translate @@ -1 +1 @@ -Subproject commit ed5bbec209451bd56237783e783fdadfb3c1fdd2 +Subproject commit e65fc00f723cec2bae1927ed8d45fd7067025e3a From f1bf8e554ebb56709ae055d755dd3ffa8c1375fd Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 3 Mar 2026 14:44:56 +0300 Subject: [PATCH 012/415] Fixed display of show original button in translate box. --- .../boxes/translate_box_content.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Telegram/SourceFiles/boxes/translate_box_content.cpp b/Telegram/SourceFiles/boxes/translate_box_content.cpp index daf204eee3..1e86c1d585 100644 --- a/Telegram/SourceFiles/boxes/translate_box_content.cpp +++ b/Telegram/SourceFiles/boxes/translate_box_content.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/loading_element.h" #include "ui/layers/generic_box.h" #include "ui/painter.h" +#include "ui/rect.h" #include "ui/power_saving.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" @@ -84,14 +85,16 @@ void TranslateBoxContent( const auto text = std::move(args.text); const auto hasCopyRestriction = args.hasCopyRestriction; const auto textContext = std::move(args.textContext); - const auto chooseTo = std::make_shared>(std::move(args.chooseTo)); + const auto chooseTo = std::make_shared>( + std::move(args.chooseTo)); const auto request = std::make_shared< Fn)>>( std::move(args.request)); auto to = std::move(args.to) | rpl::start_spawning(box->lifetime()); const auto toTitle = rpl::duplicate(to) | rpl::map(LanguageName); - const auto toDirection = rpl::duplicate(to) | rpl::map([=](LanguageId id) { + const auto toDirection = rpl::duplicate(to) | rpl::map([=]( + LanguageId id) { return id.locale().textDirection() == Qt::RightToLeft; }); @@ -133,11 +136,12 @@ void TranslateBoxContent( width - show->width() - st::boxRowPadding.right(), rect.y() + std::abs(lineHeight - show->height()) / 2); }, show->lifetime()); - original->entity()->heightValue( - ) | rpl::filter([](int height) { - return height > 0; - }) | rpl::take(1) | rpl::on_next([=](int height) { - if (height > lineHeight) { + container->widthValue( + ) | rpl::filter([](int width) { + return width > 0; + }) | rpl::take(1) | rpl::on_next([=](int width) { + if (original->entity()->textMaxWidth() + > (width - rect::m::sum::h(st::boxRowPadding))) { show->show(anim::type::instant); } }, show->lifetime()); From 66a5e67cbfac353c475e1bbb3797d3a590dfa28d Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 4 Mar 2026 11:08:19 +0300 Subject: [PATCH 013/415] Fixed losing of extension when rename image from clipboard. --- Telegram/SourceFiles/boxes/send_files_box.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index 487a33007b..58ed48001e 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -128,6 +128,9 @@ void RenameFileBox( rpl::single(QString()), currentName)); const auto extension = [&] { + if (currentName.isEmpty()) { + return u".png"_q; + } const auto dot = currentName.lastIndexOf('.'); return (dot >= 0) ? currentName.mid(dot) : QString(); }(); From 1782f5c463db400200114d647eeec70bbbda7edb Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 4 Mar 2026 08:41:48 +0000 Subject: [PATCH 014/415] Add experimental sticker size option --- .../history/view/media/history_view_sticker.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp index 5f4d999a48..109cf7ce9b 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/media/history_view_sticker.h" +#include "base/options.h" #include "boxes/sticker_set_box.h" #include "history/history.h" #include "history/history_item_components.h" @@ -46,6 +47,11 @@ constexpr auto kPremiumMultiplier = (1 + 0.245 * 2); constexpr auto kEmojiMultiplier = 3; constexpr auto kMessageEffectMultiplier = 2; +base::options::option OptionStickerSize({ + .id = "sticker-size", + .name = "Sticker size", +}); + [[nodiscard]] QImage CacheDiceImage( const QString &emoji, int index, @@ -192,7 +198,9 @@ bool Sticker::readyToDrawAnimationFrame() { } QSize Sticker::Size() { - const auto side = std::min(st::maxStickerSize, kMaxSizeFixed); + const auto side = OptionStickerSize.value() > 0 + ? style::ConvertScale(OptionStickerSize.value()) + : std::min(st::maxStickerSize, kMaxSizeFixed); return { side, side }; } From 25026d165749b1ec85209bfe893182894246548a Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 4 Mar 2026 07:58:22 +0000 Subject: [PATCH 015/415] Support Crow Translate on Linux --- Telegram/CMakeLists.txt | 1 + Telegram/Resources/langs/lang.strings | 1 + Telegram/SourceFiles/boxes/language_box.cpp | 16 ++-- .../linux/translate_provider_linux.cpp | 79 +++++++++++++++++++ .../platform/linux/translate_provider_linux.h | 12 --- 5 files changed, 91 insertions(+), 18 deletions(-) create mode 100644 Telegram/SourceFiles/platform/linux/translate_provider_linux.cpp diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index b5b4c7436a..8401e42790 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1453,6 +1453,7 @@ PRIVATE platform/linux/overlay_widget_linux.h platform/linux/specific_linux.cpp platform/linux/specific_linux.h + platform/linux/translate_provider_linux.cpp platform/linux/translate_provider_linux.h platform/linux/tray_linux.cpp platform/linux/tray_linux.h diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 931ea862bd..03d3c6585c 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -6780,6 +6780,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_translate_settings_show" = "Show Translate Button"; "lng_translate_settings_use_platform_mac" = "Use Apple Translations"; "lng_translate_settings_use_platform_mac_about" = "Translation on macOS won't work until you download local language packs in System Settings."; +"lng_translate_settings_use_platform_linux" = "Use KDE's Crow Translate"; "lng_translate_settings_chat" = "Translate Entire Chats"; "lng_translate_settings_choose" = "Do Not Translate"; "lng_translate_settings_about" = "The 'Translate' button will appear in the context menu of messages containing text."; diff --git a/Telegram/SourceFiles/boxes/language_box.cpp b/Telegram/SourceFiles/boxes/language_box.cpp index 1a3e9935b8..c9b9b22a52 100644 --- a/Telegram/SourceFiles/boxes/language_box.cpp +++ b/Telegram/SourceFiles/boxes/language_box.cpp @@ -1219,7 +1219,7 @@ void LanguageBox::setupTop(not_null container) { Core::App().saveSettingsDelayed(); }, translateEnabled->lifetime()); - if (Platform::IsMac() && Platform::IsTranslateProviderAvailable()) { + if (Platform::IsTranslateProviderAvailable()) { const auto platformTranslateWrap = container->add( object_ptr>( container, @@ -1231,7 +1231,9 @@ void LanguageBox::setupTop(not_null container) { const auto platformTranslateEnabled = platformTranslateWrap->entity()->add( object_ptr( platformTranslateWrap->entity(), - tr::lng_translate_settings_use_platform_mac(), + Platform::IsMac() + ? tr::lng_translate_settings_use_platform_mac() + : tr::lng_translate_settings_use_platform_linux(), st::settingsButtonNoIcon))->toggleOn( rpl::single( Core::App().settings().usePlatformTranslation())); @@ -1243,10 +1245,12 @@ void LanguageBox::setupTop(not_null container) { Core::App().settings().setUsePlatformTranslation(checked); Core::App().saveSettingsDelayed(); }, platformTranslateEnabled->lifetime()); - Ui::AddSkip(platformTranslateWrap->entity()); - Ui::AddDividerText( - platformTranslateWrap->entity(), - tr::lng_translate_settings_use_platform_mac_about()); + if (Platform::IsMac()) { + Ui::AddSkip(platformTranslateWrap->entity()); + Ui::AddDividerText( + platformTranslateWrap->entity(), + tr::lng_translate_settings_use_platform_mac_about()); + } } using namespace rpl::mappers; diff --git a/Telegram/SourceFiles/platform/linux/translate_provider_linux.cpp b/Telegram/SourceFiles/platform/linux/translate_provider_linux.cpp new file mode 100644 index 0000000000..81bc599c5d --- /dev/null +++ b/Telegram/SourceFiles/platform/linux/translate_provider_linux.cpp @@ -0,0 +1,79 @@ +/* +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 "platform/mac/translate_provider_mac.h" + +#include "spellcheck/platform/platform_language.h" + +#include +#include +#include + +namespace Platform { +namespace { + +[[nodiscard]] QString Command() { + const auto commands = { + u"crow"_q, + u"org.kde.CrowTranslate"_q, + }; + const auto it = ranges::find_if(commands, [](const auto &command) { + return !QStandardPaths::findExecutable(command).isEmpty(); + }); + return it != end(commands) ? *it : QString(); +} + +class TranslateProvider final : public Ui::TranslateProvider { +public: + [[nodiscard]] bool supportsMessageId() const override { + return false; + } + + void request( + Ui::TranslateProviderRequest request, + LanguageId to, + Fn done) override { + const auto from = Platform::Language::Recognize(request.text.text); + _process.setProgram(Command()); + _process.setArguments( + QStringList{ u"-i"_q, u"-b"_q, u"-t"_q, to.twoLetterCode() } + + (from.known() + ? QStringList{ u"-s"_q, from.twoLetterCode() } + : QStringList())); + QObject::connect(&_process, &QProcess::finished, [=] { + _document.setHtml(_process.readAllStandardOutput()); + const auto text = _document.toPlainText(); + done(!text.isEmpty() + ? Ui::TranslateProviderResult{ + .text = TextWithEntities{ .text = text }, + } + : Ui::TranslateProviderResult{ + .error = Ui::TranslateProviderError::Unknown, + } + ); + }); + _process.start(); + _process.write(request.text.text.toUtf8()); + _process.closeWriteChannel(); + } + +private: + QProcess _process; + QTextDocument _document; +}; + +} // namespace + +std::unique_ptr CreateTranslateProvider() { + return std::make_unique(); +} + +bool IsTranslateProviderAvailable() { + return !Command().isEmpty(); +} + +} // namespace Platform diff --git a/Telegram/SourceFiles/platform/linux/translate_provider_linux.h b/Telegram/SourceFiles/platform/linux/translate_provider_linux.h index 8fed093cb4..8e7ad9a263 100644 --- a/Telegram/SourceFiles/platform/linux/translate_provider_linux.h +++ b/Telegram/SourceFiles/platform/linux/translate_provider_linux.h @@ -8,15 +8,3 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "platform/platform_translate_provider.h" - -namespace Platform { - -inline bool IsTranslateProviderAvailable() { - return false; -} - -inline std::unique_ptr CreateTranslateProvider() { - return nullptr; -} - -} // namespace Platform From f06ca5a07c8cfb864f3b54a7827e658e405a49c2 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 4 Mar 2026 13:13:40 +0400 Subject: [PATCH 016/415] Force a newline before ``` in markdown parsing. Fixes #27739. --- Telegram/lib_ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/lib_ui b/Telegram/lib_ui index ac1c811cd7..5e3b43565e 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit ac1c811cd7c76a1d3ff1a199173d30d7f366b319 +Subproject commit 5e3b43565eaadff8146373881316537a76cd6df7 From b84345c0831ac86ed3e306b88955fbbe3c11b560 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Mon, 2 Mar 2026 14:26:34 +0400 Subject: [PATCH 017/415] Update GCC to 15 in Docker --- Telegram/build/docker/centos_env/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 06b31ebc52..a1969f5a70 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -2,7 +2,7 @@ FROM rockylinux:8 AS builder ENV LANG=C.UTF-8 -ENV TOOLSET=gcc-toolset-14 +ENV TOOLSET=gcc-toolset-15 ENV PATH=/opt/rh/$TOOLSET/root/usr/bin:$PATH ENV LIBRARY_PATH=/opt/rh/$TOOLSET/root/usr/lib64:/opt/rh/$TOOLSET/root/usr/lib:/usr/local/lib64:/usr/local/lib:/lib64:/lib:/usr/lib64:/usr/lib ENV LD_LIBRARY_PATH=$LIBRARY_PATH From f68db94c8104b27076b8762f1ac36ae285524774 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 4 Mar 2026 09:35:25 +0000 Subject: [PATCH 018/415] Qt 6.10.2 -> 6.11.0-rc1 --- Telegram/build/docker/centos_env/Dockerfile | 6 +++--- Telegram/build/prepare/prepare.py | 4 ++-- Telegram/build/qt_version.py | 2 +- snap/snapcraft.yaml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index a1969f5a70..b18b5dd522 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -93,7 +93,7 @@ FROM builder AS patches RUN git init patches \ && cd patches \ && git remote add origin https://github.com/desktop-app/patches.git \ - && git fetch --depth=1 origin bfbc264ea8a9b39405f2f0ec549481bbd39d1b91 \ + && git fetch --depth=1 origin 4519c85c924b9da81f29d4aac045886f896ee479 \ && git reset --hard FETCH_HEAD \ && rm -rf .git @@ -737,8 +737,8 @@ COPY --link --from=xcb-cursor /usr/src/xcb-cursor-cache / COPY --link --from=openssl /usr/src/openssl-cache / COPY --link --from=xkbcommon /usr/src/xkbcommon-cache / -ENV QT=6.10.2 -RUN git clone -b v$QT --depth=1 https://github.com/qt/qt5.git \ +ENV QT=6.11.0 +RUN git clone -b v$QT-rc1 --depth=1 https://github.com/qt/qt5.git \ && cd qt5 \ && git submodule update --init --recursive --depth=1 qtbase qtdeclarative qtwayland qtimageformats qtsvg qtshadertools \ && cd qtbase \ diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index 7e436d8df4..6547418cf9 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -452,7 +452,7 @@ if customRunCommand: stage('patches', """ git clone https://github.com/desktop-app/patches.git cd patches - git checkout bfbc264ea8a9b39405f2f0ec549481bbd39d1b91 + git checkout 4519c85c924b9da81f29d4aac045886f896ee479 """) stage('msys64', """ @@ -1580,7 +1580,7 @@ mac: make install """) else: # qt > '6' - branch = 'v$QT' + ('-lts-lgpl' if qt.startswith('6.2.') else '') + branch = 'v$QT' + ('-lts-lgpl' if qt.startswith('6.2.') else '-rc1') stage('qt_' + qt, """ git clone -b """ + branch + """ https://github.com/qt/qt5.git qt_$QT cd qt_$QT diff --git a/Telegram/build/qt_version.py b/Telegram/build/qt_version.py index f3678ed185..77b0668e7c 100644 --- a/Telegram/build/qt_version.py +++ b/Telegram/build/qt_version.py @@ -6,7 +6,7 @@ def resolve(arch): elif sys.platform == 'win32': if arch == 'arm' or 'qt6' in sys.argv: print('Choosing Qt 6.') - os.environ['QT'] = '6.10.2' + os.environ['QT'] = '6.11.0' else: print('Choosing Qt 5.') os.environ['QT'] = '5.15.18' diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index ff548488cd..065c59f5e7 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -122,7 +122,7 @@ parts: patches: source: https://github.com/desktop-app/patches.git source-depth: 1 - source-commit: b8c3a8ffc1514861837b9213383133c63023e0de + source-commit: 4519c85c924b9da81f29d4aac045886f896ee479 plugin: nil override-pull: | craftctl default @@ -403,7 +403,7 @@ parts: qt: source: https://github.com/qt/qt5.git source-depth: 1 - source-tag: v6.10.2 + source-tag: v6.11.0-rc1 source-submodules: - qtbase - qtdeclarative From d9ddb125007f7772a08dbcb8f4ff565f0d3fa1ab Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Sat, 28 Feb 2026 22:57:39 +0000 Subject: [PATCH 019/415] Avoid using streaming loader with external video player Streaming loader is unnecessary slow for this use-case Co-authored-by: Codex --- Telegram/SourceFiles/data/data_document.cpp | 33 +++++++++------- Telegram/SourceFiles/data/data_document.h | 6 ++- .../SourceFiles/data/data_document_media.cpp | 2 +- Telegram/SourceFiles/data/data_photo.cpp | 3 +- Telegram/SourceFiles/data/data_photo.h | 3 +- Telegram/SourceFiles/data/data_streaming.cpp | 39 ++++++++++++++++--- Telegram/SourceFiles/data/data_streaming.h | 2 + Telegram/SourceFiles/iv/iv_instance.cpp | 5 ++- 8 files changed, 68 insertions(+), 25 deletions(-) diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp index 7df823b626..f97129f351 100644 --- a/Telegram/SourceFiles/data/data_document.cpp +++ b/Telegram/SourceFiles/data/data_document.cpp @@ -547,7 +547,7 @@ void DocumentData::setVideoQualities( return document->isVideoFile() && !document->dimensions.isEmpty() && !document->inappPlaybackFailed() - && document->useStreamingLoader() + && document->useStreamingLoader(nullptr) && document->canBeStreamed(nullptr); }; ranges::sort( @@ -1533,29 +1533,35 @@ bool DocumentData::hasRemoteLocation() const { return (_dc != 0 && _access != 0); } -bool DocumentData::useStreamingLoader() const { +bool DocumentData::canVideoBeStreamed(HistoryItem *item) const { + if (!isVideoFile()) { + return false; + } + // Streaming couldn't be used with external player + // Maybe someone brave will implement this once upon a time... + static const auto &ExternalVideoPlayer = base::options::lookup( + Data::kOptionExternalVideoPlayer); + return storyMedia() + || !ExternalVideoPlayer.value() + || (item && !item->allowsForward()); +} + +bool DocumentData::useStreamingLoader(HistoryItem *item) const { if (size <= 0) { return false; } else if (const auto info = sticker()) { return info->isWebm(); } return isAnimation() - || isVideoFile() + || canVideoBeStreamed(item) || isAudioFile() || isVoiceMessage(); } bool DocumentData::canBeStreamed(HistoryItem *item) const { - // Streaming couldn't be used with external player - // Maybe someone brave will implement this once upon a time... - static const auto &ExternalVideoPlayer = base::options::lookup( - Data::kOptionExternalVideoPlayer); return hasRemoteLocation() && supportsStreaming() - && (!isVideoFile() - || storyMedia() - || !ExternalVideoPlayer.value() - || (item && !item->allowsForward())); + && (!isVideoFile() || canVideoBeStreamed(item)); } void DocumentData::setInappPlaybackFailed() { @@ -1585,9 +1591,10 @@ StorageFileLocation DocumentData::videoPreloadLocation() const { auto DocumentData::createStreamingLoader( Data::FileOrigin origin, - bool forceRemoteLoader) const + bool forceRemoteLoader, + HistoryItem *item) const -> std::unique_ptr { - if (!useStreamingLoader()) { + if (!useStreamingLoader(item)) { return nullptr; } if (!forceRemoteLoader) { diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h index b0918a37c8..6fd2ba0ed8 100644 --- a/Telegram/SourceFiles/data/data_document.h +++ b/Telegram/SourceFiles/data/data_document.h @@ -294,9 +294,10 @@ public: [[nodiscard]] bool canBeStreamed(HistoryItem *item) const; [[nodiscard]] auto createStreamingLoader( Data::FileOrigin origin, - bool forceRemoteLoader) const + bool forceRemoteLoader, + HistoryItem *item) const -> std::unique_ptr; - [[nodiscard]] bool useStreamingLoader() const; + [[nodiscard]] bool useStreamingLoader(HistoryItem *item) const; void setInappPlaybackFailed(); [[nodiscard]] bool inappPlaybackFailed() const; @@ -358,6 +359,7 @@ private: friend class Serialize::Document; [[nodiscard]] LocationType locationType() const; + [[nodiscard]] bool canVideoBeStreamed(HistoryItem *item) const; void validateLottieSticker(); void setMaybeSupportsStreaming(bool supports); void setLoadedInMediaCacheLocation(); diff --git a/Telegram/SourceFiles/data/data_document_media.cpp b/Telegram/SourceFiles/data/data_document_media.cpp index 6bf6bc22f1..045c894216 100644 --- a/Telegram/SourceFiles/data/data_document_media.cpp +++ b/Telegram/SourceFiles/data/data_document_media.cpp @@ -360,7 +360,7 @@ float64 DocumentMedia::progress() const { bool DocumentMedia::canBePlayed(HistoryItem *item) const { return !_owner->inappPlaybackFailed() - && _owner->useStreamingLoader() + && _owner->useStreamingLoader(item) && (loaded() || _owner->canBeStreamed(item)); } diff --git a/Telegram/SourceFiles/data/data_photo.cpp b/Telegram/SourceFiles/data/data_photo.cpp index 0a2d37f8f3..8364a3e7ff 100644 --- a/Telegram/SourceFiles/data/data_photo.cpp +++ b/Telegram/SourceFiles/data/data_photo.cpp @@ -565,7 +565,8 @@ bool PhotoData::videoCanBePlayed() const { auto PhotoData::createStreamingLoader( Data::FileOrigin origin, - bool forceRemoteLoader) const + bool forceRemoteLoader, + HistoryItem *item) const -> std::unique_ptr { if (!hasVideo()) { return nullptr; diff --git a/Telegram/SourceFiles/data/data_photo.h b/Telegram/SourceFiles/data/data_photo.h index 3cd749a715..d159364171 100644 --- a/Telegram/SourceFiles/data/data_photo.h +++ b/Telegram/SourceFiles/data/data_photo.h @@ -150,7 +150,8 @@ public: [[nodiscard]] bool videoCanBePlayed() const; [[nodiscard]] auto createStreamingLoader( Data::FileOrigin origin, - bool forceRemoteLoader) const + bool forceRemoteLoader, + HistoryItem *item) const -> std::unique_ptr; [[nodiscard]] bool hasAttachedStickers() const; diff --git a/Telegram/SourceFiles/data/data_streaming.cpp b/Telegram/SourceFiles/data/data_streaming.cpp index 658c83807e..12dcae48bd 100644 --- a/Telegram/SourceFiles/data/data_streaming.cpp +++ b/Telegram/SourceFiles/data/data_streaming.cpp @@ -78,6 +78,15 @@ bool PruneDestroyedAndSet( return {}; } +[[nodiscard]] HistoryItem *LookupContext( + not_null owner, + const FileOrigin &origin) { + if (const auto message = std::get_if(&origin.data)) { + return owner->message(*message); + } + return nullptr; +} + } // namespace Streaming::Streaming(not_null owner) @@ -92,6 +101,7 @@ template base::flat_map, std::weak_ptr> &readers, not_null data, FileOrigin origin, + HistoryItem *context, bool forceRemoteLoader) { const auto i = readers.find(data); if (i != end(readers)) { @@ -101,7 +111,10 @@ template } } } - auto loader = data->createStreamingLoader(origin, forceRemoteLoader); + auto loader = data->createStreamingLoader( + origin, + forceRemoteLoader, + context); if (!loader) { return nullptr; } @@ -133,7 +146,7 @@ template return result; } } - auto reader = sharedReader(readers, data, origin); + auto reader = sharedReader(readers, data, origin, context); if (!reader) { return nullptr; } @@ -175,18 +188,25 @@ std::shared_ptr Streaming::sharedReader( not_null document, FileOrigin origin, bool forceRemoteLoader) { - return sharedReader(_fileReaders, document, origin, forceRemoteLoader); + const auto context = LookupContext(_owner, origin); + return sharedReader( + _fileReaders, + document, + origin, + context, + forceRemoteLoader); } std::shared_ptr Streaming::sharedDocument( not_null document, FileOrigin origin) { + const auto context = LookupContext(_owner, origin); return sharedDocument( _fileDocuments, _fileReaders, document, nullptr, - nullptr, + context, origin); } @@ -208,18 +228,25 @@ std::shared_ptr Streaming::sharedReader( not_null photo, FileOrigin origin, bool forceRemoteLoader) { - return sharedReader(_photoReaders, photo, origin, forceRemoteLoader); + const auto context = LookupContext(_owner, origin); + return sharedReader( + _photoReaders, + photo, + origin, + context, + forceRemoteLoader); } std::shared_ptr Streaming::sharedDocument( not_null photo, FileOrigin origin) { + const auto context = LookupContext(_owner, origin); return sharedDocument( _photoDocuments, _photoReaders, photo, nullptr, - nullptr, + context, origin); } diff --git a/Telegram/SourceFiles/data/data_streaming.h b/Telegram/SourceFiles/data/data_streaming.h index 51a6f18549..6799b0fd79 100644 --- a/Telegram/SourceFiles/data/data_streaming.h +++ b/Telegram/SourceFiles/data/data_streaming.h @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class PhotoData; class DocumentData; +class HistoryItem; namespace Media::Streaming { class Reader; @@ -64,6 +65,7 @@ private: base::flat_map, std::weak_ptr> &readers, not_null data, FileOrigin origin, + HistoryItem *context, bool forceRemoteLoader = false); template diff --git a/Telegram/SourceFiles/iv/iv_instance.cpp b/Telegram/SourceFiles/iv/iv_instance.cpp index 607ab87037..1c6f6df3c9 100644 --- a/Telegram/SourceFiles/iv/iv_instance.cpp +++ b/Telegram/SourceFiles/iv/iv_instance.cpp @@ -476,7 +476,10 @@ void Shown::streamFile( requestFail(std::move(request)); return; } - auto loader = document->createStreamingLoader(fileOrigin(page), false); + auto loader = document->createStreamingLoader( + fileOrigin(page), + false, + nullptr); if (!loader) { if (document->size >= Storage::kMaxFileInMemory) { requestFail(std::move(request)); From a08f8d890d51ac59d53dd0b10474eeb1ee466a99 Mon Sep 17 00:00:00 2001 From: InternalProgramError <47084951+InternalProgramError@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:55:10 +0300 Subject: [PATCH 020/415] Update menu_send.cpp Unification of Send menu (https://bugs.telegram.org/c/55220) --- Telegram/SourceFiles/menu/menu_send.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Telegram/SourceFiles/menu/menu_send.cpp b/Telegram/SourceFiles/menu/menu_send.cpp index 9c4794d5d0..db0c5c25d6 100644 --- a/Telegram/SourceFiles/menu/menu_send.cpp +++ b/Telegram/SourceFiles/menu/menu_send.cpp @@ -738,11 +738,6 @@ FillMenuResult FillSendMenu( ? *iconsOverride : st::defaultComposeIcons; - if (sending && type != Type::Reminder) { - menu->addAction( - tr::lng_send_silent_message(tr::now), - [=] { action({ Api::SendOptions{ .silent = true } }, details); }, - &icons.menuMute); } if (sending && type != Type::SilentOnly) { menu->addAction( @@ -760,6 +755,11 @@ FillMenuResult FillSendMenu( details); }, &icons.menuWhenOnline); } + if (sending && type != Type::Reminder) { + menu->addAction( + tr::lng_send_silent_message(tr::now), + [=] { action({ Api::SendOptions{ .silent = true } }, details); }, + &icons.menuMute); if ((type != Type::Disabled) && ((details.spoiler != SpoilerState::None) From 87caf0e2c45ea9f440fcb63e42bd9e7e5de8a26f Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 4 Mar 2026 11:54:02 +0000 Subject: [PATCH 021/415] Disable 32-bit Qt 6 Windows action builds They don't build anymore --- .github/workflows/win.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index cbdff19e04..b05dc885c9 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -54,6 +54,8 @@ jobs: exclude: - arch: arm64 qt: "" + - arch: x64_x86 + qt: qt6 env: UPLOAD_ARTIFACT: "true" From d804a327aafe56a16d1ba0503b118988f9a62d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Novomesk=C3=BD?= Date: Thu, 19 Feb 2026 15:26:59 +0100 Subject: [PATCH 022/415] Update dav1d, libwebp, libheif, libjxl --- Telegram/build/docker/centos_env/Dockerfile | 9 +++++---- Telegram/build/prepare/prepare.py | 10 ++++++---- snap/snapcraft.yaml | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index b18b5dd522..aa2b2c2a90 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -174,7 +174,7 @@ RUN git clone -b v1.5.2 --depth=1 https://github.com/xiph/opus.git \ && rm -rf opus FROM builder AS dav1d -RUN git clone -b 1.5.1 --depth=1 https://github.com/videolan/dav1d.git \ +RUN git clone -b 1.5.3 --depth=1 https://github.com/videolan/dav1d.git \ && cd dav1d \ && meson build \ --buildtype=plain \ @@ -231,7 +231,7 @@ RUN git init libvpx \ && rm -rf libvpx FROM builder AS webp -RUN git clone -b v1.5.0 --depth=1 https://github.com/webmproject/libwebp.git \ +RUN git clone -b v1.6.0 --depth=1 https://github.com/webmproject/libwebp.git \ && cd libwebp \ && cmake -B build . \ -DWEBP_BUILD_ANIM_UTILS=OFF \ @@ -265,7 +265,7 @@ RUN git clone -b v1.3.0 --depth=1 https://github.com/AOMediaCodec/libavif.git \ FROM builder AS heif COPY --link --from=de265 /usr/src/de265-cache / -RUN git clone -b v1.19.8 --depth=1 https://github.com/strukturag/libheif.git \ +RUN git clone -b v1.21.2 --depth=1 https://github.com/strukturag/libheif.git \ && cd libheif \ && cmake -B build . \ -DBUILD_SHARED_LIBS=OFF \ @@ -275,6 +275,7 @@ RUN git clone -b v1.19.8 --depth=1 https://github.com/strukturag/libheif.git \ -DWITH_X265=OFF \ -DWITH_AOM_DECODER=OFF \ -DWITH_AOM_ENCODER=OFF \ + -DWITH_X264=OFF \ -DWITH_OpenH264_DECODER=OFF \ -DWITH_RAV1E=OFF \ -DWITH_RAV1E_PLUGIN=OFF \ @@ -293,7 +294,7 @@ COPY --link --from=lcms2 /usr/src/lcms2-cache / COPY --link --from=brotli /usr/src/brotli-cache / COPY --link --from=highway /usr/src/highway-cache / -RUN git clone -b v0.11.1 --depth=1 https://github.com/libjxl/libjxl.git \ +RUN git clone -b v0.11.2 --depth=1 https://github.com/libjxl/libjxl.git \ && cd libjxl \ && git submodule update --init --recursive --depth=1 third_party/libjpeg-turbo \ && curl -sSL https://github.com/libjxl/libjxl/commit/ee3955b1553bcc10304d45b85dfef9afa9349d72.patch | sed 's/offset + t/offset + i/' | git apply \ diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index 6547418cf9..5ebba95be6 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -715,7 +715,7 @@ win: # Somehow in x86 Debug build dav1d crashes on AV1 10bpc videos. stage('dav1d', """ - git clone -b 1.5.1 https://code.videolan.org/videolan/dav1d.git + git clone -b 1.5.3 https://code.videolan.org/videolan/dav1d.git cd dav1d win32: SET "TARGET=x86" @@ -899,7 +899,7 @@ mac: """) stage('libwebp', """ - git clone -b v1.5.0 https://github.com/webmproject/libwebp.git + git clone -b v1.6.0 https://github.com/webmproject/libwebp.git cd libwebp win: nmake /f Makefile.vc CFG=debug-static OBJDIR=out RTLIBCFG=static all @@ -938,7 +938,7 @@ mac: """) stage('libheif', """ - git clone -b v1.19.8 https://github.com/strukturag/libheif.git + git clone -b v1.21.2 https://github.com/strukturag/libheif.git cd libheif win: %THIRDPARTY_DIR%\\msys64\\usr\\bin\\sed.exe -i 's/LIBHEIF_EXPORTS/LIBDE265_STATIC_BUILD/g' libheif/CMakeLists.txt @@ -953,6 +953,7 @@ win: -DBUILD_TESTING=OFF ^ -DENABLE_PLUGIN_LOADING=OFF ^ -DWITH_LIBDE265=ON ^ + -DWITH_X264=OFF ^ -DWITH_OpenH264_DECODER=OFF ^ -DWITH_SvtEnc=OFF ^ -DWITH_SvtEnc_PLUGIN=OFF ^ @@ -979,6 +980,7 @@ mac: -D WITH_AOM_ENCODER=OFF \\ -D WITH_AOM_DECODER=OFF \\ -D WITH_X265=OFF \\ + -D WITH_X264=OFF \\ -D WITH_OpenH264_DECODER=OFF \\ -D WITH_SvtEnc=OFF \\ -D WITH_RAV1E=OFF \\ @@ -996,7 +998,7 @@ mac: """) stage('libjxl', """ - git clone -b v0.11.1 --recursive --shallow-submodules https://github.com/libjxl/libjxl.git + git clone -b v0.11.2 --recursive --shallow-submodules https://github.com/libjxl/libjxl.git cd libjxl """ + setVar("cmake_defines", """ -DBUILD_SHARED_LIBS=OFF diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 065c59f5e7..54dc548bad 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -320,7 +320,7 @@ parts: jpegli: source: https://github.com/libjxl/libjxl.git source-depth: 1 - source-tag: v0.11.1 + source-tag: v0.11.2 plugin: cmake build-packages: - curl From 7a9b62fc4076496ed2d4cb9c805d5b31dbc22358 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:19:01 +0000 Subject: [PATCH 023/415] Bump actions/upload-artifact from 6 to 7 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/linux.yml | 2 +- .github/workflows/mac.yml | 2 +- .github/workflows/mac_packaged.yml | 2 +- .github/workflows/snap.yml | 2 +- .github/workflows/win.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 1912beafa8..da7155984f 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -149,7 +149,7 @@ jobs: cd out/Debug mkdir artifact mv {Telegram,Updater} artifact/ - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 if: env.UPLOAD_ARTIFACT == 'true' name: Upload artifact. with: diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 456fc0335a..c80ff4db98 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -125,7 +125,7 @@ jobs: mkdir artifact mv Telegram.app artifact/ mv Updater artifact/ - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 if: env.UPLOAD_ARTIFACT == 'true' name: Upload artifact. with: diff --git a/.github/workflows/mac_packaged.yml b/.github/workflows/mac_packaged.yml index e331d3d596..5153f3492a 100644 --- a/.github/workflows/mac_packaged.yml +++ b/.github/workflows/mac_packaged.yml @@ -176,7 +176,7 @@ jobs: cd $REPO_NAME/build mkdir artifact mv Telegram.app artifact/ - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 if: env.UPLOAD_ARTIFACT == 'true' name: Upload artifact. with: diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index cc96465554..8080e600f5 100644 --- a/.github/workflows/snap.yml +++ b/.github/workflows/snap.yml @@ -79,7 +79,7 @@ jobs: mkdir artifact mv $artifact_name artifact - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 if: env.UPLOAD_ARTIFACT == 'true' name: Upload artifact. with: diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index b05dc885c9..0e36af68c5 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -228,7 +228,7 @@ jobs: mkdir artifact move %OUT%\Telegram.exe artifact/ move %OUT%\Updater.exe artifact/ - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 name: Upload artifact. if: (env.UPLOAD_ARTIFACT == 'true') || (github.ref == 'refs/heads/nightly') with: From 719cae0406daac3d68e7e316b4c78c1ef04a5585 Mon Sep 17 00:00:00 2001 From: cumdev1337 Date: Wed, 4 Mar 2026 02:39:06 +0200 Subject: [PATCH 024/415] building-win: update .sln => .slnx --- docs/building-win.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/building-win.md b/docs/building-win.md index 1e9f7df47b..d333539fc0 100644 --- a/docs/building-win.md +++ b/docs/building-win.md @@ -58,7 +58,7 @@ For `win64` (64-bit): configure.bat x64 -D TDESKTOP_API_ID=YOUR_API_ID -D TDESKTOP_API_HASH=YOUR_API_HASH -* Open ***BuildPath*\\tdesktop\\out\\Telegram.sln** in Visual Studio 2026 +* Open ***BuildPath*\\tdesktop\\out\\Telegram.slnx** in Visual Studio 2026 * Select Telegram project and press Build > Build Telegram (Debug and Release configurations) * The result Telegram.exe will be located in **D:\TBuild\tdesktop\out\Debug** (and **Release**) From f8fc9956d06e114bf95fc95462099baa98bf58c7 Mon Sep 17 00:00:00 2001 From: paterkleomenis Date: Sun, 22 Feb 2026 21:55:39 +0200 Subject: [PATCH 025/415] Calls: enable audio sharing flow on Linux Enables the desktop share audio flow on Linux within Calls UI and plumbing. - calls_call: add withAudio parameter to toggleScreenSharing, track _screenWithAudio flag, create/destroy SystemAudioCapture when sharing starts with audio, and clean it up in destroyController. - calls_panel / calls_group_panel: use LoopbackAudioCaptureSupported() to report audio support on all platforms; show a GenericBox with audio toggle when a unique desktop capture source is available (PipeWire), then start sharing with the chosen audio setting. Depends on desktop-app/lib_webrtc#22 Closes #26642 --- Telegram/Resources/langs/lang.strings | 2 + Telegram/SourceFiles/calls/calls_call.cpp | 40 ++++++++++++++++++- Telegram/SourceFiles/calls/calls_call.h | 10 ++++- Telegram/SourceFiles/calls/calls_panel.cpp | 18 +++++---- .../calls/group/calls_group_common.cpp | 23 +++++++++++ .../calls/group/calls_group_common.h | 4 ++ .../calls/group/calls_group_panel.cpp | 14 ++++--- 7 files changed, 96 insertions(+), 15 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 03d3c6585c..5e9de2d5f9 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -5788,6 +5788,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_call_screen_share_stop" = "Stop Sharing"; "lng_group_call_screen_title" = "Screen {index}"; "lng_group_call_screen_share_audio" = "Share System Audio"; +"lng_group_call_sharing_screen_options" = "Sharing Options"; +"lng_group_call_choose_source" = "Choose Source"; "lng_group_call_unmute" = "Unmute"; "lng_group_call_unmute_sub" = "Hold space bar to temporarily unmute."; "lng_group_call_you_are_live" = "You are Live"; diff --git a/Telegram/SourceFiles/calls/calls_call.cpp b/Telegram/SourceFiles/calls/calls_call.cpp index 1abcf70328..91b46b0665 100644 --- a/Telegram/SourceFiles/calls/calls_call.cpp +++ b/Telegram/SourceFiles/calls/calls_call.cpp @@ -1433,7 +1433,9 @@ void Call::toggleCameraSharing(bool enabled) { }), true); } -void Call::toggleScreenSharing(std::optional uniqueId) { +void Call::toggleScreenSharing( + std::optional uniqueId, + bool withAudio) { if (!uniqueId) { if (isSharingScreen()) { if (_videoCapture) { @@ -1443,13 +1445,20 @@ void Call::toggleScreenSharing(std::optional uniqueId) { } _videoCaptureDeviceId = QString(); _videoCaptureIsScreencast = false; + _screenWithAudio = false; + if (_systemAudioCapture) { + _systemAudioCapture->stop(); + _systemAudioCapture = nullptr; + } return; - } else if (screenSharingDeviceId() == *uniqueId) { + } else if (screenSharingDeviceId() == *uniqueId + && _screenWithAudio == withAudio) { return; } toggleCameraSharing(false); _videoCaptureIsScreencast = true; _videoCaptureDeviceId = *uniqueId; + _screenWithAudio = withAudio; if (_videoCapture) { _videoCapture->switchToDevice(uniqueId->toStdString(), true); if (_instance) { @@ -1457,6 +1466,29 @@ void Call::toggleScreenSharing(std::optional uniqueId) { } } _videoOutgoing->setState(Webrtc::VideoState::Active); + + if (_systemAudioCapture) { + _systemAudioCapture->stop(); + _systemAudioCapture = nullptr; + } + if (withAudio && Webrtc::SystemAudioCaptureSupported()) { + _systemAudioCapture = Webrtc::CreateSystemAudioCapture( + [weak = base::make_weak(this)](std::vector &&samples) { + crl::on_main( + weak, + [weak, samples = std::move(samples)]() mutable { + if (const auto strong = weak.get(); strong + && strong->_instance + && strong->_screenWithAudio) { + strong->_instance->addExternalAudioSamples( + std::move(samples)); + } + }); + }); + if (_systemAudioCapture) { + _systemAudioCapture->start(); + } + } } auto Call::peekVideoCapture() const @@ -1617,6 +1649,10 @@ void Call::handleControllerError(const QString &error) { void Call::destroyController() { _instanceLifetime.destroy(); Core::App().mediaDevices().setCaptureMuteTracker(this, false); + if (_systemAudioCapture) { + _systemAudioCapture->stop(); + _systemAudioCapture = nullptr; + } if (_instance) { _instance->stop([](tgcalls::FinalState) { diff --git a/Telegram/SourceFiles/calls/calls_call.h b/Telegram/SourceFiles/calls/calls_call.h index 41ee5dd9ad..d40d0794c4 100644 --- a/Telegram/SourceFiles/calls/calls_call.h +++ b/Telegram/SourceFiles/calls/calls_call.h @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mtproto/sender.h" #include "mtproto/mtproto_auth_key.h" #include "webrtc/webrtc_device_resolver.h" +#include "webrtc/webrtc_system_audio_capture.h" namespace Data { class GroupCall; @@ -252,7 +253,12 @@ public: [[nodiscard]] QString cameraSharingDeviceId() const; [[nodiscard]] QString screenSharingDeviceId() const; void toggleCameraSharing(bool enabled); - void toggleScreenSharing(std::optional uniqueId); + void toggleScreenSharing( + std::optional uniqueId, + bool withAudio = false); + [[nodiscard]] bool screenSharingWithAudio() const { + return _screenWithAudio; + } [[nodiscard]] auto peekVideoCapture() const -> std::shared_ptr; @@ -366,6 +372,8 @@ private: std::shared_ptr _videoCapture; QString _videoCaptureDeviceId; bool _videoCaptureIsScreencast = false; + bool _screenWithAudio = false; + std::unique_ptr _systemAudioCapture; const std::unique_ptr _videoIncoming; const std::unique_ptr _videoOutgoing; diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp index 97a3c9295b..082cedb1b6 100644 --- a/Telegram/SourceFiles/calls/calls_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -61,6 +61,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/streaming/media_streaming_utility.h" #include "window/main_window.h" #include "window/window_controller.h" +#include "webrtc/webrtc_create_adm.h" #include "webrtc/webrtc_environment.h" #include "webrtc/webrtc_video_track.h" #include "styles/style_calls.h" @@ -401,6 +402,13 @@ void Panel::initControls() { } else if (const auto source = env->uniqueDesktopCaptureSource()) { if (!chooseSourceActiveDeviceId().isEmpty()) { chooseSourceStop(); + } else if (chooseSourceWithAudioSupported()) { + const auto sourceId = *source; + Group::ShowUniqueCaptureOptions( + uiShow(), + crl::guard(this, [=](bool audio) { + chooseSourceAccepted(sourceId, audio); + })); } else { chooseSourceAccepted(*source, false); } @@ -570,15 +578,11 @@ QString Panel::chooseSourceActiveDeviceId() { } bool Panel::chooseSourceActiveWithAudio() { - return false;// _call->screenSharingWithAudio(); + return _call->screenSharingWithAudio(); } bool Panel::chooseSourceWithAudioSupported() { -//#ifdef Q_OS_WIN -// return true; -//#else // Q_OS_WIN - return false; -//#endif // Q_OS_WIN + return Webrtc::LoopbackAudioCaptureSupported(); } rpl::lifetime &Panel::chooseSourceInstanceLifetime() { @@ -595,7 +599,7 @@ rpl::producer Panel::startOutgoingRequests() const { void Panel::chooseSourceAccepted( const QString &deviceId, bool withAudio) { - _call->toggleScreenSharing(deviceId/*, withAudio*/); + _call->toggleScreenSharing(deviceId, withAudio); } void Panel::chooseSourceStop() { diff --git a/Telegram/SourceFiles/calls/group/calls_group_common.cpp b/Telegram/SourceFiles/calls/group/calls_group_common.cpp index 030ba2bdf5..90c1c18b9f 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_common.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_common.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "tde2e/tde2e_integration.h" #include "ui/boxes/boost_box.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" #include "ui/widgets/labels.h" #include "ui/widgets/popup_menu.h" #include "ui/layers/generic_box.h" @@ -77,6 +78,28 @@ object_ptr ScreenSharingPrivacyRequestBox() { #endif // Q_OS_MAC } +void ShowUniqueCaptureOptions( + std::shared_ptr show, + Fn done) { + show->showBox(Box([=](not_null box) { + box->setTitle(tr::lng_group_call_sharing_screen_options()); + const auto withAudio = box->addRow( + object_ptr( + box, + tr::lng_group_call_screen_share_audio(tr::now), + false, + st::groupCallCheckbox)); + box->addButton( + tr::lng_group_call_choose_source(), + [=] { + const auto audio = withAudio->checked(); + box->closeBox(); + done(audio); + }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + })); +} + object_ptr MakeRoundActiveLogo( not_null parent, const style::icon &icon, diff --git a/Telegram/SourceFiles/calls/group/calls_group_common.h b/Telegram/SourceFiles/calls/group/calls_group_common.h index c0b255c498..a75872c4bd 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_common.h +++ b/Telegram/SourceFiles/calls/group/calls_group_common.h @@ -162,6 +162,10 @@ using StickedTooltips = base::flags; [[nodiscard]] object_ptr ScreenSharingPrivacyRequestBox(); +void ShowUniqueCaptureOptions( + std::shared_ptr show, + Fn done); + [[nodiscard]] object_ptr MakeRoundActiveLogo( not_null parent, const style::icon &icon, diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index 4c8fb805d3..fc6112da01 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -65,6 +65,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "webrtc/webrtc_environment.h" #include "webrtc/webrtc_video_track.h" #include "webrtc/webrtc_audio_input_tester.h" +#include "webrtc/webrtc_create_adm.h" #include "styles/style_calls.h" #include "styles/style_layers.h" @@ -356,11 +357,7 @@ bool Panel::chooseSourceActiveWithAudio() { } bool Panel::chooseSourceWithAudioSupported() { -#ifdef Q_OS_WIN - return true; -#else // Q_OS_WIN - return false; -#endif // Q_OS_WIN + return Webrtc::LoopbackAudioCaptureSupported(); } rpl::lifetime &Panel::chooseSourceInstanceLifetime() { @@ -1543,6 +1540,13 @@ void Panel::chooseShareScreenSource() { } else if (const auto source = env->uniqueDesktopCaptureSource()) { if (_call->isSharingScreen()) { _call->toggleScreenSharing(std::nullopt); + } else if (chooseSourceWithAudioSupported()) { + const auto sourceId = *source; + ShowUniqueCaptureOptions( + uiShow(), + crl::guard(this, [=](bool audio) { + chooseSourceAccepted(sourceId, audio); + })); } else { chooseSourceAccepted(*source, false); } From 48e2e2c2db460a58cffef2182d9aeab6792202dd Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Sat, 31 Jan 2026 15:04:36 +0400 Subject: [PATCH 026/415] Avoid saving viewer position unless it's really on screen --- Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index fbd11de2e2..1ca2724b70 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -980,7 +980,7 @@ void OverlayWidget::savePosition() { realPosition.maximized = 1; realPosition.moncrc = 0; DEBUG_LOG(("Viewer Pos: Saving maximized position.")); - } else if (!_wasWindowedMode && !Platform::IsMac()) { + } else if (!_wasWindowedMode) { return; } else { auto r = _normalGeometry = _window->body()->mapToGlobal( From cc7ad543ee3f6c7cdace0fa3ff7c4fcd57b54fa0 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 5 Mar 2026 10:21:35 +0400 Subject: [PATCH 027/415] Update submodule. --- Telegram/lib_ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 5e3b43565e..a4a878a3d7 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 5e3b43565eaadff8146373881316537a76cd6df7 +Subproject commit a4a878a3d7079228da500d5dac9892058a91fed4 From 63a76b273acf9e8725fb3deadf0545e2a723d9fe Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 5 Mar 2026 11:25:07 +0400 Subject: [PATCH 028/415] Fix trailing seconds in date formatting. --- Telegram/SourceFiles/core/ui_integration.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index f92ade2989..ddfa3e25ef 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -195,7 +195,7 @@ const auto kBadPrefix = u"http://"_q; if (flags & FormattedDateFlag::Relative) { return FormatDateRelative(date); } - const auto dateTime = base::unixtime::parse(date); + const auto dateTime = QDateTime::fromSecsSinceEpoch(date); const auto locale = QLocale(); auto parts = QStringList(); const auto hasDayOfWeek = (flags & FormattedDateFlag::DayOfWeek); From c65ab016471d650568a7cb6678cf6a8b81d02d75 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 5 Mar 2026 11:25:19 +0400 Subject: [PATCH 029/415] Fix possible crash in members list dropdown. --- Telegram/lib_ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/lib_ui b/Telegram/lib_ui index a4a878a3d7..5099dadbd3 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit a4a878a3d7079228da500d5dac9892058a91fed4 +Subproject commit 5099dadbd3b2cb479bb9fbdbd9a0d3231a232fe7 From e3689a1f12654fadbfb6070c03201c2089c0a771 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 5 Mar 2026 11:26:46 +0400 Subject: [PATCH 030/415] Update submodules. --- Telegram/lib_webrtc | 2 +- cmake | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Telegram/lib_webrtc b/Telegram/lib_webrtc index 553102f8c2..01e1575326 160000 --- a/Telegram/lib_webrtc +++ b/Telegram/lib_webrtc @@ -1 +1 @@ -Subproject commit 553102f8c244609253720e7a03c2ea2d3c7fee8e +Subproject commit 01e157532633b5a56398f0d974c199a5769fcc39 diff --git a/cmake b/cmake index 66f6a0318c..34de471d6f 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit 66f6a0318cf1fdd91c52b1cb11c638e0b6b6a0a6 +Subproject commit 34de471d6f99077fea60a76d60b2a05ece2abbb1 From f9bf1e937233c4220d17d45e5710d9a8f52c326f Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 5 Mar 2026 11:30:38 +0400 Subject: [PATCH 031/415] Fix the build on Windows. --- Telegram/SourceFiles/menu/menu_send.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/menu/menu_send.cpp b/Telegram/SourceFiles/menu/menu_send.cpp index db0c5c25d6..d829cabec6 100644 --- a/Telegram/SourceFiles/menu/menu_send.cpp +++ b/Telegram/SourceFiles/menu/menu_send.cpp @@ -737,8 +737,6 @@ FillMenuResult FillSendMenu( const auto &icons = iconsOverride ? *iconsOverride : st::defaultComposeIcons; - - } if (sending && type != Type::SilentOnly) { menu->addAction( ((type == Type::Reminder) @@ -761,6 +759,7 @@ FillMenuResult FillSendMenu( [=] { action({ Api::SendOptions{ .silent = true } }, details); }, &icons.menuMute); + } if ((type != Type::Disabled) && ((details.spoiler != SpoilerState::None) || (details.caption != CaptionState::None) From dc6b9dda4d9398d231eea276cb0af9136c873b3a Mon Sep 17 00:00:00 2001 From: futpib Date: Wed, 18 Feb 2026 00:00:04 +0000 Subject: [PATCH 032/415] Use faster containers for bulk-populated custom emoji maps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace base::flat_map (sorted vector with O(n) insertion) with std::unordered_map for CustomEmojiManager::_instances and std::map for EmojiListWidget::_customEmoji. These maps accumulate ~8000 entries during startup with unsorted keys, causing O(n²) total insertion cost. This change reduces refreshCustom() from ~3s to ~50ms (57x speedup). --- Telegram/SourceFiles/chat_helpers/emoji_list_widget.h | 4 +++- Telegram/SourceFiles/data/stickers/data_custom_emoji.h | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h index 6402935140..59f623a8c3 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h @@ -13,6 +13,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/round_rect.h" #include "base/timer.h" +#include + class StickerPremiumMark; namespace style { @@ -438,7 +440,7 @@ private: QVector _emoji[kEmojiSectionCount]; std::vector _custom; base::flat_set _restrictedCustomList; - base::flat_map _customEmoji; + std::map _customEmoji; base::flat_map< DocumentId, std::unique_ptr> _customRecent; diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h index c35d7520fb..1351bb0c1d 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h @@ -149,7 +149,7 @@ private: const not_null _owner; std::array< - base::flat_map< + std::unordered_map< DocumentId, std::unique_ptr>, kSizeCount> _instances; From 151f3f6cc4a0cdc3ebbf5764c71306f9c73faab1 Mon Sep 17 00:00:00 2001 From: linux Date: Sun, 1 Mar 2026 16:14:12 +0530 Subject: [PATCH 033/415] accessibility: label QR code button in settings cover --- Telegram/SourceFiles/settings/sections/settings_main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Telegram/SourceFiles/settings/sections/settings_main.cpp b/Telegram/SourceFiles/settings/sections/settings_main.cpp index c8dc269642..da9822a5cd 100644 --- a/Telegram/SourceFiles/settings/sections/settings_main.cpp +++ b/Telegram/SourceFiles/settings/sections/settings_main.cpp @@ -208,6 +208,7 @@ Cover::Cover( }, _name->lifetime()); _qrButton.create(this, st::infoProfileLabeledButtonQr); + _qrButton->setAccessibleName(tr::lng_group_invite_context_qr(tr::now)); _qrButton->setClickedCallback([=, show = controller->uiShow()] { Ui::DefaultShowFillPeerQrBoxCallback(show, _user); }); From 1a35929eb8a6178e70cb9d2d2d8c4b1aa7f24c57 Mon Sep 17 00:00:00 2001 From: linux Date: Sun, 1 Mar 2026 16:14:28 +0530 Subject: [PATCH 034/415] accessibility: label profile photo buttons by role --- Telegram/SourceFiles/ui/controls/userpic_button.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.cpp b/Telegram/SourceFiles/ui/controls/userpic_button.cpp index 99d272f42f..5a0a42c03b 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.cpp +++ b/Telegram/SourceFiles/ui/controls/userpic_button.cpp @@ -236,6 +236,12 @@ void UserpicButton::prepare() { prepareUserpicPixmap(); } setClickHandlerByRole(); + + if (_role == Role::OpenPhoto) { + setAccessibleName(tr::lng_mediaview_profile_photo(tr::now)); + } else if (_role == Role::ChangePhoto || _role == Role::ChoosePhoto) { + setAccessibleName(tr::lng_profile_set_photo_for(tr::now)); + } } void UserpicButton::showCustomOnChosen() { From ec7c76b8d2e32243aff48e6fa18821b7d8d6e150 Mon Sep 17 00:00:00 2001 From: futpib Date: Wed, 18 Feb 2026 14:44:39 +0000 Subject: [PATCH 035/415] Cache Webview::Availability() result for startup checks Iv::ShowButton() and LocationPicker::Available() each called Webview::Availability() separately (~200ms each on Linux). Replace the per-caller static caches with a single shared cache in Core::CachedWebviewAvailability(), reducing startup from two ~200ms calls to one. --- Telegram/CMakeLists.txt | 1 + Telegram/SourceFiles/core/application.cpp | 7 +++---- .../core/cached_webview_availability.h | 19 +++++++++++++++++++ Telegram/SourceFiles/iv/iv_data.cpp | 11 ++++------- .../ui/controls/location_picker.cpp | 11 +++++------ 5 files changed, 32 insertions(+), 17 deletions(-) create mode 100644 Telegram/SourceFiles/core/cached_webview_availability.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 8401e42790..f5fb7d93e4 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -491,6 +491,7 @@ PRIVATE chat_helpers/ttl_media_layer_widget.h core/application.cpp core/application.h + core/cached_webview_availability.h core/bank_card_click_handler.cpp core/bank_card_click_handler.h core/base_integration.cpp diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 4a0d319454..9b5835b6c7 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -88,7 +88,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/premium_limits_box.h" #include "ui/accessible/ui_accessible_factory.h" #include "ui/boxes/confirm_box.h" -#include "ui/controls/location_picker.h" +#include "core/cached_webview_availability.h" #include "styles/style_window.h" #include @@ -326,9 +326,8 @@ void Application::run() { QMimeDatabase().mimeTypeForName(u"text/plain"_q); // Check now to avoid re-entrance later. - [[maybe_unused]] const auto ivSupported = Iv::ShowButton(); - [[maybe_unused]] const auto lpAvailable = Ui::LocationPicker::Available( - {}); + [[maybe_unused]] const auto &webviewAvailability + = Core::CachedWebviewAvailability(); _windows.emplace(nullptr, std::make_unique()); setLastActiveWindow(_windows.front().second.get()); diff --git a/Telegram/SourceFiles/core/cached_webview_availability.h b/Telegram/SourceFiles/core/cached_webview_availability.h new file mode 100644 index 0000000000..cef8d5fd28 --- /dev/null +++ b/Telegram/SourceFiles/core/cached_webview_availability.h @@ -0,0 +1,19 @@ +/* +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 "webview/webview_interface.h" + +namespace Core { + +[[nodiscard]] inline const Webview::Available &CachedWebviewAvailability() { + static const auto result = Webview::Availability(); + return result; +} + +} // namespace Core diff --git a/Telegram/SourceFiles/iv/iv_data.cpp b/Telegram/SourceFiles/iv/iv_data.cpp index 8d424c9188..8e228ab16d 100644 --- a/Telegram/SourceFiles/iv/iv_data.cpp +++ b/Telegram/SourceFiles/iv/iv_data.cpp @@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "iv/iv_data.h" #include "iv/iv_prepare.h" -#include "webview/webview_interface.h" +#include "core/cached_webview_availability.h" #include #include @@ -102,12 +102,9 @@ QString SiteNameFromUrl(const QString &url) { } bool ShowButton() { - static const auto Supported = [&] { - const auto availability = Webview::Availability(); - return availability.customSchemeRequests - && availability.customRangeRequests; - }(); - return Supported; + const auto &availability = Core::CachedWebviewAvailability(); + return availability.customSchemeRequests + && availability.customRangeRequests; } void RecordShowFailure() { diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp index 3f0e5e26ee..ef5d346075 100644 --- a/Telegram/SourceFiles/ui/controls/location_picker.cpp +++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp @@ -38,6 +38,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "webview/webview_data_stream_memory.h" #include "webview/webview_embed.h" #include "webview/webview_interface.h" +#include "core/cached_webview_availability.h" #include "window/themes/window_theme.h" #include "styles/style_chat_helpers.h" #include "styles/style_dialogs.h" @@ -772,12 +773,10 @@ std::shared_ptr LocationPicker::uiShow() { } bool LocationPicker::Available(const LocationPickerConfig &config) { - static const auto Supported = [&] { - const auto availability = Webview::Availability(); - return availability.customSchemeRequests - && availability.customReferer; - }(); - return Supported && !config.mapsToken.isEmpty(); + const auto &availability = Core::CachedWebviewAvailability(); + return availability.customSchemeRequests + && availability.customReferer + && !config.mapsToken.isEmpty(); } void LocationPicker::setup(const Descriptor &descriptor) { From 64562b6171fbe12b1ab944c83b011c1546a90ff1 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 5 Mar 2026 22:12:21 +0400 Subject: [PATCH 036/415] Update lib_ui. --- Telegram/lib_ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 5099dadbd3..e85ba0fe2a 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 5099dadbd3b2cb479bb9fbdbd9a0d3231a232fe7 +Subproject commit e85ba0fe2a68fc380fb3a4977db49004837b3fba From 61b7b2d5dcfcbff0502c6a3cb242d0412aab0623 Mon Sep 17 00:00:00 2001 From: futpib Date: Wed, 18 Feb 2026 12:50:11 +0000 Subject: [PATCH 037/415] Use rename instead of copy for log file rotation at startup Replace the expensive file copy in Logs::instanceChecked() with an atomic rename. The old code copied log_startX.txt to log.txt then deleted the original, which took ~83ms of synchronous I/O. A simple rename on the same filesystem is nearly instant (~0.3ms). --- Telegram/SourceFiles/logs.cpp | 56 +++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/Telegram/SourceFiles/logs.cpp b/Telegram/SourceFiles/logs.cpp index 1d78548e14..4e26ddc427 100644 --- a/Telegram/SourceFiles/logs.cpp +++ b/Telegram/SourceFiles/logs.cpp @@ -144,36 +144,48 @@ private: if (postfix.isEmpty()) { // instance checked, need to move to log.txt Assert(!files[type]->fileName().isEmpty()); // one of log_startXX.txt should've been opened already - auto to = std::make_unique(_logsFilePath(type, postfix)); - if (to->exists() && !to->remove()) { - LOG(("Could not delete '%1' file to start new logging: %2").arg(to->fileName(), to->errorString())); + const auto startName = files[type]->fileName(); + const auto targetName = _logsFilePath(type, postfix); + + auto target = QFile(targetName); + if (target.exists() && !target.remove()) { + LOG(("Could not delete '%1' file to start new logging: %2").arg(targetName, target.errorString())); return false; } - if (auto from = QFile(files[type]->fileName()); !from.copy(to->fileName())) { // don't close files[type] yet - LOG(("Could not copy '%1' to '%2' to start new logging: %3").arg(files[type]->fileName(), to->fileName(), from.errorString())); + + files[type]->close(); + + const auto reopenStart = [&] { + files[type]->setFileName(startName); + files[type]->open(mode | QIODevice::Append); + }; + + auto source = QFile(startName); + if (!source.rename(targetName)) { + reopenStart(); + LOG(("Could not rename '%1' to '%2' to start new logging: %3").arg(startName, targetName, source.errorString())); return false; } - if (to->open(mode | QIODevice::Append)) { - std::swap(files[type], to); - LOG(("Moved logging from '%1' to '%2'!").arg(to->fileName(), files[type]->fileName())); - to->remove(); + files[type]->setFileName(targetName); + if (!files[type]->open(mode | QIODevice::Append)) { + LOG(("Could not open '%1' file to start new logging: %2").arg(targetName, files[type]->errorString())); + return false; + } + LOG(("Moved logging from '%1' to '%2'!").arg(startName, files[type]->fileName())); - LogsStartIndexChosen = -1; + LogsStartIndexChosen = -1; - QDir working(cWorkingDir()); // delete all other log_startXX.txt that we can - QStringList oldlogs = working.entryList(QStringList("log_start*.txt"), QDir::Files); - for (QStringList::const_iterator i = oldlogs.cbegin(), e = oldlogs.cend(); i != e; ++i) { - QString oldlog = cWorkingDir() + *i, oldlogend = i->mid(u"log_start"_q.size()); - if (oldlogend.size() == 1 + u".txt"_q.size() && oldlogend.at(0).isDigit() && base::StringViewMid(oldlogend, 1) == u".txt"_q) { - bool removed = QFile(oldlog).remove(); - LOG(("Old start log '%1' found, deleted: %2").arg(*i, Logs::b(removed))); - } + QDir working(cWorkingDir()); // delete all other log_startXX.txt that we can + QStringList oldlogs = working.entryList(QStringList("log_start*.txt"), QDir::Files); + for (QStringList::const_iterator i = oldlogs.cbegin(), e = oldlogs.cend(); i != e; ++i) { + QString oldlog = cWorkingDir() + *i, oldlogend = i->mid(u"log_start"_q.size()); + if (oldlogend.size() == 1 + u".txt"_q.size() && oldlogend.at(0).isDigit() && base::StringViewMid(oldlogend, 1) == u".txt"_q) { + bool removed = QFile(oldlog).remove(); + LOG(("Old start log '%1' found, deleted: %2").arg(*i, Logs::b(removed))); } - - return true; } - LOG(("Could not open '%1' file to start new logging: %2").arg(to->fileName(), to->errorString())); - return false; + + return true; } else { bool found = false; int32 oldest = -1; // find not existing log_startX.txt or pick the oldest one (by lastModified) From 56e675ec9c9a691e6fcf918c108cd593a76cfc17 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 6 Mar 2026 12:04:40 +0400 Subject: [PATCH 038/415] Revert "Fixed ability to delete chat as forum with all topics." This reverts commit f40064333e33a35f8e96de07248666792998e5f2. This destroys messages that were not really deleted. --- Telegram/SourceFiles/history/history.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 447a16a8f1..998e2d2876 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -4020,11 +4020,6 @@ void History::clear(ClearType type, bool markEmpty) { } else if (const auto channel = peer->asMegagroup()) { channel->mgInfo->markupSenders.clear(); } - if (const auto forum = peer->forum()) { - forum->enumerateTopics([&](not_null topic) { - destroyMessagesByTopic(topic->rootId()); - }); - } owner().notifyHistoryChangeDelayed(this); owner().sendHistoryChangeNotifications(); From a4c4ea129c0583defd88f2b828e89015b95b0a9d Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 6 Mar 2026 14:08:37 +0400 Subject: [PATCH 039/415] Always process item destroy in topics. --- Telegram/SourceFiles/data/data_replies_list.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/data/data_replies_list.cpp b/Telegram/SourceFiles/data/data_replies_list.cpp index 3971a303e0..4bf7d11868 100644 --- a/Telegram/SourceFiles/data/data_replies_list.cpp +++ b/Telegram/SourceFiles/data/data_replies_list.cpp @@ -456,16 +456,16 @@ bool RepliesList::applyItemDestroyed( bool RepliesList::applyUpdate(const MessageUpdate &update) { using Flag = MessageUpdate::Flag; - if (update.item->history() != _history - || !update.item->isRegular() - || !update.item->inThread(_rootId)) { + if (update.item->history() != _history || !update.item->isRegular()) { return false; } + const auto id = update.item->id; + const auto inThread = update.item->inThread(_rootId); const auto added = (update.flags & Flag::ReplyToTopAdded); const auto i = ranges::lower_bound(_list, id, std::greater<>()); if (update.flags & Flag::Destroyed) { - if (!added) { + if (!added && inThread) { changeUnreadCountByPost(id, -1); } if (i == end(_list) || *i != id) { @@ -480,6 +480,8 @@ bool RepliesList::applyUpdate(const MessageUpdate &update) { } } return true; + } else if (!inThread) { + return false; } if (added) { changeUnreadCountByPost(id, 1); From e86fec4ab6b1183db4dd474b89e38aa31b1d76af Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 6 Mar 2026 14:08:51 +0400 Subject: [PATCH 040/415] Fix messages clear on history delete. --- Telegram/SourceFiles/history/history.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 998e2d2876..a36e0f97fb 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -3999,9 +3999,11 @@ void History::clear(ClearType type, bool markEmpty) { _lastMessage = std::nullopt; } } - const auto tillId = (_lastMessage && *_lastMessage) + const auto tillId = (_lastMessage + && (*_lastMessage) + && (*_lastMessage)->isRegular()) ? (*_lastMessage)->id - : std::numeric_limits::max(); + : MsgId(std::numeric_limits::max()); clearUpTill(tillId); if (blocks.empty() && _lastMessage && *_lastMessage) { addItemToBlock(*_lastMessage); From b1d08b3e309187ddd91100386c91d17ecd26b8bb Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 6 Mar 2026 14:09:06 +0400 Subject: [PATCH 041/415] Preserve topicRootId() on edition-to-history-clear. --- Telegram/SourceFiles/history/history_item.cpp | 17 +++++++++++++++-- .../history/history_item_components.h | 5 +++++ .../history/history_item_helpers.cpp | 5 ++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 1506ba64e7..b235521824 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -839,6 +839,8 @@ bool HistoryItem::awaitingVideoProcessing() const { HistoryServiceDependentData *HistoryItem::GetServiceDependentData() { if (const auto pinned = Get()) { return pinned; + } else if (const auto clear = Get()) { + return clear; } else if (const auto gamescore = Get()) { return gamescore; } else if (const auto payment = Get()) { @@ -2357,14 +2359,23 @@ void HistoryItem::updateForwardedInfo(const MTPMessageFwdHeader *fwd) { } void HistoryItem::applyEditionToHistoryCleared() { + const auto rootId = topicRootId(); + const auto topicPost = (rootId != Data::ForumTopic::kGeneralId); + auto action = Api::SendAction(history()); + action.replyTo = FullReplyTo{ + .messageId = FullMsgId(_history->peer->id, topicPost ? rootId : 0), + .topicRootId = rootId, + }; + using Flag = MTPDmessageService::Flag; applyEdition( MTP_messageService( - MTP_flags(0), + MTP_flags(Flag() + | (topicPost ? Flag::f_reply_to : Flag())), MTP_int(id), peerToMTP(PeerId(0)), // from_id peerToMTP(_history->peer->id), MTPPeer(), // saved_peer_id - MTPMessageReplyHeader(), + NewMessageReplyHeader(action), MTP_int(date()), MTP_messageActionHistoryClear(), MTPMessageReactions(), @@ -4738,6 +4749,8 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { const auto type = action.type(); if (type == mtpc_messageActionPinMessage) { UpdateComponents(HistoryServicePinned::Bit()); + } else if (type == mtpc_messageActionHistoryClear) { + UpdateComponents(HistoryServiceClearHistory::Bit()); } else if (type == mtpc_messageActionTopicCreate || type == mtpc_messageActionTopicEdit) { UpdateComponents(HistoryServiceTopicInfo::Bit()); diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 0bf0d3a1ee..f42041c9da 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -673,6 +673,11 @@ struct HistoryServicePinned , HistoryServiceDependentData { }; +struct HistoryServiceClearHistory +: RuntimeComponent +, HistoryServiceDependentData { +}; + struct HistoryServiceTopicInfo : RuntimeComponent , HistoryServiceDependentData { diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index a696d85178..f4f170f153 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -887,6 +887,8 @@ MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) { ? PeerId() : replyTo.messageId.peer; const auto replyToTop = LookupReplyToTop(action.history, replyTo); + const auto topicPost = replyTo.topicRootId + && (replyTo.topicRootId != Data::ForumTopic::kGeneralId); auto quoteEntities = Api::EntitiesToMTP( &action.history->session(), replyTo.quote.entities, @@ -903,7 +905,8 @@ MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) { | (quoteEntities.v.empty() ? Flag() : Flag::f_quote_entities) - | (replyTo.todoItemId ? Flag::f_todo_item_id : Flag())), + | (replyTo.todoItemId ? Flag::f_todo_item_id : Flag()) + | (topicPost ? Flag::f_forum_topic : Flag())), MTP_int(replyTo.messageId.msg), peerToMTP(externalPeerId), MTPMessageFwdHeader(), // reply_from From 485d820fe257574ff35ee56ecd85474170cf263e Mon Sep 17 00:00:00 2001 From: cumdev1337 Date: Wed, 4 Mar 2026 01:08:43 +0200 Subject: [PATCH 042/415] Do not exit fullscreen mode when applying video quality --- Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 1ca2724b70..737f59c88a 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -4817,11 +4817,14 @@ void OverlayWidget::applyVideoQuality(VideoQuality value) { } _streamingStartPaused = _streamedQualityChangeFinished || (_streamed && _streamed->instance.player().paused()); + const auto wasFullScreen = _fullScreenVideo; clearStreaming(); const auto time = _streamedPosition; const auto startStreaming = StartStreaming(false, time); if (!canInitStreaming() || !initStreaming(startStreaming)) { redisplayContent(); + } else { + _fullScreenVideo = wasFullScreen; } } From 2de7fb5c25049a635035539eafdfbd1912732706 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 6 Mar 2026 15:54:38 +0400 Subject: [PATCH 043/415] Simplify caption management in SendFilesBox. --- Telegram/SourceFiles/boxes/send_files_box.cpp | 137 +++++++----------- Telegram/SourceFiles/boxes/send_files_box.h | 8 +- .../ui/chat/attach/attach_prepare.cpp | 34 +++-- 3 files changed, 77 insertions(+), 102 deletions(-) diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index 58ed48001e..e5bb332396 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -298,6 +298,14 @@ void EditPriceBox( }); } +[[nodiscard]] bool SkipCaption( + const Ui::PreparedFile &file, + const Ui::SendFilesWay &way) { + return way.sendImagesAsPhotos() + && (file.type == Ui::PreparedFile::Type::Photo + || file.type == Ui::PreparedFile::Type::Video); +} + } // namespace SendFilesLimits DefaultLimitsForPeer(not_null peer) { @@ -664,7 +672,7 @@ Fn SendFilesBox::prepareSendMenuDetails( const auto canMoveCaption = _list.canMoveCaption( way.groupFiles() && way.sendImagesAsPhotos(), way.sendImagesAsPhotos() - ) && _caption && HasSendText(_caption); + ) && HasSendText(_caption); result.caption = !canMoveCaption ? SendMenu::CaptionState::None : _invertCaption @@ -857,46 +865,6 @@ bool SendFilesBox::mainCaptionWillBeAttached() const { return Ui::CaptionWillBeAttached(_list, way, slowmode); } -void SendFilesBox::applyMainCaptionToFirstFile() { - if (!_caption - || _caption->isHidden() - || (_list.files.size() <= 1) - || _list.files.empty()) { - if (_mainCaptionAttachedToFirstFile && !_list.files.empty()) { - _list.files.front().caption = _firstFileCaptionBackup.value_or( - TextWithTags()); - if (!setCaptionInSingleFilePreview( - 0, - _list.files.front().caption)) { - } - } - _mainCaptionAttachedToFirstFile = false; - _firstFileCaptionBackup = std::nullopt; - return; - } - if (mainCaptionWillBeAttached()) { - if (!_mainCaptionAttachedToFirstFile) { - _firstFileCaptionBackup = _list.files.front().caption; - } - auto text = _caption->getTextWithAppliedMarkdown(); - _list.files.front().caption = text; - _mainCaptionAttachedToFirstFile = true; - if (!setCaptionInSingleFilePreview( - 0, - text)) { - } - } else if (_mainCaptionAttachedToFirstFile) { - _list.files.front().caption = _firstFileCaptionBackup.value_or( - TextWithTags()); - _mainCaptionAttachedToFirstFile = false; - _firstFileCaptionBackup = std::nullopt; - if (!setCaptionInSingleFilePreview( - 0, - _list.files.front().caption)) { - } - } -} - void SendFilesBox::openDialogToAddFileToAlbum() { const auto show = uiShow(); const auto checkResult = [=](const Ui::PreparedList &list) { @@ -928,7 +896,6 @@ void SendFilesBox::openDialogToAddFileToAlbum() { void SendFilesBox::refreshMessagesCount() { const auto withCaption = mainCaptionWillBeAttached(); const auto withComment = !withCaption - && _caption && !_caption->isHidden() && !_caption->getTextWithTags().text.isEmpty(); _messagesCount = _list.files.size() + (withComment ? 1 : 0); @@ -1180,7 +1147,7 @@ void SendFilesBox::initSendWay() { _sendWay.changes( ) | rpl::on_next([=](SendFilesWay value) { const auto hidden = [&] { - return !_caption || _caption->isHidden(); + return _caption->isHidden(); }; const auto was = hidden(); updateCaptionPlaceholder(); @@ -1199,9 +1166,6 @@ void SendFilesBox::initSendWay() { } void SendFilesBox::updateCaptionPlaceholder() { - if (!_caption) { - return; - } const auto way = _sendWay.current(); if (!_list.canAddCaption( way.groupFiles() && way.sendImagesAsPhotos(), @@ -1317,6 +1281,12 @@ void SendFilesBox::pushBlock(int from, int till) { } refreshAllAfterChanges(index, [&] { _list.files.erase(_list.files.begin() + index); + if (syncMainCaption()) { + const auto was = base::take(_list.files.front().caption); + if (fieldText().empty()) { + _caption->setTextWithTags(was); + } + } }); }); }, widget->lifetime()); @@ -1521,10 +1491,7 @@ void SendFilesBox::pushBlock(int from, int till) { widget, _st.tabbed.menu); const auto &file = _list.files[fileIndex]; - const auto canEditFileData - = !_sendWay.current().sendImagesAsPhotos() - || (file.type != Ui::PreparedFile::Type::Photo - && file.type != Ui::PreparedFile::Type::Video); + const auto canEditFileData = !SkipCaption(file, _sendWay.current()); if (canEditFileData) { state->menu->addAction(tr::lng_rename_file(tr::now), [=] { auto &file = _list.files[fileIndex]; @@ -1543,32 +1510,24 @@ void SendFilesBox::pushBlock(int from, int till) { tr::lng_context_upload_edit_caption(tr::now), [=] { auto &file = _list.files[fileIndex]; + const auto sync = syncMainCaption(); _show->show(Box( EditFileCaptionBox, _st, _captionToPeer, - file.caption, + sync ? fieldText() : file.caption, [=](TextWithTags text) { if (!validateSingleCaptionLength(text.text)) { return false; } - auto updated = text; - const auto syncMainCaption = (fileIndex == 0) - && _caption - && !_caption->isHidden() - && mainCaptionWillBeAttached(); - if (fileIndex == 0) { - _mainCaptionAttachedToFirstFile = false; - _firstFileCaptionBackup = text; - } - _list.files[fileIndex].caption - = std::move(text); - if (syncMainCaption) { - _caption->setTextWithTags(updated); + if (sync) { + _caption->setTextWithTags( + base::take(text)); } + _list.files[fileIndex].caption = text; if (!setCaptionInSingleFilePreview( fileIndex, - updated)) { + text)) { refreshAllAfterChanges(from); } return true; @@ -1593,7 +1552,6 @@ void SendFilesBox::refreshControls(bool initial) { refreshTitleText(); updateSendWayControls(); updateCaptionPlaceholder(); - applyMainCaptionToFirstFile(); } void SendFilesBox::setupSendWayControls() { @@ -1784,13 +1742,12 @@ void SendFilesBox::setupCaption() { _caption->changes() ) | rpl::on_next([=] { checkCharsLimitation(); - applyMainCaptionToFirstFile(); refreshMessagesCount(); }, _caption->lifetime()); } void SendFilesBox::setupCaptionAutocomplete() { - if (!_captionToPeer || !_caption) { + if (!_captionToPeer) { return; } const auto parent = getDelegate()->outerContainer(); @@ -1842,11 +1799,22 @@ void SendFilesBox::setupCaptionAutocomplete() { } } -void SendFilesBox::checkCharsLimitation() { - const auto limits = Data::PremiumLimits(&_show->session()); - const auto caption = (_caption && !_caption->isHidden()) +TextWithTags SendFilesBox::fieldText() const { + return !_caption->isHidden() ? _caption->getTextWithAppliedMarkdown() : TextWithTags(); +} + +bool SendFilesBox::syncMainCaption() const { + return (_list.files.size() == 1) + && !SkipCaption(_list.files.front(), _sendWay.current()) + && !_caption->isHidden() + && mainCaptionWillBeAttached(); +} + +void SendFilesBox::checkCharsLimitation() { + const auto limits = Data::PremiumLimits(&_show->session()); + const auto caption = fieldText(); const auto remove = caption.text.size() - limits.captionLengthCurrent(); if ((remove > 0) && _emojiToggle) { if (!_charsLimitation) { @@ -1870,8 +1838,6 @@ void SendFilesBox::checkCharsLimitation() { } void SendFilesBox::setupEmojiPanel() { - Expects(_caption != nullptr); - const auto container = getDelegate()->outerContainer(); using Selector = ChatHelpers::TabbedSelector; _emojiPanel = base::make_unique_q( @@ -2067,7 +2033,7 @@ void SendFilesBox::refreshTitleText() { void SendFilesBox::updateBoxSize() { auto footerHeight = 0; - if (_caption && !_caption->isHidden()) { + if (!_caption->isHidden()) { footerHeight += st::boxPhotoCaptionSkip + _caption->height(); } const auto pairs = std::array, 4>{ { @@ -2122,7 +2088,7 @@ void SendFilesBox::resizeEvent(QResizeEvent *e) { void SendFilesBox::updateControlsGeometry() { auto bottom = height(); - if (_caption && !_caption->isHidden()) { + if (!_caption->isHidden()) { _caption->resize(st::sendMediaPreviewSize, _caption->height()); _caption->moveToLeft( st::boxPhotoPadding.left(), @@ -2170,13 +2136,13 @@ rpl::producer SendFilesBox::takeTextWithTagsRequests() const { } void SendFilesBox::requestToTakeTextWithTags() const { - if (_caption && !_caption->isHidden()) { + if (!_caption->isHidden()) { _textWithTagsRequests.fire_copy(_caption->getTextWithTags()); } } void SendFilesBox::setInnerFocus() { - if (_caption && !_caption->isHidden()) { + if (!_caption->isHidden()) { _caption->setFocusFast(); } else { BoxContent::setInnerFocus(); @@ -2252,18 +2218,18 @@ void SendFilesBox::send( applyBlockChanges(); Storage::ApplyModifications(_list); - applyMainCaptionToFirstFile(); _confirmed = true; if (_confirmedCallback) { - auto caption = (_caption && !_caption->isHidden()) - ? _caption->getTextWithAppliedMarkdown() - : TextWithTags(); + const auto way = _sendWay.current(); + auto caption = fieldText(); if (!validateLength(caption.text)) { return; } for (const auto &file : _list.files) { - if (!validateSingleCaptionLength(file.caption.text)) { + if (SkipCaption(file, way)) { + continue; + } else if (!validateSingleCaptionLength(file.caption.text)) { return; } } @@ -2274,9 +2240,14 @@ void SendFilesBox::send( file.spoiler = false; } } + for (auto &file : _list.files) { + if (SkipCaption(file, way)) { + file.caption = {}; + } + } _confirmedCallback( std::move(_list), - _sendWay.current(), + way, std::move(caption), options, ctrlShiftEnter); diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index cf0c9490f0..0c982a9cdb 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -269,13 +269,15 @@ private: void requestToTakeTextWithTags() const; bool validateSingleCaptionLength(const QString &text) const; bool mainCaptionWillBeAttached() const; - void applyMainCaptionToFirstFile(); [[nodiscard]] Fn prepareSendMenuDetails( const SendFilesBoxDescriptor &descriptor); [[nodiscard]] auto prepareSendMenuCallback() -> Fn; + [[nodiscard]] bool syncMainCaption() const; + [[nodiscard]] TextWithTags fieldText() const; + const std::shared_ptr _show; const style::ComposeControls &_st; const Api::SendType _sendType = Api::SendType(); @@ -300,10 +302,8 @@ private: QImage _priceTagBg; bool _confirmed = false; bool _invertCaption = false; - bool _mainCaptionAttachedToFirstFile = false; - std::optional _firstFileCaptionBackup; - object_ptr _caption = { nullptr }; + const object_ptr _caption; std::unique_ptr _autocomplete; TextWithTags _prefilledCaptionText; object_ptr _emojiToggle = { nullptr }; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp index b1223b508c..ac5f3f2eca 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp @@ -96,11 +96,13 @@ struct GroupRange { [[nodiscard]] bool CaptionWillBeAttachedFromRanges( const std::vector &ranges, int filesCount) { - const auto hasGroupedFileAlbum = ranges::any_of(ranges, [](const auto &r) { - return (r.size() > 1) && (r.type == AlbumType::File); - }); - return ((filesCount > 1) && hasGroupedFileAlbum) - || ((ranges.size() == 1) && ranges.front().sentWithCaption()); + // Let's send text after media, as it is shown in SendFilesBox. + + //const auto hasGroupedFileAlbum = ranges::any_of(ranges, [](const auto &r) { + // return (r.size() > 1) && (r.type == AlbumType::File); + //}); + return /*((filesCount > 1) && hasGroupedFileAlbum) + || */((ranges.size() == 1) && ranges.front().sentWithCaption()); } } // namespace @@ -359,16 +361,18 @@ bool PreparedList::hasSpoilerMenu(bool compress) const { bool AttachCaptionToFirstAsFile( const std::vector &groups) { - auto filesCount = 0; - auto hasGroupedFileAlbum = false; - for (const auto &group : groups) { - filesCount += group.list.files.size(); - hasGroupedFileAlbum = hasGroupedFileAlbum - || ((group.list.files.size() > 1) - && (group.type == AlbumType::File)); - } - const auto result = (filesCount > 1) && hasGroupedFileAlbum; - return result; + // Let's send text after media, as it is shown in SendFilesBox. + return false; + //auto filesCount = 0; + //auto hasGroupedFileAlbum = false; + //for (const auto &group : groups) { + // filesCount += group.list.files.size(); + // hasGroupedFileAlbum = hasGroupedFileAlbum + // || ((group.list.files.size() > 1) + // && (group.type == AlbumType::File)); + //} + //const auto result = (filesCount > 1) && hasGroupedFileAlbum; + //return result; } bool CaptionWillBeAttached(const std::vector &groups) { From 5507e9c00d57fbfe0d3528b200374929258a1790 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 6 Mar 2026 21:08:22 +0400 Subject: [PATCH 044/415] Simplify file sending, always attach a caption. --- Telegram/SourceFiles/apiwrap.cpp | 36 +--- Telegram/SourceFiles/apiwrap.h | 1 - Telegram/SourceFiles/boxes/send_files_box.cpp | 161 +++++++----------- Telegram/SourceFiles/boxes/send_files_box.h | 19 +-- .../data/data_chat_participant_status.cpp | 70 ++++++++ .../data/data_chat_participant_status.h | 13 ++ .../SourceFiles/history/history_widget.cpp | 98 ++--------- Telegram/SourceFiles/history/history_widget.h | 11 +- .../view/history_view_chat_section.cpp | 96 ++--------- .../history/view/history_view_chat_section.h | 11 +- .../view/history_view_scheduled_section.cpp | 101 +++-------- .../view/history_view_scheduled_section.h | 13 +- .../media/stories/media_stories_reply.cpp | 83 ++------- .../media/stories/media_stories_reply.h | 11 +- .../business/settings_shortcut_messages.cpp | 105 +++--------- .../settings/sections/settings_chat.cpp | 2 +- .../SourceFiles/support/support_helper.cpp | 21 ++- .../ui/chat/attach/attach_prepare.cpp | 104 +++-------- .../ui/chat/attach/attach_prepare.h | 16 +- 19 files changed, 288 insertions(+), 684 deletions(-) diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index eff5a0107e..b7e86f20ca 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3835,35 +3835,8 @@ void ApiWrap::editMedia( void ApiWrap::sendFiles( Ui::PreparedList &&list, SendMediaType type, - TextWithTags &&caption, std::shared_ptr album, const SendAction &action) { - auto sharedCaption = std::move(caption); - const auto haveCaption = !sharedCaption.text.isEmpty(); - const auto captionAttached = !haveCaption - ? false - : (list.files.size() == 1) - ? list.canAddCaption( - album != nullptr, - type == SendMediaType::Photo) - : Ui::CaptionWillBeAttached( - list, - [&] { - auto way = Ui::SendFilesWay(); - way.setGroupFiles(album != nullptr); - way.setSendImagesAsPhotos(type == SendMediaType::Photo); - return way; - }(), - false); - if (haveCaption && !captionAttached) { - auto message = MessageToSend(action); - message.textWithTags = base::take(sharedCaption); - message.action.clearDraft = false; - sendMessage(std::move(message)); - } - auto attachSharedCaption = haveCaption && captionAttached; - auto sharedCaptionConsumed = false; - const auto to = FileLoadTaskOptions(action); if (album) { album->options = to.options; @@ -3871,13 +3844,6 @@ void ApiWrap::sendFiles( auto tasks = std::vector>(); tasks.reserve(list.files.size()); for (auto &file : list.files) { - auto fileCaption = std::move(file.caption); - if (attachSharedCaption && !sharedCaptionConsumed) { - sharedCaptionConsumed = true; - if (fileCaption.text.isEmpty()) { - fileCaption = base::take(sharedCaption); - } - } const auto uploadWithType = !album ? type : (file.type == Ui::PreparedFile::Type::Photo @@ -3909,7 +3875,7 @@ void ApiWrap::sendFiles( : nullptr), .type = uploadWithType, .to = to, - .caption = std::move(fileCaption), + .caption = std::move(file.caption), .spoiler = file.spoiler, .album = album, .forceFile = forceFile, diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 4f78924b9d..52e846525b 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -337,7 +337,6 @@ public: void sendFiles( Ui::PreparedList &&list, SendMediaType type, - TextWithTags &&caption, std::shared_ptr album, const SendAction &action); void sendFile( diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index e5bb332396..d4987651ac 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -108,15 +108,6 @@ void FileDialogCallback( callback(std::move(*list)); } -rpl::producer FieldPlaceholder( - const Ui::PreparedList &list, - SendFilesWay way, - bool slowmode) { - return Ui::CaptionWillBeAttached(list, way, slowmode) - ? tr::lng_photo_caption() - : tr::lng_photos_comment(); -} - void RenameFileBox( not_null box, const QString ¤tName, @@ -302,8 +293,9 @@ void EditPriceBox( const Ui::PreparedFile &file, const Ui::SendFilesWay &way) { return way.sendImagesAsPhotos() - && (file.type == Ui::PreparedFile::Type::Photo - || file.type == Ui::PreparedFile::Type::Video); + ? (file.type == Ui::PreparedFile::Type::Photo + || file.type == Ui::PreparedFile::Type::Video) + : file.isSticker(); } } // namespace @@ -626,7 +618,7 @@ SendFilesBox::SendFilesBox( .show = controller->uiShow(), .list = std::move(list), .caption = caption, - .captionToPeer = toPeer, + .toPeer = toPeer, .limits = DefaultLimitsForPeer(toPeer), .check = DefaultCheckForPeer(controller, toPeer), .sendType = sendType, @@ -645,11 +637,15 @@ SendFilesBox::SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor) , _limits(descriptor.limits) , _sendMenuDetails(prepareSendMenuDetails(descriptor)) , _sendMenuCallback(prepareSendMenuCallback()) -, _captionToPeer(descriptor.captionToPeer) +, _toPeer(descriptor.toPeer) , _check(std::move(descriptor.check)) , _confirmedCallback(std::move(descriptor.confirmed)) , _cancelledCallback(std::move(descriptor.cancelled)) -, _caption(this, _st.files.caption, Ui::InputField::Mode::MultiLine) +, _caption( + this, + _st.files.caption, + Ui::InputField::Mode::MultiLine, + tr::lng_photo_caption()) , _prefilledCaptionText(std::move(descriptor.caption)) , _scroll(this, st::boxScroll) , _inner( @@ -858,13 +854,6 @@ bool SendFilesBox::setCaptionInSingleFilePreview( return false; } -bool SendFilesBox::mainCaptionWillBeAttached() const { - const auto way = _sendWay.current(); - const auto slowmode = (_limits & SendFilesAllow::OnlyOne) - && (_list.files.size() > 1); - return Ui::CaptionWillBeAttached(_list, way, slowmode); -} - void SendFilesBox::openDialogToAddFileToAlbum() { const auto show = uiShow(); const auto checkResult = [=](const Ui::PreparedList &list) { @@ -894,11 +883,7 @@ void SendFilesBox::openDialogToAddFileToAlbum() { } void SendFilesBox::refreshMessagesCount() { - const auto withCaption = mainCaptionWillBeAttached(); - const auto withComment = !withCaption - && !_caption->isHidden() - && !_caption->getTextWithTags().text.isEmpty(); - _messagesCount = _list.files.size() + (withComment ? 1 : 0); + _messagesCount = _list.files.size(); } void SendFilesBox::refreshButtons() { @@ -911,9 +896,7 @@ void SendFilesBox::refreshButtons() { [=] { send({}); }); refreshMessagesCount(); - const auto perMessage = _captionToPeer - ? _captionToPeer->starsPerMessageChecked() - : 0; + const auto perMessage = _toPeer->starsPerMessageChecked(); if (perMessage > 0) { _send->setText(PaidSendButtonText(_messagesCount.value( ) | rpl::map(rpl::mappers::_1 * perMessage))); @@ -953,9 +936,7 @@ bool SendFilesBox::hasSpoilerMenu() const { bool SendFilesBox::canChangePrice() const { const auto way = _sendWay.current(); - const auto broadcast = _captionToPeer - ? _captionToPeer->asBroadcast() - : nullptr; + const auto broadcast = _toPeer->asBroadcast(); return broadcast && broadcast->canPostPaidMedia() && _list.canChangePrice( @@ -1150,7 +1131,7 @@ void SendFilesBox::initSendWay() { return _caption->isHidden(); }; const auto was = hidden(); - updateCaptionPlaceholder(); + updateCaptionVisibility(); updateEmojiPanelGeometry(); applyBlockChanges(); generatePreviewFrom(0); @@ -1165,25 +1146,12 @@ void SendFilesBox::initSendWay() { }, lifetime()); } -void SendFilesBox::updateCaptionPlaceholder() { +void SendFilesBox::updateCaptionVisibility() { const auto way = _sendWay.current(); - if (!_list.canAddCaption( - way.groupFiles() && way.sendImagesAsPhotos(), - way.sendImagesAsPhotos()) - && ((_limits & SendFilesAllow::OnlyOne) - || !(_limits & SendFilesAllow::Texts))) { - _caption->hide(); - if (_emojiToggle) { - _emojiToggle->hide(); - } - } else { - const auto slowmode = (_limits & SendFilesAllow::OnlyOne) - && (_list.files.size() > 1); - _caption->setPlaceholder(FieldPlaceholder(_list, way, slowmode)); - _caption->show(); - if (_emojiToggle) { - _emojiToggle->show(); - } + const auto can = _list.canAddCaption(way.sendImagesAsPhotos()); + _caption->setVisible(can); + if (_emojiToggle) { + _emojiToggle->setVisible(can); } } @@ -1251,9 +1219,7 @@ void SendFilesBox::pushBlock(int from, int till) { ? !hasPrice() : (type == Ui::AttachActionType::EditCover) ? (file.isVideoFile() - && _captionToPeer - && (_captionToPeer->isBroadcast() - || _captionToPeer->isSelf())) + && (_toPeer->isBroadcast() || _toPeer->isSelf())) : (file.videoCover != nullptr); }); auto &block = _blocks.back(); @@ -1281,9 +1247,10 @@ void SendFilesBox::pushBlock(int from, int till) { } refreshAllAfterChanges(index, [&] { _list.files.erase(_list.files.begin() + index); - if (syncMainCaption()) { - const auto was = base::take(_list.files.front().caption); - if (fieldText().empty()) { + if (index == _list.files.size()) { + auto &last = _list.files.back(); + const auto was = base::take(last.caption); + if (fieldText().empty() && !last.isSticker()) { _caption->setTextWithTags(was); } } @@ -1491,7 +1458,9 @@ void SendFilesBox::pushBlock(int from, int till) { widget, _st.tabbed.menu); const auto &file = _list.files[fileIndex]; - const auto canEditFileData = !SkipCaption(file, _sendWay.current()); + const auto canEditFileData = !SkipCaption( + file, + _sendWay.current()); if (canEditFileData) { state->menu->addAction(tr::lng_rename_file(tr::now), [=] { auto &file = _list.files[fileIndex]; @@ -1510,14 +1479,15 @@ void SendFilesBox::pushBlock(int from, int till) { tr::lng_context_upload_edit_caption(tr::now), [=] { auto &file = _list.files[fileIndex]; - const auto sync = syncMainCaption(); + const auto count = int(_list.files.size()); + const auto sync = (fileIndex + 1 == count); _show->show(Box( EditFileCaptionBox, _st, - _captionToPeer, + _toPeer, sync ? fieldText() : file.caption, [=](TextWithTags text) { - if (!validateSingleCaptionLength(text.text)) { + if (!validateLength(text.text)) { return false; } if (sync) { @@ -1551,7 +1521,7 @@ void SendFilesBox::refreshControls(bool initial) { refreshPriceTag(); refreshTitleText(); updateSendWayControls(); - updateCaptionPlaceholder(); + updateCaptionVisibility(); } void SendFilesBox::setupSendWayControls() { @@ -1669,9 +1639,7 @@ void SendFilesBox::updateSendWayControls() { void SendFilesBox::setupCaption() { const auto allow = [=](not_null emoji) { - return _captionToPeer - ? Data::AllowEmojiWithoutPremium(_captionToPeer, emoji) - : (_limits & SendFilesAllow::EmojiWithoutPremium); + return Data::AllowEmojiWithoutPremium(_toPeer, emoji); }; const auto show = _show; InitMessageFieldHandlers({ @@ -1735,7 +1703,7 @@ void SendFilesBox::setupCaption() { Unexpected("action in MimeData hook."); }); - updateCaptionPlaceholder(); + updateCaptionVisibility(); setupEmojiPanel(); rpl::single(rpl::empty_value()) | rpl::then( @@ -1747,15 +1715,12 @@ void SendFilesBox::setupCaption() { } void SendFilesBox::setupCaptionAutocomplete() { - if (!_captionToPeer) { - return; - } const auto parent = getDelegate()->outerContainer(); ChatHelpers::InitFieldAutocomplete(_autocomplete, { .parent = parent, .show = _show, .field = _caption.data(), - .peer = _captionToPeer, + .peer = _toPeer, .features = [=] { auto result = ChatHelpers::ComposeFeatures(); result.autocompleteCommands = false; @@ -1805,13 +1770,6 @@ TextWithTags SendFilesBox::fieldText() const { : TextWithTags(); } -bool SendFilesBox::syncMainCaption() const { - return (_list.files.size() == 1) - && !SkipCaption(_list.files.front(), _sendWay.current()) - && !_caption->isHidden() - && mainCaptionWillBeAttached(); -} - void SendFilesBox::checkCharsLimitation() { const auto limits = Data::PremiumLimits(&_show->session()); const auto caption = fieldText(); @@ -1861,7 +1819,7 @@ void SendFilesBox::setupEmojiPanel() { st::emojiPanMinHeight / 2, st::emojiPanMinHeight); _emojiPanel->hide(); - _emojiPanel->selector()->setCurrentPeer(_captionToPeer); + _emojiPanel->selector()->setCurrentPeer(_toPeer); _emojiPanel->selector()->setAllowEmojiWithoutPremium( _limits & SendFilesAllow::EmojiWithoutPremium); _emojiPanel->selector()->emojiChosen( @@ -1874,11 +1832,7 @@ void SendFilesBox::setupEmojiPanel() { if (info && info->setType == Data::StickersType::Emoji && !_show->session().premium() - && !(_captionToPeer - ? Data::AllowEmojiWithoutPremium( - _captionToPeer, - data.document) - : (_limits & SendFilesAllow::EmojiWithoutPremium))) { + && !Data::AllowEmojiWithoutPremium(_toPeer, data.document)) { ShowPremiumPreviewBox(_show, PremiumFeature::AnimatedEmoji); } else { Data::InsertCustomEmoji(_caption.data(), data.document); @@ -2166,16 +2120,6 @@ void SendFilesBox::saveSendWaySettings() { } bool SendFilesBox::validateLength(const QString &text) const { - const auto way = _sendWay.current(); - if (!_list.canAddCaption( - way.groupFiles() && way.sendImagesAsPhotos(), - way.sendImagesAsPhotos())) { - return true; - } - return validateSingleCaptionLength(text); -} - -bool SendFilesBox::validateSingleCaptionLength(const QString &text) const { const auto session = &_show->session(); const auto limit = Data::PremiumLimits(session).captionLengthCurrent(); const auto remove = int(text.size()) - limit; @@ -2227,9 +2171,8 @@ void SendFilesBox::send( return; } for (const auto &file : _list.files) { - if (SkipCaption(file, way)) { - continue; - } else if (!validateSingleCaptionLength(file.caption.text)) { + if (!SkipCaption(file, way) + && !validateLength(file.caption.text)) { return; } } @@ -2245,12 +2188,32 @@ void SendFilesBox::send( file.caption = {}; } } - _confirmedCallback( + + Assert(_list.filesToProcess.empty()); + + auto groups = DivideByGroups( std::move(_list), way, - std::move(caption), - options, + (_limits & SendFilesAllow::OnlyOne)); + auto bundle = PrepareFilesBundle( + std::move(groups), + way, ctrlShiftEnter); + + if (!bundle->groups.empty()) { + auto &group = bundle->groups.back(); + auto &files = group.list.files; + if (!files.empty()) { + auto &captioned = (group.type == Ui::AlbumType::PhotoVideo) + ? files.front() + : files.back(); + if (!captioned.isSticker() || way.sendImagesAsPhotos()) { + captioned.caption = std::move(caption); + } + } + } + + _confirmedCallback(std::move(bundle), options); } closeBox(); } diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index 0c982a9cdb..7531631546 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -40,6 +40,7 @@ class Checkbox; class RoundButton; class InputField; struct GroupMediaLayout; +struct PreparedBundle; class EmojiButton; class AlbumPreview; class VerticalLayout; @@ -88,17 +89,14 @@ using SendFilesCheck = Fn peer); using SendFilesConfirmed = Fn; + std::shared_ptr, + Api::SendOptions)>; struct SendFilesBoxDescriptor { std::shared_ptr show; Ui::PreparedList list; TextWithTags caption; - PeerData *captionToPeer = nullptr; + not_null toPeer; SendFilesLimits limits = {}; SendFilesCheck check; Api::SendType sendType = {}; @@ -220,7 +218,6 @@ private: void refreshPriceTag(); [[nodiscard]] QImage preparePriceTagBg(QSize size) const; - bool validateLength(const QString &text) const; void refreshButtons(); void refreshControls(bool initial = false); void setupSendWayControls(); @@ -244,7 +241,7 @@ private: void refreshTitleText(); void updateBoxSize(); void updateControlsGeometry(); - void updateCaptionPlaceholder(); + void updateCaptionVisibility(); bool addFiles(not_null data); bool addFiles(Ui::PreparedList list); @@ -267,15 +264,13 @@ private: void refreshMessagesCount(); void requestToTakeTextWithTags() const; - bool validateSingleCaptionLength(const QString &text) const; - bool mainCaptionWillBeAttached() const; + bool validateLength(const QString &text) const; [[nodiscard]] Fn prepareSendMenuDetails( const SendFilesBoxDescriptor &descriptor); [[nodiscard]] auto prepareSendMenuCallback() -> Fn; - [[nodiscard]] bool syncMainCaption() const; [[nodiscard]] TextWithTags fieldText() const; const std::shared_ptr _show; @@ -293,7 +288,7 @@ private: Fn _sendMenuDetails; Fn _sendMenuCallback; - PeerData *_captionToPeer = nullptr; + not_null _toPeer; SendFilesCheck _check; SendFilesConfirmed _confirmedCallback; Fn _cancelledCallback; diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.cpp b/Telegram/SourceFiles/data/data_chat_participant_status.cpp index d9b3975d56..4e152fb818 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.cpp +++ b/Telegram/SourceFiles/data/data_chat_participant_status.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "boxes/peers/edit_peer_permissions_box.h" +#include "boxes/premium_limits_box.h" // FileSizeLimitBox. #include "chat_helpers/compose/compose_show.h" #include "data/data_chat.h" #include "data/data_channel.h" @@ -20,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/confirm_box.h" #include "ui/chat/attach/attach_prepare.h" #include "ui/layers/generic_box.h" +#include "ui/text/format_values.h" // FormatDurationWordsSlowmode. #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "window/window_session_controller.h" @@ -540,4 +542,72 @@ void ShowSendErrorToast( }); } +bool ShowSendError( + std::shared_ptr show, + not_null peer, + const Ui::PreparedList &list, + std::optional compress, + bool ignoreSlowmodeLeft) { + const auto error = [&]() -> Data::SendError { + const auto error = Data::FileRestrictionError(peer, list, compress); + if (error) { + return error; + } else if (const auto left = peer->slowmodeSecondsLeft()) { + if (!ignoreSlowmodeLeft) { + return tr::lng_slowmode_enabled( + tr::now, + lt_left, + Ui::FormatDurationWordsSlowmode(left)); + } + } + using Error = Ui::PreparedList::Error; + switch (list.error) { + case Error::None: return QString(); + case Error::EmptyFile: + case Error::Directory: + case Error::NonLocalUrl: return tr::lng_send_image_empty( + tr::now, + lt_name, + list.errorData); + case Error::TooLargeFile: return u"(toolarge)"_q; + } + return tr::lng_forward_send_files_cant(tr::now); + }(); + if (!error) { + return false; + } else if (error.text == u"(toolarge)"_q) { + const auto max = ranges::max_element( + list.files, + {}, + &Ui::PreparedFile::size); + const auto session = &show->session(); + show->show(Box(FileSizeLimitBox, session, max->size, nullptr)); + return true; + } + ShowSendErrorToast(show, peer, error); + return true; +} + +bool ShowSendError( + std::shared_ptr show, + not_null peer, + const Ui::PreparedBundle &bundle, + bool ignoreSlowmodeLeft) { + if (peer->slowmodeApplied() && bundle.groups.size() > 1) { + Data::ShowSendErrorToast( + show, + peer, + tr::lng_slowmode_no_many(tr::now)); + return true; + } + const auto ignore = ignoreSlowmodeLeft; + const auto compress = bundle.way.sendImagesAsPhotos(); + for (const auto &group : bundle.groups) { + if (ShowSendError(show, peer, group.list, compress, ignore)) { + return true; + } + } + return false; +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.h b/Telegram/SourceFiles/data/data_chat_participant_status.h index 0d14508a1e..523b95bb24 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.h +++ b/Telegram/SourceFiles/data/data_chat_participant_status.h @@ -14,6 +14,7 @@ class Show; namespace Ui { struct PreparedList; struct PreparedFile; +struct PreparedBundle; } // namespace Ui namespace Window { @@ -259,4 +260,16 @@ void ShowSendErrorToast( not_null peer, SendError error); +bool ShowSendError( + std::shared_ptr show, + not_null peer, + const Ui::PreparedList &list, + std::optional compress, + bool ignoreSlowmodeLeft = false); +bool ShowSendError( + std::shared_ptr show, + not_null peer, + const Ui::PreparedBundle &bundle, + bool ignoreSlowmodeLeft = false); + } // namespace Data diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 546f41deca..111f41e807 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -6331,47 +6331,13 @@ void HistoryWidget::updateFieldPlaceholder() { bool HistoryWidget::showSendingFilesError( const Ui::PreparedList &list) const { - return showSendingFilesError(list, std::nullopt); + const auto show = controller()->uiShow(); + return Data::ShowSendError(show, _peer, list, std::nullopt); } bool HistoryWidget::showSendingFilesError( - const Ui::PreparedList &list, - std::optional compress) const { - const auto error = [&]() -> Data::SendError { - const auto error = _peer - ? Data::FileRestrictionError(_peer, list, compress) - : Data::SendError(); - if (!_peer || error) { - return error; - } else if (const auto left = _peer->slowmodeSecondsLeft()) { - return tr::lng_slowmode_enabled( - tr::now, - lt_left, - Ui::FormatDurationWordsSlowmode(left)); - } - using Error = Ui::PreparedList::Error; - switch (list.error) { - case Error::None: return QString(); - case Error::EmptyFile: - case Error::Directory: - case Error::NonLocalUrl: return tr::lng_send_image_empty( - tr::now, - lt_name, - list.errorData); - case Error::TooLargeFile: return u"(toolarge)"_q; - } - return tr::lng_forward_send_files_cant(tr::now); - }(); - if (!error) { - return false; - } else if (error.text == u"(toolarge)"_q) { - const auto fileSize = list.files.back().size; - controller()->show( - Box(FileSizeLimitBox, &session(), fileSize, nullptr)); - return true; - } - Data::ShowSendErrorToast(controller(), _peer, error); - return true; + const Ui::PreparedBundle &bundle) const { + return Data::ShowSendError(controller()->uiShow(), _peer, bundle); } MsgId HistoryWidget::resolveReplyToTopicRootId() { @@ -6467,7 +6433,7 @@ bool HistoryWidget::confirmSendingFiles( } controller()->showToast(tr::lng_edit_caption_attach(tr::now)); return false; - } else if (showSendingFilesError(list)) { + } else if (!_peer || showSendingFilesError(list)) { return false; } @@ -6484,17 +6450,9 @@ bool HistoryWidget::confirmSendingFiles( sendMenuDetails()); _field->setTextWithTags({}); box->setConfirmedCallback(crl::guard(this, [=]( - Ui::PreparedList &&list, - Ui::SendFilesWay way, - TextWithTags &&caption, - Api::SendOptions options, - bool ctrlShiftEnter) { - sendingFilesConfirmed( - std::move(list), - way, - std::move(caption), - options, - ctrlShiftEnter); + std::shared_ptr bundle, + Api::SendOptions options) { + sendingFilesConfirmed(std::move(bundle), options); })); box->setCancelledCallback(crl::guard(this, [=] { _field->setTextWithTags(text); @@ -6519,33 +6477,12 @@ bool HistoryWidget::confirmSendingFiles( } void HistoryWidget::sendingFilesConfirmed( - Ui::PreparedList &&list, - Ui::SendFilesWay way, - TextWithTags &&caption, - Api::SendOptions options, - bool ctrlShiftEnter) { - Expects(list.filesToProcess.empty()); - - const auto compress = way.sendImagesAsPhotos(); - if (showSendingFilesError(list, compress)) { + std::shared_ptr bundle, + Api::SendOptions options) { + if (!_peer || showSendingFilesError(*bundle)) { return; } - auto groups = DivideByGroups( - std::move(list), - way, - _peer->slowmodeApplied()); - auto bundle = PrepareFilesBundle( - std::move(groups), - way, - std::move(caption), - ctrlShiftEnter); - sendingFilesConfirmed(std::move(bundle), options); -} - -void HistoryWidget::sendingFilesConfirmed( - std::shared_ptr bundle, - Api::SendOptions options) { const auto compress = bundle->way.sendImagesAsPhotos(); const auto type = compress ? SendMediaType::Photo : SendMediaType::File; auto action = prepareSendAction(options); @@ -6564,21 +6501,12 @@ void HistoryWidget::sendingFilesConfirmed( return; } - if (bundle->sendComment) { - auto message = Api::MessageToSend(action); - message.textWithTags = base::take(bundle->caption); - session().api().sendMessage(std::move(message)); - } + auto &api = session().api(); for (auto &group : bundle->groups) { const auto album = (group.type != Ui::AlbumType::None) ? std::make_shared() : nullptr; - session().api().sendFiles( - std::move(group.list), - type, - base::take(bundle->caption), - album, - action); + api.sendFiles(std::move(group.list), type, album, action); } } diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index e2a187c124..dc6bed1fce 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -484,21 +484,14 @@ private: Ui::PreparedList &&list, const QString &insertTextOnCancel = QString()); bool showSendingFilesError(const Ui::PreparedList &list) const; - bool showSendingFilesError( - const Ui::PreparedList &list, - std::optional compress) const; + bool showSendingFilesError(const Ui::PreparedBundle &bundle) const; + bool showSendMessageError( const TextWithTags &textWithTags, bool ignoreSlowmodeCountdown, Fn withPaymentApproved = nullptr, Api::SendOptions options = {}); - void sendingFilesConfirmed( - Ui::PreparedList &&list, - Ui::SendFilesWay way, - TextWithTags &&caption, - Api::SendOptions options, - bool ctrlShiftEnter); void sendingFilesConfirmed( std::shared_ptr bundle, Api::SendOptions options); diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 8a8f8ac224..de0276d577 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -1168,17 +1168,9 @@ bool ChatWidget::confirmSendingFiles( sendMenuDetails()); box->setConfirmedCallback(crl::guard(this, [=]( - Ui::PreparedList &&list, - Ui::SendFilesWay way, - TextWithTags &&caption, - Api::SendOptions options, - bool ctrlShiftEnter) { - sendingFilesConfirmed( - std::move(list), - way, - std::move(caption), - options, - ctrlShiftEnter); + std::shared_ptr bundle, + Api::SendOptions options) { + sendingFilesConfirmed(std::move(bundle), options); })); box->setCancelledCallback(_composeControls->restoreTextCallback( insertTextOnCancel)); @@ -1192,29 +1184,6 @@ bool ChatWidget::confirmSendingFiles( return true; } -void ChatWidget::sendingFilesConfirmed( - Ui::PreparedList &&list, - Ui::SendFilesWay way, - TextWithTags &&caption, - Api::SendOptions options, - bool ctrlShiftEnter) { - Expects(list.filesToProcess.empty()); - - if (showSendingFilesError(list, way.sendImagesAsPhotos())) { - return; - } - auto groups = DivideByGroups( - std::move(list), - way, - _peer->slowmodeApplied()); - auto bundle = PrepareFilesBundle( - std::move(groups), - way, - std::move(caption), - ctrlShiftEnter); - sendingFilesConfirmed(std::move(bundle), options); -} - bool ChatWidget::checkSendPayment( int messagesCount, Api::SendOptions options, @@ -1230,6 +1199,10 @@ bool ChatWidget::checkSendPayment( void ChatWidget::sendingFilesConfirmed( std::shared_ptr bundle, Api::SendOptions options) { + if (showSendingFilesError(*bundle)) { + return; + } + const auto withPaymentApproved = [=](int approved) { auto copy = options; copy.starsApproved = approved; @@ -1247,21 +1220,12 @@ void ChatWidget::sendingFilesConfirmed( const auto type = compress ? SendMediaType::Photo : SendMediaType::File; auto action = prepareSendAction(options); action.clearDraft = false; - if (bundle->sendComment) { - auto message = Api::MessageToSend(action); - message.textWithTags = base::take(bundle->caption); - session().api().sendMessage(std::move(message)); - } + auto &api = session().api(); for (auto &group : bundle->groups) { const auto album = (group.type != Ui::AlbumType::None) ? std::make_shared() : nullptr; - session().api().sendFiles( - std::move(group.list), - type, - base::take(bundle->caption), - album, - action); + api.sendFiles(std::move(group.list), type, album, action); } if (_composeControls->replyingToMessage().messageId == action.replyTo.messageId) { @@ -1342,47 +1306,13 @@ void ChatWidget::uploadFile( bool ChatWidget::showSendingFilesError( const Ui::PreparedList &list) const { - return showSendingFilesError(list, std::nullopt); + const auto show = controller()->uiShow(); + return Data::ShowSendError(show, _peer, list, std::nullopt); } bool ChatWidget::showSendingFilesError( - const Ui::PreparedList &list, - std::optional compress) const { - const auto error = [&]() -> Data::SendError { - const auto peer = _peer; - const auto error = Data::FileRestrictionError(peer, list, compress); - if (error) { - return error; - } else if (const auto left = _peer->slowmodeSecondsLeft()) { - return tr::lng_slowmode_enabled( - tr::now, - lt_left, - Ui::FormatDurationWordsSlowmode(left)); - } - using Error = Ui::PreparedList::Error; - switch (list.error) { - case Error::None: return QString(); - case Error::EmptyFile: - case Error::Directory: - case Error::NonLocalUrl: return tr::lng_send_image_empty( - tr::now, - lt_name, - list.errorData); - case Error::TooLargeFile: return u"(toolarge)"_q; - } - return tr::lng_forward_send_files_cant(tr::now); - }(); - if (!error) { - return false; - } else if (error.text == u"(toolarge)"_q) { - const auto fileSize = list.files.back().size; - controller()->show( - Box(FileSizeLimitBox, &session(), fileSize, nullptr)); - return true; - } - - Data::ShowSendErrorToast(controller(), _peer, error); - return true; + const Ui::PreparedBundle &bundle) const { + return Data::ShowSendError(controller()->uiShow(), _peer, bundle); } Api::SendAction ChatWidget::prepareSendAction( diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.h b/Telegram/SourceFiles/history/view/history_view_chat_section.h index 09ec40e9ae..03dca92c99 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.h +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.h @@ -341,15 +341,8 @@ private: std::optional overrideSendImagesAsPhotos, const QString &insertTextOnCancel = QString()); bool showSendingFilesError(const Ui::PreparedList &list) const; - bool showSendingFilesError( - const Ui::PreparedList &list, - std::optional compress) const; - void sendingFilesConfirmed( - Ui::PreparedList &&list, - Ui::SendFilesWay way, - TextWithTags &&caption, - Api::SendOptions options, - bool ctrlShiftEnter); + bool showSendingFilesError(const Ui::PreparedBundle &bundle) const; + void sendingFilesConfirmed( std::shared_ptr bundle, Api::SendOptions options); diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 201bf390c4..dfc56da4e0 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -567,17 +567,9 @@ bool ScheduledWidget::confirmSendingFiles( SendMenu::Details()); box->setConfirmedCallback(crl::guard(this, [=]( - Ui::PreparedList &&list, - Ui::SendFilesWay way, - TextWithTags &&caption, - Api::SendOptions options, - bool ctrlShiftEnter) { - sendingFilesConfirmed( - std::move(list), - way, - std::move(caption), - options, - ctrlShiftEnter); + std::shared_ptr bundle, + Api::SendOptions options) { + sendingFilesConfirmed(std::move(bundle), options); })); box->setCancelledCallback(_composeControls->restoreTextCallback( insertTextOnCancel)); @@ -592,46 +584,29 @@ bool ScheduledWidget::confirmSendingFiles( } void ScheduledWidget::sendingFilesConfirmed( - Ui::PreparedList &&list, - Ui::SendFilesWay way, - TextWithTags &&caption, - Api::SendOptions options, - bool ctrlShiftEnter) { - Expects(list.filesToProcess.empty()); - - if (showSendingFilesError(list, way.sendImagesAsPhotos())) { + std::shared_ptr bundle, + Api::SendOptions options) { + if (showSendingFilesError(*bundle)) { return; } - auto groups = DivideByGroups(std::move(list), way, false); - const auto captionAttached = CaptionWillBeAttached(groups); - const auto type = way.sendImagesAsPhotos() - ? SendMediaType::Photo - : SendMediaType::File; + const auto compress = bundle->way.sendImagesAsPhotos(); + const auto type = compress ? SendMediaType::Photo : SendMediaType::File; auto action = prepareSendAction(options); action.clearDraft = false; - if (!captionAttached && !caption.text.isEmpty()) { - auto message = Api::MessageToSend(action); - message.textWithTags = base::take(caption); - session().api().sendMessage(std::move(message)); - } - for (auto &group : groups) { + auto &api = session().api(); + for (auto &group : bundle->groups) { const auto album = (group.type != Ui::AlbumType::None) ? std::make_shared() : nullptr; - session().api().sendFiles( - std::move(group.list), - type, - base::take(caption), - album, - action); + api.sendFiles(std::move(group.list), type, album, action); } } bool ScheduledWidget::confirmSendingFiles( - QImage &&image, - QByteArray &&content, - std::optional overrideSendImagesAsPhotos, - const QString &insertTextOnCancel) { + QImage &&image, + QByteArray &&content, + std::optional overrideSendImagesAsPhotos, + const QString &insertTextOnCancel) { if (image.isNull()) { return false; } @@ -667,8 +642,8 @@ void ScheduledWidget::checkReplyReturns() { } void ScheduledWidget::uploadFile( - const QByteArray &fileContent, - SendMediaType type) { + const QByteArray &fileContent, + SendMediaType type) { const auto callback = [=](Api::SendOptions options) { session().api().sendFile( fileContent, @@ -680,46 +655,20 @@ void ScheduledWidget::uploadFile( } bool ScheduledWidget::showSendingFilesError( - const Ui::PreparedList &list) const { - return showSendingFilesError(list, std::nullopt); + const Ui::PreparedList &list) const { + const auto peer = _history->peer; + const auto show = controller()->uiShow(); + return Data::ShowSendError(show, peer, list, std::nullopt, true); } bool ScheduledWidget::showSendingFilesError( - const Ui::PreparedList &list, - std::optional compress) const { - const auto error = [&]() -> Data::SendError { - using Error = Ui::PreparedList::Error; - const auto peer = _history->peer; - const auto error = Data::FileRestrictionError(peer, list, compress); - if (error) { - return error; - } else switch (list.error) { - case Error::None: return QString(); - case Error::EmptyFile: - case Error::Directory: - case Error::NonLocalUrl: return tr::lng_send_image_empty( - tr::now, - lt_name, - list.errorData); - case Error::TooLargeFile: return u"(toolarge)"_q; - } - return tr::lng_forward_send_files_cant(tr::now); - }(); - if (!error) { - return false; - } else if (error.text == u"(toolarge)"_q) { - const auto fileSize = list.files.back().size; - controller()->show( - Box(FileSizeLimitBox, &session(), fileSize, nullptr)); - return true; - } - - Data::ShowSendErrorToast(controller(), _history->peer, error); - return true; + const Ui::PreparedBundle &bundle) const { + const auto show = controller()->uiShow(); + return Data::ShowSendError(show, _history->peer, bundle, true); } Api::SendAction ScheduledWidget::prepareSendAction( - Api::SendOptions options) const { + Api::SendOptions options) const { auto result = Api::SendAction(_history, options); result.options.sendAs = _composeControls->sendAsPeer(); if (_forumTopic) { diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h index bd79ded77b..d5239a2e86 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h @@ -40,6 +40,7 @@ class ScrollArea; class PlainShadow; class FlatButton; struct PreparedList; +struct PreparedBundle; class SendFilesWay; class ImportantTooltip; } // namespace Ui @@ -256,15 +257,11 @@ private: std::optional overrideSendImagesAsPhotos, const QString &insertTextOnCancel = QString()); bool showSendingFilesError(const Ui::PreparedList &list) const; - bool showSendingFilesError( - const Ui::PreparedList &list, - std::optional compress) const; + bool showSendingFilesError(const Ui::PreparedBundle &bundle) const; + void sendingFilesConfirmed( - Ui::PreparedList &&list, - Ui::SendFilesWay way, - TextWithTags &&caption, - Api::SendOptions options, - bool ctrlShiftEnter); + std::shared_ptr bundle, + Api::SendOptions options); bool sendExistingDocument( not_null document, diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 726a4803d0..fc80a65e40 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -513,45 +513,15 @@ void ReplyArea::uploadFile( bool ReplyArea::showSendingFilesError( const Ui::PreparedList &list) const { - return showSendingFilesError(list, std::nullopt); + const auto show = _controller->uiShow(); + const auto peer = _data.peer; + return Data::ShowSendError(show, peer, list, std::nullopt, true); } bool ReplyArea::showSendingFilesError( - const Ui::PreparedList &list, - std::optional compress) const { - const auto error = [&]() -> Data::SendError { - const auto peer = _data.peer; - const auto error = Data::FileRestrictionError(peer, list, compress); - if (error) { - return error; - } - using Error = Ui::PreparedList::Error; - switch (list.error) { - case Error::None: return QString(); - case Error::EmptyFile: - case Error::Directory: - case Error::NonLocalUrl: return tr::lng_send_image_empty( - tr::now, - lt_name, - list.errorData); - case Error::TooLargeFile: return u"(toolarge)"_q; - } - return tr::lng_forward_send_files_cant(tr::now); - }(); - if (!error) { - return false; - } else if (error.text == u"(toolarge)"_q) { - const auto fileSize = list.files.back().size; - _controller->uiShow()->showBox(Box( - FileSizeLimitBox, - &session(), - fileSize, - &st::storiesComposePremium)); - return true; - } - - Data::ShowSendErrorToast(_controller->uiShow(), _data.peer, error); - return true; + const Ui::PreparedBundle &bundle) const { + const auto show = _controller->uiShow(); + return Data::ShowSendError(show, _data.peer, bundle, true); } not_null ReplyArea::history() const { @@ -705,7 +675,7 @@ bool ReplyArea::confirmSendingFiles( .show = show, .list = std::move(list), .caption = _controls->getTextWithAppliedMarkdown(), - .captionToPeer = _data.peer, + .toPeer = _data.peer, .limits = DefaultLimitsForPeer(_data.peer), .check = DefaultCheckForPeer(show, _data.peer), .sendType = Api::SendType::Normal, @@ -718,32 +688,12 @@ bool ReplyArea::confirmSendingFiles( return true; } -void ReplyArea::sendingFilesConfirmed( - Ui::PreparedList &&list, - Ui::SendFilesWay way, - TextWithTags &&caption, - Api::SendOptions options, - bool ctrlShiftEnter) { - Expects(list.filesToProcess.empty()); - - if (showSendingFilesError(list, way.sendImagesAsPhotos())) { - return; - } - auto groups = DivideByGroups( - std::move(list), - way, - _data.peer->slowmodeApplied()); - auto bundle = PrepareFilesBundle( - std::move(groups), - way, - std::move(caption), - ctrlShiftEnter); - sendingFilesConfirmed(std::move(bundle), options); -} - void ReplyArea::sendingFilesConfirmed( std::shared_ptr bundle, Api::SendOptions options) { + if (showSendingFilesError(*bundle)) { + return; + } const auto compress = bundle->way.sendImagesAsPhotos(); const auto type = compress ? SendMediaType::Photo : SendMediaType::File; auto action = prepareSendAction(options); @@ -762,21 +712,12 @@ void ReplyArea::sendingFilesConfirmed( return; } - if (bundle->sendComment) { - auto message = Api::MessageToSend(action); - message.textWithTags = base::take(bundle->caption); - session().api().sendMessage(std::move(message)); - } + auto &api = session().api(); for (auto &group : bundle->groups) { const auto album = (group.type != Ui::AlbumType::None) ? std::make_shared() : nullptr; - session().api().sendFiles( - std::move(group.list), - type, - base::take(bundle->caption), - album, - action); + api.sendFiles(std::move(group.list), type, album, action); } finishSending(); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h index bbc9414afa..461e58ce8b 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.h +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h @@ -127,15 +127,8 @@ private: std::optional overrideSendImagesAsPhotos, const QString &insertTextOnCancel = QString()); bool showSendingFilesError(const Ui::PreparedList &list) const; - bool showSendingFilesError( - const Ui::PreparedList &list, - std::optional compress) const; - void sendingFilesConfirmed( - Ui::PreparedList &&list, - Ui::SendFilesWay way, - TextWithTags &&caption, - Api::SendOptions options, - bool ctrlShiftEnter); + bool showSendingFilesError(const Ui::PreparedBundle &bundle) const; + void sendingFilesConfirmed( std::shared_ptr bundle, Api::SendOptions options); diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 0c833f8701..b85873fd6b 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/file_utilities.h" #include "core/mime_type.h" #include "data/business/data_shortcut_messages.h" +#include "data/data_chat_participant_status.h" #include "data/data_message_reaction_id.h" #include "data/data_premium_limits.h" #include "data/data_session.h" @@ -194,9 +195,6 @@ private: QByteArray &&content, std::optional overrideSendImagesAsPhotos = std::nullopt, const QString &insertTextOnCancel = QString()); - bool confirmSendingFiles( - const QStringList &files, - const QString &insertTextOnCancel); bool confirmSendingFiles( Ui::PreparedList &&list, const QString &insertTextOnCancel = QString()); @@ -205,15 +203,11 @@ private: std::optional overrideSendImagesAsPhotos, const QString &insertTextOnCancel = QString()); bool showSendingFilesError(const Ui::PreparedList &list) const; - bool showSendingFilesError( - const Ui::PreparedList &list, - std::optional compress) const; + bool showSendingFilesError(const Ui::PreparedBundle &bundle) const; + void sendingFilesConfirmed( - Ui::PreparedList &&list, - Ui::SendFilesWay way, - TextWithTags &&caption, - Api::SendOptions options, - bool ctrlShiftEnter); + std::shared_ptr bundle, + Api::SendOptions options); bool sendExistingDocument( not_null document, @@ -1143,40 +1137,21 @@ void ShortcutMessages::uploadFile( bool ShortcutMessages::showSendingFilesError( const Ui::PreparedList &list) const { - return showSendingFilesError(list, std::nullopt); -} - -bool ShortcutMessages::showSendingFilesError( - const Ui::PreparedList &list, - std::optional compress) const { if (showPremiumRequired()) { return true; } - const auto text = [&] { - using Error = Ui::PreparedList::Error; - switch (list.error) { - case Error::None: return QString(); - case Error::EmptyFile: - case Error::Directory: - case Error::NonLocalUrl: return tr::lng_send_image_empty( - tr::now, - lt_name, - list.errorData); - case Error::TooLargeFile: return u"(toolarge)"_q; - } - return tr::lng_forward_send_files_cant(tr::now); - }(); - if (text.isEmpty()) { - return false; - } else if (text == u"(toolarge)"_q) { - const auto fileSize = list.files.back().size; - _controller->show( - Box(FileSizeLimitBox, _session, fileSize, nullptr)); + const auto show = _controller->uiShow(); + const auto peer = _controller->session().user(); + return Data::ShowSendError(show, peer, list, std::nullopt, true); +} + +bool ShortcutMessages::showSendingFilesError( + const Ui::PreparedBundle &bundle) const { + if (showPremiumRequired()) { return true; } - - _controller->showToast(text); - return true; + const auto peer = _controller->session().user(); + return Data::ShowSendError(_controller->uiShow(), peer, bundle, true); } Api::SendAction ShortcutMessages::prepareSendAction( @@ -1360,17 +1335,9 @@ bool ShortcutMessages::confirmSendingFiles( SendMenu::Details()); box->setConfirmedCallback(crl::guard(this, [=]( - Ui::PreparedList &&list, - Ui::SendFilesWay way, - TextWithTags &&caption, - Api::SendOptions options, - bool ctrlShiftEnter) { - sendingFilesConfirmed( - std::move(list), - way, - std::move(caption), - options, - ctrlShiftEnter); + std::shared_ptr bundle, + Api::SendOptions options) { + sendingFilesConfirmed(std::move(bundle), options); })); box->setCancelledCallback(_composeControls->restoreTextCallback( insertTextOnCancel)); @@ -1402,41 +1369,21 @@ bool ShortcutMessages::confirmSendingFiles( } void ShortcutMessages::sendingFilesConfirmed( - Ui::PreparedList &&list, - Ui::SendFilesWay way, - TextWithTags &&caption, - Api::SendOptions options, - bool ctrlShiftEnter) { - Expects(list.filesToProcess.empty()); - - if (showSendingFilesError(list, way.sendImagesAsPhotos())) { + std::shared_ptr bundle, + Api::SendOptions options) { + if (showSendingFilesError(*bundle)) { return; } - auto groups = DivideByGroups( - std::move(list), - way, - _history->peer->slowmodeApplied()); - const auto captionAttached = CaptionWillBeAttached(groups); - const auto type = way.sendImagesAsPhotos() - ? SendMediaType::Photo - : SendMediaType::File; + const auto compress = bundle->way.sendImagesAsPhotos(); + const auto type = compress ? SendMediaType::Photo : SendMediaType::File; auto action = prepareSendAction(options); action.clearDraft = false; - if (!captionAttached && !caption.text.isEmpty()) { - auto message = Api::MessageToSend(action); - message.textWithTags = base::take(caption); - _session->api().sendMessage(std::move(message)); - } - for (auto &group : groups) { + auto &api = _session->api(); + for (auto &group : bundle->groups) { const auto album = (group.type != Ui::AlbumType::None) ? std::make_shared() : nullptr; - _session->api().sendFiles( - std::move(group.list), - type, - base::take(caption), - album, - action); + api.sendFiles(std::move(group.list), type, album, action); } if (_composeControls->replyingToMessage() == action.replyTo) { _composeControls->cancelReplyMessage(); diff --git a/Telegram/SourceFiles/settings/sections/settings_chat.cpp b/Telegram/SourceFiles/settings/sections/settings_chat.cpp index 2cac29431a..c5634ab125 100644 --- a/Telegram/SourceFiles/settings/sections/settings_chat.cpp +++ b/Telegram/SourceFiles/settings/sections/settings_chat.cpp @@ -102,7 +102,7 @@ constexpr auto kCustomColorButtonParts = 7; #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) return true; #else - return !Platform::IsWindows() || !Platform::IsWindows8OrGreater(); + return !Platform::IsWindows() || Platform::IsWindows8OrGreater(); #endif } diff --git a/Telegram/SourceFiles/support/support_helper.cpp b/Telegram/SourceFiles/support/support_helper.cpp index 1644fb89e4..da298ace5b 100644 --- a/Telegram/SourceFiles/support/support_helper.cpp +++ b/Telegram/SourceFiles/support/support_helper.cpp @@ -719,15 +719,18 @@ QString InterpretSendPath( const auto sendTo = [=](not_null thread) { window->showThread(thread); const auto premium = thread->session().user()->isPremium(); - thread->session().api().sendFiles( - Storage::PrepareMediaList( - QStringList(filePath), - st::sendMediaPreviewSize, - premium), - SendMediaType::File, - { caption }, - nullptr, - Api::SendAction(thread)); + auto list = Storage::PrepareMediaList( + QStringList(filePath), + st::sendMediaPreviewSize, + premium); + if (!list.files.empty()) { + list.files.back().caption.text = caption; + thread->session().api().sendFiles( + std::move(list), + SendMediaType::File, + nullptr, + Api::SendAction(thread)); + } }; if (!history) { return "App Error: Could not find channel with id: " diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp index ac5f3f2eca..61b43fb979 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp @@ -30,9 +30,6 @@ struct GroupRange { [[nodiscard]] int size() const { return till - from; } - [[nodiscard]] bool sentWithCaption() const { - return (size() == 1) || (type == AlbumType::PhotoVideo); - } }; [[nodiscard]] AlbumType GroupTypeForFile( @@ -93,18 +90,6 @@ struct GroupRange { return result; } -[[nodiscard]] bool CaptionWillBeAttachedFromRanges( - const std::vector &ranges, - int filesCount) { - // Let's send text after media, as it is shown in SendFilesBox. - - //const auto hasGroupedFileAlbum = ranges::any_of(ranges, [](const auto &r) { - // return (r.size() > 1) && (r.type == AlbumType::File); - //}); - return /*((filesCount > 1) && hasGroupedFileAlbum) - || */((ranges.size() == 1) && ranges.front().sentWithCaption()); -} - } // namespace PreparedFile::PreparedFile(const QString &path) : path(path) { @@ -256,45 +241,33 @@ bool PreparedList::canBeSentInSlowmodeWith(const PreparedList &other) const { return !hasNonGrouping && (!hasFiles || !hasVideos); } -bool PreparedList::canAddCaption(bool sendingAlbum, bool compress) const { - if (!filesToProcess.empty() - || files.empty() - || files.size() > kMaxAlbumCount) { +bool PreparedList::canAddCaption(bool compress) const { + if (files.empty()) { return false; } - if (files.size() == 1) { - Assert(files.front().information != nullptr); - const auto isSticker = (!compress - && Core::IsMimeSticker(files.front().information->filemime)) - || files.front().path.endsWith(u".tgs"_q, Qt::CaseInsensitive); - return !isSticker; - } else if (!sendingAlbum) { - return false; - } - const auto hasFiles = ranges::contains( - files, - PreparedFile::Type::File, - &PreparedFile::type); - const auto hasMusic = ranges::contains( - files, - PreparedFile::Type::Music, - &PreparedFile::type); - const auto hasNotGrouped = ranges::contains( - files, - PreparedFile::Type::None, - &PreparedFile::type); - return !hasFiles && !hasMusic && !hasNotGrouped; + const auto &last = files.back(); + const auto isSticker = last.path.endsWith(u".tgs"_q, Qt::CaseInsensitive) + || (!compress + && last.information + && Core::IsMimeSticker(last.information->filemime)); + return !isSticker; } bool PreparedList::canMoveCaption(bool sendingAlbum, bool compress) const { - if (!canAddCaption(sendingAlbum, compress)) { + if (!canAddCaption(compress)) { return false; - } else if (files.size() != 1) { - return true; + } else if (files.size() > kMaxAlbumCount) { + return false; + } else if (!sendingAlbum || !compress) { + return (files.size() == 1); } - const auto &file = files.front(); - return (file.type == PreparedFile::Type::Video) - || (file.type == PreparedFile::Type::Photo && compress); + for (const auto &file : files) { + if (file.type != PreparedFile::Type::Photo + && file.type != PreparedFile::Type::Video) { + return false; + } + } + return true; } bool PreparedList::canChangePrice(bool sendingAlbum, bool compress) const { @@ -359,53 +332,18 @@ bool PreparedList::hasSpoilerMenu(bool compress) const { return allAreVideo || (allAreMedia && compress); } -bool AttachCaptionToFirstAsFile( - const std::vector &groups) { - // Let's send text after media, as it is shown in SendFilesBox. - return false; - //auto filesCount = 0; - //auto hasGroupedFileAlbum = false; - //for (const auto &group : groups) { - // filesCount += group.list.files.size(); - // hasGroupedFileAlbum = hasGroupedFileAlbum - // || ((group.list.files.size() > 1) - // && (group.type == AlbumType::File)); - //} - //const auto result = (filesCount > 1) && hasGroupedFileAlbum; - //return result; -} - -bool CaptionWillBeAttached(const std::vector &groups) { - return AttachCaptionToFirstAsFile(groups) - || ((groups.size() == 1) && groups.front().sentWithCaption()); -} - -bool CaptionWillBeAttached( - const PreparedList &list, - SendFilesWay way, - bool slowmode) { - const auto ranges = GroupRanges(list.files, way, slowmode); - return CaptionWillBeAttachedFromRanges(ranges, int(list.files.size())); -} - std::shared_ptr PrepareFilesBundle( std::vector groups, SendFilesWay way, - TextWithTags caption, bool ctrlShiftEnter) { auto totalCount = 0; for (const auto &group : groups) { totalCount += group.list.files.size(); } - const auto captionAttached = CaptionWillBeAttached(groups); - const auto sendComment = !caption.text.isEmpty() - && !captionAttached; return std::make_shared(PreparedBundle{ .groups = std::move(groups), .way = way, - .caption = std::move(caption), - .totalCount = totalCount + (sendComment ? 1 : 0), - .sendComment = sendComment, + .totalCount = totalCount, .ctrlShiftEnter = ctrlShiftEnter, }); } diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h index bee70e6421..f87197539d 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h @@ -117,7 +117,7 @@ struct PreparedList { std::vector order); void mergeToEnd(PreparedList &&other, bool cutToAlbumSize = false); - [[nodiscard]] bool canAddCaption(bool sendingAlbum, bool compress) const; + [[nodiscard]] bool canAddCaption(bool compress) const; [[nodiscard]] bool canMoveCaption( bool sendingAlbum, bool compress) const; @@ -144,36 +144,22 @@ struct PreparedList { struct PreparedGroup { PreparedList list; AlbumType type = AlbumType::None; - - [[nodiscard]] bool sentWithCaption() const { - return (list.files.size() == 1) - || (type == AlbumType::PhotoVideo); - } }; [[nodiscard]] std::vector DivideByGroups( PreparedList &&list, SendFilesWay way, bool slowmode); -[[nodiscard]] bool CaptionWillBeAttached( - const std::vector &groups); -[[nodiscard]] bool CaptionWillBeAttached( - const PreparedList &list, - SendFilesWay way, - bool slowmode); struct PreparedBundle { std::vector groups; SendFilesWay way; - TextWithTags caption; int totalCount = 0; - bool sendComment = false; bool ctrlShiftEnter = false; }; [[nodiscard]] std::shared_ptr PrepareFilesBundle( std::vector groups, SendFilesWay way, - TextWithTags caption, bool ctrlShiftEnter); [[nodiscard]] int MaxAlbumItems(); From 58d334c1c8e0bde8073a833ccbf85cb2f8a8636b Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 6 Mar 2026 21:59:51 +0400 Subject: [PATCH 045/415] Fix crash in restrict-from-recent-actions. --- .../boxes/peers/edit_participant_box.cpp | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp index 72d88cacfe..153bcf203b 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp @@ -942,20 +942,27 @@ void EditRestrictedBox::prepare() { if (!_saveCallback) { return; } + const auto show = uiShow(); + const auto rankPeer = peer(); + const auto rankUser = user(); + const auto rank = _tagControl + ? _tagControl->currentRank() + : _oldRank; + const auto saveRank = (rank != _oldRank); + + // May destroy the box. _saveCallback( _oldRights, ChatRestrictionsInfo{ value(), getRealUntilValue() }); - if (_tagControl) { - const auto rank = _tagControl->currentRank(); - if (rank != _oldRank) { - SaveMemberRank( - uiShow(), - peer(), - user(), - rank, - nullptr, - nullptr); - } + + if (saveRank) { + SaveMemberRank( + show, + rankPeer, + rankUser, + rank, + nullptr, + nullptr); } }; addButton(tr::lng_settings_save(), save); From 9d371ddb94ac965a39db54aac982c64034bd871e Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 6 Mar 2026 22:02:43 +0400 Subject: [PATCH 046/415] Fix controls. --- .../SourceFiles/media/view/media_view_overlay_widget.cpp | 5 ++++- Telegram/SourceFiles/settings/sections/settings_chat.cpp | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 737f59c88a..6d4a20f478 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -4823,8 +4823,11 @@ void OverlayWidget::applyVideoQuality(VideoQuality value) { const auto startStreaming = StartStreaming(false, time); if (!canInitStreaming() || !initStreaming(startStreaming)) { redisplayContent(); - } else { + } else if (_fullScreenVideo != wasFullScreen) { _fullScreenVideo = wasFullScreen; + if (_streamed->controls) { + _streamed->controls->setInFullScreen(_fullScreenVideo); + } } } diff --git a/Telegram/SourceFiles/settings/sections/settings_chat.cpp b/Telegram/SourceFiles/settings/sections/settings_chat.cpp index c5634ab125..2cac29431a 100644 --- a/Telegram/SourceFiles/settings/sections/settings_chat.cpp +++ b/Telegram/SourceFiles/settings/sections/settings_chat.cpp @@ -102,7 +102,7 @@ constexpr auto kCustomColorButtonParts = 7; #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) return true; #else - return !Platform::IsWindows() || Platform::IsWindows8OrGreater(); + return !Platform::IsWindows() || !Platform::IsWindows8OrGreater(); #endif } From 8eab8af02db6b248150b70a0390961ec868e6ae4 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 6 Mar 2026 22:03:20 +0400 Subject: [PATCH 047/415] Don't always lower members count when banning. Fixes #30371. --- Telegram/SourceFiles/data/data_channel.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index a79b0ca10b..203495d52d 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -562,13 +562,17 @@ void ChannelData::applyEditBanned( not_null{ user }); if (i != mgInfo->lastParticipants.end()) { mgInfo->lastParticipants.erase(i); - } - if (membersCount() > 1) { - setMembersCount(membersCount() - 1); + if (membersCount() > 1) { + setMembersCount(membersCount() - 1); + } else { + mgInfo->lastParticipantsStatus |= MegagroupInfo::LastParticipantsCountOutdated; + mgInfo->lastParticipantsCount = 0; + } } else { mgInfo->lastParticipantsStatus |= MegagroupInfo::LastParticipantsCountOutdated; - mgInfo->lastParticipantsCount = 0; } + flags |= UpdateFlag::Members; + owner().removeMegagroupParticipant(this, user); setKickedCount(kickedCount() + 1); if (mgInfo->bots.contains(user)) { mgInfo->bots.remove(user); @@ -576,17 +580,11 @@ void ChannelData::applyEditBanned( mgInfo->botStatus = -1; } } - flags |= UpdateFlag::Members; - owner().removeMegagroupParticipant(this, user); } } Data::ChannelAdminChanges(this).remove(peerToUser(user->id)); } else if (!mgInfo) { if (isKicked) { - if (user && membersCount() > 1) { - setMembersCount(membersCount() - 1); - flags |= UpdateFlag::Members; - } setKickedCount(kickedCount() + 1); } } From ffea1e3917a5471fa3405966d5d8244790b8f547 Mon Sep 17 00:00:00 2001 From: futpib Date: Wed, 18 Feb 2026 02:08:06 +0000 Subject: [PATCH 048/415] Defer sticker reads via crl::on_main chain for faster first paint Instead of reading all sticker sets synchronously in crl::on_main, chain each read step via crl::on_main so that paint events can be processed between heavy file reads. This moves first paint from ~2807ms to ~1294ms by allowing the UI to render before sticker loading completes. --- Telegram/SourceFiles/main/main_session.cpp | 66 ++++++++++++++++------ 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index 934e82ad99..31be2eaa86 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -181,6 +181,55 @@ Session::Session( _selfUserpicView = view.cloud; }, lifetime()); + // Storage::Account uses Main::Account::session() in those methods. + // So they can't be called during Main::Session construction. + // They are deferred via crl::on_main which fires after the + // constructor returns and _session is set. + // + // Steps are chained via crl::on_main so that paint events + // can be processed between heavy file reads. + const auto steps = std::make_shared>>( + std::initializer_list>{ + [=] { + local().readInstalledStickers(); + }, [=] { + local().readInstalledMasks(); + }, [=] { + local().readInstalledCustomEmoji(); + }, [=] { + data().stickers().notifyUpdated(Data::StickersType::Stickers); + data().stickers().notifyUpdated(Data::StickersType::Masks); + data().stickers().notifyUpdated(Data::StickersType::Emoji); + }, [=] { + local().readFeaturedStickers(); + }, [=] { + local().readFeaturedCustomEmoji(); + }, [=] { + local().readRecentStickers(); + local().readRecentMasks(); + local().readFavedStickers(); + local().readSavedGifs(); + }, [=] { + data().stickers().notifyUpdated(Data::StickersType::Stickers); + data().stickers().notifyUpdated(Data::StickersType::Masks); + data().stickers().notifyUpdated(Data::StickersType::Emoji); + data().stickers().notifySavedGifsUpdated(); + DEBUG_LOG(("Init: Account stored data load finished.")); + }, + }); + const auto runNext = std::make_shared>(); + *runNext = crl::guard(this, [=] { + if (steps->empty()) { + return; + } + auto step = std::move(steps->front()); + steps->erase(steps->begin()); + step(); + if (!steps->empty()) { + crl::on_main(*runNext); + } + }); + crl::on_main(this, [=] { using Flag = Data::PeerUpdate::Flag; changes().peerUpdates( @@ -211,22 +260,7 @@ Session::Session( saveSettingsDelayed(); } - // Storage::Account uses Main::Account::session() in those methods. - // So they can't be called during Main::Session construction. - local().readInstalledStickers(); - local().readInstalledMasks(); - local().readInstalledCustomEmoji(); - local().readFeaturedStickers(); - local().readFeaturedCustomEmoji(); - local().readRecentStickers(); - local().readRecentMasks(); - local().readFavedStickers(); - local().readSavedGifs(); - data().stickers().notifyUpdated(Data::StickersType::Stickers); - data().stickers().notifyUpdated(Data::StickersType::Masks); - data().stickers().notifyUpdated(Data::StickersType::Emoji); - data().stickers().notifySavedGifsUpdated(); - DEBUG_LOG(("Init: Account stored data load finished.")); + crl::on_main(*runNext); }); #ifndef TDESKTOP_DISABLE_SPELLCHECK From 68e1504685854156b85bde73d6555d051e39f034 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 6 Mar 2026 22:37:17 +0400 Subject: [PATCH 049/415] Use crl::on_main_queue for chained init steps. --- Telegram/SourceFiles/main/main_session.cpp | 84 ++++++++-------------- Telegram/lib_crl | 2 +- 2 files changed, 32 insertions(+), 54 deletions(-) diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index 31be2eaa86..3b00a73f31 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -181,56 +181,7 @@ Session::Session( _selfUserpicView = view.cloud; }, lifetime()); - // Storage::Account uses Main::Account::session() in those methods. - // So they can't be called during Main::Session construction. - // They are deferred via crl::on_main which fires after the - // constructor returns and _session is set. - // - // Steps are chained via crl::on_main so that paint events - // can be processed between heavy file reads. - const auto steps = std::make_shared>>( - std::initializer_list>{ - [=] { - local().readInstalledStickers(); - }, [=] { - local().readInstalledMasks(); - }, [=] { - local().readInstalledCustomEmoji(); - }, [=] { - data().stickers().notifyUpdated(Data::StickersType::Stickers); - data().stickers().notifyUpdated(Data::StickersType::Masks); - data().stickers().notifyUpdated(Data::StickersType::Emoji); - }, [=] { - local().readFeaturedStickers(); - }, [=] { - local().readFeaturedCustomEmoji(); - }, [=] { - local().readRecentStickers(); - local().readRecentMasks(); - local().readFavedStickers(); - local().readSavedGifs(); - }, [=] { - data().stickers().notifyUpdated(Data::StickersType::Stickers); - data().stickers().notifyUpdated(Data::StickersType::Masks); - data().stickers().notifyUpdated(Data::StickersType::Emoji); - data().stickers().notifySavedGifsUpdated(); - DEBUG_LOG(("Init: Account stored data load finished.")); - }, - }); - const auto runNext = std::make_shared>(); - *runNext = crl::guard(this, [=] { - if (steps->empty()) { - return; - } - auto step = std::move(steps->front()); - steps->erase(steps->begin()); - step(); - if (!steps->empty()) { - crl::on_main(*runNext); - } - }); - - crl::on_main(this, [=] { + crl::on_main_queue(this, { [=] { using Flag = Data::PeerUpdate::Flag; changes().peerUpdates( _user, @@ -259,9 +210,36 @@ Session::Session( }); saveSettingsDelayed(); } - - crl::on_main(*runNext); - }); + }, [=] { + // Storage::Account uses Main::Account::session() in those methods. + // So they can't be called during Main::Session construction. + // + // They are deferred via crl::on_main which fires after the + // constructor returns and _session is set. + // + // Steps are chained via crl::on_main so that paint events + // can be processed between heavy file reads. + local().readInstalledStickers(); + }, [=] { + local().readInstalledMasks(); + }, [=] { + local().readInstalledCustomEmoji(); + }, [=] { + local().readFeaturedStickers(); + }, [=] { + local().readFeaturedCustomEmoji(); + }, [=] { + local().readRecentStickers(); + local().readRecentMasks(); + local().readFavedStickers(); + local().readSavedGifs(); + }, [=] { + data().stickers().notifyUpdated(Data::StickersType::Stickers); + data().stickers().notifyUpdated(Data::StickersType::Masks); + data().stickers().notifyUpdated(Data::StickersType::Emoji); + data().stickers().notifySavedGifsUpdated(); + DEBUG_LOG(("Init: Account stored data load finished.")); + } }).dispatch(); #ifndef TDESKTOP_DISABLE_SPELLCHECK Spellchecker::Start(this); diff --git a/Telegram/lib_crl b/Telegram/lib_crl index a41edfcfa8..f770e4e8be 160000 --- a/Telegram/lib_crl +++ b/Telegram/lib_crl @@ -1 +1 @@ -Subproject commit a41edfcfa8c04057deb8a1a38fca145248a9421a +Subproject commit f770e4e8be24553b22278a8ac07f5eb3c7aa6547 From 5f1121123c6cccbb9ed9efa456c211f1c32d7299 Mon Sep 17 00:00:00 2001 From: futpib Date: Thu, 19 Feb 2026 10:17:29 +0000 Subject: [PATCH 050/415] Freeze chat list order while mouse hovers over it When subscribed to many active chats, the list reorders too quickly, causing misclicks. Freeze the visual order while the mouse is over the chat list and replay deferred reorderings when the mouse leaves or after 5 seconds of inactivity. Fixes #1504. --- .../dialogs/dialogs_indexed_list.cpp | 14 ++++++++++++++ .../SourceFiles/dialogs/dialogs_indexed_list.h | 2 ++ .../dialogs/dialogs_inner_widget.cpp | 17 ++++++++++++++++- .../SourceFiles/dialogs/dialogs_inner_widget.h | 1 + Telegram/SourceFiles/dialogs/dialogs_list.cpp | 17 +++++++++++++++++ Telegram/SourceFiles/dialogs/dialogs_list.h | 4 ++++ 6 files changed, 54 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_indexed_list.cpp b/Telegram/SourceFiles/dialogs/dialogs_indexed_list.cpp index 2254346cf8..43636f81a8 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_indexed_list.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_indexed_list.cpp @@ -194,6 +194,20 @@ void IndexedList::adjustNames( } } +void IndexedList::freeze() { + _list.freeze(); + for (auto &[ch, list] : _index) { + list.freeze(); + } +} + +void IndexedList::unfreeze() { + _list.unfreeze(); + for (auto &[ch, list] : _index) { + list.unfreeze(); + } +} + void IndexedList::remove(Key key, Row *replacedBy) { if (_list.remove(key, replacedBy)) { for (const auto &ch : key.entry()->chatListFirstLetters()) { diff --git a/Telegram/SourceFiles/dialogs/dialogs_indexed_list.h b/Telegram/SourceFiles/dialogs/dialogs_indexed_list.h index dfdf3b1de3..97cc709151 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_indexed_list.h +++ b/Telegram/SourceFiles/dialogs/dialogs_indexed_list.h @@ -24,6 +24,8 @@ public: Row *addByName(Key key); void adjustByDate(const RowsByLetter &links); void moveToTop(Key key); + void freeze(); + void unfreeze(); bool updateHeight(Key key, float64 narrowRatio); bool updateHeights(float64 narrowRatio); diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index aa4dc87f85..7c13b16b78 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -95,6 +95,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Dialogs { namespace { +constexpr auto kFreezeTimeout = crl::time(5000); constexpr auto kHashtagResultsLimit = 5; constexpr auto kStartReorderThreshold = 30; constexpr auto kStartDragToFilterThresholdX = kStartReorderThreshold; @@ -291,7 +292,8 @@ InnerWidget::InnerWidget( , _narrowWidth(st::defaultDialogRow.padding.left() + st::defaultDialogRow.photoSize + st::defaultDialogRow.padding.left()) -, _childListShown(std::move(childListShown)) { +, _childListShown(std::move(childListShown)) +, _freezeTimer([=] { _shownList->unfreeze(); update(); }) { setAttribute(Qt::WA_OpaquePaintEvent, true); style::PaletteChanged( @@ -1678,6 +1680,13 @@ void InnerWidget::mouseMoveEvent(QMouseEvent *e) { return; } + if (_lastMousePosition && *_lastMousePosition != globalPosition) { + if (!_freezeTimer.isActive()) { + _shownList->freeze(); + } + _freezeTimer.callOnce(kFreezeTimeout); + } + if (_pressed && (e->buttons() & Qt::LeftButton)) { const auto local = e->pos(); const auto outside = _dragging ? false : true; @@ -3052,6 +3061,8 @@ void InnerWidget::updateDialogRow( void InnerWidget::enterEventHook(QEnterEvent *e) { setMouseTracking(true); + _shownList->freeze(); + _freezeTimer.callOnce(kFreezeTimeout); } Row *InnerWidget::shownRowByKey(Key key) { @@ -3134,6 +3145,7 @@ void InnerWidget::refreshShownList() { ? session().data().chatsFilters().chatsList(_filterId)->indexed() : session().data().chatsList(_openedFolder)->indexed(); if (_shownList != list) { + _shownList->unfreeze(); _shownList = list; _shownList->updateHeights(_narrowRatio); } @@ -3141,7 +3153,10 @@ void InnerWidget::refreshShownList() { void InnerWidget::leaveEventHook(QEvent *e) { setMouseTracking(false); + _freezeTimer.cancel(); + _shownList->unfreeze(); clearSelection(); + update(); } void InnerWidget::dragLeft() { diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index afd8dd3b15..ad6970454d 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -678,6 +678,7 @@ private: rpl::event_stream<> _touchCancelRequests; rpl::variable _childListShown; + base::Timer _freezeTimer; float64 _narrowRatio = 0.; bool _geometryInited = false; diff --git a/Telegram/SourceFiles/dialogs/dialogs_list.cpp b/Telegram/SourceFiles/dialogs/dialogs_list.cpp index e3c8044413..2a215176fe 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_list.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_list.cpp @@ -84,6 +84,11 @@ void List::adjustByName(not_null row) { void List::adjustByDate(not_null row) { Expects(_sortMode == SortMode::Date); + if (_frozen) { + _pendingAdjust.emplace(row); + return; + } + const auto key = row->sortKey(_filterId); const auto index = row->index(); const auto i = _rows.begin() + index; @@ -103,6 +108,17 @@ void List::adjustByDate(not_null row) { } } +void List::freeze() { + _frozen = true; +} + +void List::unfreeze() { + _frozen = false; + for (const auto &row : base::take(_pendingAdjust)) { + adjustByDate(row); + } +} + bool List::updateHeight(Key key, float64 narrowRatio) { const auto i = _rowByKey.find(key); if (i == _rowByKey.cend()) { @@ -170,6 +186,7 @@ bool List::remove(Key key, Row *replacedBy) { } const auto row = i->second.get(); + _pendingAdjust.remove(row); row->entry()->owner().dialogsRowReplaced({ row, replacedBy }); auto top = row->top(); diff --git a/Telegram/SourceFiles/dialogs/dialogs_list.h b/Telegram/SourceFiles/dialogs/dialogs_list.h index 7b1bd84f24..e1d9057225 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_list.h +++ b/Telegram/SourceFiles/dialogs/dialogs_list.h @@ -52,6 +52,8 @@ public: not_null addByName(Key key); bool moveToTop(Key key); void adjustByDate(not_null row); + void freeze(); + void unfreeze(); bool updateHeight(Key key, float64 narrowRatio); bool updateHeights(float64 narrowRatio); bool remove(Key key, Row *replacedBy = nullptr); @@ -82,8 +84,10 @@ private: SortMode _sortMode = SortMode(); FilterId _filterId = 0; float64 _narrowRatio = 0.; + bool _frozen = false; std::vector> _rows; std::map> _rowByKey; + base::flat_set> _pendingAdjust; }; From 4212be1b6c56f16debf025c1ef199cf4501c8df0 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 9 Mar 2026 17:16:45 +0400 Subject: [PATCH 051/415] Add ripple effect to reply button. --- .../history/history_inner_widget.cpp | 2 +- .../history/view/history_view_list_widget.cpp | 2 +- .../view/history_view_reply_button.cpp | 41 +++++++++++++++++++ .../history/view/history_view_reply_button.h | 12 +++++- 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index ae3f356b06..fe2397c40f 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -4442,7 +4442,7 @@ void HistoryInner::mouseActionUpdate() { lnkhost = reactionView; } else if (overReplyBtn) { dragState = replyBtnState; - lnkhost = replyBtnView; + lnkhost = _replyButtonManager.get(); } else if (item) { if (item != _mouseActionItem || ((m + selectionViewOffset) - _dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) { if (_mouseAction == MouseAction::PrepareDrag) { diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 1423497211..d59dc0ae1b 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -3871,7 +3871,7 @@ void ListWidget::mouseActionUpdate() { lnkhost = reactionView; } else if (overReplyBtn) { dragState = replyBtnState; - lnkhost = replyBtnView; + lnkhost = _replyButtonManager.get(); } else if (view) { auto cursorDeltaLength = [&] { auto cursorDelta = (_overState.point - _pressState.point); diff --git a/Telegram/SourceFiles/history/view/history_view_reply_button.cpp b/Telegram/SourceFiles/history/view/history_view_reply_button.cpp index ad97680db7..834995b293 100644 --- a/Telegram/SourceFiles/history/view/history_view_reply_button.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reply_button.cpp @@ -9,8 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_cursor_state.h" #include "ui/chat/chat_style.h" +#include "ui/effects/ripple_animation.h" #include "lang/lang_keys.h" #include "styles/style_chat.h" +#include "styles/style_widgets.h" namespace HistoryView::ReplyButton { namespace { @@ -182,8 +184,10 @@ void Manager::updateButton(ButtonParameters parameters) { } _buttonShowTimer.cancel(); _scheduledParameters = std::nullopt; + _ripple = nullptr; } _buttonContext = parameters.context; + _lastPointer = parameters.pointer; if (parameters.link) { _link = parameters.link; } @@ -279,6 +283,19 @@ void Manager::paintButton( radius); p.drawImage(position, *frame.image, frame.rect); + if (_ripple && !_ripple->empty() && _button && button == _button.get()) { + const auto color = context.st->windowBgOver()->c; + _ripple->paint( + p, + position.x() + _inner.x(), + position.y() + _inner.y(), + _inner.width(), + &color); + if (_ripple->empty()) { + _ripple.reset(); + } + } + const auto textLeft = position.x() + _inner.x() + st::replyCornerTextPadding.left(); @@ -334,6 +351,30 @@ void Manager::remove(FullMsgId context) { if (_buttonContext == context) { _buttonContext = {}; _button = nullptr; + _ripple = nullptr; + } +} + +void Manager::clickHandlerPressedChanged( + const ClickHandlerPtr &action, + bool pressed) { + if (action != _link || !_button) { + return; + } + if (pressed) { + const auto inner = buttonInner(); + if (!_ripple) { + const auto mask = Ui::RippleAnimation::RoundRectMask( + inner.size(), + inner.height() / 2); + _ripple = std::make_unique( + st::defaultRippleAnimation, + mask, + [=] { if (_button) _buttonUpdate(_button->geometry()); }); + } + _ripple->add(_lastPointer - inner.topLeft()); + } else if (_ripple) { + _ripple->lastStop(); } } diff --git a/Telegram/SourceFiles/history/view/history_view_reply_button.h b/Telegram/SourceFiles/history/view/history_view_reply_button.h index 37d33bd95f..77223a467f 100644 --- a/Telegram/SourceFiles/history/view/history_view_reply_button.h +++ b/Telegram/SourceFiles/history/view/history_view_reply_button.h @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text.h" namespace Ui { +class RippleAnimation; struct ChatPaintContext; } // namespace Ui @@ -93,7 +94,9 @@ private: }; -class Manager final : public base::has_weak_ptr { +class Manager final + : public base::has_weak_ptr + , public ClickHandlerHost { public: Manager(Fn buttonUpdate); ~Manager(); @@ -103,6 +106,11 @@ public: [[nodiscard]] TextState buttonTextState(QPoint position) const; void remove(FullMsgId context); +protected: + void clickHandlerPressedChanged( + const ClickHandlerPtr &action, + bool pressed) override; + private: void showButtonDelayed(); [[nodiscard]] bool overCurrentButton(QPoint position) const; @@ -136,6 +144,8 @@ private: std::vector> _buttonHiding; Ui::Text::String _text; + std::unique_ptr _ripple; + QPoint _lastPointer; }; From b24a13d9629fb4c5160edae5a9a1d6a7766b78cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:00:27 +0000 Subject: [PATCH 052/415] Bump docker/setup-buildx-action from 3 to 4 Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index da7155984f..b77e445bef 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -79,7 +79,7 @@ jobs: tool-cache: true - name: Set up Docker Buildx. - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Libraries cache. uses: actions/cache@v5 From 71497e592c3eefb023f70770ea92827e0ef45cec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:00:22 +0000 Subject: [PATCH 053/415] Bump docker/build-push-action from 6 to 7 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v6...v7) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index b77e445bef..2f8091b621 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -89,7 +89,7 @@ jobs: restore-keys: ${{ runner.OS }}-libs- - name: Libraries. - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: Telegram/build/docker/centos_env load: ${{ env.ONLY_CACHE == 'false' }} From 7f71e9dd526b15b655224a5a82b35823e208a4b0 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 9 Mar 2026 21:23:12 +0400 Subject: [PATCH 054/415] Fix build with Xcode. --- Telegram/SourceFiles/data/data_msg_id.h | 2 +- Telegram/cmake/telegram_apple_swift_runtime.cmake | 12 +++++++++++- Telegram/lib_translate | 2 +- cmake | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index 9ed1d3036a..bd8d183d36 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -107,7 +107,7 @@ static_assert(-(SpecialMsgIdShift + 0xFF) > ServerMaxMsgId); return MsgId(StartClientMsgId.bare + index); } -[[nodiscrd]] constexpr inline bool IsStoryMsgId(MsgId id) noexcept { +[[nodiscard]] constexpr inline bool IsStoryMsgId(MsgId id) noexcept { return (id >= StartStoryMsgId && id < EndStoryMsgId); } [[nodiscard]] constexpr inline StoryId StoryIdFromMsgId(MsgId id) noexcept { diff --git a/Telegram/cmake/telegram_apple_swift_runtime.cmake b/Telegram/cmake/telegram_apple_swift_runtime.cmake index 36125c26f9..3b331a7b41 100644 --- a/Telegram/cmake/telegram_apple_swift_runtime.cmake +++ b/Telegram/cmake/telegram_apple_swift_runtime.cmake @@ -9,8 +9,18 @@ function(telegram_add_apple_swift_runtime target_name) return() endif() + execute_process( + COMMAND xcode-select -p + OUTPUT_VARIABLE DEVELOPER_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + set(SWIFT_LIB_DIR + "${DEVELOPER_DIR}/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx") + target_link_options(${target_name} PRIVATE + "-L${SWIFT_LIB_DIR}" "-Wl,-rpath,/usr/lib/swift" "-Wl,-rpath,@executable_path/../Frameworks" ) @@ -24,4 +34,4 @@ function(telegram_add_apple_swift_runtime target_name) --destination $/../Frameworks VERBATIM ) -endfunction() +endfunction() \ No newline at end of file diff --git a/Telegram/lib_translate b/Telegram/lib_translate index e65fc00f72..03534a63dc 160000 --- a/Telegram/lib_translate +++ b/Telegram/lib_translate @@ -1 +1 @@ -Subproject commit e65fc00f723cec2bae1927ed8d45fd7067025e3a +Subproject commit 03534a63dcd115b05e3e1e75abfe0acdb0235eb3 diff --git a/cmake b/cmake index 34de471d6f..4088db229d 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit 34de471d6f99077fea60a76d60b2a05ece2abbb1 +Subproject commit 4088db229d9132e1ff2e6758ad7ae28e5116fea6 From b9d3b9364cc970409dd9934e2ed3f7fab0126fbe Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 9 Mar 2026 18:48:33 +0400 Subject: [PATCH 055/415] Fix t.me/bot?start=param for bot-forums. --- .../window/window_session_controller.cpp | 84 +++++++++---------- .../window/window_session_controller.h | 6 +- 2 files changed, 46 insertions(+), 44 deletions(-) diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 07de157154..e1e49fa9c4 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -616,6 +616,14 @@ void SessionNavigation::showPeerByLinkResolved( using Scope = AddBotToGroupBoxController::Scope; const auto user = peer->asUser(); const auto bot = (user && user->isBot()) ? user : nullptr; + const auto applyBotStartToken = [&] { + if (bot && bot->botInfo->startToken != info.startToken) { + bot->botInfo->startToken = info.startToken; + bot->session().changes().peerUpdated( + bot, + Data::PeerUpdate::Flag::BotStartToken); + } + }; // t.me/username/012345 - we thought it was a channel post link, but // after resolving the username we found out it is a bot. @@ -625,6 +633,16 @@ void SessionNavigation::showPeerByLinkResolved( ? ResolveType::BotApp : info.resolveType; + // Show specific posts only in channels / supergroups. + const auto useRequestedMessageId = peer->isChannel(); + const auto msgId = useRequestedMessageId + ? info.messageId + : info.startAutoSubmit + ? ShowAndStartBotMsgId + : (bot && !info.startToken.isEmpty()) + ? ShowAndMaybeStartBotMsgId + : ShowAtUnreadMsgId; + const auto &replies = info.repliesInfo; if (const auto threadId = std::get_if(&replies)) { const auto history = peer->owner().history(peer); @@ -637,39 +655,29 @@ void SessionNavigation::showPeerByLinkResolved( controller->showForum(forum); } } - showRepliesForMessage( - history, - threadId->id, - info.messageId, - params); + showRepliesForMessage(history, threadId->id, msgId, params); } else if (const auto commentId = std::get_if(&replies)) { - showRepliesForMessage( - session().data().history(peer), - info.messageId, - commentId->id, - params); + const auto history = peer->owner().history(peer); + showRepliesForMessage(history, msgId, commentId->id, params); } else if (resolveType == ResolveType::Profile) { showPeerInfo(peer, params); } else if (resolveType == ResolveType::HashtagSearch) { searchMessages(info.text, peer->owner().history(peer)); } else if (peer->isForum() && resolveType != ResolveType::Boost) { - const auto itemId = info.messageId; - if (!itemId) { - parentController()->showForum(peer->forum(), params); - } else if (const auto item = peer->owner().message(peer, itemId)) { + if (!msgId || !useRequestedMessageId) { + applyBotStartToken(); + parentController()->showForum(peer->forum(), params, msgId); + } else if (const auto item = peer->owner().message(peer, msgId)) { showMessageByLinkResolved(item, info); } else { const auto callback = crl::guard(this, [=] { - if (const auto item = peer->owner().message(peer, itemId)) { + if (const auto item = peer->owner().message(peer, msgId)) { showMessageByLinkResolved(item, info); } else { - showPeerHistory(peer, params, itemId); + showPeerHistory(peer, params, msgId); } }); - peer->session().api().requestMessageData( - peer, - info.messageId, - callback); + peer->session().api().requestMessageData(peer, msgId, callback); } } else if (info.storyParam == u"live"_q) { parentController()->openPeerStories(peer->id, std::nullopt, true); @@ -752,21 +760,8 @@ void SessionNavigation::showPeerByLinkResolved( ; monoforum && resolveType == ResolveType::ChannelDirect) { showPeerHistory(monoforum, params, ShowAtUnreadMsgId); } else { - // Show specific posts only in channels / supergroups. - const auto msgId = peer->isChannel() - ? info.messageId - : info.startAutoSubmit - ? ShowAndStartBotMsgId - : (bot && !info.startToken.isEmpty()) - ? ShowAndMaybeStartBotMsgId - : ShowAtUnreadMsgId; const auto attachBotUsername = info.attachBotUsername; - if (bot && bot->botInfo->startToken != info.startToken) { - bot->botInfo->startToken = info.startToken; - bot->session().changes().peerUpdated( - bot, - Data::PeerUpdate::Flag::BotStartToken); - } + applyBotStartToken(); if (!attachBotUsername.isEmpty()) { crl::on_main(this, [=] { const auto history = peer->owner().history(peer); @@ -2021,12 +2016,13 @@ void SessionController::closeFolder() { bool SessionController::showForumInDifferentWindow( not_null forum, - const SectionShow ¶ms) { + const SectionShow ¶ms, + MsgId showAtMsgId) { const auto window = Core::App().windowForShowingForum(forum); if (window == _window) { return false; } else if (window) { - window->sessionController()->showForum(forum, params); + window->sessionController()->showForum(forum, params, showAtMsgId); window->activate(); return true; } else if (windowId().hasChatsList()) { @@ -2039,7 +2035,7 @@ bool SessionController::showForumInDifferentWindow( primary = Core::App().separateWindowFor(account); } if (primary && &primary->account() == account) { - primary->sessionController()->showForum(forum, params); + primary->sessionController()->showForum(forum, params, showAtMsgId); primary->activate(); } return true; @@ -2047,15 +2043,19 @@ bool SessionController::showForumInDifferentWindow( void SessionController::showForum( not_null forum, - const SectionShow ¶ms) { + const SectionShow ¶ms, + MsgId showAtMsgId) { const auto forced = params.forceTopicsList; - if (showForumInDifferentWindow(forum, params)) { + if (showForumInDifferentWindow(forum, params, showAtMsgId)) { return; } else if (!forced && forum->peer()->useSubsectionTabs()) { - if (const auto active = forum->activeSubsectionThread()) { - showThread(active, ShowAtUnreadMsgId, params); + const auto active = (showAtMsgId == ShowAtUnreadMsgId) + ? forum->activeSubsectionThread() + : nullptr; + if (active) { + showThread(active, showAtMsgId, params); } else { - showPeerHistory(forum->peer(), params); + showPeerHistory(forum->peer(), params, showAtMsgId); } return; } diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 5ed494e7ad..fed0de87c9 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -444,7 +444,8 @@ public: void showForum( not_null forum, - const SectionShow ¶ms = SectionShow::Way::ClearStack); + const SectionShow ¶ms = SectionShow::Way::ClearStack, + MsgId showAtMsgId = ShowAtUnreadMsgId); void closeForum(); const rpl::variable &shownForum() const; @@ -772,7 +773,8 @@ private: bool openFolderInDifferentWindow(not_null folder); bool showForumInDifferentWindow( not_null forum, - const SectionShow ¶ms); + const SectionShow ¶ms, + MsgId showAtMsgId); const not_null _window; const std::unique_ptr _emojiInteractions; From 3223e6389c369f9da9805d54fc1497b54af569a7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 10 Mar 2026 11:59:13 +0400 Subject: [PATCH 056/415] Add ripple effect to send button. --- .../calls/group/calls_group_message_field.cpp | 30 ++-- .../chat_helpers/chat_helpers.style | 2 + .../SourceFiles/media/view/media_view.style | 1 + .../SourceFiles/ui/controls/send_button.cpp | 143 +++++++++++++++--- .../SourceFiles/ui/controls/send_button.h | 12 ++ 5 files changed, 156 insertions(+), 32 deletions(-) diff --git a/Telegram/SourceFiles/calls/group/calls_group_message_field.cpp b/Telegram/SourceFiles/calls/group/calls_group_message_field.cpp index a517de5a59..eabf30e620 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_message_field.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_message_field.cpp @@ -332,8 +332,9 @@ void MessageField::createControls(PeerData *peer) { Ui::InputField::Mode::MultiLine, tr::lng_message_ph()); _field->setMaxLength(_limit + kErrorLimit); - _field->setMinHeight( - st::historySendSize.height() - 2 * st::historySendPadding); + _field->setMinHeight(st.attach.height + - st.padding.top() + - st.padding.bottom()); _field->setMaxHeight(st::historyComposeFieldMaxHeight); _field->setDocumentMargin(4.); _field->setAdditionalMargin(style::ConvertScale(4) - 4); @@ -468,15 +469,13 @@ void MessageField::createControls(PeerData *peer) { ) | rpl::filter( rpl::mappers::_1 > 0 ) | rpl::on_next([=](int newWidth) { + const auto &st = st::storiesComposeControls; const auto fieldWidth = newWidth - - st::historySendPadding + - st.padding.top() - _emojiToggle->width() - _send->width(); _field->resizeToWidth(fieldWidth); - _field->moveToLeft( - st::historySendPadding, - st::historySendPadding, - newWidth); + _field->moveToLeft(st.padding.top(), st.padding.top(), newWidth); updateWrapSize(newWidth); }, _lifetime); @@ -487,8 +486,10 @@ void MessageField::createControls(PeerData *peer) { if (width <= 0) { return; } - const auto minHeight = st::historySendSize.height() - - 2 * st::historySendPadding; + const auto &st = st::storiesComposeControls; + const auto minHeight = st.attach.height + - st.padding.top() + - st.padding.bottom(); _send->moveToRight(0, height - minHeight, width); _emojiToggle->moveToRight(_send->width(), height - minHeight, width); updateWrapSize(); @@ -499,7 +500,8 @@ void MessageField::createControls(PeerData *peer) { }, _lifetime); const auto updateLimitPosition = [=](QSize parent, QSize label) { - const auto skip = st::historySendPadding; + const auto &st = st::storiesComposeControls; + const auto skip = st.padding.top(); return QPoint(parent.width() - label.width() - skip, skip); }; Ui::AddLengthLimitLabel(_field, _limit, { @@ -528,7 +530,8 @@ void MessageField::updateEmojiPanelGeometry() { void MessageField::setupBackground() { _wrap->paintRequest() | rpl::on_next([=] { - const auto radius = st::historySendSize.height() / 2.; + const auto &st = st::storiesComposeControls; + const auto radius = st.attach.height / 2.; auto p = QPainter(_wrap.get()); auto hq = PainterHighQualityEnabler(p); @@ -609,8 +612,11 @@ void MessageField::raise() { } void MessageField::updateWrapSize(int widthOverride) { + const auto &st = st::storiesComposeControls; const auto width = widthOverride ? widthOverride : _wrap->width(); - const auto height = _field->height() + 2 * st::historySendPadding; + const auto height = _field->height() + + st.padding.top() + + st.padding.bottom(); _wrap->resize(width, height); updateHeight(); } diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 29e350ce15..84d69c305d 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -155,6 +155,7 @@ SendButton { stars: RoundButton; recordSize: size; sendDisabledFg: color; + sendIconFg: color; } RecordBarLock { @@ -1403,6 +1404,7 @@ historySend: SendButton { } recordSize: size(26px, 26px); sendDisabledFg: historyComposeIconFg; + sendIconFg: historySendIconFg; } historyRecordFrameIndex: 30; diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index dea81b209a..6c8e6aca4f 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -725,6 +725,7 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { rippleAreaPosition: point(1px, 1px); } sendDisabledFg: storiesComposeGrayText; + sendIconFg: windowFgActive; } attach: storiesAttach; emoji: storiesAttachEmoji; diff --git a/Telegram/SourceFiles/ui/controls/send_button.cpp b/Telegram/SourceFiles/ui/controls/send_button.cpp index dd1ce33024..6d148049bd 100644 --- a/Telegram/SourceFiles/ui/controls/send_button.cpp +++ b/Telegram/SourceFiles/ui/controls/send_button.cpp @@ -31,7 +31,8 @@ constexpr auto kForbiddenOpacity = 0.5; SendButton::SendButton(QWidget *parent, const style::SendButton &st) : RippleButton(parent, st.inner.ripple) -, _st(st) { +, _st(st) +, _lastRippleShape(currentRippleShape()) { updateSize(); } @@ -82,6 +83,12 @@ void SendButton::setState(State state) { } _state = state; + const auto newShape = currentRippleShape(); + if (_lastRippleShape != newShape) { + _lastRippleShape = newShape; + RippleButton::finishAnimating(); + } + setAccessibleName([&] { switch (_state.type) { case Type::Send: return tr::lng_send_button(tr::now); @@ -262,6 +269,15 @@ void SendButton::paintLottieIcon(QPainter &p, int index, bool over) { } void SendButton::paintSave(QPainter &p, bool over) { + if (!isDisabled()) { + auto color = _st.sendIconFg->c; + color.setAlpha(25); + paintRipple( + p, + (width() - _st.inner.rippleAreaSize) / 2, + _st.inner.rippleAreaPosition.y(), + &color); + } const auto &saveIcon = over ? st::historyEditSaveIconOver : st::historyEditSaveIcon; @@ -283,16 +299,30 @@ void SendButton::paintCancel(QPainter &p, bool over) { void SendButton::paintSend(QPainter &p, bool over) { const auto &sendIcon = over ? _st.inner.iconOver : _st.inner.icon; if (const auto padding = _st.sendIconFillPadding; padding > 0) { - auto hq = PainterHighQualityEnabler(p); - p.setPen(Qt::NoPen); - if (_state.fillBgOverride.isValid()) { - p.setBrush(_state.fillBgOverride); - } else { - p.setBrush(st::windowBgActive); + const auto ellipse = sendEllipseRect(); + { + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + if (_state.fillBgOverride.isValid()) { + p.setBrush(_state.fillBgOverride); + } else { + p.setBrush(st::windowBgActive); + } + p.drawEllipse(ellipse); } - p.drawEllipse( - QRect(_st.sendIconPosition, sendIcon.size()).marginsAdded( - { padding, padding, padding, padding })); + if (!isDisabled()) { + auto color = _st.sendIconFg->c; + color.setAlpha(25); + paintRipple(p, ellipse.topLeft(), &color); + } + } else if (!isDisabled()) { + auto color = _st.sendIconFg->c; + color.setAlpha(25); + paintRipple( + p, + (width() - _st.inner.rippleAreaSize) / 2, + _st.inner.rippleAreaPosition.y(), + &color); } if (isDisabled()) { const auto color = st::historyRecordVoiceFg->c; @@ -315,6 +345,11 @@ void SendButton::paintStarsToSend(QPainter &p, bool over) { const auto radius = geometry.rounded.height() / 2; p.drawRoundedRect(geometry.rounded, radius, radius); } + if (!isDisabled()) { + auto color = _st.stars.textFg->c; + color.setAlpha(25); + paintRipple(p, geometry.rounded.topLeft(), &color); + } p.setPen(over ? _st.stars.textFgOver : _st.stars.textFg); _starsToSendText.draw(p, { .position = geometry.inner.topLeft(), @@ -324,15 +359,17 @@ void SendButton::paintStarsToSend(QPainter &p, bool over) { } void SendButton::paintSchedule(QPainter &p, bool over) { + const auto ellipse = scheduleEllipseRect(); { PainterHighQualityEnabler hq(p); p.setPen(Qt::NoPen); p.setBrush(over ? st::historySendIconFgOver : st::historySendIconFg); - p.drawEllipse( - st::historyScheduleIconPosition.x(), - st::historyScheduleIconPosition.y(), - st::historyScheduleIcon.width(), - st::historyScheduleIcon.height()); + p.drawEllipse(ellipse); + } + if (!isDisabled()) { + auto color = st::historyComposeAreaBg->c; + color.setAlpha(25); + paintRipple(p, ellipse.topLeft(), &color); } st::historyScheduleIcon.paint( p, @@ -375,6 +412,43 @@ SendButton::StarsGeometry SendButton::starsGeometry() const { }; } +SendButton::RippleShape SendButton::currentRippleShape() const { + switch (_state.type) { + case Type::Send: + if (!_starsToSendText.isEmpty()) { + return RippleShape::StarsRoundRect; + } else if (_st.sendIconFillPadding > 0) { + return RippleShape::SendEllipse; + } + return RippleShape::InnerEllipse; + case Type::Schedule: + return RippleShape::ScheduleEllipse; + case Type::Save: + case Type::Record: + case Type::Round: + case Type::Cancel: + case Type::Slowmode: + case Type::EditPrice: + return RippleShape::InnerEllipse; + } + Unexpected("Type in SendButton::currentRippleShape."); +} + +QRect SendButton::sendEllipseRect() const { + const auto &sendIcon = _st.inner.icon; + const auto padding = _st.sendIconFillPadding; + return QRect(_st.sendIconPosition, sendIcon.size()).marginsAdded( + { padding, padding, padding, padding }); +} + +QRect SendButton::scheduleEllipseRect() const { + return QRect( + st::historyScheduleIconPosition, + QSize( + st::historyScheduleIcon.width(), + st::historyScheduleIcon.height())); +} + void SendButton::updateSize() { if (_state.type == Type::EditPrice) { resize(0, _st.inner.height); @@ -406,15 +480,44 @@ QPixmap SendButton::grabContent() { } QImage SendButton::prepareRippleMask() const { - const auto size = _st.inner.rippleAreaSize; - return RippleAnimation::EllipseMask(QSize(size, size)); + switch (_lastRippleShape) { + case RippleShape::InnerEllipse: { + const auto size = _st.inner.rippleAreaSize; + return RippleAnimation::EllipseMask(QSize(size, size)); + } + case RippleShape::SendEllipse: { + const auto r = sendEllipseRect(); + return RippleAnimation::EllipseMask(r.size()); + } + case RippleShape::StarsRoundRect: { + const auto r = starsGeometry().rounded; + const auto radius = r.height() / 2; + return RippleAnimation::RoundRectMask(r.size(), radius); + } + case RippleShape::ScheduleEllipse: { + const auto r = scheduleEllipseRect(); + return RippleAnimation::EllipseMask(r.size()); + } + } + Unexpected("RippleShape in SendButton::prepareRippleMask."); } QPoint SendButton::prepareRippleStartPosition() const { const auto real = mapFromGlobal(QCursor::pos()); - const auto size = _st.inner.rippleAreaSize; - const auto y = (height() - _st.inner.rippleAreaSize) / 2; - return real - QPoint((width() - size) / 2, y); + switch (_lastRippleShape) { + case RippleShape::InnerEllipse: { + const auto size = _st.inner.rippleAreaSize; + const auto y = (height() - size) / 2; + return real - QPoint((width() - size) / 2, y); + } + case RippleShape::SendEllipse: + return real - sendEllipseRect().topLeft(); + case RippleShape::StarsRoundRect: + return real - starsGeometry().rounded.topLeft(); + case RippleShape::ScheduleEllipse: + return real - scheduleEllipseRect().topLeft(); + } + Unexpected("RippleShape in SendButton::prepareRippleStartPosition."); } void SendButton::initVoiceRoundIcon(int index) { diff --git a/Telegram/SourceFiles/ui/controls/send_button.h b/Telegram/SourceFiles/ui/controls/send_button.h index 9ae3d8a231..c4ab503a86 100644 --- a/Telegram/SourceFiles/ui/controls/send_button.h +++ b/Telegram/SourceFiles/ui/controls/send_button.h @@ -70,11 +70,22 @@ private: QRect rounded; QRect outer; }; + enum class RippleShape : uchar { + InnerEllipse, + SendEllipse, + StarsRoundRect, + ScheduleEllipse, + }; + [[nodiscard]] QPixmap grabContent(); void updateSize(); [[nodiscard]] StarsGeometry starsGeometry() const; + [[nodiscard]] RippleShape currentRippleShape() const; + [[nodiscard]] QRect sendEllipseRect() const; + [[nodiscard]] QRect scheduleEllipseRect() const; + void paintRecord(QPainter &p, bool over); void paintRound(QPainter &p, bool over); void paintSave(QPainter &p, bool over); @@ -102,6 +113,7 @@ private: std::array, 2> _voiceRoundIcons; bool _voiceRoundAnimating = false; + RippleShape _lastRippleShape = RippleShape::SendEllipse; }; From 73686ae930792439c1ed344c583e31b65527777f Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 10 Mar 2026 12:07:30 +0400 Subject: [PATCH 057/415] Add ripple animation to reactions in messages. --- .../history/view/history_view_message.cpp | 5 + .../view/reactions/history_view_reactions.cpp | 167 ++++++++++++++---- .../view/reactions/history_view_reactions.h | 8 + 3 files changed, 143 insertions(+), 37 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 61f5bc2f03..3e96201585 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -2162,6 +2162,11 @@ void Message::clickHandlerPressedChanged( } else { _summarize->stopRipple(); } + } else if (_reactions) { + _reactions->clickHandlerPressedChanged( + handler, + pressed, + [=] { repaint(); }); } } diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp index 889e51aa1a..4a3378ff76 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp @@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_custom_emoji.h" #include "ui/chat/chat_style.h" #include "ui/effects/reaction_fly_animation.h" +#include "ui/effects/ripple_animation.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/power_saving.h" @@ -44,6 +45,37 @@ constexpr auto kMaxNicePerRow = 5; return serviceBg; } +void PaintTagShape(QPainter &p, QSizeF size, const QColor &color) { + const auto arrow = st::reactionInlineTagArrow; + const auto rradius = st::reactionInlineTagRightRadius * 1.; + const auto radius = st::reactionInlineTagLeftRadius - rradius; + auto pen = QPen(color); + pen.setWidthF(rradius * 2.); + pen.setJoinStyle(Qt::RoundJoin); + const auto rect = QRectF(QPointF(), size).marginsRemoved( + { rradius, rradius, rradius, rradius }); + const auto right = rect.x() + rect.width(); + const auto bottom = rect.y() + rect.height(); + auto path = QPainterPath(); + path.moveTo(rect.x() + radius, rect.y()); + path.lineTo(right - arrow, rect.y()); + path.lineTo(right, rect.y() + rect.height() / 2); + path.lineTo(right - arrow, bottom); + path.lineTo(rect.x() + radius, bottom); + path.arcTo( + QRectF(rect.x(), bottom - radius * 2, radius * 2, radius * 2), + 270, + -90); + path.lineTo(rect.x(), rect.y() + radius); + path.arcTo( + QRectF(rect.x(), rect.y(), radius * 2, radius * 2), + 180, + -90); + path.closeSubpath(); + p.setPen(pen); + p.drawPath(path); +} + } // namespace struct InlineList::Button { @@ -62,6 +94,22 @@ struct InlineList::Button { bool tag = false; }; +struct InlineList::RippleEffect : Ui::RippleAnimation { + RippleEffect( + const style::RippleAnimation &st, + QImage mask, + Fn update, + ReactionId buttonId, + int width) + : RippleAnimation(st, std::move(mask), std::move(update)) + , buttonId(std::move(buttonId)) + , width(width) { + } + + ReactionId buttonId; + int width = 0; +}; + InlineList::InlineList( not_null<::Data::Reactions*> owner, Fn handlerFactory, @@ -506,6 +554,26 @@ void InlineList::paint( : (context.selected() ? st->historyFileInIconFgSelected() : st->historyFileInIconFg()); + if (_ripple && _ripple->buttonId == button.id) { + if (!bubbleReady || _ripple->width != geometry.width()) { + _ripple.reset(); + } else { + const auto savedOpacity = p.opacity(); + p.setOpacity(1.); + auto rippleColor = textFg.color(); + rippleColor.setAlpha(25); + _ripple->paint( + p, + geometry.x(), + geometry.y(), + outerWidth, + &rippleColor); + if (_ripple->empty()) { + _ripple.reset(); + } + p.setOpacity(savedOpacity); + } + } const auto image = QRect( inner.topLeft() + QPoint(skip, skip), QSize(st::reactionInlineImage, st::reactionInlineImage)); @@ -603,48 +671,21 @@ QImage InlineList::PrepareTagBg(QColor tagBg, QColor dotBg) { result.fill(Qt::transparent); auto p = QPainter(&result); - auto path = QPainterPath(); - const auto arrow = st::reactionInlineTagArrow; - const auto rradius = st::reactionInlineTagRightRadius * 1.; - const auto radius = st::reactionInlineTagLeftRadius - rradius; - auto pen = QPen(tagBg); - pen.setWidthF(rradius * 2.); - pen.setJoinStyle(Qt::RoundJoin); - const auto rect = QRectF(0, 0, width, height).marginsRemoved( - { rradius, rradius, rradius, rradius }); - - const auto right = rect.x() + rect.width(); - const auto bottom = rect.y() + rect.height(); - path.moveTo(rect.x() + radius, rect.y()); - path.lineTo(right - arrow, rect.y()); - path.lineTo(right, rect.y() + rect.height() / 2); - path.lineTo(right - arrow, bottom); - path.lineTo(rect.x() + radius, bottom); - path.arcTo( - QRectF(rect.x(), bottom - radius * 2, radius * 2, radius * 2), - 270, - -90); - path.lineTo(rect.x(), rect.y() + radius); - path.arcTo( - QRectF(rect.x(), rect.y(), radius * 2, radius * 2), - 180, - -90); - path.closeSubpath(); - - const auto dsize = st::reactionInlineTagDot; - const auto dot = QRectF( - right - st::reactionInlineTagDotSkip - dsize, - rect.y() + (rect.height() - dsize) / 2., - dsize, - dsize); - auto hq = PainterHighQualityEnabler(p); p.setCompositionMode(QPainter::CompositionMode_Source); - p.setPen(pen); p.setBrush(tagBg); - p.drawPath(path); + PaintTagShape(p, QSizeF(width, height), tagBg); if (dotBg.alpha() > 0) { + const auto rradius = st::reactionInlineTagRightRadius * 1.; + const auto rect = QRectF(0, 0, width, height).marginsRemoved( + { rradius, rradius, rradius, rradius }); + const auto dsize = st::reactionInlineTagDot; + const auto dot = QRectF( + rect.x() + rect.width() - st::reactionInlineTagDotSkip - dsize, + rect.y() + (rect.height() - dsize) / 2., + dsize, + dsize); p.setPen(Qt::NoPen); p.setBrush(dotBg); p.drawEllipse(dot); @@ -723,6 +764,8 @@ bool InlineList::getState( QVariant::fromValue(button.id)); _owner->preloadAnimationsFor(button.id); } + _lastPoint = point - button.geometry.topLeft(); + _lastPointButton = button.id; outResult->link = button.link; return true; } @@ -730,6 +773,56 @@ bool InlineList::getState( return false; } +void InlineList::clickHandlerPressedChanged( + const ClickHandlerPtr &handler, + bool pressed, + Fn repaint) { + if (pressed) { + const auto id = ReactionIdOfLink(handler); + if (id.empty()) { + return; + } + const auto i = ranges::find(_buttons, id, &Button::id); + if (i == end(_buttons)) { + return; + } + const auto &geometry = i->geometry; + if (!_ripple + || _ripple->buttonId != id + || _ripple->width != geometry.width()) { + const auto mask = [&] { + if (areTags()) { + const auto s = geometry.size(); + return Ui::RippleAnimation::MaskByDrawer( + s, + false, + [&](QPainter &p) { + PaintTagShape( + p, + QSizeF(s.width(), s.height()), + QColor(255, 255, 255)); + }); + } + return Ui::RippleAnimation::RoundRectMask( + geometry.size(), + geometry.height() / 2); + }(); + _ripple = std::make_unique( + st::defaultRippleAnimation, + std::move(mask), + std::move(repaint), + id, + geometry.width()); + } + const auto point = (_lastPointButton == id) + ? _lastPoint + : QPoint(geometry.width() / 2, geometry.height() / 2); + _ripple->add(point); + } else if (_ripple) { + _ripple->lastStop(); + } +} + void InlineList::animate( Ui::ReactionFlyAnimationArgs &&args, Fn repaint) { diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h index 89b22c980f..4462578411 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h @@ -84,6 +84,10 @@ public: [[nodiscard]] bool getState( QPoint point, not_null outResult) const; + void clickHandlerPressedChanged( + const ClickHandlerPtr &handler, + bool pressed, + Fn repaint); void animate( Ui::ReactionFlyAnimationArgs &&args, @@ -110,6 +114,7 @@ private: bool someNotLoaded = false; }; struct Button; + struct RippleEffect; void layout(); void layoutButtons(); @@ -149,6 +154,9 @@ private: mutable QImage _customCache; mutable int _customSkip = 0; bool _hasCustomEmoji = false; + mutable std::unique_ptr _ripple; + mutable QPoint _lastPoint; + mutable ReactionId _lastPointButton; }; From 008600f6d34cbe33fe97b2c1c07f5d7d06aee1e0 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 10 Mar 2026 12:25:58 +0400 Subject: [PATCH 058/415] Faster controls disappearing in media viewer. --- Telegram/SourceFiles/media/view/media_view.style | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 6c8e6aca4f..f39e3426de 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -223,8 +223,8 @@ mediaviewControlSize: 90px; mediaviewIconSize: size(46px, 54px); mediaviewIconOver: 36px; -mediaviewWaitHide: 2000; -mediaviewHideDuration: 1000; +mediaviewWaitHide: 1100; +mediaviewHideDuration: 600; mediaviewShowDuration: 200; mediaviewFadeDuration: 150; From b30948fd81498454d6089b0657e31c76cc18a53f Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 10 Mar 2026 12:35:55 +0400 Subject: [PATCH 059/415] In GroupAdmin scope send /start only with token. Fixes #30026. --- Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp b/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp index de701a5215..0dc3bd9d06 100644 --- a/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp @@ -234,7 +234,7 @@ void AddBotToGroupBoxController::addBotToGroup(not_null chat) { const auto done = [=]( ChatAdminRightsInfo newRights, const std::optional &rank) { - if (scope == Scope::GroupAdmin) { + if (scope == Scope::GroupAdmin && !token.isEmpty()) { chat->session().api().sendBotStart(show, bot, chat, token); } close(); From ff08ab1023f21ff264d0ee3f326678d29d9ada46 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Sat, 7 Mar 2026 02:36:02 +0400 Subject: [PATCH 060/415] Revert "Avoid using streaming loader with external video player" This reverts commit d9ddb125007f7772a08dbcb8f4ff565f0d3fa1ab. --- Telegram/SourceFiles/data/data_document.cpp | 33 +++++++--------- Telegram/SourceFiles/data/data_document.h | 6 +-- .../SourceFiles/data/data_document_media.cpp | 2 +- Telegram/SourceFiles/data/data_photo.cpp | 3 +- Telegram/SourceFiles/data/data_photo.h | 3 +- Telegram/SourceFiles/data/data_streaming.cpp | 39 +++---------------- Telegram/SourceFiles/data/data_streaming.h | 2 - Telegram/SourceFiles/iv/iv_instance.cpp | 5 +-- 8 files changed, 25 insertions(+), 68 deletions(-) diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp index f97129f351..7df823b626 100644 --- a/Telegram/SourceFiles/data/data_document.cpp +++ b/Telegram/SourceFiles/data/data_document.cpp @@ -547,7 +547,7 @@ void DocumentData::setVideoQualities( return document->isVideoFile() && !document->dimensions.isEmpty() && !document->inappPlaybackFailed() - && document->useStreamingLoader(nullptr) + && document->useStreamingLoader() && document->canBeStreamed(nullptr); }; ranges::sort( @@ -1533,35 +1533,29 @@ bool DocumentData::hasRemoteLocation() const { return (_dc != 0 && _access != 0); } -bool DocumentData::canVideoBeStreamed(HistoryItem *item) const { - if (!isVideoFile()) { - return false; - } - // Streaming couldn't be used with external player - // Maybe someone brave will implement this once upon a time... - static const auto &ExternalVideoPlayer = base::options::lookup( - Data::kOptionExternalVideoPlayer); - return storyMedia() - || !ExternalVideoPlayer.value() - || (item && !item->allowsForward()); -} - -bool DocumentData::useStreamingLoader(HistoryItem *item) const { +bool DocumentData::useStreamingLoader() const { if (size <= 0) { return false; } else if (const auto info = sticker()) { return info->isWebm(); } return isAnimation() - || canVideoBeStreamed(item) + || isVideoFile() || isAudioFile() || isVoiceMessage(); } bool DocumentData::canBeStreamed(HistoryItem *item) const { + // Streaming couldn't be used with external player + // Maybe someone brave will implement this once upon a time... + static const auto &ExternalVideoPlayer = base::options::lookup( + Data::kOptionExternalVideoPlayer); return hasRemoteLocation() && supportsStreaming() - && (!isVideoFile() || canVideoBeStreamed(item)); + && (!isVideoFile() + || storyMedia() + || !ExternalVideoPlayer.value() + || (item && !item->allowsForward())); } void DocumentData::setInappPlaybackFailed() { @@ -1591,10 +1585,9 @@ StorageFileLocation DocumentData::videoPreloadLocation() const { auto DocumentData::createStreamingLoader( Data::FileOrigin origin, - bool forceRemoteLoader, - HistoryItem *item) const + bool forceRemoteLoader) const -> std::unique_ptr { - if (!useStreamingLoader(item)) { + if (!useStreamingLoader()) { return nullptr; } if (!forceRemoteLoader) { diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h index 6fd2ba0ed8..b0918a37c8 100644 --- a/Telegram/SourceFiles/data/data_document.h +++ b/Telegram/SourceFiles/data/data_document.h @@ -294,10 +294,9 @@ public: [[nodiscard]] bool canBeStreamed(HistoryItem *item) const; [[nodiscard]] auto createStreamingLoader( Data::FileOrigin origin, - bool forceRemoteLoader, - HistoryItem *item) const + bool forceRemoteLoader) const -> std::unique_ptr; - [[nodiscard]] bool useStreamingLoader(HistoryItem *item) const; + [[nodiscard]] bool useStreamingLoader() const; void setInappPlaybackFailed(); [[nodiscard]] bool inappPlaybackFailed() const; @@ -359,7 +358,6 @@ private: friend class Serialize::Document; [[nodiscard]] LocationType locationType() const; - [[nodiscard]] bool canVideoBeStreamed(HistoryItem *item) const; void validateLottieSticker(); void setMaybeSupportsStreaming(bool supports); void setLoadedInMediaCacheLocation(); diff --git a/Telegram/SourceFiles/data/data_document_media.cpp b/Telegram/SourceFiles/data/data_document_media.cpp index 045c894216..6bf6bc22f1 100644 --- a/Telegram/SourceFiles/data/data_document_media.cpp +++ b/Telegram/SourceFiles/data/data_document_media.cpp @@ -360,7 +360,7 @@ float64 DocumentMedia::progress() const { bool DocumentMedia::canBePlayed(HistoryItem *item) const { return !_owner->inappPlaybackFailed() - && _owner->useStreamingLoader(item) + && _owner->useStreamingLoader() && (loaded() || _owner->canBeStreamed(item)); } diff --git a/Telegram/SourceFiles/data/data_photo.cpp b/Telegram/SourceFiles/data/data_photo.cpp index 8364a3e7ff..0a2d37f8f3 100644 --- a/Telegram/SourceFiles/data/data_photo.cpp +++ b/Telegram/SourceFiles/data/data_photo.cpp @@ -565,8 +565,7 @@ bool PhotoData::videoCanBePlayed() const { auto PhotoData::createStreamingLoader( Data::FileOrigin origin, - bool forceRemoteLoader, - HistoryItem *item) const + bool forceRemoteLoader) const -> std::unique_ptr { if (!hasVideo()) { return nullptr; diff --git a/Telegram/SourceFiles/data/data_photo.h b/Telegram/SourceFiles/data/data_photo.h index d159364171..3cd749a715 100644 --- a/Telegram/SourceFiles/data/data_photo.h +++ b/Telegram/SourceFiles/data/data_photo.h @@ -150,8 +150,7 @@ public: [[nodiscard]] bool videoCanBePlayed() const; [[nodiscard]] auto createStreamingLoader( Data::FileOrigin origin, - bool forceRemoteLoader, - HistoryItem *item) const + bool forceRemoteLoader) const -> std::unique_ptr; [[nodiscard]] bool hasAttachedStickers() const; diff --git a/Telegram/SourceFiles/data/data_streaming.cpp b/Telegram/SourceFiles/data/data_streaming.cpp index 12dcae48bd..658c83807e 100644 --- a/Telegram/SourceFiles/data/data_streaming.cpp +++ b/Telegram/SourceFiles/data/data_streaming.cpp @@ -78,15 +78,6 @@ bool PruneDestroyedAndSet( return {}; } -[[nodiscard]] HistoryItem *LookupContext( - not_null owner, - const FileOrigin &origin) { - if (const auto message = std::get_if(&origin.data)) { - return owner->message(*message); - } - return nullptr; -} - } // namespace Streaming::Streaming(not_null owner) @@ -101,7 +92,6 @@ template base::flat_map, std::weak_ptr> &readers, not_null data, FileOrigin origin, - HistoryItem *context, bool forceRemoteLoader) { const auto i = readers.find(data); if (i != end(readers)) { @@ -111,10 +101,7 @@ template } } } - auto loader = data->createStreamingLoader( - origin, - forceRemoteLoader, - context); + auto loader = data->createStreamingLoader(origin, forceRemoteLoader); if (!loader) { return nullptr; } @@ -146,7 +133,7 @@ template return result; } } - auto reader = sharedReader(readers, data, origin, context); + auto reader = sharedReader(readers, data, origin); if (!reader) { return nullptr; } @@ -188,25 +175,18 @@ std::shared_ptr Streaming::sharedReader( not_null document, FileOrigin origin, bool forceRemoteLoader) { - const auto context = LookupContext(_owner, origin); - return sharedReader( - _fileReaders, - document, - origin, - context, - forceRemoteLoader); + return sharedReader(_fileReaders, document, origin, forceRemoteLoader); } std::shared_ptr Streaming::sharedDocument( not_null document, FileOrigin origin) { - const auto context = LookupContext(_owner, origin); return sharedDocument( _fileDocuments, _fileReaders, document, nullptr, - context, + nullptr, origin); } @@ -228,25 +208,18 @@ std::shared_ptr Streaming::sharedReader( not_null photo, FileOrigin origin, bool forceRemoteLoader) { - const auto context = LookupContext(_owner, origin); - return sharedReader( - _photoReaders, - photo, - origin, - context, - forceRemoteLoader); + return sharedReader(_photoReaders, photo, origin, forceRemoteLoader); } std::shared_ptr Streaming::sharedDocument( not_null photo, FileOrigin origin) { - const auto context = LookupContext(_owner, origin); return sharedDocument( _photoDocuments, _photoReaders, photo, nullptr, - context, + nullptr, origin); } diff --git a/Telegram/SourceFiles/data/data_streaming.h b/Telegram/SourceFiles/data/data_streaming.h index 6799b0fd79..51a6f18549 100644 --- a/Telegram/SourceFiles/data/data_streaming.h +++ b/Telegram/SourceFiles/data/data_streaming.h @@ -11,7 +11,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class PhotoData; class DocumentData; -class HistoryItem; namespace Media::Streaming { class Reader; @@ -65,7 +64,6 @@ private: base::flat_map, std::weak_ptr> &readers, not_null data, FileOrigin origin, - HistoryItem *context, bool forceRemoteLoader = false); template diff --git a/Telegram/SourceFiles/iv/iv_instance.cpp b/Telegram/SourceFiles/iv/iv_instance.cpp index 1c6f6df3c9..607ab87037 100644 --- a/Telegram/SourceFiles/iv/iv_instance.cpp +++ b/Telegram/SourceFiles/iv/iv_instance.cpp @@ -476,10 +476,7 @@ void Shown::streamFile( requestFail(std::move(request)); return; } - auto loader = document->createStreamingLoader( - fileOrigin(page), - false, - nullptr); + auto loader = document->createStreamingLoader(fileOrigin(page), false); if (!loader) { if (document->size >= Storage::kMaxFileInMemory) { requestFail(std::move(request)); From f6ac2c5aa91515c36e8ed77db03b9823a435029c Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Sat, 7 Mar 2026 02:54:14 +0400 Subject: [PATCH 061/415] Revert "Don't stream videos when external player is used" This reverts commit b0ce88395ffe2abc1d7fbef7d258a9b60bf738aa. --- Telegram/SourceFiles/data/data_document.cpp | 15 +++---------- Telegram/SourceFiles/data/data_document.h | 2 +- .../SourceFiles/data/data_document_media.cpp | 4 ++-- .../SourceFiles/data/data_document_media.h | 2 +- .../data/data_document_resolver.cpp | 2 +- .../SourceFiles/data/data_media_preload.cpp | 2 +- .../SourceFiles/data/data_shared_media.cpp | 2 +- .../view/media/history_view_document.cpp | 10 ++++----- .../history/view/media/history_view_gif.cpp | 14 ++++++------ .../inline_bots/inline_bot_layout_item.cpp | 2 +- .../media/view/media_view_overlay_widget.cpp | 10 ++++----- .../SourceFiles/media/view/media_view_pip.cpp | 2 +- .../SourceFiles/overview/overview_layout.cpp | 22 +++++++++---------- 13 files changed, 40 insertions(+), 49 deletions(-) diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp index 7df823b626..f5e88a0dc3 100644 --- a/Telegram/SourceFiles/data/data_document.cpp +++ b/Telegram/SourceFiles/data/data_document.cpp @@ -548,7 +548,7 @@ void DocumentData::setVideoQualities( && !document->dimensions.isEmpty() && !document->inappPlaybackFailed() && document->useStreamingLoader() - && document->canBeStreamed(nullptr); + && document->canBeStreamed(); }; ranges::sort( qualities, @@ -1545,17 +1545,8 @@ bool DocumentData::useStreamingLoader() const { || isVoiceMessage(); } -bool DocumentData::canBeStreamed(HistoryItem *item) const { - // Streaming couldn't be used with external player - // Maybe someone brave will implement this once upon a time... - static const auto &ExternalVideoPlayer = base::options::lookup( - Data::kOptionExternalVideoPlayer); - return hasRemoteLocation() - && supportsStreaming() - && (!isVideoFile() - || storyMedia() - || !ExternalVideoPlayer.value() - || (item && !item->allowsForward())); +bool DocumentData::canBeStreamed() const { + return hasRemoteLocation() && supportsStreaming(); } void DocumentData::setInappPlaybackFailed() { diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h index b0918a37c8..ec0a47921d 100644 --- a/Telegram/SourceFiles/data/data_document.h +++ b/Telegram/SourceFiles/data/data_document.h @@ -291,7 +291,7 @@ public: [[nodiscard]] Storage::Cache::Key cacheKey() const; [[nodiscard]] uint8 cacheTag() const; - [[nodiscard]] bool canBeStreamed(HistoryItem *item) const; + [[nodiscard]] bool canBeStreamed() const; [[nodiscard]] auto createStreamingLoader( Data::FileOrigin origin, bool forceRemoteLoader) const diff --git a/Telegram/SourceFiles/data/data_document_media.cpp b/Telegram/SourceFiles/data/data_document_media.cpp index 6bf6bc22f1..38e34ac5c1 100644 --- a/Telegram/SourceFiles/data/data_document_media.cpp +++ b/Telegram/SourceFiles/data/data_document_media.cpp @@ -358,10 +358,10 @@ float64 DocumentMedia::progress() const { : (loaded() ? 1. : 0.); } -bool DocumentMedia::canBePlayed(HistoryItem *item) const { +bool DocumentMedia::canBePlayed() const { return !_owner->inappPlaybackFailed() && _owner->useStreamingLoader() - && (loaded() || _owner->canBeStreamed(item)); + && (loaded() || _owner->canBeStreamed()); } bool DocumentMedia::thumbnailEnoughForSticker() const { diff --git a/Telegram/SourceFiles/data/data_document_media.h b/Telegram/SourceFiles/data/data_document_media.h index 9dfb3588a4..de94713dbe 100644 --- a/Telegram/SourceFiles/data/data_document_media.h +++ b/Telegram/SourceFiles/data/data_document_media.h @@ -78,7 +78,7 @@ public: [[nodiscard]] QByteArray bytes() const; [[nodiscard]] bool loaded(bool check = false) const; [[nodiscard]] float64 progress() const; - [[nodiscard]] bool canBePlayed(HistoryItem *item) const; + [[nodiscard]] bool canBePlayed() const; void automaticLoad(Data::FileOrigin origin, const HistoryItem *item); diff --git a/Telegram/SourceFiles/data/data_document_resolver.cpp b/Telegram/SourceFiles/data/data_document_resolver.cpp index fc83ca8e39..57ffe8f9e4 100644 --- a/Telegram/SourceFiles/data/data_document_resolver.cpp +++ b/Telegram/SourceFiles/data/data_document_resolver.cpp @@ -243,7 +243,7 @@ void ResolveDocument( if (document->isTheme() && media->loaded(true)) { showDocument(); location.accessDisable(); - } else if (media->canBePlayed(item)) { + } else if (media->canBePlayed()) { if (document->isAudioFile() || document->isVoiceMessage() || document->isVideoMessage()) { diff --git a/Telegram/SourceFiles/data/data_media_preload.cpp b/Telegram/SourceFiles/data/data_media_preload.cpp index 934bbf13ad..b1288f24ce 100644 --- a/Telegram/SourceFiles/data/data_media_preload.cpp +++ b/Telegram/SourceFiles/data/data_media_preload.cpp @@ -151,7 +151,7 @@ VideoPreload::~VideoPreload() { } bool VideoPreload::Can(not_null video) { - return video->canBeStreamed(nullptr) + return video->canBeStreamed() && video->videoPreloadLocation().valid() && video->bigFileBaseCacheKey(); } diff --git a/Telegram/SourceFiles/data/data_shared_media.cpp b/Telegram/SourceFiles/data/data_shared_media.cpp index 6a208106f4..96d32eae4c 100644 --- a/Telegram/SourceFiles/data/data_shared_media.cpp +++ b/Telegram/SourceFiles/data/data_shared_media.cpp @@ -66,7 +66,7 @@ bool IsItemGoodForType(const not_null item, Type type) { || ((videoType || photoVideoType) && videoDoc) || (fileType && (document->isTheme() || document->isImage() - || !document->canBeStreamed(item))); + || !document->canBeStreamed())); } } // namespace diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp index e0a899ac51..7b8759f446 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp @@ -637,7 +637,7 @@ void Document::draw( const auto cornerDownload = downloadInCorner(); - if (!_dataMedia->canBePlayed(_realParent)) { + if (!_dataMedia->canBePlayed()) { _dataMedia->automaticLoad(_realParent->fullId(), _realParent); } bool loaded = dataLoaded(), displayLoading = _data->displayLoading(); @@ -773,8 +773,8 @@ void Document::draw( return _data->isSongWithCover() ? sti->historyFileThumbPause : stm->historyFilePause; - } else if (loaded || _dataMedia->canBePlayed(_realParent)) { - return _dataMedia->canBePlayed(_realParent) + } else if (loaded || _dataMedia->canBePlayed()) { + return _dataMedia->canBePlayed() ? (_data->isSongWithCover() ? sti->historyFileThumbPlay : stm->historyFilePlay) @@ -1064,7 +1064,7 @@ void Document::ensureDataMediaCreated() const { bool Document::downloadInCorner() const { return _data->isAudioFile() && _realParent->allowsForward() - && _data->canBeStreamed(_realParent) + && _data->canBeStreamed() && !_data->inappPlaybackFailed(); } @@ -1293,7 +1293,7 @@ TextState Document::textState( && (!_data->loading() || downloadInCorner()) && !_data->uploading() && !_data->isNull()) { - if (loaded || _dataMedia->canBePlayed(_realParent)) { + if (loaded || _dataMedia->canBePlayed()) { result.link = _openl; } else { result.link = _savel; diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index c30410c49c..1d1fa149cd 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -184,7 +184,7 @@ Gif::Gif( }); } else { setDocumentLinks(_data, realParent, [=] { - if (!_data->createMediaView()->canBePlayed(realParent) + if (!_data->createMediaView()->canBePlayed() || !_data->isAnimation() || _data->isVideoMessage() || !CanPlayInline(_data)) { @@ -400,7 +400,7 @@ bool Gif::downloadInCorner() const { return _data->isVideoFile() && (_data->loading() || !autoplayEnabled()) && _realParent->allowsForward() - && _data->canBeStreamed(_realParent) + && _data->canBeStreamed() && !_data->inappPlaybackFailed(); } @@ -438,7 +438,7 @@ void Gif::draw(Painter &p, const PaintContext &context) const { const auto st = context.st; const auto sti = context.imageStyle(); const auto cornerDownload = downloadInCorner(); - const auto canBePlayed = _dataMedia->canBePlayed(_realParent); + const auto canBePlayed = _dataMedia->canBePlayed(); const auto autoplay = autoplayEnabled() && canBePlayed && CanPlayInline(_data); @@ -1403,7 +1403,7 @@ void Gif::drawGrouped( const auto sti = context.imageStyle(); _smallGroupPart = !fullFeaturedGrouped(sides); const auto cornerDownload = !_smallGroupPart && downloadInCorner(); - const auto canBePlayed = _dataMedia->canBePlayed(_realParent); + const auto canBePlayed = _dataMedia->canBePlayed(); const auto revealed = _spoiler ? _spoiler->revealAnimation.value(_spoiler->revealed ? 1. : 0.) @@ -1637,7 +1637,7 @@ ClickHandlerPtr Gif::currentVideoLink() const { ? _openl : (_data->loading() && _smallGroupPart) ? _cancell - : _dataMedia->canBePlayed(_realParent) + : _dataMedia->canBePlayed() ? _openl : _data->loading() ? _cancell @@ -1879,7 +1879,7 @@ void Gif::updateStatusText() const { statusSize = _data->uploadingData->offset; } else if (!downloadInCorner() && _data->loading()) { statusSize = _data->loadOffset(); - } else if (dataLoaded() || _dataMedia->canBePlayed(_realParent)) { + } else if (dataLoaded() || _dataMedia->canBePlayed()) { statusSize = Ui::FileStatusSizeLoaded; } else { statusSize = Ui::FileStatusSizeReady; @@ -2004,7 +2004,7 @@ void Gif::playAnimation(bool autoplay) { } if (_streamed) { stopAnimation(); - } else if (_dataMedia->canBePlayed(_realParent)) { + } else if (_dataMedia->canBePlayed()) { if (!autoplayEnabled()) { history()->owner().checkPlayingAnimations(); } diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp index 89b0e41b12..146730aa72 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp @@ -201,7 +201,7 @@ ClickHandlerPtr ItemBase::getResultPreviewHandler() const { _result->_content_url, false); } else if (const auto document = _result->_document - ; document && document->createMediaView()->canBePlayed(nullptr)) { + ; document && document->createMediaView()->canBePlayed()) { return std::make_shared(); } else if (_result->_photo) { return std::make_shared(); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 6d4a20f478..33de067e28 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -2278,7 +2278,7 @@ bool OverlayWidget::radialAnimationCallback(crl::time now) { update(radialRect()); } const auto ready = _document && _documentMedia->loaded(); - const auto streamVideo = ready && _documentMedia->canBePlayed(_message); + const auto streamVideo = ready && _documentMedia->canBePlayed(); const auto tryOpenImage = ready && (_document->size < Images::kReadBytesLimit); if (ready && ((tryOpenImage && !_radial.animating()) || streamVideo)) { @@ -2947,7 +2947,7 @@ void OverlayWidget::downloadMedia() { void OverlayWidget::saveCancel() { if (_document && _document->loading()) { _document->cancel(); - if (_documentMedia->canBePlayed(_message)) { + if (_documentMedia->canBePlayed()) { redisplayContent(); } } @@ -3885,7 +3885,7 @@ void OverlayWidget::displayDocument( } } else { initSponsoredButton(); - if (_documentMedia->canBePlayed(_message) + if (_documentMedia->canBePlayed() && initStreaming(startStreaming)) { } else if (_document->isVideoFile()) { _documentMedia->automaticLoad(fileOrigin(), _message); @@ -4094,7 +4094,7 @@ void OverlayWidget::showAndActivate() { } bool OverlayWidget::canInitStreaming() const { - return (_document && _documentMedia->canBePlayed(_message)) + return (_document && _documentMedia->canBePlayed()) || (_photo && _photo->videoCanBePlayed()); } @@ -6246,7 +6246,7 @@ void OverlayWidget::preloadData(int delta) { const auto &[i, ok] = documents.emplace( (*document)->createMediaView()); (*i)->thumbnailWanted(fileOrigin(entity)); - if (!(*i)->canBePlayed(entity.item)) { + if (!(*i)->canBePlayed()) { (*i)->automaticLoad(fileOrigin(entity), entity.item); } } diff --git a/Telegram/SourceFiles/media/view/media_view_pip.cpp b/Telegram/SourceFiles/media/view/media_view_pip.cpp index 0563bf099a..8de61898f4 100644 --- a/Telegram/SourceFiles/media/view/media_view_pip.cpp +++ b/Telegram/SourceFiles/media/view/media_view_pip.cpp @@ -1356,7 +1356,7 @@ void Pip::setupStreaming() { void Pip::applyVideoQuality(VideoQuality value) { if (_quality == value - || !_dataMedia->canBePlayed(_context)) { + || !_dataMedia->canBePlayed()) { return; } const auto resolved = _data->chooseQuality(_context, value); diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp index 2a83822262..d46518dc3f 100644 --- a/Telegram/SourceFiles/overview/overview_layout.cpp +++ b/Telegram/SourceFiles/overview/overview_layout.cpp @@ -626,7 +626,7 @@ void Video::paint( if (!selected && !context->selecting && radialOpacity < 1.) { if (clip.intersects(QRect(0, _height - st::normalFont->height, _width, st::normalFont->height))) { - const auto download = !loaded && !_dataMedia->canBePlayed(parent()); + const auto download = !loaded && !_dataMedia->canBePlayed(); const auto &icon = download ? (selected ? st::overviewVideoDownloadSelected : st::overviewVideoDownload) : (selected ? st::overviewVideoPlaySelected : st::overviewVideoPlay); @@ -652,7 +652,7 @@ void Video::paint( if (selected) { p.setBrush(st::msgDateImgBgSelected); } else { - auto over = ClickHandler::showAsActive((_data->loading() || _data->uploading()) ? _cancell : (loaded || _dataMedia->canBePlayed(parent())) ? _openl : _savel); + auto over = ClickHandler::showAsActive((_data->loading() || _data->uploading()) ? _cancell : (loaded || _dataMedia->canBePlayed()) ? _openl : _savel); p.setBrush(anim::brush(st::msgDateImgBg, st::msgDateImgBgOver, _a_iconOver.value(over ? 1. : 0.))); } @@ -751,7 +751,7 @@ TextState Video::getState( ? _openl : (_data->loading() || _data->uploading()) ? _cancell - : (dataLoaded() || _dataMedia->canBePlayed(parent())) + : (dataLoaded() || _dataMedia->canBePlayed()) ? _openl : _savel; return { parent(), link }; @@ -879,7 +879,7 @@ void Voice::paint(Painter &p, const QRect &clip, TextSelection selection, const } const auto &checkLink = (_data->loading() || _data->uploading()) ? _cancell - : (_dataMedia->canBePlayed(parent()) || loaded) + : (_dataMedia->canBePlayed() || loaded) ? _openl : _savel; if (selected) { @@ -907,7 +907,7 @@ void Voice::paint(Painter &p, const QRect &clip, TextSelection selection, const return &(selected ? _st.voiceCancelSelected : _st.voiceCancel); } else if (showPause) { return &(selected ? _st.voicePauseSelected : _st.voicePause); - } else if (_dataMedia->canBePlayed(parent())) { + } else if (_dataMedia->canBePlayed()) { return &(selected ? _st.voicePlaySelected : _st.voicePlay); } return &(selected @@ -994,7 +994,7 @@ TextState Voice::getState( if (inner.contains(point)) { const auto link = (_data->loading() || _data->uploading()) ? _cancell - : (_dataMedia->canBePlayed(parent()) || loaded) + : (_dataMedia->canBePlayed() || loaded) ? _openl : _savel; return { parent(), link }; @@ -1187,7 +1187,7 @@ Document::Document( bool Document::downloadInCorner() const { return _data->isAudioFile() && parent()->allowsForward() - && _data->canBeStreamed(parent()) + && _data->canBeStreamed() && !_data->inappPlaybackFailed(); } @@ -1250,7 +1250,7 @@ void Document::paint(Painter &p, const QRect &clip, TextSelection selection, con } else { const auto over = ClickHandler::showAsActive(isLoading ? _cancell - : (loaded || _dataMedia->canBePlayed(parent())) + : (loaded || _dataMedia->canBePlayed()) ? _openl : _savel); p.setBrush(anim::brush( @@ -1272,7 +1272,7 @@ void Document::paint(Painter &p, const QRect &clip, TextSelection selection, con return &(selected ? _st.voicePauseSelected : _st.voicePause); - } else if (loaded || _dataMedia->canBePlayed(parent())) { + } else if (loaded || _dataMedia->canBePlayed()) { return &(selected ? _st.voicePlaySelected : _st.voicePlay); @@ -1285,7 +1285,7 @@ void Document::paint(Painter &p, const QRect &clip, TextSelection selection, con return &(selected ? _st.songCancelSelected : _st.songCancel); } else if (showPause) { return &(selected ? _st.songPauseSelected : _st.songPause); - } else if (loaded || _dataMedia->canBePlayed(parent())) { + } else if (loaded || _dataMedia->canBePlayed()) { return &(selected ? _st.songPlaySelected : _st.songPlay); } return &(selected ? _st.songDownloadSelected : _st.songDownload); @@ -1523,7 +1523,7 @@ TextState Document::getState( const auto link = (!downloadInCorner() && (_data->loading() || _data->uploading())) ? _cancell - : (loaded || _dataMedia->canBePlayed(parent())) + : (loaded || _dataMedia->canBePlayed()) ? _openl : _savel; return { parent(), link }; From c2f64700c1bbcb2c3f9cf0e94183ac3f9aeffc09 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Sat, 7 Mar 2026 00:27:20 +0000 Subject: [PATCH 062/415] Move external video player option handling to SessionController::openDocument --- .../data/data_document_resolver.cpp | 16 +--------------- .../SourceFiles/data/data_document_resolver.h | 2 -- .../settings/settings_experimental.cpp | 3 +-- .../window/window_session_controller.cpp | 18 ++++++++++++++++++ .../window/window_session_controller.h | 2 ++ 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/Telegram/SourceFiles/data/data_document_resolver.cpp b/Telegram/SourceFiles/data/data_document_resolver.cpp index 57ffe8f9e4..47d58a1636 100644 --- a/Telegram/SourceFiles/data/data_document_resolver.cpp +++ b/Telegram/SourceFiles/data/data_document_resolver.cpp @@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_document_resolver.h" -#include "base/options.h" #include "base/platform/base_platform_info.h" #include "boxes/abstract_box.h" // Ui::show(). #include "chat_helpers/ttl_media_layer_widget.h" @@ -39,13 +38,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Data { namespace { -base::options::toggle OptionExternalVideoPlayer({ - .id = kOptionExternalVideoPlayer, - .name = "External video player", - .description = "Use system video player instead of the internal one. " - "This disabes video playback in messages.", -}); - void ConfirmDontWarnBox( not_null box, rpl::producer &&text, @@ -153,8 +145,6 @@ void LaunchWithWarning( } // namespace -const char kOptionExternalVideoPlayer[] = "external-video-player"; - base::binary_guard ReadBackgroundImageAsync( not_null media, FnMut postprocess, @@ -199,11 +189,7 @@ void ResolveDocument( const auto msgId = item ? item->fullId() : FullMsgId(); const auto showDocument = [&] { - if (OptionExternalVideoPlayer.value() - && document->isVideoFile() - && !document->filepath().isEmpty()) { - File::Launch(document->location(false).fname); - } else if (controller) { + if (controller) { controller->openDocument( document, true, diff --git a/Telegram/SourceFiles/data/data_document_resolver.h b/Telegram/SourceFiles/data/data_document_resolver.h index de9327312f..077a795476 100644 --- a/Telegram/SourceFiles/data/data_document_resolver.h +++ b/Telegram/SourceFiles/data/data_document_resolver.h @@ -20,8 +20,6 @@ namespace Data { class DocumentMedia; -extern const char kOptionExternalVideoPlayer[]; - base::binary_guard ReadBackgroundImageAsync( not_null media, FnMut postprocess, diff --git a/Telegram/SourceFiles/settings/settings_experimental.cpp b/Telegram/SourceFiles/settings/settings_experimental.cpp index 07a48ffe70..5a9129a01f 100644 --- a/Telegram/SourceFiles/settings/settings_experimental.cpp +++ b/Telegram/SourceFiles/settings/settings_experimental.cpp @@ -38,7 +38,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_controller.h" #include "window/notifications_manager.h" #include "storage/localimageloader.h" -#include "data/data_document_resolver.h" #include "info/info_flexible_scroll.h" #include "chat_helpers/stickers_list_widget.h" #include "styles/style_settings.h" @@ -168,7 +167,7 @@ void SetupExperimental( addToggle(Core::kOptionFreeType); addToggle(Core::kOptionSkipUrlSchemeRegister); addToggle(Core::kOptionDeadlockDetector); - addToggle(Data::kOptionExternalVideoPlayer); + addToggle(Window::kOptionExternalVideoPlayer); addToggle(Window::kOptionNewWindowsSizeAsFirst); addToggle(MTP::details::kOptionPreferIPv6); if (base::options::lookup(kOptionFastButtonsMode).value()) { diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index e1e49fa9c4..8a06e4ddde 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -69,7 +69,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/shortcuts.h" #include "core/application.h" #include "core/click_handler_types.h" +#include "core/file_utilities.h" #include "core/ui_integration.h" +#include "base/options.h" #include "base/unixtime.h" #include "info/channel_statistics/earn/earn_icons.h" #include "ui/controls/userpic_button.h" @@ -127,6 +129,13 @@ namespace { constexpr auto kCustomThemesInMemory = 5; constexpr auto kMaxChatEntryHistorySize = 50; +base::options::toggle OptionExternalVideoPlayer({ + .id = kOptionExternalVideoPlayer, + .name = "External video player", + .description = "Use system video player instead of the internal one. " + "This disabes video playback in messages.", +}); + class MainWindowShow final : public ChatHelpers::Show { public: explicit MainWindowShow(not_null controller); @@ -313,6 +322,8 @@ void MainWindowShow::processChosenSticker( } // namespace +const char kOptionExternalVideoPlayer[] = "external-video-player"; + void ActivateWindow(not_null controller) { Ui::ActivateWindow(controller->widget()); } @@ -3196,6 +3207,13 @@ void SessionController::openDocument( if (openSharedStory(item) || openFakeItemStory(message.id, stories)) { return; } else if (showInMediaView) { + const auto filepath = document->filepath(); + if (OptionExternalVideoPlayer.value() + && document->isVideoFile() + && !filepath.isEmpty()) { + File::Launch(filepath); + return; + } using namespace Media::View; const auto saved = session().local().mediaLastPlaybackPosition( document->id); diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index fed0de87c9..915b909719 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -108,6 +108,8 @@ class ChatSwitchProcess; struct PeerByLinkInfo; struct SeparateId; +extern const char kOptionExternalVideoPlayer[]; + struct PeerThemeOverride { PeerData *peer = nullptr; std::shared_ptr theme; From 0b98b88dfc4de1d8f93b629bc6877125a6b53e18 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Fri, 6 Mar 2026 22:48:21 +0000 Subject: [PATCH 063/415] Support file saving for streaming videos with external video player --- .../window/window_session_controller.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 8a06e4ddde..2dad6aad79 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -52,6 +52,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/data_document.h" #include "data/data_document_media.h" +#include "data/data_file_click_handler.h" #include "data/data_changes.h" #include "data/data_group_call.h" #include "data/data_forum.h" @@ -132,8 +133,7 @@ constexpr auto kMaxChatEntryHistorySize = 50; base::options::toggle OptionExternalVideoPlayer({ .id = kOptionExternalVideoPlayer, .name = "External video player", - .description = "Use system video player instead of the internal one. " - "This disabes video playback in messages.", + .description = "Use system video player instead of the internal one.", }); class MainWindowShow final : public ChatHelpers::Show { @@ -3207,10 +3207,15 @@ void SessionController::openDocument( if (openSharedStory(item) || openFakeItemStory(message.id, stories)) { return; } else if (showInMediaView) { - const auto filepath = document->filepath(); - if (OptionExternalVideoPlayer.value() - && document->isVideoFile() - && !filepath.isEmpty()) { + if (OptionExternalVideoPlayer.value() && document->isVideoFile()) { + const auto filepath = document->filepath(); + if (filepath.isEmpty()) { + DocumentSaveClickHandler::Save( + message.id, + document, + DocumentSaveClickHandler::Mode::ToFile); + return; + } File::Launch(filepath); return; } From a2c268c9235749c3988307ca2b6834014871131d Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Sat, 7 Mar 2026 01:58:21 +0000 Subject: [PATCH 064/415] Don't require second click to open cached videos in external viewer --- .../SourceFiles/window/window_session_controller.cpp | 12 ++++++++++++ .../SourceFiles/window/window_session_controller.h | 2 ++ 2 files changed, 14 insertions(+) diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 2dad6aad79..1ed7ed980e 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -1663,6 +1663,15 @@ SessionController::SessionController( } }, lifetime()); + session->data().documentLoadProgress( + ) | rpl::filter([=](not_null document) { + return _pendingOpenDocumentId == document->id + && !document->filepath().isEmpty(); + }) | rpl::on_next([=](not_null document) { + _pendingOpenDocumentId = {}; + File::Launch(document->filepath()); + }, _lifetime); + session->api().globalPrivacy().suggestArchiveAndMute( ) | rpl::take(1) | rpl::on_next([=] { session->api().globalPrivacy().reload(crl::guard(this, [=] { @@ -3210,6 +3219,9 @@ void SessionController::openDocument( if (OptionExternalVideoPlayer.value() && document->isVideoFile()) { const auto filepath = document->filepath(); if (filepath.isEmpty()) { + if (document->loadedInMediaCache()) { + _pendingOpenDocumentId = document->id; + } DocumentSaveClickHandler::Save( message.id, document, diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 915b909719..a7439f770a 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -837,6 +837,8 @@ private: std::unique_ptr _chatSwitchProcess; + DocumentId _pendingOpenDocumentId = 0; + base::has_weak_ptr _storyOpenGuard; QString _premiumRef; From 07649b3c168437676dd88a7f8c1b8c33142331a2 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Fri, 6 Mar 2026 22:34:33 +0000 Subject: [PATCH 065/415] External video player -> External media viewer --- .../SourceFiles/settings/settings_experimental.cpp | 2 +- .../SourceFiles/window/window_session_controller.cpp | 12 ++++++------ .../SourceFiles/window/window_session_controller.h | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Telegram/SourceFiles/settings/settings_experimental.cpp b/Telegram/SourceFiles/settings/settings_experimental.cpp index 5a9129a01f..a57e08e413 100644 --- a/Telegram/SourceFiles/settings/settings_experimental.cpp +++ b/Telegram/SourceFiles/settings/settings_experimental.cpp @@ -167,7 +167,7 @@ void SetupExperimental( addToggle(Core::kOptionFreeType); addToggle(Core::kOptionSkipUrlSchemeRegister); addToggle(Core::kOptionDeadlockDetector); - addToggle(Window::kOptionExternalVideoPlayer); + addToggle(Window::kOptionExternalMediaViewer); addToggle(Window::kOptionNewWindowsSizeAsFirst); addToggle(MTP::details::kOptionPreferIPv6); if (base::options::lookup(kOptionFastButtonsMode).value()) { diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 1ed7ed980e..3523445c54 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -130,10 +130,10 @@ namespace { constexpr auto kCustomThemesInMemory = 5; constexpr auto kMaxChatEntryHistorySize = 50; -base::options::toggle OptionExternalVideoPlayer({ - .id = kOptionExternalVideoPlayer, - .name = "External video player", - .description = "Use system video player instead of the internal one.", +base::options::toggle OptionExternalMediaViewer({ + .id = kOptionExternalMediaViewer, + .name = "External media viewer", + .description = "Use system media viewer instead of the internal one.", }); class MainWindowShow final : public ChatHelpers::Show { @@ -322,7 +322,7 @@ void MainWindowShow::processChosenSticker( } // namespace -const char kOptionExternalVideoPlayer[] = "external-video-player"; +const char kOptionExternalMediaViewer[] = "external-media-viewer"; void ActivateWindow(not_null controller) { Ui::ActivateWindow(controller->widget()); @@ -3216,7 +3216,7 @@ void SessionController::openDocument( if (openSharedStory(item) || openFakeItemStory(message.id, stories)) { return; } else if (showInMediaView) { - if (OptionExternalVideoPlayer.value() && document->isVideoFile()) { + if (OptionExternalMediaViewer.value() && document->isVideoFile()) { const auto filepath = document->filepath(); if (filepath.isEmpty()) { if (document->loadedInMediaCache()) { diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index a7439f770a..09e114689c 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -108,7 +108,7 @@ class ChatSwitchProcess; struct PeerByLinkInfo; struct SeparateId; -extern const char kOptionExternalVideoPlayer[]; +extern const char kOptionExternalMediaViewer[]; struct PeerThemeOverride { PeerData *peer = nullptr; From 6acc2633a541ede7c6aa0b96f846dffe8818b430 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Fri, 6 Mar 2026 22:30:51 +0000 Subject: [PATCH 066/415] Open any document type via external media viewer --- Telegram/SourceFiles/window/window_session_controller.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 3523445c54..a3453ebd9b 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -3216,7 +3216,7 @@ void SessionController::openDocument( if (openSharedStory(item) || openFakeItemStory(message.id, stories)) { return; } else if (showInMediaView) { - if (OptionExternalMediaViewer.value() && document->isVideoFile()) { + if (OptionExternalMediaViewer.value() && !document->isTheme()) { const auto filepath = document->filepath(); if (filepath.isEmpty()) { if (document->loadedInMediaCache()) { From 59ecfd67f0fa727dfb128cb0eb7d0a5db1798fca Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Mon, 9 Mar 2026 18:04:17 +0000 Subject: [PATCH 067/415] Add location tracking to PhotoData Add persistent file location storage for downloaded photos, similar to DocumentData. This allows the app to remember where a photo was saved and reuse that path. Co-Authored-By: GLM-5 --- Telegram/SourceFiles/data/data_photo.cpp | 24 ++++++++++++++++++++++++ Telegram/SourceFiles/data/data_photo.h | 7 +++++++ 2 files changed, 31 insertions(+) diff --git a/Telegram/SourceFiles/data/data_photo.cpp b/Telegram/SourceFiles/data/data_photo.cpp index 0a2d37f8f3..c354ca551b 100644 --- a/Telegram/SourceFiles/data/data_photo.cpp +++ b/Telegram/SourceFiles/data/data_photo.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_photo.h" +#include "data/data_document.h" #include "data/data_session.h" #include "data/data_reply_preview.h" #include "data/data_photo_media.h" @@ -15,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "media/streaming/media_streaming_loader_local.h" #include "media/streaming/media_streaming_loader_mtproto.h" +#include "storage/storage_account.h" #include "storage/file_download.h" #include "core/application.h" @@ -475,6 +477,28 @@ int PhotoData::height() const { return _images[PhotoSizeIndex(PhotoSize::Large)].location.height(); } +MediaKey PhotoData::mediaKey() const { + return ::mediaKey(UnknownFileLocation, _dc, id); +} + +const Core::FileLocation &PhotoData::location(bool check) const { + if (check && !_location.check()) { + const auto location = session().local().readFileLocation(mediaKey()); + const auto that = const_cast(this); + if (!location.inMediaCache()) { + that->_location = location; + } + } + return _location; +} + +void PhotoData::setLocation(const Core::FileLocation &loc) { + if (!loc.inMediaCache() && loc.check()) { + _location = loc; + session().local().writeFileLocation(mediaKey(), _location); + } +} + Data::CloudFile &PhotoData::videoFile(PhotoSize size) { Expects(_videoSizes != nullptr); diff --git a/Telegram/SourceFiles/data/data_photo.h b/Telegram/SourceFiles/data/data_photo.h index 3cd749a715..f33078082a 100644 --- a/Telegram/SourceFiles/data/data_photo.h +++ b/Telegram/SourceFiles/data/data_photo.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_types.h" #include "data/data_cloud_file.h" +#include "core/file_location.h" namespace Main { class Session; @@ -160,6 +161,10 @@ public: [[nodiscard]] int width() const; [[nodiscard]] int height() const; + [[nodiscard]] MediaKey mediaKey() const; + [[nodiscard]] const Core::FileLocation &location(bool check) const; + void setLocation(const Core::FileLocation &loc); + PhotoId id = 0; PeerData *peer = nullptr; // for chat and channel photos connection @@ -195,4 +200,6 @@ private: not_null _owner; + Core::FileLocation _location; + }; From 34f0d7026e92855692ee19beea8f6e4b56f14bc8 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Fri, 6 Mar 2026 21:31:13 +0000 Subject: [PATCH 068/415] Open photos with external media viewer Co-Authored-By: GLM-5 --- .../window/window_session_controller.cpp | 58 +++++++++++++++++++ .../window/window_session_controller.h | 10 ++++ 2 files changed, 68 insertions(+) diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index a3453ebd9b..067a93b073 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -53,6 +53,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_file_click_handler.h" +#include "data/data_photo_media.h" #include "data/data_changes.h" #include "data/data_group_call.h" #include "data/data_forum.h" @@ -1672,6 +1673,18 @@ SessionController::SessionController( File::Launch(document->filepath()); }, _lifetime); + session->downloaderTaskFinished( + ) | rpl::filter([=] { + return _pendingOpenPhoto.media && _pendingOpenPhoto.media->loaded(); + }) | rpl::on_next([=] { + if (_pendingOpenPhoto.media->saveToFile(_pendingOpenPhoto.filepath)) { + _pendingOpenPhoto.data->setLocation( + Core::FileLocation(_pendingOpenPhoto.filepath)); + File::Launch(_pendingOpenPhoto.filepath); + } + _pendingOpenPhoto = {}; + }, _lifetime); + session->api().globalPrivacy().suggestArchiveAndMute( ) | rpl::take(1) | rpl::on_next([=] { session->api().globalPrivacy().reload(crl::guard(this, [=] { @@ -3184,6 +3197,37 @@ void SessionController::hideLayer(anim::type animated) { _window->hideLayer(animated); } +bool SessionController::openPhotoExternal( + not_null photo, + Data::FileOrigin origin) { + if (!OptionExternalMediaViewer.value()) { + return false; + } + const auto media = photo->createMediaView(); + const auto existing = photo->location(true).name(); + if (!existing.isEmpty()) { + File::Launch(existing); + return true; + } + const auto filepath = FileNameForSave( + &session(), + tr::lng_save_photo(tr::now), + u"JPEG Image (*.jpg);;"_q + FileDialog::AllFilesFilter(), + u"photo"_q, + u".jpg"_q, + false); + if (media->loaded()) { + if (media->saveToFile(filepath)) { + photo->setLocation(Core::FileLocation(filepath)); + File::Launch(filepath); + } + return true; + } + _pendingOpenPhoto = { photo, media, filepath }; + photo->load(origin, LoadFromCloudOrLocal, true); + return true; +} + void SessionController::openPhoto( not_null photo, MessageContext message, @@ -3192,6 +3236,12 @@ void SessionController::openPhoto( if (openSharedStory(item) || openFakeItemStory(message.id, stories)) { return; } + const auto origin = item + ? Data::FileOrigin(item->fullId()) + : Data::FileOrigin(); + if (openPhotoExternal(photo, origin)) { + return; + } _window->openInMediaView(Media::View::OpenRequest( this, photo, @@ -3203,6 +3253,14 @@ void SessionController::openPhoto( void SessionController::openPhoto( not_null photo, not_null peer) { + const auto origin = peer->isUser() + ? Data::FileOrigin(Data::FileOriginUserPhoto( + peerToUser(peer->id), + photo->id)) + : Data::FileOrigin(Data::FileOriginPeerPhoto(peer->id)); + if (openPhotoExternal(photo, origin)) { + return; + } _window->openInMediaView(Media::View::OpenRequest(this, photo, peer)); } diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 09e114689c..8caf9b9f50 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -75,6 +75,7 @@ struct ChatPaintContextArgs; namespace Data { struct CloudTheme; enum class CloudThemeType; +class PhotoMedia; class Thread; class Forum; class ForumTopic; @@ -778,6 +779,10 @@ private: const SectionShow ¶ms, MsgId showAtMsgId); + [[nodiscard]] bool openPhotoExternal( + not_null photo, + Data::FileOrigin origin); + const not_null _window; const std::unique_ptr _emojiInteractions; const std::unique_ptr _chatPreviewManager; @@ -838,6 +843,11 @@ private: std::unique_ptr _chatSwitchProcess; DocumentId _pendingOpenDocumentId = 0; + struct PendingOpenPhoto { + PhotoData *data = nullptr; + std::shared_ptr media; + QString filepath; + } _pendingOpenPhoto; base::has_weak_ptr _storyOpenGuard; From dbb7e349504d7241068f885b766c3b101e4193ac Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 10 Mar 2026 15:01:32 +0400 Subject: [PATCH 069/415] Fix editing of checklist in Saved Messages. --- Telegram/SourceFiles/data/data_media_types.cpp | 2 +- Telegram/SourceFiles/window/window_peer_menu.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 8e613a2851..fe00d7967e 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -2404,7 +2404,7 @@ TextForMimeData MediaTodoList::clipboardText() const { } bool MediaTodoList::allowsEdit() const { - return parent()->out(); + return parent()->out() || parent()->history()->peer->isSelf(); } bool MediaTodoList::updateInlineResultMedia(const MTPMessageMedia &media) { diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index e0eb08759b..30531d82bc 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -2416,7 +2416,9 @@ bool PeerMenuShowAddTodoListTasks(not_null item) { && !item->Has() && todolist && (todolist->items.size() < appConfig->todoListItemsLimit()) - && (item->out() || todolist->othersCanAppend()); + && (item->out() + || item->history()->peer->isSelf() + || todolist->othersCanAppend()); } void PeerMenuAddTodoListTasks( From f7679c50d8929d8e4b0b87494e6a26cf2cbeadd4 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 10 Mar 2026 15:31:17 +0400 Subject: [PATCH 070/415] Always spoiler codes in notifications. --- Telegram/SourceFiles/window/notifications_manager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp index 3ef706f761..daf0b84b9e 100644 --- a/Telegram/SourceFiles/window/notifications_manager.cpp +++ b/Telegram/SourceFiles/window/notifications_manager.cpp @@ -1071,8 +1071,8 @@ Manager::DisplayOptions Manager::getNotificationOptions( || HideReplyButtonOption.value(); result.spoilerLoginCode = item && !item->out() - && peer->isNotificationsUser() - && Core::App().isSharingScreen(); + && (peer->isNotificationsUser() + || peer->isVerifyCodes()); return result; } From 6564a42f21187e423daa659ecd265088542d201c Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 10 Mar 2026 15:47:28 +0400 Subject: [PATCH 071/415] Fix saved music shuffled playing. Fixes https://bugs.telegram.org/c/60020 --- Telegram/SourceFiles/media/player/media_player_instance.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/media/player/media_player_instance.cpp b/Telegram/SourceFiles/media/player/media_player_instance.cpp index 739ce3c721..e106f9147a 100644 --- a/Telegram/SourceFiles/media/player/media_player_instance.cpp +++ b/Telegram/SourceFiles/media/player/media_player_instance.cpp @@ -974,7 +974,10 @@ void Instance::validateShuffleData(not_null data) { const auto last = raw->playlist.empty() ? MsgId(ServerMaxMsgId - 1) : raw->playlist.back(); - SharedMediaMergedViewer( + const auto sharedMediaViewer = raw->savedMusic + ? SavedMusicMediaViewer + : SharedMediaMergedViewer; + sharedMediaViewer( &raw->history->session(), SharedMediaMergedKey( SliceKey( From 97345dbd5972d576d9eb580cad46346f164b1f99 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 10 Mar 2026 15:55:22 +0400 Subject: [PATCH 072/415] Fix "Deleted Message" reply in streamed drafts. --- Telegram/SourceFiles/history/history_streamed_drafts.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Telegram/SourceFiles/history/history_streamed_drafts.cpp b/Telegram/SourceFiles/history/history_streamed_drafts.cpp index 1178a60faa..6a06611742 100644 --- a/Telegram/SourceFiles/history/history_streamed_drafts.cpp +++ b/Telegram/SourceFiles/history/history_streamed_drafts.cpp @@ -56,7 +56,6 @@ void HistoryStreamedDrafts::apply( .flags = MessageFlag::Local | MessageFlag::HasReplyInfo, .from = fromId, .replyTo = { - .messageId = { _history->peer->id, rootId }, .topicRootId = rootId, }, .date = when, From 4dff43a65eb7586fd0c2b7cf186d5bafc79e1d3e Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 10 Mar 2026 17:14:07 +0400 Subject: [PATCH 073/415] Improve code-spoilering regular expression. --- Telegram/SourceFiles/history/history_item.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index b235521824..b73ff7068a 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -95,7 +95,8 @@ template } [[nodiscard]] TextWithEntities SpoilerLoginCode(TextWithEntities text) { - const auto r = QRegularExpression(u"(? Date: Tue, 10 Mar 2026 19:03:13 +0400 Subject: [PATCH 074/415] Fix possible crash in streamed-reply-drafts. --- Telegram/SourceFiles/history/history.cpp | 3 +++ .../history/history_streamed_drafts.cpp | 21 ++++++++++++++----- .../history/history_streamed_drafts.h | 1 + 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index a36e0f97fb..b7dc560cbe 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -171,6 +171,9 @@ void History::itemRemoved(not_null item) { if (const auto sublist = item->savedSublist()) { sublist->applyItemRemoved(item->id); } + if (const auto streamed = _streamedDrafts.get()) { + streamed->applyItemRemoved(item); + } if (const auto chat = peer->asChat()) { if (const auto to = chat->getMigrateToChannel()) { if (const auto history = owner().historyLoaded(to)) { diff --git a/Telegram/SourceFiles/history/history_streamed_drafts.cpp b/Telegram/SourceFiles/history/history_streamed_drafts.cpp index 6a06611742..d719e81dcc 100644 --- a/Telegram/SourceFiles/history/history_streamed_drafts.cpp +++ b/Telegram/SourceFiles/history/history_streamed_drafts.cpp @@ -82,10 +82,8 @@ bool HistoryStreamedDrafts::update( } void HistoryStreamedDrafts::clear(MsgId rootId) { - const auto i = _drafts.find(rootId); - if (i != end(_drafts)) { - i->second.message->destroy(); - _drafts.erase(i); + if (const auto draft = _drafts.take(rootId)) { + draft->message->destroy(); } if (_drafts.empty()) { scheduleDestroy(); @@ -101,13 +99,26 @@ void HistoryStreamedDrafts::applyItemAdded(not_null item) { clear(rootId); } +void HistoryStreamedDrafts::applyItemRemoved(not_null item) { + for (auto i = begin(_drafts); i != end(_drafts); ++i) { + if (i->second.message == item) { + _drafts.erase(i); + if (_drafts.empty()) { + scheduleDestroy(); + } + return; + } + } +} + void HistoryStreamedDrafts::check() { auto closest = crl::time(); const auto now = crl::now(); for (auto i = begin(_drafts); i != end(_drafts);) { if (now - i->second.updated >= kClearTimeout) { - i->second.message->destroy(); + const auto message = i->second.message; i = _drafts.erase(i); + message->destroy(); } else { if (!closest || closest > i->second.updated) { closest = i->second.updated; diff --git a/Telegram/SourceFiles/history/history_streamed_drafts.h b/Telegram/SourceFiles/history/history_streamed_drafts.h index 270e3079b6..657a0177ed 100644 --- a/Telegram/SourceFiles/history/history_streamed_drafts.h +++ b/Telegram/SourceFiles/history/history_streamed_drafts.h @@ -26,6 +26,7 @@ public: const MTPDsendMessageTextDraftAction &data); void applyItemAdded(not_null item); + void applyItemRemoved(not_null item); private: struct Draft { From 3670e1d9db9171e4258ac7a5b996f8a1c1eb7b8c Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 10 Mar 2026 14:31:47 +0400 Subject: [PATCH 075/415] Fix crash in formatting applying. Fixes #30426. --- Telegram/lib_ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/lib_ui b/Telegram/lib_ui index e85ba0fe2a..ab270986c4 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit e85ba0fe2a68fc380fb3a4977db49004837b3fba +Subproject commit ab270986c470616f37113f0cfc8944900ef02465 From c61c2494054a416a395a2cdcc6641e0ee318f117 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 10 Mar 2026 21:14:19 +0400 Subject: [PATCH 076/415] Ripple effects for links. --- .../history/view/history_view_message.cpp | 297 +++++++++++++++++- .../history/view/history_view_message.h | 24 ++ Telegram/SourceFiles/ui/chat/chat.style | 4 + Telegram/lib_ui | 2 +- 4 files changed, 322 insertions(+), 5 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 3e96201585..dce702b4af 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -32,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/share_box.h" #include "boxes/peers/tag_info_box.h" #include "ui/effects/reaction_fly_animation.h" +#include "ui/effects/ripple_animation.h" #include "ui/text/text_utilities.h" #include "ui/text/text_extended_data.h" #include "ui/power_saving.h" @@ -65,6 +66,25 @@ constexpr auto kSummarizeThreshold = 512; constexpr auto kPlayStatusLimit = 2; const auto kPsaTooltipPrefix = "cloud_lng_tooltip_psa_"; +[[nodiscard]] bool IsRippleLink(const ClickHandlerPtr &handler) { + switch (handler->getTextEntity().type) { + case EntityType::Url: + case EntityType::CustomUrl: + case EntityType::Email: + case EntityType::Hashtag: + case EntityType::Cashtag: + case EntityType::Mention: + case EntityType::MentionName: + case EntityType::BotCommand: + case EntityType::Phone: + case EntityType::BankCard: + case EntityType::FormattedDate: + return true; + default: + return false; + } +} + [[nodiscard]] ClickHandlerPtr MakeTopicButtonLink( not_null topic, MsgId messageId) { @@ -112,6 +132,13 @@ struct Message::RightAction { std::unique_ptr second; }; +struct Message::LinkRipple { + std::unique_ptr ripple; + ClickHandlerPtr link; + QPoint maskOffset; + int cachedWidth = 0; +}; + LogEntryOriginal::LogEntryOriginal() = default; LogEntryOriginal::LogEntryOriginal(LogEntryOriginal &&other) @@ -1704,6 +1731,15 @@ void Message::paintFromName( } p.setFont(st::msgNameFont); p.setPen(nameFg); + const auto nameLinkHandler = fromLink(); + const auto nameWidth = std::min( + nameText->maxWidth(), + availableWidth); + paintLinkRipple( + p, + nameLinkHandler, + QRect(availableLeft, trect.top(), nameWidth, st::msgNameFont->height), + trect.topLeft()); nameText->draw(p, { .position = { availableLeft, trect.top() }, .availableWidth = availableWidth, @@ -1721,6 +1757,11 @@ void Message::paintFromName( auto via = item->Get(); if (via && !displayForwardedFrom() && availableWidth > 0) { p.setPen(stm->msgServiceFg); + paintLinkRipple( + p, + via->link, + QRect(availableLeft, trect.top(), via->width, st::msgServiceFont->height), + trect.topLeft()); p.drawText(availableLeft, trect.top() + st::msgServiceFont->ascent, via->text); auto skipWidth = via->width + st::msgServiceFont->spacew; availableLeft += skipWidth; @@ -1888,10 +1929,60 @@ void Message::paintForwardedInfo( ? st->boxTextFgGood() : stm->msgServiceFg); p.setFont(serviceFont); - p.setTextPalette(!forwarded->psaType.isEmpty() + const auto &fwdPalette = !forwarded->psaType.isEmpty() ? st->historyPsaForwardPalette() - : stm->fwdTextPalette); - forwarded->text.drawElided(p, trect.x(), trect.y(), useWidth, 2, style::al_left, 0, -1, 0, breakEverywhere); + : stm->fwdTextPalette; + const auto rippleLinkRange = (_linkRipple && _linkRipple->link) + ? forwarded->text.linkRangeFor(_linkRipple->link) + : TextSelection(); + const auto rippleBelongsHere = !rippleLinkRange.empty(); + if (_linkRipple + && _linkRipple->ripple + && _linkRipple->cachedWidth != useWidth + && rippleBelongsHere) { + _linkRipple = nullptr; + } + if (_linkRipple && _linkRipple->ripple && rippleBelongsHere) { + auto color = p.pen().color(); + color.setAlphaF(0.1); + _linkRipple->ripple->paint( + p, + trect.x() + _linkRipple->maskOffset.x(), + trect.y() + _linkRipple->maskOffset.y(), + width(), + &color); + if (_linkRipple->ripple->empty()) { + _linkRipple = nullptr; + } + } + const auto needRippleMask = _linkRipple + && _linkRipple->link + && !_linkRipple->ripple + && rippleBelongsHere; + auto highlightPath = QPainterPath(); + auto highlightRequest = Ui::Text::HighlightInfoRequest{ + .range = rippleLinkRange, + .outPath = &highlightPath, + }; + forwarded->text.draw(p, { + .position = { trect.x(), trect.y() }, + .availableWidth = useWidth, + .palette = &fwdPalette, + .paused = p.inactive(), + .highlight = needRippleMask ? &highlightRequest : nullptr, + .elisionLines = 2, + .elisionBreakEverywhere = breakEverywhere, + }); + if (needRippleMask && !highlightPath.isEmpty()) { + createLinkRippleMask( + highlightPath, + trect.topLeft(), + useWidth, + st::nameRipplePadding, + st::nameRippleRadius); + } else if (needRippleMask) { + _linkRipple = nullptr; + } p.setTextPalette(stm->textPalette); if (!forwarded->psaType.isEmpty()) { @@ -1967,6 +2058,11 @@ void Message::paintViaBotIdInfo( const auto stm = context.messageStyle(); p.setFont(st::msgServiceNameFont); p.setPen(stm->msgServiceFg); + paintLinkRipple( + p, + via->link, + QRect(trect.x(), trect.y(), via->width, st::msgServiceNameFont->height), + trect.topLeft()); p.drawTextLeft(trect.left(), trect.top(), width(), via->text); trect.setY(trect.y() + st::msgServiceNameFont->height); } @@ -1998,6 +2094,40 @@ void Message::paintText( return; } prepareCustomEmojiPaint(p, context, text()); + + const auto rippleLinkRange = (_linkRipple && _linkRipple->link) + ? text().linkRangeFor(_linkRipple->link) + : TextSelection(); + const auto rippleBelongsHere = !rippleLinkRange.empty(); + if (_linkRipple + && _linkRipple->ripple + && _linkRipple->cachedWidth != trect.width() + && rippleBelongsHere) { + _linkRipple = nullptr; + } + if (_linkRipple && _linkRipple->ripple && rippleBelongsHere) { + auto color = stm->textPalette.linkFg->c; + color.setAlphaF(0.1); + _linkRipple->ripple->paint( + p, + trect.x() + _linkRipple->maskOffset.x(), + trect.y() + _linkRipple->maskOffset.y(), + width(), + &color); + if (_linkRipple->ripple->empty()) { + _linkRipple = nullptr; + } + } + const auto needRippleMask = _linkRipple + && _linkRipple->link + && !_linkRipple->ripple + && rippleBelongsHere; + auto ripplePath = QPainterPath(); + auto rippleRequest = Ui::Text::HighlightInfoRequest{ + .range = rippleLinkRange, + .outPath = &ripplePath, + }; + auto highlightRequest = context.computeHighlightCache(); text().draw(p, { .position = trect.topLeft(), @@ -2013,9 +2143,21 @@ void Message::paintText( .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), .selection = context.selection, - .highlight = highlightRequest ? &*highlightRequest : nullptr, + .highlight = needRippleMask + ? &rippleRequest + : (highlightRequest ? &*highlightRequest : nullptr), .useFullWidth = true, }); + if (needRippleMask && !ripplePath.isEmpty()) { + createLinkRippleMask( + ripplePath, + trect.topLeft(), + trect.width(), + st::linkRipplePadding, + st::linkRippleRadius); + } else if (needRippleMask) { + _linkRipple = nullptr; + } } PointState Message::pointState(QPoint point) const { @@ -2108,6 +2250,13 @@ bool Message::displayFromPhoto() const { void Message::clickHandlerPressedChanged( const ClickHandlerPtr &handler, bool pressed) { + const auto startLinkRipple = [&] { + if (!_linkRipple) { + _linkRipple = std::make_unique(); + } + _linkRipple->link = handler; + toggleLinkRipple(pressed); + }; if (const auto markup = data()->Get()) { if (const auto keyboard = markup->inlineKeyboard.get()) { keyboard->clickHandlerPressedChanged( @@ -2162,6 +2311,22 @@ void Message::clickHandlerPressedChanged( } else { _summarize->stopRipple(); } + } else if (displayFromName() && handler == fromLink()) { + startLinkRipple(); + } else if (const auto via = data()->Get() + ; via + && (handler == via->link) + && !displayForwardedFrom()) { + startLinkRipple(); + } else if (const auto forwarded = data()->Get() + ; forwarded + && displayForwardedFrom() + && !forwarded->text.linkRangeFor(handler).empty()) { + startLinkRipple(); + } else if (hasVisibleText() + && IsRippleLink(handler) + && !text().linkRangeFor(handler).empty()) { + startLinkRipple(); } else if (_reactions) { _reactions->clickHandlerPressedChanged( handler, @@ -2344,6 +2509,119 @@ void Message::toggleTopicButtonRipple(bool pressed) { } } +void Message::paintLinkRipple( + Painter &p, + const ClickHandlerPtr &handler, + QRect linkRect, + QPoint textPosition) const { + const auto raw = _linkRipple.get(); + if (!raw || raw->link != handler) { + return; + } + if (const auto ripple = raw->ripple.get()) { + auto color = p.pen().color(); + color.setAlpha(25); + ripple->paint( + p, + textPosition.x() + raw->maskOffset.x(), + textPosition.y() + raw->maskOffset.y(), + width(), + &color); + if (ripple->empty()) { + _linkRipple = nullptr; + } + } else { + createLinkRippleMask( + linkRect, + textPosition, + st::nameRipplePadding, + st::nameRippleRadius); + } +} + +void Message::toggleLinkRipple(bool pressed) { + if (!drawBubble()) { + return; + } else if (pressed) { + repaint(); + } else if (const auto ripple = _linkRipple + ? _linkRipple->ripple.get() + : nullptr) { + ripple->lastStop(); + } +} + +void Message::recordLinkRipplePoint( + QPoint point, + QPoint textOrigin) const { + _linkRippleLastPoint = point - textOrigin; +} + +void Message::createLinkRippleMask( + const QPainterPath &path, + QPoint textPosition, + int useWidth, + style::margins padding, + int radius) const { + auto rects = std::vector(); + for (const auto &polygon : path.toSubpathPolygons()) { + rects.push_back(polygon.boundingRect().toAlignedRect()); + } + auto boundingRect = QRect(); + for (auto &rect : rects) { + rect = rect.marginsAdded(padding); + if (boundingRect.isEmpty()) { + boundingRect = rect; + } else { + boundingRect = boundingRect.united(rect); + } + } + if (boundingRect.isEmpty()) { + return; + } + const auto topLeft = boundingRect.topLeft(); + const auto maskOrigin = topLeft - textPosition; + auto mask = Ui::RippleAnimation::MaskByDrawer( + boundingRect.size(), + false, + [&](QPainter &p) { + for (const auto &rect : rects) { + const auto shifted = rect.translated(-topLeft); + p.drawRoundedRect(shifted, radius, radius); + } + }); + _linkRipple->maskOffset = maskOrigin; + _linkRipple->cachedWidth = useWidth; + _linkRipple->ripple = std::make_unique( + st::defaultRippleAnimation, + std::move(mask), + [=] { repaint(); }); + _linkRipple->ripple->add(_linkRippleLastPoint - maskOrigin); +} + +void Message::createLinkRippleMask( + QRect linkRect, + QPoint textPosition, + style::margins padding, + int radius) const { + auto rect = linkRect.marginsAdded(padding); + const auto maskOrigin = rect.topLeft() - textPosition; + const auto size = rect.size(); + auto mask = Ui::RippleAnimation::MaskByDrawer( + size, + false, + [&](QPainter &p) { + p.drawRoundedRect(QRect(QPoint(), size), radius, radius); + }); + _linkRipple->maskOffset = maskOrigin; + _linkRipple->cachedWidth = 0; + _linkRipple->ripple = std::make_unique( + st::defaultRippleAnimation, + std::move(mask), + [=] { repaint(); }); + _linkRipple->ripple->add(_linkRippleLastPoint - maskOrigin); +} + void Message::createTopicButtonRipple() { const auto geometry = countGeometry().marginsRemoved(st::msgPadding); const auto availableWidth = geometry.width(); @@ -2798,6 +3076,7 @@ bool Message::getStateFromName( && point.x() < availableLeft + availableWidth && point.x() < availableLeft + nameText->maxWidth()) { outResult->link = fromLink(); + recordLinkRipplePoint(point, trect.topLeft()); return true; } auto via = item->Get(); @@ -2807,6 +3086,7 @@ bool Message::getStateFromName( && point.x() < availableLeft + availableWidth && point.x() < availableLeft + nameText->maxWidth() + st::msgServiceFont->spacew + via->width) { outResult->link = via->link; + recordLinkRipplePoint(point, trect.topLeft()); return true; } if (badgeWidth) { @@ -2976,6 +3256,9 @@ bool Message::getStateForwardedInfo( point - trect.topLeft(), useWidth, textRequest)); + if (outResult->link) { + recordLinkRipplePoint(point, trect.topLeft()); + } outResult->symbol = 0; outResult->afterSymbol = false; if (breakEverywhere) { @@ -3092,6 +3375,7 @@ bool Message::getStateViaBotIdInfo( if (!displayFromName() && !displayForwardedFrom()) { if (QRect(trect.x(), trect.y(), via->width, st::msgNameFont->height).contains(point)) { outResult->link = via->link; + recordLinkRipplePoint(point, trect.topLeft()); return true; } trect.setTop(trect.top() + st::msgNameFont->height); @@ -3116,6 +3400,11 @@ bool Message::getStateText( point - trect.topLeft(), trect.width(), request.forText())); + if (outResult->link + && IsRippleLink(outResult->link) + && !text().linkRangeFor(outResult->link).empty()) { + recordLinkRipplePoint(point, trect.topLeft()); + } return true; } return false; diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index 8f69238af7..d14c0edbc1 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -191,6 +191,7 @@ public: private: struct CommentsButton; + struct LinkRipple; struct FromNameStatus; struct RightAction; @@ -219,6 +220,27 @@ private: void toggleTopicButtonRipple(bool pressed); void createTopicButtonRipple(); + void toggleLinkRipple(bool pressed); + void recordLinkRipplePoint( + QPoint point, + QPoint textOrigin) const; + void paintLinkRipple( + Painter &p, + const ClickHandlerPtr &handler, + QRect linkRect, + QPoint textPosition) const; + void createLinkRippleMask( + const QPainterPath &path, + QPoint textPosition, + int useWidth, + style::margins padding, + int radius) const; + void createLinkRippleMask( + QRect linkRect, + QPoint textPosition, + style::margins padding, + int radius) const; + void toggleRightActionRipple(bool pressed); void toggleReplyRipple(bool pressed); @@ -353,6 +375,8 @@ private: mutable ClickHandlerPtr _fastReplyLink; mutable std::unique_ptr _viewButton; std::unique_ptr _topicButton; + mutable std::unique_ptr _linkRipple; + mutable QPoint _linkRippleLastPoint; mutable std::unique_ptr _comments; mutable std::unique_ptr _summarize; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index e3ff85c83f..a691a735a7 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -149,6 +149,10 @@ priceTagTextPalette: TextPalette(defaultTextPalette) { linkFg: creditsBg1; } fwdTextUserpicPadding: margins(0px, 1px, 3px, 0px); +nameRippleRadius: 4px; +nameRipplePadding: margins(3px, 1px, 3px, 1px); +linkRippleRadius: 3px; +linkRipplePadding: margins(3px, 0px, 3px, 0px); fwdTextStyle: TextStyle(semiboldTextStyle) { linkUnderline: kLinkUnderlineNever; } diff --git a/Telegram/lib_ui b/Telegram/lib_ui index ab270986c4..8c2dbeaf7a 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit ab270986c470616f37113f0cfc8944900ef02465 +Subproject commit 8c2dbeaf7a915d24998b6c7be6bd906dce36697c From cf2a44792f1abe4cd67c56cb734dbb133a4701bb Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 10 Mar 2026 22:48:42 +0400 Subject: [PATCH 077/415] Add member tag ripple. --- .../history/view/history_view_message.cpp | 130 +++++++++++++++--- .../history/view/history_view_message.h | 3 + 2 files changed, 112 insertions(+), 21 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index dce702b4af..8cc6624a2b 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -66,6 +66,17 @@ constexpr auto kSummarizeThreshold = 512; constexpr auto kPlayStatusLimit = 2; const auto kPsaTooltipPrefix = "cloud_lng_tooltip_psa_"; +struct SecondRightAction { + std::unique_ptr ripple; + ClickHandlerPtr link; +}; + +struct BadgePillGeometry { + int textWidth = 0; + int width = 0; + int height = 0; +}; + [[nodiscard]] bool IsRippleLink(const ClickHandlerPtr &handler) { switch (handler->getTextEntity().type) { case EntityType::Url: @@ -102,10 +113,22 @@ const auto kPsaTooltipPrefix = "cloud_lng_tooltip_psa_"; }); } -struct SecondRightAction final { - std::unique_ptr ripple; - ClickHandlerPtr link; -}; +[[nodiscard]] BadgePillGeometry ComputeBadgePillGeometry( + not_null badge) { + const auto &padding = st::msgTagBadgePadding; + const auto textWidth = badge->tag.maxWidth(); + const auto contentWidth = padding.left() + + textWidth + + padding.right(); + const auto height = padding.top() + + st::msgFont->height + + padding.bottom(); + return { + .textWidth = textWidth, + .width = std::max(contentWidth, height), + .height = height, + }; +} } // namespace @@ -367,6 +390,7 @@ void Message::refreshRightBadge() { badge->role = role; badge->special = special || (text.isEmpty() && !tagText.empty()); badge->tagLink = nullptr; + badge->ripple = nullptr; if (tagText.empty()) { badge->tag.clear(); } else { @@ -398,9 +422,9 @@ void Message::refreshRightBadge() { badge->width = tagWidth + boostWidth; } else { const auto &padding = st::msgTagBadgePadding; - const auto tagTextWidth = badge->tag.maxWidth(); + const auto textWidth = badge->tag.maxWidth(); const auto contentWidth = padding.left() - + tagTextWidth + + textWidth + padding.right(); const auto pillHeight = padding.top() + st::msgFont->height @@ -1781,40 +1805,64 @@ void Message::paintFromName( if (badge->role != BadgeRole::User) { auto bgColor = badgeColor; bgColor.setAlphaF(0.15); + const auto pill = ComputeBadgePillGeometry(badge); const auto &padding = st::msgTagBadgePadding; - const auto tagTextWidth = badge->tag.maxWidth(); - const auto contentWidth = padding.left() - + tagTextWidth - + padding.right(); - const auto pillHeight = padding.top() - + st::msgFont->height - + padding.bottom(); - const auto pillWidth = std::max(contentWidth, pillHeight); const auto badgeTop = trect.top() - + (st::msgNameFont->height - pillHeight) / 2; + + (st::msgNameFont->height - pill.height) / 2; const auto pillRect = QRect( badgeLeft, badgeTop, - pillWidth, - pillHeight); + pill.width, + pill.height); p.setPen(Qt::NoPen); p.setBrush(bgColor); { auto hq = PainterHighQualityEnabler(p); p.drawRoundedRect( pillRect, - pillHeight / 2., - pillHeight / 2.); + pill.height / 2., + pill.height / 2.); + } + if (badge->ripple) { + auto rippleColor = badgeColor; + rippleColor.setAlphaF(0.1); + badge->ripple->paint( + p, + badgeLeft, + badgeTop, + width(), + &rippleColor); + if (badge->ripple->empty()) { + badge->ripple.reset(); + } } p.setPen(badgeColor); badge->tag.draw(p, { .position = QPoint( - badgeLeft + (pillWidth - tagTextWidth) / 2, + badgeLeft + (pill.width - pill.textWidth) / 2, badgeTop + padding.top()), - .availableWidth = tagTextWidth, + .availableWidth = pill.textWidth, .now = context.now, }); } else if (!badge->tag.isEmpty()) { + if (badge->ripple) { + const auto pill = ComputeBadgePillGeometry(badge); + const auto &padding = st::msgTagBadgePadding; + const auto pillLeft = badgeLeft + - (pill.width - pill.textWidth) / 2; + const auto pillTop = trect.top() - padding.top(); + auto rippleColor = badgeColor; + rippleColor.setAlphaF(0.1); + badge->ripple->paint( + p, + pillLeft, + pillTop, + width(), + &rippleColor); + if (badge->ripple->empty()) { + badge->ripple.reset(); + } + } p.setPen(st::rankUserFg); badge->tag.draw(p, { .position = QPoint(badgeLeft, trect.top()), @@ -2311,6 +2359,9 @@ void Message::clickHandlerPressedChanged( } else { _summarize->stopRipple(); } + } else if (const auto badge = Get() + ; badge && badge->tagLink && handler == badge->tagLink) { + toggleBadgeRipple(pressed); } else if (displayFromName() && handler == fromLink()) { startLinkRipple(); } else if (const auto via = data()->Get() @@ -2374,6 +2425,27 @@ void Message::toggleRightActionRipple(bool pressed) { } } +void Message::toggleBadgeRipple(bool pressed) { + const auto badge = Get(); + if (!badge) { + return; + } else if (pressed) { + if (!badge->ripple) { + const auto pill = ComputeBadgePillGeometry(badge); + auto mask = Ui::RippleAnimation::RoundRectMask( + QSize(pill.width, pill.height), + pill.height / 2); + badge->ripple = std::make_unique( + st::defaultRippleAnimation, + std::move(mask), + [=] { repaint(); }); + } + badge->ripple->add(badge->lastPoint); + } else if (badge->ripple) { + badge->ripple->lastStop(); + } +} + void Message::toggleReplyRipple(bool pressed) { const auto reply = Get(); if (!reply) { @@ -3155,6 +3227,22 @@ bool Message::getStateFromName( } }); } + { + const auto pill = ComputeBadgePillGeometry(badge); + const auto &padding = st::msgTagBadgePadding; + if (badge->role != BadgeRole::User) { + const auto badgeTop = trect.top() + + (st::msgNameFont->height - pill.height) / 2; + badge->lastPoint = point + - QPoint(badgeLeft, badgeTop); + } else { + const auto pillLeft = badgeLeft + - (pill.width - pill.textWidth) / 2; + const auto pillTop = trect.top() - padding.top(); + badge->lastPoint = point + - QPoint(pillLeft, pillTop); + } + } outResult->link = badge->tagLink; return true; } diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index d14c0edbc1..095f70744e 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -78,6 +78,8 @@ struct RightBadge : RuntimeComponent { BadgeRole role = BadgeRole::User; bool overridden = false; bool special = false; + mutable std::unique_ptr ripple; + mutable QPoint lastPoint; }; struct BottomRippleMask { @@ -242,6 +244,7 @@ private: int radius) const; void toggleRightActionRipple(bool pressed); + void toggleBadgeRipple(bool pressed); void toggleReplyRipple(bool pressed); void toggleSummaryHeaderRipple(bool pressed); From 09f9819233960afcc155536019fbf9b7e9f1dba5 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 11 Mar 2026 15:54:08 +0400 Subject: [PATCH 078/415] Fix reorder freeze in chats list. --- .../dialogs/dialogs_inner_widget.cpp | 44 ++++++++++++++--- .../dialogs/dialogs_inner_widget.h | 2 + Telegram/SourceFiles/dialogs/dialogs_list.cpp | 47 +++++++++++++++++-- Telegram/SourceFiles/dialogs/dialogs_list.h | 4 ++ 4 files changed, 87 insertions(+), 10 deletions(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 7c13b16b78..1e7a74c523 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -1681,10 +1681,14 @@ void InnerWidget::mouseMoveEvent(QMouseEvent *e) { } if (_lastMousePosition && *_lastMousePosition != globalPosition) { - if (!_freezeTimer.isActive()) { - _shownList->freeze(); + if (skipChatsListFreeze()) { + unfreezeShownList(true); + } else { + if (!_freezeTimer.isActive()) { + _shownList->freeze(); + } + _freezeTimer.callOnce(kFreezeTimeout); } - _freezeTimer.callOnce(kFreezeTimeout); } if (_pressed && (e->buttons() & Qt::LeftButton)) { @@ -1700,6 +1704,7 @@ void InnerWidget::mouseMoveEvent(QMouseEvent *e) { if (!_qdragging && outside && distanceExceeded) { if (_pressed->history()) { + unfreezeShownList(true); _dragging = _pressed; _qdragging = _pressed; InvokeQueued(this, [=] { performDrag(); }); @@ -1716,6 +1721,19 @@ void InnerWidget::mouseMoveEvent(QMouseEvent *e) { } } +bool InnerWidget::skipChatsListFreeze() const { + return _dragging != nullptr; +} + +void InnerWidget::unfreezeShownList(bool updateIfWasFrozen) { + const auto wasFrozen = _freezeTimer.isActive(); + _freezeTimer.cancel(); + _shownList->unfreeze(); + if (updateIfWasFrozen && wasFrozen) { + update(); + } +} + void InnerWidget::performDrag() { if (!_qdragging || !session().data().chatsFilters().has()) { return; @@ -2278,6 +2296,7 @@ void InnerWidget::checkReorderPinnedStart(QPoint localPosition) { != Dialogs::Ui::QuickDialogAction::Disabled)) { return; } + unfreezeShownList(true); _dragging = _pressed; startReorderPinned(localPosition); } @@ -3061,8 +3080,12 @@ void InnerWidget::updateDialogRow( void InnerWidget::enterEventHook(QEnterEvent *e) { setMouseTracking(true); - _shownList->freeze(); - _freezeTimer.callOnce(kFreezeTimeout); + if (skipChatsListFreeze()) { + unfreezeShownList(false); + } else { + _shownList->freeze(); + _freezeTimer.callOnce(kFreezeTimeout); + } } Row *InnerWidget::shownRowByKey(Key key) { @@ -3153,15 +3176,16 @@ void InnerWidget::refreshShownList() { void InnerWidget::leaveEventHook(QEvent *e) { setMouseTracking(false); - _freezeTimer.cancel(); - _shownList->unfreeze(); + unfreezeShownList(false); clearSelection(); update(); } void InnerWidget::dragLeft() { setMouseTracking(false); + unfreezeShownList(false); clearSelection(); + update(); } FilterId InnerWidget::filterId() const { @@ -3498,6 +3522,7 @@ void InnerWidget::dragPinnedFromTouch() { return; } _dragStart = mapFromGlobal(global); + unfreezeShownList(true); _dragging = _selected; const auto now = mapFromGlobal(_touchDragNowGlobal.value_or(global)); startReorderPinned(now); @@ -3697,6 +3722,7 @@ void InnerWidget::appendToFiltered(Key key) { } InnerWidget::~InnerWidget() { + unfreezeShownList(false); session().data().stories().decrementPreloadingMainSources(); clearSearchResults(); } @@ -3811,6 +3837,10 @@ void InnerWidget::trackResultsHistory(not_null history) { } Data::Thread *InnerWidget::updateFromParentDrag(QPoint globalPosition) { + if (!_freezeTimer.isActive()) { + _shownList->freeze(); + } + _freezeTimer.callOnce(kFreezeTimeout); selectByMouse(globalPosition); const auto fromRow = [](Row *row) { diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index ad6970454d..ee8b0164f5 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -487,6 +487,8 @@ private: void startReorderPinned(QPoint localPosition); int updateReorderIndexGetCount(); bool updateReorderPinned(QPoint localPosition); + [[nodiscard]] bool skipChatsListFreeze() const; + void unfreezeShownList(bool updateIfWasFrozen); void finishReorderPinned(); bool finishReorderOnRelease(); void stopReorderPinned(); diff --git a/Telegram/SourceFiles/dialogs/dialogs_list.cpp b/Telegram/SourceFiles/dialogs/dialogs_list.cpp index 2a215176fe..3d6515baba 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_list.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_list.cpp @@ -85,8 +85,13 @@ void List::adjustByDate(not_null row) { Expects(_sortMode == SortMode::Date); if (_frozen) { - _pendingAdjust.emplace(row); - return; + const auto canAdjustWhileFrozen = _pendingAdjust.empty() + && (row->entry()->fixedOnTopIndex() + || row->entry()->isPinnedDialog(_filterId)); + if (!canAdjustWhileFrozen) { + _pendingAdjust.emplace(row); + return; + } } const auto key = row->sortKey(_filterId); @@ -114,9 +119,45 @@ void List::freeze() { void List::unfreeze() { _frozen = false; - for (const auto &row : base::take(_pendingAdjust)) { + auto pending = base::take(_pendingAdjust); + if (pending.empty()) { + return; + } else if (pending.size() == 1) { + adjustByDate(*pending.begin()); + return; + } + for (const auto &row : pending) { adjustByDate(row); } + if (!sortedByDate()) { + sortByDate(); + } +} + +bool List::sortedByDate() const { + Expects(_sortMode == SortMode::Date); + + for (auto i = 1, count = int(_rows.size()); i != count; ++i) { + if (_rows[i - 1]->sortKey(_filterId) < _rows[i]->sortKey(_filterId)) { + return false; + } + } + return true; +} + +void List::sortByDate() { + Expects(_sortMode == SortMode::Date); + + ranges::stable_sort(_rows, [&](Row *a, Row *b) { + return a->sortKey(_filterId) > b->sortKey(_filterId); + }); + auto top = 0; + for (auto i = 0, count = int(_rows.size()); i != count; ++i) { + const auto row = _rows[i]; + row->_index = i; + row->_top = top; + top += row->height(); + } } bool List::updateHeight(Key key, float64 narrowRatio) { diff --git a/Telegram/SourceFiles/dialogs/dialogs_list.h b/Telegram/SourceFiles/dialogs/dialogs_list.h index e1d9057225..050852a37d 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_list.h +++ b/Telegram/SourceFiles/dialogs/dialogs_list.h @@ -24,6 +24,8 @@ public: ~List() = default; void clear() { + _frozen = false; + _pendingAdjust.clear(); _rows.clear(); _rowByKey.clear(); } @@ -76,6 +78,8 @@ public: private: void adjustByName(not_null row); + [[nodiscard]] bool sortedByDate() const; + void sortByDate(); void rotate( std::vector>::iterator first, std::vector>::iterator middle, From b7d3aa4b0773f3f88ec223b481619657df7e64c8 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 11 Mar 2026 09:43:13 +0400 Subject: [PATCH 079/415] Fix missing QFile::open result check in logs --- Telegram/SourceFiles/logs.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Telegram/SourceFiles/logs.cpp b/Telegram/SourceFiles/logs.cpp index 4e26ddc427..ef8f205fce 100644 --- a/Telegram/SourceFiles/logs.cpp +++ b/Telegram/SourceFiles/logs.cpp @@ -155,19 +155,19 @@ private: files[type]->close(); - const auto reopenStart = [&] { - files[type]->setFileName(startName); - files[type]->open(mode | QIODevice::Append); + const auto reopenStart = [&](const QString &name) { + files[type]->setFileName(name); + return files[type]->open(mode | QIODevice::Append); }; auto source = QFile(startName); if (!source.rename(targetName)) { - reopenStart(); - LOG(("Could not rename '%1' to '%2' to start new logging: %3").arg(startName, targetName, source.errorString())); + if (reopenStart(startName)) { + LOG(("Could not rename '%1' to '%2' to start new logging: %3").arg(startName, targetName, source.errorString())); + } return false; } - files[type]->setFileName(targetName); - if (!files[type]->open(mode | QIODevice::Append)) { + if (!reopenStart(targetName)) { LOG(("Could not open '%1' file to start new logging: %2").arg(targetName, files[type]->errorString())); return false; } From 9672c147e2af6f288426f08bc166d5c51729edad Mon Sep 17 00:00:00 2001 From: linux Date: Sun, 8 Mar 2026 12:46:21 +0530 Subject: [PATCH 080/415] Fix top bar action buttons focus on selection cancel --- .../history/view/history_view_top_bar_widget.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index d1b0b7dac6..ec0035847e 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -1181,10 +1181,12 @@ void TopBarWidget::updateControlsVisibility() { hideChildren(); return; } - _clear->show(); - _delete->setVisible(_canDelete); - _forward->setVisible(_canForward); - _sendNow->setVisible(_canSendNow); + const auto visible = showSelectedState() || _selectedShown.animating(); + _clear->setVisible(visible); + _delete->setVisible(_canDelete && visible); + _forward->setVisible(_canForward && visible); + _sendNow->setVisible(_canSendNow && visible); + const auto isOneColumn = _controller->adaptive().isOneColumn(); const auto backVisible = !rootChatsListBar() @@ -1591,6 +1593,9 @@ bool TopBarWidget::showSelectedActions() const { } void TopBarWidget::slideAnimationCallback() { + if (!_selectedShown.animating() && !_searchShown.animating()) { + updateControlsVisibility(); + } updateControlsGeometry(); update(); } From 37037d07131176b1e2a905370fb6f9ef50ff36a9 Mon Sep 17 00:00:00 2001 From: linux Date: Sun, 8 Mar 2026 23:14:04 +0530 Subject: [PATCH 081/415] Add accessible names to Compose Controls buttons --- .../view/controls/history_view_compose_controls.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 7ad40e3ce1..94c62567c5 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -280,6 +280,7 @@ FieldHeader::FieldHeader( std::make_unique([=] { customEmojiRepaint(); })) , _data(&_show->session().data()) , _cancel(Ui::CreateChild(this, st::historyReplyCancel)) { + _cancel->setAccessibleName(tr::lng_cancel(tr::now)); resize(QSize(parent->width(), st::historyReplyHeight)); init(); } @@ -1997,6 +1998,11 @@ void ComposeControls::show() { } void ComposeControls::init() { + if (_attachToggle) { + _attachToggle->setAccessibleName(tr::lng_attach(tr::now)); + } + _tabbedSelectorToggle->setAccessibleName(tr::lng_emoji_sticker_gif(tr::now)); + initField(); initTabbedSelector(); initSendButton(); @@ -2015,6 +2021,7 @@ void ComposeControls::init() { }, _wrap->lifetime()); if (_botCommandStart) { + _botCommandStart->setAccessibleName(tr::lng_bot_commands_start(tr::now)); _botCommandStart->setClickedCallback([=] { setText({ "/" }); }); } @@ -3803,6 +3810,9 @@ bool ComposeControls::updateReplaceMediaButton() { _replaceMedia = std::make_unique( _wrap.get(), _canReplaceMedia ? st::historyReplaceMedia : st::historyAddMedia); + _replaceMedia->setAccessibleName(_canReplaceMedia + ? tr::lng_attach_replace(tr::now) + : tr::lng_attach(tr::now)); const auto hideDuration = st::historyReplaceMedia.ripple.hideDuration; _replaceMedia->setClickedCallback([=] { base::call_delayed(hideDuration, _wrap.get(), [=] { From 5521ff621aee7e49a351c0411b9aca3263e5d715 Mon Sep 17 00:00:00 2001 From: linux Date: Tue, 10 Mar 2026 21:07:42 +0530 Subject: [PATCH 082/415] Add accessible names to pinned and contact status bar buttons. --- Telegram/SourceFiles/history/history_widget.cpp | 3 +++ .../SourceFiles/history/view/history_view_chat_section.cpp | 3 +++ .../SourceFiles/history/view/history_view_contact_status.cpp | 3 +++ 3 files changed, 9 insertions(+) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 111f41e807..62b0c43530 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -8220,6 +8220,9 @@ void HistoryWidget::refreshPinnedBarButton(bool many, HistoryItem *item) { auto button = object_ptr( this, close ? st::historyReplyCancel : st::historyPinnedShowAll); + button->setAccessibleName(close + ? tr::lng_cancel(tr::now) + : tr::lng_settings_events_pinned(tr::now)); button->clicks( ) | rpl::on_next([=] { if (close) { diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index de0276d577..1d0d8ee759 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -2145,6 +2145,9 @@ void ChatWidget::refreshPinnedBarButton(bool many, HistoryItem *item) { auto button = object_ptr( this, close ? st::historyReplyCancel : st::historyPinnedShowAll); + button->setAccessibleName(close + ? tr::lng_cancel(tr::now) + : tr::lng_settings_events_pinned(tr::now)); button->clicks( ) | rpl::on_next([=] { if (close) { diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp index a9fccef31b..fc609e005d 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp @@ -261,6 +261,9 @@ ContactStatus::Bar::Bar( st::historyContactStatusMinSkip, st::topBarArrowPadding.top())) , _emojiStatusShadow(this) { + _close->setAccessibleName(tr::lng_cancel(tr::now)); + _unarchiveIcon->setAccessibleName(tr::lng_new_contact_unarchive(tr::now)); + _reportIcon->setAccessibleName(tr::lng_report_spam(tr::now)); _requestChatInfo->setAttribute(Qt::WA_TransparentForMouseEvents); _emojiStatusInfo->paintRequest( ) | rpl::on_next([=, raw = _emojiStatusInfo.data()](QRect clip) { From b1a537801c3f8f547e75b7162d186cccf8ba8398 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 12 Mar 2026 12:59:39 +0400 Subject: [PATCH 083/415] Update lib_ui. --- Telegram/lib_ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 8c2dbeaf7a..f6af49a763 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 8c2dbeaf7a915d24998b6c7be6bd906dce36697c +Subproject commit f6af49a763c8254606c8ff0cddd31d551228a100 From f4a2be34eaecc882909811893f0cacd10ab85f61 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 4 Mar 2026 14:10:20 +0300 Subject: [PATCH 084/415] Clamped experimental sticker size override to a safe scaled range. --- .../history/view/media/history_view_sticker.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp index 109cf7ce9b..a5871d4157 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp @@ -198,9 +198,14 @@ bool Sticker::readyToDrawAnimationFrame() { } QSize Sticker::Size() { - const auto side = OptionStickerSize.value() > 0 - ? style::ConvertScale(OptionStickerSize.value()) - : std::min(st::maxStickerSize, kMaxSizeFixed); + const auto side = std::min(st::maxStickerSize, kMaxSizeFixed); + if (OptionStickerSize.value() > 0) [[unlikely]] { + const auto scaled = std::clamp( + OptionStickerSize.value(), + style::ConvertScale(50), + side); + return { scaled, scaled }; + } return { side, side }; } From ff3dc8987d7346b50bb4ef200a9a333bb1f7db8e Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 6 Mar 2026 08:43:45 +0300 Subject: [PATCH 085/415] Implemented Opus audio trimming and concatenation with waveform. --- Telegram/CMakeLists.txt | 2 + .../media/audio/media_audio_edit.cpp | 407 ++++++++++++++++++ .../media/audio/media_audio_edit.h | 29 ++ 3 files changed, 438 insertions(+) create mode 100644 Telegram/SourceFiles/media/audio/media_audio_edit.cpp create mode 100644 Telegram/SourceFiles/media/audio/media_audio_edit.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index f5fb7d93e4..f484089579 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1264,6 +1264,8 @@ PRIVATE main/session/session_show.h media/audio/media_audio.cpp media/audio/media_audio.h + media/audio/media_audio_edit.cpp + media/audio/media_audio_edit.h media/audio/media_audio_capture.cpp media/audio/media_audio_capture.h media/audio/media_audio_capture_common.h diff --git a/Telegram/SourceFiles/media/audio/media_audio_edit.cpp b/Telegram/SourceFiles/media/audio/media_audio_edit.cpp new file mode 100644 index 0000000000..744d4dce4a --- /dev/null +++ b/Telegram/SourceFiles/media/audio/media_audio_edit.cpp @@ -0,0 +1,407 @@ +/* +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 "media/audio/media_audio_edit.h" + +#include "ffmpeg/ffmpeg_bytes_io_wrap.h" +#include "ffmpeg/ffmpeg_utility.h" + +namespace Media { + +[[nodiscard]] AudioEditResult TrimAudioToRange( + const QByteArray &content, + crl::time from, + crl::time till) { + using namespace FFmpeg; + + if (content.isEmpty() || (from < 0) || (till <= from)) { + return {}; + } + + auto inputWrap = ReadBytesWrap{ + .size = content.size(), + .data = reinterpret_cast(content.constData()), + }; + auto input = MakeFormatPointer( + &inputWrap, + &ReadBytesWrap::Read, + nullptr, + &ReadBytesWrap::Seek); + if (!input) { + return {}; + } + + auto error = AvErrorWrap(avformat_find_stream_info(input.get(), nullptr)); + if (error) { + LogError(u"avformat_find_stream_info"_q, error); + return {}; + } + + const auto streamId = av_find_best_stream( + input.get(), + AVMEDIA_TYPE_AUDIO, + -1, + -1, + nullptr, + 0); + if (streamId < 0) { + LogError(u"av_find_best_stream"_q, AvErrorWrap(streamId)); + return {}; + } + + const auto inStream = input->streams[streamId]; + auto outputWrap = WriteBytesWrap(); + auto output = MakeWriteFormatPointer( + static_cast(&outputWrap), + nullptr, + &WriteBytesWrap::Write, + &WriteBytesWrap::Seek, + "opus"_q); + if (!output) { + return {}; + } + + const auto outStream = avformat_new_stream(output.get(), nullptr); + if (!outStream) { + LogError(u"avformat_new_stream"_q); + return {}; + } + + error = AvErrorWrap(avcodec_parameters_copy( + outStream->codecpar, + inStream->codecpar)); + if (error) { + LogError(u"avcodec_parameters_copy"_q, error); + return {}; + } + outStream->codecpar->codec_tag = 0; + outStream->time_base = inStream->time_base; + + error = AvErrorWrap(avformat_write_header(output.get(), nullptr)); + if (error) { + LogError(u"avformat_write_header"_q, error); + return {}; + } + + const auto fromPts = TimeToPts(from, inStream->time_base); + const auto tillPts = TimeToPts(till, inStream->time_base); + auto firstPts = int64(AV_NOPTS_VALUE); + auto firstDts = int64(AV_NOPTS_VALUE); + auto lastPts = std::numeric_limits::min(); + auto lastDts = std::numeric_limits::min(); + auto durationPts = int64(0); + auto copied = 0; + + auto packet = AVPacket(); + av_init_packet(&packet); + while (true) { + error = AvErrorWrap(av_read_frame(input.get(), &packet)); + if (error.code() == AVERROR_EOF) { + break; + } else if (error) { + LogError(u"av_read_frame"_q, error); + return {}; + } + const auto guard = gsl::finally([&] { + av_packet_unref(&packet); + }); + if (packet.stream_index != streamId) { + continue; + } + + const auto packetStart = (packet.pts != AV_NOPTS_VALUE) + ? packet.pts + : packet.dts; + const auto packetDuration = std::max(packet.duration, int64(0)); + const auto packetEnd = (packetStart != AV_NOPTS_VALUE) + ? (packetStart + packetDuration) + : AV_NOPTS_VALUE; + if ((packetStart != AV_NOPTS_VALUE) && (packetStart >= tillPts)) { + break; + } + if ((packetEnd != AV_NOPTS_VALUE) && (packetEnd <= fromPts)) { + continue; + } + + if (packet.pts != AV_NOPTS_VALUE) { + if (firstPts == AV_NOPTS_VALUE) { + firstPts = packet.pts; + } + packet.pts -= firstPts; + if (packet.pts < 0) { + packet.pts = 0; + } + if (packet.pts <= lastPts) { + packet.pts = lastPts + 1; + } + lastPts = packet.pts; + } + if (packet.dts != AV_NOPTS_VALUE) { + if (firstDts == AV_NOPTS_VALUE) { + firstDts = packet.dts; + } + packet.dts -= firstDts; + if (packet.dts < 0) { + packet.dts = 0; + } + if (packet.dts <= lastDts) { + packet.dts = lastDts + 1; + } + lastDts = packet.dts; + } + + const auto packetPosition = (packet.pts != AV_NOPTS_VALUE) + ? packet.pts + : packet.dts; + if (packetPosition != AV_NOPTS_VALUE) { + durationPts = std::max( + durationPts, + packetPosition + std::max(packet.duration, int64(0))); + } + + packet.stream_index = outStream->index; + error = AvErrorWrap(av_interleaved_write_frame(output.get(), &packet)); + if (error) { + LogError(u"av_interleaved_write_frame"_q, error); + return {}; + } + ++copied; + } + + if (!copied) { + return {}; + } + + error = AvErrorWrap(av_write_trailer(output.get())); + if (error) { + LogError(u"av_write_trailer"_q, error); + return {}; + } + + auto result = AudioEditResult(); + result.content = std::move(outputWrap.content); + result.waveform = audioCountWaveform(Core::FileLocation(), result.content); + result.duration = durationPts + ? PtsToTimeCeil(durationPts, outStream->time_base) + : (till - from); + return result; +} + +[[nodiscard]] AudioEditResult ConcatAudio( + const QByteArray &first, + const QByteArray &second) { + using namespace FFmpeg; + + if (first.isEmpty() || second.isEmpty()) { + return {}; + } + + auto firstWrap = ReadBytesWrap{ + .size = first.size(), + .data = reinterpret_cast(first.constData()), + }; + auto firstInput = MakeFormatPointer( + &firstWrap, + &ReadBytesWrap::Read, + nullptr, + &ReadBytesWrap::Seek); + if (!firstInput) { + return {}; + } + + auto secondWrap = ReadBytesWrap{ + .size = second.size(), + .data = reinterpret_cast(second.constData()), + }; + auto secondInput = MakeFormatPointer( + &secondWrap, + &ReadBytesWrap::Read, + nullptr, + &ReadBytesWrap::Seek); + if (!secondInput) { + return {}; + } + + const auto prepareStream = [](not_null input) { + auto error = AvErrorWrap(avformat_find_stream_info(input, nullptr)); + if (error) { + LogError(u"avformat_find_stream_info"_q, error); + return static_cast(nullptr); + } + const auto streamId = av_find_best_stream( + input, + AVMEDIA_TYPE_AUDIO, + -1, + -1, + nullptr, + 0); + if (streamId < 0) { + LogError(u"av_find_best_stream"_q, AvErrorWrap(streamId)); + return static_cast(nullptr); + } + return input->streams[streamId]; + }; + const auto firstStream = prepareStream(firstInput.get()); + if (!firstStream) { + return {}; + } + const auto secondStream = prepareStream(secondInput.get()); + if (!secondStream) { + return {}; + } + + auto outputWrap = WriteBytesWrap(); + auto output = MakeWriteFormatPointer( + static_cast(&outputWrap), + nullptr, + &WriteBytesWrap::Write, + &WriteBytesWrap::Seek, + "opus"_q); + if (!output) { + return {}; + } + const auto outStream = avformat_new_stream(output.get(), nullptr); + if (!outStream) { + LogError(u"avformat_new_stream"_q); + return {}; + } + + auto error = AvErrorWrap(avcodec_parameters_copy( + outStream->codecpar, + firstStream->codecpar)); + if (error) { + LogError(u"avcodec_parameters_copy"_q, error); + return {}; + } + outStream->codecpar->codec_tag = 0; + outStream->time_base = firstStream->time_base; + + error = AvErrorWrap(avformat_write_header(output.get(), nullptr)); + if (error) { + LogError(u"avformat_write_header"_q, error); + return {}; + } + + auto offsetPts = int64(0); + auto durationPts = int64(0); + auto lastPts = std::numeric_limits::min(); + auto lastDts = std::numeric_limits::min(); + auto copied = 0; + + const auto append = [&]( + not_null input, + not_null inStream) { + auto firstPts = int64(AV_NOPTS_VALUE); + auto firstDts = int64(AV_NOPTS_VALUE); + auto sourceEndPts = offsetPts; + + auto packet = AVPacket(); + av_init_packet(&packet); + while (true) { + auto error = AvErrorWrap(av_read_frame(input, &packet)); + if (error.code() == AVERROR_EOF) { + break; + } else if (error) { + LogError(u"av_read_frame"_q, error); + return false; + } + const auto guard = gsl::finally([&] { + av_packet_unref(&packet); + }); + if (packet.stream_index != inStream->index) { + continue; + } + + if (packet.pts != AV_NOPTS_VALUE) { + if (firstPts == AV_NOPTS_VALUE) { + firstPts = packet.pts; + } + packet.pts -= firstPts; + if (packet.pts < 0) { + packet.pts = 0; + } + packet.pts = av_rescale_q( + packet.pts, + inStream->time_base, + outStream->time_base); + packet.pts += offsetPts; + if (packet.pts <= lastPts) { + packet.pts = lastPts + 1; + } + lastPts = packet.pts; + } + if (packet.dts != AV_NOPTS_VALUE) { + if (firstDts == AV_NOPTS_VALUE) { + firstDts = packet.dts; + } + packet.dts -= firstDts; + if (packet.dts < 0) { + packet.dts = 0; + } + packet.dts = av_rescale_q( + packet.dts, + inStream->time_base, + outStream->time_base); + packet.dts += offsetPts; + if (packet.dts <= lastDts) { + packet.dts = lastDts + 1; + } + lastDts = packet.dts; + } + packet.duration = (packet.duration > 0) + ? av_rescale_q( + packet.duration, + inStream->time_base, + outStream->time_base) + : 0; + + const auto packetPosition = (packet.pts != AV_NOPTS_VALUE) + ? packet.pts + : packet.dts; + if (packetPosition != AV_NOPTS_VALUE) { + const auto packetEnd = packetPosition + + std::max(packet.duration, int64(0)); + durationPts = std::max(durationPts, packetEnd); + sourceEndPts = std::max(sourceEndPts, packetEnd); + } + + packet.stream_index = outStream->index; + error = AvErrorWrap(av_interleaved_write_frame(output.get(), &packet)); + if (error) { + LogError(u"av_interleaved_write_frame"_q, error); + return false; + } + ++copied; + } + + offsetPts = sourceEndPts; + return true; + }; + if (!append(firstInput.get(), firstStream) + || !append(secondInput.get(), secondStream)) { + return {}; + } + if (!copied) { + return {}; + } + + error = AvErrorWrap(av_write_trailer(output.get())); + if (error) { + LogError(u"av_write_trailer"_q, error); + return {}; + } + + auto result = AudioEditResult(); + result.content = std::move(outputWrap.content); + result.waveform = audioCountWaveform(Core::FileLocation(), result.content); + result.duration = durationPts + ? PtsToTimeCeil(durationPts, outStream->time_base) + : 0; + return result; +} + +} // namespace Media diff --git a/Telegram/SourceFiles/media/audio/media_audio_edit.h b/Telegram/SourceFiles/media/audio/media_audio_edit.h new file mode 100644 index 0000000000..6d360ffde1 --- /dev/null +++ b/Telegram/SourceFiles/media/audio/media_audio_edit.h @@ -0,0 +1,29 @@ +/* +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 "media/audio/media_audio.h" + +namespace Media { + +struct AudioEditResult { + QByteArray content; + VoiceWaveform waveform; + crl::time duration = 0; +}; + +[[nodiscard]] AudioEditResult TrimAudioToRange( + const QByteArray &content, + crl::time from, + crl::time till); + +[[nodiscard]] AudioEditResult ConcatAudio( + const QByteArray &first, + const QByteArray &second); + +} // namespace Media From 5e441700917c0c20a726eeab91514950926574da Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 5 Mar 2026 09:16:01 +0300 Subject: [PATCH 086/415] Added initial ability to trim recorded voice messages. --- .../chat_helpers/chat_helpers.style | 13 + .../history_view_voice_record_bar.cpp | 1078 +++++++++++++++-- .../controls/history_view_voice_record_bar.h | 13 + 3 files changed, 987 insertions(+), 117 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 84d69c305d..42f1fd14ed 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1345,8 +1345,21 @@ historyRecordDelete: IconButton(historyAttach) { } historyRecordWaveformRightSkip: 10px; historyRecordWaveformBgMargins: margins(5px, 8px, 5px, 9px); +historyRecordWaveformBgRadius: 7px; +historyRecordWaveformOutsideAlpha: 0.6; +historyRecordWaveformInactiveAlpha: 0.7; +historyRecordCenterControlHeight: 18px; +historyRecordCenterControlIconScale: 0.6; +historyRecordCenterControlPadding: 4px; +historyRecordCenterControlTextSkip: 2px; +historyRecordCenterControlMinimumProgressPadding: 5px; historyRecordWaveformBar: 3px; +historyRecordTrimFrameRadius: 5px; +historyRecordTrimFrameBorder: 1px; +historyRecordTrimHandleWidth: 10px; +historyRecordTrimHandleInnerSize: size(2px, 8px); +historyRecordTrimHandleInnerSkip: 2px; historyRecordLockPosition: point(1px, 22px); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp index f302398f8c..65dfe29a28 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp @@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwidget.h" // MainWidget::stopAndClosePlayer #include "mainwindow.h" #include "media/audio/media_audio.h" +#include "media/audio/media_audio_edit.h" #include "media/audio/media_audio_capture.h" #include "media/player/media_player_button.h" #include "media/player/media_player_instance.h" @@ -62,13 +63,12 @@ constexpr auto kMaxSamples constexpr auto kMinSamples = ::Media::Player::kDefaultFrequency / 5; // 0.2 seconds -constexpr auto kInactiveWaveformBarAlpha = int(255 * 0.6); - constexpr auto kPrecision = 10; constexpr auto kLockArcAngle = 15.; constexpr auto kHideWaveformBgOffset = 50; +constexpr auto kTrimPlaybackEpsilon = 0.0001; enum class FilterType { Continue, @@ -143,6 +143,16 @@ void SoundedPreview::subscribeToUpdates(Fn callback) { return durationString + QLocale().decimalPoint() + decimalPart; } +[[nodiscard]] QString FormatTrimDuration(crl::time duration) { + auto result = Ui::FormatDurationText(duration / 1000); + if ((result.size() == 5) + && (result[0] == QChar('0')) + && (result[2] == QChar(':'))) { + result.remove(0, 1); + } + return result; +} + [[nodiscard]] std::unique_ptr ProcessCaptureResult( const VoiceWaveform &waveform) { auto voiceData = std::make_unique(); @@ -170,6 +180,137 @@ void SoundedPreview::subscribeToUpdates(Fn callback) { int32(0)); } +[[nodiscard]] VoiceWaveform ResampleWaveformToRange( + const VoiceWaveform &source, + float64 left, + float64 right) { + if (source.isEmpty() || (source[0] < 0)) { + return {}; + } + const auto size = int(source.size()); + if (size <= 0) { + return {}; + } + left = std::clamp(left, 0., 1.); + right = std::clamp(right, left, 1.); + if ((right - left) <= 0.) { + return {}; + } + const auto begin = left * size; + const auto end = right * size; + const auto range = end - begin; + if (range <= 0.) { + return {}; + } + auto result = VoiceWaveform(size, 0); + for (auto i = 0; i != size; ++i) { + const auto segmentStart = begin + (range * i) / size; + const auto segmentEnd = begin + (range * (i + 1)) / size; + const auto from = std::clamp( + int(std::floor(segmentStart)), + 0, + size - 1); + const auto till = std::clamp( + int(std::ceil(segmentEnd)) - 1, + from, + size - 1); + auto peak = uchar(0); + for (auto j = from; j <= till; ++j) { + peak = std::max(peak, uchar(source[j])); + } + result[i] = char(peak); + } + return result; +} + +[[nodiscard]] VoiceWaveform ResampleWaveformToSize( + const VoiceWaveform &source, + int targetSize) { + if (source.isEmpty() || (source[0] < 0) || (targetSize <= 0)) { + return {}; + } + const auto sourceSize = int(source.size()); + if (sourceSize <= 0) { + return {}; + } + if (sourceSize == targetSize) { + return source; + } + auto result = VoiceWaveform(targetSize, 0); + for (auto i = 0; i != targetSize; ++i) { + const auto segmentStart = (float64(sourceSize) * i) / targetSize; + const auto segmentEnd = (float64(sourceSize) * (i + 1)) / targetSize; + const auto from = std::clamp( + int(std::floor(segmentStart)), + 0, + sourceSize - 1); + const auto till = std::clamp( + int(std::ceil(segmentEnd)) - 1, + from, + sourceSize - 1); + auto peak = uchar(0); + for (auto j = from; j <= till; ++j) { + peak = std::max(peak, uchar(source[j])); + } + result[i] = char(peak); + } + return result; +} + +[[nodiscard]] VoiceWaveform MergeWaveformsByDuration( + const VoiceWaveform &first, + crl::time firstDuration, + const VoiceWaveform &second, + crl::time secondDuration) { + const auto totalDuration = firstDuration + secondDuration; + if (totalDuration <= 0) { + return {}; + } + const auto targetSize = int(::Media::Player::kWaveformSamplesCount); + if (targetSize <= 0) { + return {}; + } + auto firstSize = int( + ((firstDuration * targetSize) + (totalDuration / 2)) + / totalDuration); + if ((firstDuration > 0) && (secondDuration > 0)) { + firstSize = std::clamp(firstSize, 1, targetSize - 1); + } else { + firstSize = std::clamp(firstSize, 0, targetSize); + } + const auto secondSize = targetSize - firstSize; + auto result = VoiceWaveform(); + result.reserve(targetSize); + if (firstSize > 0) { + const auto part = ResampleWaveformToSize(first, firstSize); + if (part.isEmpty()) { + return {}; + } + for (const auto value : part) { + result.push_back(value); + } + } + if (secondSize > 0) { + const auto part = ResampleWaveformToSize(second, secondSize); + if (part.isEmpty()) { + return {}; + } + for (const auto value : part) { + result.push_back(value); + } + } + return (int(result.size()) == targetSize) ? result : VoiceWaveform(); +} + +[[nodiscard]] Ui::RoundVideoResult ToRoundVideoResult( + ::Media::Capture::Result &&data) { + return Ui::RoundVideoResult{ + .content = std::move(data.bytes), + .waveform = std::move(data.waveform), + .duration = data.duration, + }; +} + void PaintWaveform( QPainter &p, not_null voiceData, @@ -528,10 +669,13 @@ public: std::shared_ptr send, not_null session, not_null data, + bool allowTrim, const style::font &font); void requestPaintProgress(float64 progress); + void prepareForSendAnimation(); [[nodiscard]] rpl::producer<> stopRequests() const; + void applyTrimBeforeSend(); void playPause(); [[nodiscard]] std::shared_ptr videoPreview(); @@ -539,13 +683,42 @@ public: [[nodiscard]] rpl::lifetime &lifetime(); private: + struct TrimGeometry { + QRect frame; + QRect leftHandle; + QRect rightHandle; + }; + + struct TrimRange { + crl::time from = 0; + crl::time till = 0; + }; + + struct TrimBoundaries { + float64 left = 0.; + float64 right = 1.; + }; + void init(); void initPlayButton(); void initPlayProgress(); + void applyTrimSelection(bool resetSelection); + void updateControlGeometry(); + void updateTrimGeometry(); + [[nodiscard]] TrimGeometry computeTrimGeometry( + const QRect &trimRect) const; + void updateDurationText(); [[nodiscard]] bool isInPlayer( const ::Media::Player::TrackState &state) const; [[nodiscard]] bool isInPlayer() const; + [[nodiscard]] bool canTrim() const; + [[nodiscard]] float64 trimProgressFromPosition(int x) const; + [[nodiscard]] float64 minimumTrimProgress() const; + [[nodiscard]] float64 minimumControlTrimProgress() const; + [[nodiscard]] crl::time selectedDuration() const; + [[nodiscard]] std::optional selectedTrimRange() const; + [[nodiscard]] TrimBoundaries selectedTrimBoundaries() const; [[nodiscard]] int computeTopMargin(int height) const; [[nodiscard]] QRect computeWaveformRect(const QRect ¢erRect) const; @@ -559,10 +732,11 @@ private: const std::unique_ptr _voiceData; const std::shared_ptr _mediaView; const not_null _data; + const bool _allowTrim = false; const base::unique_qptr _delete; const style::font &_durationFont; - const QString _duration; - const int _durationWidth; + QString _duration; + int _durationWidth = 0; const style::MediaPlayerButton &_playPauseSt; const base::unique_qptr _playPauseButton; const QColor _activeWaveformBar; @@ -573,6 +747,13 @@ private: QRect _waveformBgRect; QRect _waveformBgFinalCenterRect; QRect _waveformFgRect; + QRect _controlRect; + bool _controlHasDuration = true; + QRect _trimFrameRect; + QRect _trimLeftHandleRect; + QRect _trimRightHandleRect; + float64 _trimLeftProgress = 0.; + float64 _trimRightProgress = 1.; ::Media::Player::PlayButtonLayout _playPause; @@ -580,6 +761,9 @@ private: rpl::variable _showProgress = 0.; rpl::event_stream<> _videoRepaints; + QImage _sendAnimationCache; + bool _useSendAnimationCache = false; + bool _playPauseHiddenForSendAnimation = false; rpl::lifetime _lifetime; @@ -591,6 +775,7 @@ ListenWrap::ListenWrap( std::shared_ptr send, not_null session, not_null data, + bool allowTrim, const style::font &font) : _parent(parent) , _st(st) @@ -600,15 +785,18 @@ ListenWrap::ListenWrap( , _voiceData(ProcessCaptureResult(data->waveform)) , _mediaView(_document->createMediaView()) , _data(data) +, _allowTrim(allowTrim) , _delete(base::make_unique_q(parent, _st.remove)) , _durationFont(font) -, _duration(Ui::FormatDurationText(_data->duration / 1000)) +, _duration(FormatTrimDuration(_data->duration)) , _durationWidth(_durationFont->width(_duration)) , _playPauseSt(st::mediaPlayerButton) , _playPauseButton(base::make_unique_q(parent)) , _activeWaveformBar(st::historyRecordVoiceFgActiveIcon->c) , _inactiveWaveformBar( - anim::with_alpha(_activeWaveformBar, kInactiveWaveformBarAlpha)) + anim::with_alpha( + _activeWaveformBar, + st::historyRecordWaveformInactiveAlpha)) , _playPause(_playPauseSt, [=] { _playPauseButton->update(); }) { _delete->setAccessibleName(tr::lng_record_lock_delete(tr::now)); init(); @@ -628,20 +816,14 @@ void ListenWrap::init() { _waveformBgRect = QRect({ 0, 0 }, size) .marginsRemoved(st::historyRecordWaveformBgMargins); { - const auto skip = _waveformBgRect.height() / 2; - const auto left = _st.remove.width + skip; - const auto right = send + skip; + const auto left = _st.remove.width; + const auto right = send; _waveformBgFinalCenterRect = _waveformBgRect.marginsRemoved( style::margins(left, 0, right, 0)); } - { - const auto &play = _playPauseSt.playOuter; - const auto &final = _waveformBgFinalCenterRect; - _playPauseButton->moveToLeft( - final.x() - (final.height() - play.width()) / 2, - final.y()); - } _waveformFgRect = computeWaveformRect(_waveformBgFinalCenterRect); + updateTrimGeometry(); + updateControlGeometry(); }, _lifetime); _parent->paintRequest( @@ -649,9 +831,11 @@ void ListenWrap::init() { auto p = QPainter(_parent); auto hq = PainterHighQualityEnabler(p); const auto progress = _showProgress.current(); + const auto useSendAnimationCache = _useSendAnimationCache + && !_sendAnimationCache.isNull(); p.setOpacity(progress); const auto &remove = _st.remove; - if (progress > 0. && progress < 1.) { + if (progress > 0. && progress < 1. && !useSendAnimationCache) { remove.icon.paint(p, remove.iconPosition, _parent->width()); } @@ -674,16 +858,26 @@ void ListenWrap::init() { bgRectRight + hideOffset, 0); const auto bgRect = _waveformBgRect.marginsRemoved(bgRectMargins); - - const auto horizontalMargin = bgRect.width() - bgRect.height(); - const auto bgLeftCircleRect = bgRect.marginsRemoved( - style::margins(0, 0, horizontalMargin, 0)); - const auto bgRightCircleRect = bgRect.marginsRemoved( - style::margins(horizontalMargin, 0, 0, 0)); - - const auto halfHeight = bgRect.height() / 2; + const auto radius = st::historyRecordWaveformBgRadius; const auto bgCenterRect = bgRect.marginsRemoved( - style::margins(halfHeight, 0, halfHeight, 0)); + style::margins(radius, 0, radius, 0)); + if (useSendAnimationCache) { + p.save(); + p.setClipRect(bgRect); + p.drawImage(bgRect.topLeft(), _sendAnimationCache); + p.restore(); + return; + } + const auto trimGeometry = (progress == 1.) + ? TrimGeometry{ + .frame = _trimFrameRect, + .leftHandle = _trimLeftHandleRect, + .rightHandle = _trimRightHandleRect, + } + : computeTrimGeometry(bgCenterRect); + const auto &trimFrameRect = trimGeometry.frame; + const auto &trimLeftHandleRect = trimGeometry.leftHandle; + const auto &trimRightHandleRect = trimGeometry.rightHandle; if (!_isShowAnimation) { p.setOpacity(progress); @@ -691,27 +885,40 @@ void ListenWrap::init() { p.fillRect(bgRect, _st.bg); } p.setPen(Qt::NoPen); - p.setBrush(_st.cancelActive); - auto path = QPainterPath(); - path.setFillRule(Qt::WindingFill); - path.addEllipse(bgLeftCircleRect); - path.addEllipse(bgRightCircleRect); - path.addRect(bgCenterRect); - p.drawPath(path); - - // Duration paint. - { - p.setFont(_durationFont); - p.setPen(st::historyRecordVoiceFgActiveIcon); - - const auto top = computeTopMargin(_durationFont->ascent); - const auto rect = bgCenterRect.marginsRemoved( - style::margins( - bgCenterRect.width() - _durationWidth, - top, - 0, - top)); - p.drawText(rect, style::al_left, _duration); + p.setBrush(anim::with_alpha( + _st.cancelActive->c, + st::historyRecordWaveformOutsideAlpha)); + p.drawRoundedRect(bgRect, radius, radius); + if (canTrim() && !trimFrameRect.isEmpty()) { + const auto activeBgRect = trimFrameRect.intersected(bgRect); + auto clipPath = QPainterPath(); + clipPath.addRoundedRect(bgRect, radius, radius); + p.save(); + p.setClipPath(clipPath); + if (activeBgRect.isEmpty()) { + p.fillRect(bgRect, _st.cancelActive); + } else { + if (activeBgRect.x() > bgRect.x()) { + p.fillRect( + QRect( + bgRect.x(), + bgRect.y(), + activeBgRect.x() - bgRect.x(), + bgRect.height()), + _st.cancelActive); + } + if (rect::right(activeBgRect) < rect::right(bgRect)) { + p.fillRect( + QRect( + rect::right(activeBgRect) + 1, + bgRect.y(), + rect::right(bgRect) + - rect::right(activeBgRect), + bgRect.height()), + _st.cancelActive); + } + } + p.restore(); } // Waveform paint. @@ -720,22 +927,138 @@ void ListenWrap::init() { : computeWaveformRect(bgCenterRect); if (!waveformRect.isEmpty()) { const auto playProgress = _playProgress.current(); - if (_data->minithumbs.isNull()) { - p.translate(waveformRect.topLeft()); - PaintWaveform( - p, - _voiceData.get(), - waveformRect.width(), + const auto paintWaveform = [&]( + QRect rect, + float64 opacity, + const QColor &activeBar, + const QColor &inactiveBar) { + if (rect.isEmpty() || (opacity <= 0.)) { + return; + } + p.save(); + p.setClipRect(rect); + p.setOpacity(p.opacity() * opacity); + if (_data->minithumbs.isNull()) { + p.translate(waveformRect.topLeft()); + PaintWaveform( + p, + _voiceData.get(), + waveformRect.width(), + activeBar, + inactiveBar, + playProgress); + } else { + FillWithMinithumbs( + p, + _data, + waveformRect, + playProgress); + } + p.restore(); + }; + const auto activeWaveformRect = trimFrameRect.intersected( + waveformRect); + if (canTrim() && !activeWaveformRect.isEmpty()) { + const auto outsideOpacity = std::clamp( + st::historyRecordWaveformOutsideAlpha, + 0., + 1.); + paintWaveform( + activeWaveformRect, + 1., _activeWaveformBar, + _inactiveWaveformBar); + paintWaveform( + QRect( + waveformRect.x(), + waveformRect.y(), + std::max( + 0, + activeWaveformRect.x() - waveformRect.x()), + waveformRect.height()), + outsideOpacity, _inactiveWaveformBar, - playProgress); - p.resetTransform(); + _inactiveWaveformBar); + paintWaveform( + QRect( + rect::right(activeWaveformRect) + 1, + waveformRect.y(), + std::max( + 0, + rect::right(waveformRect) + - rect::right(activeWaveformRect)), + waveformRect.height()), + outsideOpacity, + _inactiveWaveformBar, + _inactiveWaveformBar); } else { - FillWithMinithumbs( - p, - _data, + paintWaveform( waveformRect, - playProgress); + 1., + _activeWaveformBar, + _inactiveWaveformBar); + } + if (canTrim() && !trimFrameRect.isEmpty()) { + p.setPen(Qt::NoPen); + const auto inner = st::historyRecordTrimHandleInnerSize; + const auto drawInner = [&](const QRect &handle) { + const auto width = std::min( + inner.width(), + handle.width()); + const auto height = std::min( + inner.height(), + handle.height()); + const auto x = handle.x() + + (handle.width() - width) / 2; + const auto y = handle.y() + + (handle.height() - height) / 2; + p.drawRoundedRect( + QRect(x, y, width, height), + width / 2., + width / 2.); + }; + p.setBrush(_activeWaveformBar); + drawInner(trimLeftHandleRect); + drawInner(trimRightHandleRect); + p.setBrush(_st.bg); + const auto lineTop = trimLeftHandleRect.y(); + const auto lineHeight = trimLeftHandleRect.height(); + const auto leftLineX = trimFrameRect.x(); + const auto rightLineX = rect::right(trimFrameRect); + if (lineHeight > 0) { + p.fillRect(leftLineX, lineTop, 1, lineHeight, _st.bg); + if (rightLineX != leftLineX) { + p.fillRect( + rightLineX, + lineTop, + 1, + lineHeight, + _st.bg); + } + } + } + + if (!_controlRect.isEmpty()) { + p.setPen(Qt::NoPen); + p.setBrush(_st.cancelActive); + p.drawRoundedRect( + _controlRect, + _controlRect.height() / 2., + _controlRect.height() / 2.); + + if (_controlHasDuration) { + p.setFont(_durationFont); + p.setPen(st::historyRecordVoiceFgActiveIcon); + const auto ascent = _durationFont->ascent; + const auto left = rect::right(_playPauseButton) + /*+ st::historyRecordCenterControlTextSkip*/; + const auto top = _controlRect.y() + + (_controlRect.height() - ascent) / 2; + p.drawText( + QRect(left, top, _durationWidth, ascent), + style::al_left, + _duration); + } } } } @@ -756,26 +1079,25 @@ void ListenWrap::initPlayButton() { : RoundVideoDocument; const auto &play = _playPauseSt.playOuter; - const auto &width = _waveformBgFinalCenterRect.height(); - _playPauseButton->resize(width, width); + updateControlGeometry(); _playPauseButton->show(); _playPauseButton->setAccessibleName(tr::lng_record_lock_play(tr::now)); _playPauseButton->paintRequest( ) | rpl::on_next([=](const QRect &clip) { auto p = QPainter(_playPauseButton); + const auto size = _playPauseButton->size(); - const auto progress = _showProgress.current(); - p.translate(width / 2, width / 2); - if (progress < 1.) { - p.scale(progress, progress); - } + const auto progress = _showProgress.current() + * st::historyRecordCenterControlIconScale; + p.translate(size.width() / 2, size.height() / 2); + p.scale(progress, progress); p.translate(-play.width() / 2, -play.height() / 2); _playPause.paint(p, st::historyRecordVoiceFgActiveIcon); }, _playPauseButton->lifetime()); _playPauseButton->setClickedCallback([=] { - instance()->playPause({ _document, FullMsgId() }); + playPause(); }); const auto showPause = _lifetime.make_state>(false); @@ -818,33 +1140,91 @@ void ListenWrap::initPlayButton() { void ListenWrap::initPlayProgress() { using namespace ::Media::Player; using State = TrackState; + enum class DragMode { + None, + Seek, + TrimLeft, + TrimRight, + }; const auto animation = _lifetime.make_state(); - const auto isPointer = _lifetime.make_state>(false); + const auto dragMode = _lifetime.make_state(DragMode::None); + const auto trimPlaybackSeekInProgress = _lifetime.make_state(false); const auto &voice = AudioMsgId::Type::Voice; + const auto stopPlayingPreviewOnTrim = [=] { + const auto state = instance()->getState(voice); + if (isInPlayer(state) && ShowPauseIcon(state.state)) { + instance()->stop(voice, true); + } + }; + const auto canSeekAt = [=](const QPoint &p) { + return isInPlayer() + && _waveformFgRect.contains(p) + && (_controlRect.isEmpty() || !_controlRect.contains(p)); + }; const auto updateCursor = [=](const QPoint &p) { - *isPointer = isInPlayer() ? _waveformFgRect.contains(p) : false; + if (canTrim() + && (_trimLeftHandleRect.contains(p) + || _trimRightHandleRect.contains(p))) { + _parent->setCursor(style::cur_sizehor); + } else if (canSeekAt(p)) { + _parent->setCursor(style::cur_pointer); + } else { + _parent->setCursor(style::cur_default); + } }; + _parent->setMouseTracking(canTrim()); rpl::merge( instance()->startsPlay(voice) | rpl::map_to(true), instance()->stops(voice) | rpl::map_to(false) ) | rpl::on_next([=](bool play) { - _parent->setMouseTracking(isInPlayer() && play); + _parent->setMouseTracking(canTrim() || (isInPlayer() && play)); updateCursor(_parent->mapFromGlobal(QCursor::pos())); }, _lifetime); instance()->updatedNotifier( ) | rpl::on_next([=](const State &state) { - if (!isInPlayer(state)) { + if (*trimPlaybackSeekInProgress) { return; } - const auto progress = state.length - ? Progress(state.position, state.length) + if (!isInPlayer(state)) { + return; + } else if (!_isShowAnimation && (_showProgress.current() < 1.)) { + return; + } + const auto [leftBoundary, rightBoundary] = selectedTrimBoundaries(); + const auto playbackTrimmed = canTrim() + && ((leftBoundary > kTrimPlaybackEpsilon) + || (rightBoundary < (1. - kTrimPlaybackEpsilon))); + const auto length = int(state.length); + const auto position = std::min(state.position, int64(length)); + auto progress = length + ? Progress(position, length) : 0.; + if (playbackTrimmed && length > 0) { + if (ShowPauseIcon(state.state) + && (progress < (leftBoundary - kTrimPlaybackEpsilon))) { + *trimPlaybackSeekInProgress = true; + instance()->startSeeking(voice); + instance()->finishSeeking(voice, leftBoundary); + *trimPlaybackSeekInProgress = false; + return; + } + if (ShowPauseIcon(state.state) + && (progress >= (rightBoundary - kTrimPlaybackEpsilon))) { + instance()->stop(voice, true); + _playProgress = anim::value(leftBoundary, leftBoundary); + _parent->update(_waveformFgRect); + return; + } + progress = std::clamp(progress, leftBoundary, rightBoundary); + } if (IsStopped(state.state)) { - _playProgress = anim::value(); + _playProgress = playbackTrimmed + ? anim::value(leftBoundary, leftBoundary) + : anim::value(); } else { _playProgress.start(progress); } @@ -869,51 +1249,91 @@ void ListenWrap::initPlayProgress() { }; animation->init(std::move(animationCallback)); - const auto isPressed = _lifetime.make_state(false); - - isPointer->changes( - ) | rpl::on_next([=](bool pointer) { - _parent->setCursor(pointer ? style::cur_pointer : style::cur_default); - }, _lifetime); - _parent->events( ) | rpl::filter([=](not_null e) { return (e->type() == QEvent::MouseMove || e->type() == QEvent::MouseButtonPress || e->type() == QEvent::MouseButtonRelease); }) | rpl::on_next([=](not_null e) { - if (!isInPlayer()) { + if (!isInPlayer() && !canTrim()) { return; } const auto type = e->type(); - const auto isMove = (type == QEvent::MouseMove); const auto &pos = static_cast(e.get())->pos(); - if (*isPressed) { - *isPointer = true; - } else if (isMove) { + if ((type == QEvent::MouseMove) && (*dragMode == DragMode::None)) { updateCursor(pos); } + if (type == QEvent::MouseButtonPress) { - if (isPointer->current() && !(*isPressed)) { - instance()->startSeeking(voice); - *isPressed = true; + if (canTrim() && _trimLeftHandleRect.contains(pos)) { + stopPlayingPreviewOnTrim(); + *dragMode = DragMode::TrimLeft; + _parent->setCursor(style::cur_sizehor); + return; + } else if (canTrim() && _trimRightHandleRect.contains(pos)) { + stopPlayingPreviewOnTrim(); + *dragMode = DragMode::TrimRight; + _parent->setCursor(style::cur_sizehor); + return; } - } else if (*isPressed) { - const auto &rect = _waveformFgRect; - const auto left = float64(pos.x() - rect.x()); - const auto progress = Progress(left, rect.width()); - const auto isRelease = (type == QEvent::MouseButtonRelease); - if (isRelease || isMove) { + if (canSeekAt(pos)) { + instance()->startSeeking(voice); + *dragMode = DragMode::Seek; + } + return; + } + + const auto isRelease = (type == QEvent::MouseButtonRelease); + if (*dragMode == DragMode::Seek) { + if (isRelease || (type == QEvent::MouseMove)) { + auto progress = trimProgressFromPosition(pos.x()); + if (canTrim()) { + const auto [left, right] = selectedTrimBoundaries(); + progress = std::clamp(progress, left, right); + } _playProgress = anim::value(progress, progress); _parent->update(_waveformFgRect); if (isRelease) { instance()->finishSeeking(voice, progress); - *isPressed = false; + *dragMode = DragMode::None; + updateCursor(pos); } } + return; } + if ((*dragMode == DragMode::TrimLeft) + || (*dragMode == DragMode::TrimRight)) { + if (isRelease || (type == QEvent::MouseMove)) { + const auto progress = trimProgressFromPosition(pos.x()); + const auto minDelta = minimumTrimProgress(); + if (*dragMode == DragMode::TrimLeft) { + _trimLeftProgress = std::clamp( + progress, + 0., + std::max(0., _trimRightProgress - minDelta)); + } else { + _trimRightProgress = std::clamp( + progress, + std::min(1., _trimLeftProgress + minDelta), + 1.); + } + updateDurationText(); + updateTrimGeometry(); + updateControlGeometry(); + _parent->update(); + if (isRelease) { + *dragMode = DragMode::None; + updateCursor(pos); + } + } + return; + } + + if (isRelease) { + updateCursor(pos); + } }, _lifetime); } @@ -927,16 +1347,327 @@ bool ListenWrap::isInPlayer() const { return isInPlayer(::Media::Player::instance()->getState(Type::Voice)); } +bool ListenWrap::canTrim() const { + return _allowTrim; +} + +float64 ListenWrap::trimProgressFromPosition(int x) const { + const auto width = _waveformBgFinalCenterRect.width() - 1; + if (width <= 0) { + return 0.; + } + return std::clamp( + float64(x - _waveformBgFinalCenterRect.x()) / width, + 0., + 1.); +} + +float64 ListenWrap::minimumTrimProgress() const { + const auto samplesProgress = [&] { + const auto samples = int((_data->duration + * ::Media::Player::kDefaultFrequency) / crl::time(1000)); + if (samples <= 0) { + return 0.; + } + return std::clamp( + float64(kMinSamples) / samples, + 0., + 1.); + }(); + return std::max(samplesProgress, minimumControlTrimProgress()); +} + +float64 ListenWrap::minimumControlTrimProgress() const { + if (!canTrim() || _waveformBgFinalCenterRect.isEmpty()) { + return 0.; + } + const auto trimRect = _waveformBgFinalCenterRect; + const auto handleWidth = std::max( + 1, + std::min( + st::historyRecordTrimHandleWidth, + trimRect.width() / 2)); + const auto previewRange = std::max( + 1, + trimRect.width() - (handleWidth * 2)); + const auto controlHeight = std::min( + st::historyRecordCenterControlHeight, + trimRect.height()); + const auto iconWidth = controlHeight; + const auto minControlWidth = (st::historyRecordCenterControlPadding * 2) + + iconWidth + + st::historyRecordCenterControlMinimumProgressPadding * 2; + return std::clamp(float64(minControlWidth) / previewRange, 0., 1.); +} + +crl::time ListenWrap::selectedDuration() const { + if (!canTrim()) { + return _data->duration; + } + if (const auto range = selectedTrimRange()) { + return std::max(crl::time(0), range->till - range->from); + } + return _data->duration; +} + +std::optional ListenWrap::selectedTrimRange() const { + if (!canTrim()) { + return std::nullopt; + } + const auto left = std::clamp(_trimLeftProgress, 0., 1.); + const auto right = std::clamp(_trimRightProgress, left, 1.); + if ((left <= 0.) && (right >= 1.)) { + return std::nullopt; + } + const auto currentSamples = int((_data->duration + * ::Media::Player::kDefaultFrequency) / crl::time(1000)); + if (currentSamples <= 0) { + return std::nullopt; + } + const auto fromSamples = base::SafeRound(currentSamples * left); + const auto tillSamples = base::SafeRound(currentSamples * right); + if (tillSamples <= fromSamples) { + return std::nullopt; + } + const auto from = (fromSamples * crl::time(1000)) + / ::Media::Player::kDefaultFrequency; + const auto till = (tillSamples * crl::time(1000)) + / ::Media::Player::kDefaultFrequency; + if (till <= from) { + return std::nullopt; + } + return TrimRange{ .from = crl::time(from), .till = crl::time(till) }; +} + +ListenWrap::TrimBoundaries ListenWrap::selectedTrimBoundaries() const { + const auto dur = _data->duration; + if (const auto range = selectedTrimRange(); range && (dur > 0)) { + const auto left = std::clamp(float64(range->from) / dur, 0., 1.); + const auto right = std::clamp(float64(range->till) / dur, left, 1.); + return { left, right }; + } + return { 0., 1. }; +} + +ListenWrap::TrimGeometry ListenWrap::computeTrimGeometry( + const QRect &trimRect) const { + auto result = TrimGeometry(); + if (!canTrim() || trimRect.isEmpty()) { + return result; + } + const auto width = trimRect.width(); + if (width <= 0) { + return result; + } + const auto handleWidth = std::max( + 1, + std::min( + st::historyRecordTrimHandleWidth, + width / 2)); + const auto previewRange = std::max(1, width - (handleWidth * 2)); + const auto minBoundary = trimRect.x() + handleWidth; + const auto maxBoundary = trimRect.right() - handleWidth + 1; + if (maxBoundary < minBoundary) { + return result; + } + const auto leftProgress = std::clamp(_trimLeftProgress, 0., 1.); + const auto rightProgress = std::clamp( + _trimRightProgress, + leftProgress, + 1.); + const auto leftBoundary = std::clamp( + minBoundary + int(base::SafeRound(previewRange * leftProgress)), + minBoundary, + maxBoundary); + const auto rightBoundary = std::clamp( + minBoundary + int(base::SafeRound(previewRange * rightProgress)), + leftBoundary, + maxBoundary); + result.leftHandle = QRect( + leftBoundary - handleWidth, + trimRect.y(), + handleWidth, + trimRect.height()); + result.rightHandle = QRect( + rightBoundary, + trimRect.y(), + handleWidth, + trimRect.height()); + const auto previewLeft = leftBoundary; + const auto previewRight = std::max(previewLeft, rightBoundary - 1); + result.frame = QRect( + QPoint(previewLeft, trimRect.y()), + QPoint(previewRight, rect::bottom(trimRect))); + return result; +} + +void ListenWrap::updateControlGeometry() { + const auto availableRect = (canTrim() && !_trimFrameRect.isEmpty()) + ? _trimFrameRect + : _waveformBgFinalCenterRect; + if (availableRect.isEmpty()) { + _controlRect = QRect(); + _controlHasDuration = false; + return; + } + const auto controlHeight = std::min( + st::historyRecordCenterControlHeight, + availableRect.height()); + const auto iconWidth = controlHeight; + const auto iconOnlyWidth = (st::historyRecordCenterControlPadding * 2) + + iconWidth; + const auto fullWidth = iconOnlyWidth + + st::historyRecordCenterControlTextSkip + + _durationWidth; + const auto skip = st::historyRecordCenterControlMinimumProgressPadding; + _controlHasDuration = (availableRect.width() - skip * 2 >= fullWidth); + auto controlWidth = _controlHasDuration ? fullWidth : iconOnlyWidth; + controlWidth = std::min(controlWidth, availableRect.width()); + if (controlWidth <= 0 || controlHeight <= 0) { + _controlRect = QRect(); + _controlHasDuration = false; + return; + } + _controlRect = QRect( + availableRect.x() + (availableRect.width() - controlWidth) / 2, + availableRect.y() + (availableRect.height() - controlHeight) / 2, + controlWidth, + controlHeight); + _playPauseButton->resize(iconWidth, controlHeight); + const auto iconLeft = _controlHasDuration + ? (_controlRect.x() + st::historyRecordCenterControlPadding) + : (_controlRect.x() + (_controlRect.width() - iconWidth) / 2); + _playPauseButton->moveToLeft( + iconLeft, + _controlRect.y()); +} + +void ListenWrap::updateTrimGeometry() { + if (!canTrim() || _waveformBgFinalCenterRect.isEmpty()) { + _trimFrameRect = QRect(); + _trimLeftHandleRect = QRect(); + _trimRightHandleRect = QRect(); + return; + } + const auto minDelta = minimumTrimProgress(); + if ((_trimRightProgress - _trimLeftProgress) < minDelta) { + const auto center = (_trimLeftProgress + _trimRightProgress) / 2.; + const auto half = minDelta / 2.; + auto left = center - half; + auto right = center + half; + if (left < 0.) { + right = std::min(1., right - left); + left = 0.; + } + if (right > 1.) { + left = std::max(0., left - (right - 1.)); + right = 1.; + } + _trimLeftProgress = left; + _trimRightProgress = right; + } + const auto geometry = computeTrimGeometry(_waveformBgFinalCenterRect); + _trimFrameRect = geometry.frame; + _trimLeftHandleRect = geometry.leftHandle; + _trimRightHandleRect = geometry.rightHandle; +} + +void ListenWrap::applyTrimSelection(bool resetSelection) { + if (!canTrim()) { + return; + } + const auto range = selectedTrimRange(); + if (!range) { + return; + } + const auto [waveLeft, waveRight] = selectedTrimBoundaries(); + auto waveform = ResampleWaveformToRange( + _data->waveform, + waveLeft, + waveRight); + const auto from = range->from; + const auto till = range->till; + const auto selected = till - from; + const auto selectedSamples = int((selected + * ::Media::Player::kDefaultFrequency) / crl::time(1000)); + if (selectedSamples < kMinSamples) { + return; + } + const auto trimmed = ::Media::TrimAudioToRange(_data->content, from, till); + if (trimmed.content.isEmpty()) { + return; + } + if (isInPlayer()) { + ::Media::Player::instance()->stop(AudioMsgId::Type::Voice, true); + } + _data->content = std::move(trimmed.content); + if (waveform.isEmpty()) { + waveform = std::move(trimmed.waveform); + } + _data->waveform = std::move(waveform); + _data->duration = trimmed.duration; + _mediaView->setBytes(_data->content); + _document->size = _data->content.size(); + _voiceData->waveform = _data->waveform; + _voiceData->wavemax = _voiceData->waveform.empty() + ? uchar(0) + : *ranges::max_element(_voiceData->waveform); + if (resetSelection) { + _trimLeftProgress = 0.; + _trimRightProgress = 1.; + } + updateDurationText(); + _waveformFgRect = computeWaveformRect(_waveformBgFinalCenterRect); + updateTrimGeometry(); + updateControlGeometry(); + _playProgress = anim::value(); + _parent->update(); +} + +void ListenWrap::updateDurationText() { + _duration = FormatTrimDuration(selectedDuration()); + _durationWidth = _durationFont->width(_duration); +} + +void ListenWrap::applyTrimBeforeSend() { + applyTrimSelection(true); +} + +void ListenWrap::prepareForSendAnimation() { + if (_waveformBgRect.isEmpty()) { + return; + } + const auto cacheRect = _waveformBgRect + - style::margins(_st.remove.width, 0, _send->width(), 0); + if (cacheRect.isEmpty()) { + return; + } + const auto deleteVisible = _delete->isVisible(); + if (deleteVisible) { + _delete->hide(); + } + _sendAnimationCache = Ui::GrabWidgetToImage(_parent, cacheRect); + if (deleteVisible) { + _delete->show(); + } + _useSendAnimationCache = !_sendAnimationCache.isNull(); + if (_useSendAnimationCache && _playPauseButton->isVisible()) { + _playPauseButton->hide(); + _playPauseHiddenForSendAnimation = true; + } + if (_useSendAnimationCache) { + _parent->update(cacheRect); + } +} + void ListenWrap::playPause() { - _playPauseButton->clicked(Qt::NoModifier, Qt::LeftButton); + ::Media::Player::instance()->playPause({ _document, FullMsgId() }); } QRect ListenWrap::computeWaveformRect(const QRect ¢erRect) const { const auto top = computeTopMargin(st::msgWaveformMax); - const auto left = (_playPauseSt.playOuter.width() + centerRect.height()) - / 2; - const auto right = st::historyRecordWaveformRightSkip + _durationWidth; - return centerRect.marginsRemoved(style::margins(left, top, right, top)); + const auto left = st::historyRecordTrimHandleWidth; + return centerRect - style::margins(left, top, left, top); } int ListenWrap::computeTopMargin(int height) const { @@ -945,6 +1676,18 @@ int ListenWrap::computeTopMargin(int height) const { void ListenWrap::requestPaintProgress(float64 progress) { _isShowAnimation = (_showProgress.current() < progress); + if (_isShowAnimation && _useSendAnimationCache) { + _useSendAnimationCache = false; + _sendAnimationCache = QImage(); + if (_playPauseHiddenForSendAnimation) { + _playPauseButton->show(); + _playPauseHiddenForSendAnimation = false; + } + } + if (!_isShowAnimation && (progress < 1.)) { + const auto value = _playProgress.current(); + _playProgress = anim::value(value, value); + } _showProgress = progress; } @@ -1608,6 +2351,7 @@ void VoiceRecordBar::init() { _lock->setClickedCallback([=] { if (isListenState()) { + applyListenTrimForResume(); startRecording(); _showListenAnimation.stop(); _showListenAnimation.start([=](float64 value) { @@ -1728,9 +2472,18 @@ void VoiceRecordBar::prepareOnSendPress() { _recordingVideo = (_send->type() == Ui::SendButton::Type::Round); _fullRecord = false; _ttlButton = nullptr; + clearResumeState(); _lock->setRecordingVideo(_recordingVideo); } +void VoiceRecordBar::applyListenTrimForResume() { + const auto beforeDuration = _data.duration; + const auto beforeSize = _data.content.size(); + _listen->applyTrimBeforeSend(); + _resumeFromTrimmedListen = (_data.duration != beforeDuration) + || (_data.content.size() != beforeSize); +} + void VoiceRecordBar::activeAnimate(bool active) { const auto to = active ? 1. : 0.; if (_activeAnimation.animating()) { @@ -1864,13 +2617,25 @@ void VoiceRecordBar::startRecording() { _recording = true; if (_paused.current()) { _paused = false; - instance()->pause(false, nullptr); if (_videoRecorder) { + instance()->pause(false, nullptr); _videoRecorder->resume({ .video = std::move(_data), }); + clearResumePrefix(); + } else { + instance()->pause(false, nullptr); + if (_resumeFromTrimmedListen && (_pausedRawDuration > 0)) { + setupResumePrefixFromCurrentData(); + _recordingSamples = _resumePrefixSamples; + } else { + clearResumePrefix(); + } + _resumeFromTrimmedListen = false; + update(_durationRect); } } else { + clearResumePrefix(); instance()->start(_videoRecorder ? _videoRecorder->audioChunkProcessor() : nullptr); @@ -2027,9 +2792,11 @@ void VoiceRecordBar::checkTipRequired() { void VoiceRecordBar::recordUpdated(quint16 level, int samples) { _level->requestPaintLevel(level); - _recordingSamples = samples; - if (samples < 0 || samples >= kMaxSamples) { - stop(samples > 0 && _inField.current()); + const auto resumedSamples = std::max(0, samples - _resumeRawSamples); + const auto totalSamples = _resumePrefixSamples + resumedSamples; + _recordingSamples = totalSamples; + if (totalSamples < 0 || totalSamples >= kMaxSamples) { + stop(totalSamples > 0 && _inField.current()); } Core::App().updateNonIdle(); update(_durationRect); @@ -2089,9 +2856,79 @@ void VoiceRecordBar::hideFast() { _keyFilterInRecordingState = nullptr; } +void VoiceRecordBar::clearResumePrefix() { + _resumePrefixData = {}; + _resumePrefixSamples = 0; + _resumeRawSamples = 0; + _resumeRawDuration = 0; + _resumeFromTrimmedListen = false; +} + +void VoiceRecordBar::clearResumeState() { + _pausedRawDuration = 0; + clearResumePrefix(); +} + +void VoiceRecordBar::setupResumePrefixFromCurrentData() { + _resumePrefixData = _data; + _resumePrefixSamples = samplesFromDuration(_resumePrefixData.duration); + _resumeRawDuration = _pausedRawDuration; + _resumeRawSamples = samplesFromDuration(_resumeRawDuration); +} + +int VoiceRecordBar::samplesFromDuration(crl::time duration) const { + return int((duration * ::Media::Player::kDefaultFrequency) + / crl::time(1000)); +} + +Ui::RoundVideoResult VoiceRecordBar::mergeWithResumePrefix( + Ui::RoundVideoResult data) { + if (_recordingVideo || _resumePrefixData.content.isEmpty()) { + return data; + } + if (data.content.isEmpty()) { + return _resumePrefixData; + } + const auto tail = (_resumeRawDuration > 0) + ? ::Media::TrimAudioToRange( + data.content, + _resumeRawDuration, + data.duration) + : ::Media::AudioEditResult(); + if ((_resumeRawDuration > 0) && tail.content.isEmpty()) { + return _resumePrefixData; + } + const auto combined = ::Media::ConcatAudio( + _resumePrefixData.content, + (_resumeRawDuration > 0) ? tail.content : data.content); + if (combined.content.isEmpty()) { + return _resumePrefixData; + } + const auto tailDuration = (_resumeRawDuration > 0) + ? tail.duration + : data.duration; + const auto duration = combined.duration + ? combined.duration + : (_resumePrefixData.duration + tailDuration); + auto waveform = MergeWaveformsByDuration( + _resumePrefixData.waveform, + _resumePrefixData.duration, + (_resumeRawDuration > 0) ? tail.waveform : data.waveform, + tailDuration); + if (waveform.isEmpty()) { + waveform = std::move(combined.waveform); + } + return Ui::RoundVideoResult{ + .content = std::move(combined.content), + .waveform = std::move(waveform), + .duration = duration, + }; +} + void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) { using namespace ::Media::Capture; if (type == StopType::Cancel) { + clearResumeState(); if (_videoRecorder) { _videoRecorder->hide(); } @@ -2114,6 +2951,7 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) { _send, &_show->session(), &_data, + false, _cancelFont); _listenChanges.fire({}); @@ -2128,17 +2966,18 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) { instance()->pause(true); } else { instance()->pause(true, crl::guard(this, [=](Result &&data) { - if (data.bytes.isEmpty()) { + const auto rawDuration = data.duration; + auto combined = mergeWithResumePrefix( + ToRoundVideoResult(std::move(data))); + clearResumePrefix(); + if (combined.content.isEmpty()) { // Close everything. stop(false); return; } + _pausedRawDuration = rawDuration; _paused = true; - _data = Ui::RoundVideoResult{ - .content = std::move(data.bytes), - .waveform = std::move(data.waveform), - .duration = data.duration, - }; + _data = std::move(combined); window()->raise(); window()->activateWindow(); @@ -2148,6 +2987,7 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) { _send, &_show->session(), &_data, + true, _cancelFont); _listenChanges.fire({}); })); @@ -2177,16 +3017,16 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) { }); } instance()->stop(crl::guard(this, [=](Result &&data) { - if (data.bytes.isEmpty()) { + _pausedRawDuration = 0; + auto combined = mergeWithResumePrefix( + ToRoundVideoResult(std::move(data))); + clearResumePrefix(); + if (combined.content.isEmpty()) { // Close everything. stop(false); return; } - _data = Ui::RoundVideoResult{ - .content = std::move(data.bytes), - .waveform = std::move(data.waveform), - .duration = data.duration, - }; + _data = std::move(combined); window()->raise(); window()->activateWindow(); @@ -2262,6 +3102,10 @@ void VoiceRecordBar::requestToSendWithOptions(Api::SendOptions options) { if (takeTTLState()) { options.ttlSeconds = std::numeric_limits::max(); } + if (_listen) { + _listen->prepareForSendAnimation(); + _listen->applyTrimBeforeSend(); + } _sendVoiceRequests.fire({ .bytes = _data.content, .waveform = _data.waveform, diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h index 1b84eea0ec..43e121422e 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h @@ -148,6 +148,13 @@ private: void startRecording(); void prepareOnSendPress(); + void applyListenTrimForResume(); + void clearResumePrefix(); + void clearResumeState(); + void setupResumePrefixFromCurrentData(); + [[nodiscard]] int samplesFromDuration(crl::time duration) const; + [[nodiscard]] Ui::RoundVideoResult mergeWithResumePrefix( + Ui::RoundVideoResult data); [[nodiscard]] bool isTypeRecord() const; [[nodiscard]] bool hasDuration() const; @@ -178,6 +185,12 @@ private: std::unique_ptr _listen; Ui::RoundVideoResult _data; + Ui::RoundVideoResult _resumePrefixData; + int _resumePrefixSamples = 0; + int _resumeRawSamples = 0; + crl::time _pausedRawDuration = 0; + crl::time _resumeRawDuration = 0; + bool _resumeFromTrimmedListen = false; rpl::variable _paused; base::Timer _startTimer; From 08be7f196078dce0a631043c3e270ae40b2054b2 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 6 Mar 2026 12:21:01 +0300 Subject: [PATCH 087/415] Enabled ability to open full checked moderate box for channel senders. Related commit: 9b483c390d. --- Telegram/SourceFiles/window/window_peer_menu.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 30531d82bc..56f5ff93d9 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -4048,16 +4048,21 @@ void AddSenderUserpicModerateAction( const auto moderateChannel = moderateItem ? moderateItem->history()->peer->asChannel() : nullptr; - const auto moderateUser = moderateItem - ? moderateItem->from()->asUser() + const auto moderateFrom = moderateItem + ? moderateItem->from() + : nullptr; + const auto moderateUser = moderateFrom + ? moderateFrom->asUser() : nullptr; const auto canDeleteAndBan = moderateItem && moderateChannel && moderateChannel->isMegagroup() - && moderateUser - && !moderateChannel->isGroupAdmin(moderateUser) + && moderateFrom + && (!moderateUser || !moderateChannel->isGroupAdmin(moderateUser)) && moderateItem->suggestBanReport() - && moderateItem->suggestDeleteAllReport(); + && moderateItem->suggestDeleteAllReport() + && CanCreateModerateMessagesBox( + HistoryItemsList{ not_null(moderateItem) }); if (canDeleteAndBan) { addAction({ .isSeparator = true }); addAction({ From f5281b74186a928ba8affc8a4818ceea174573f1 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 6 Mar 2026 12:45:06 +0300 Subject: [PATCH 088/415] Fixed compose search list freeze by resetting merged search state flags. --- Telegram/SourceFiles/api/api_messages_search_merged.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/api/api_messages_search_merged.cpp b/Telegram/SourceFiles/api/api_messages_search_merged.cpp index 8e15f5255a..49bf50ce07 100644 --- a/Telegram/SourceFiles/api/api_messages_search_merged.cpp +++ b/Telegram/SourceFiles/api/api_messages_search_merged.cpp @@ -66,6 +66,8 @@ MessagesSearchMerged::MessagesSearchMerged(not_null history) void MessagesSearchMerged::disableMigrated() { _migratedSearch = std::nullopt; + _waitingForTotal = false; + _isFull = false; } void MessagesSearchMerged::addFound(const FoundMessages &data) { @@ -85,12 +87,15 @@ const MessagesSearch::Request &MessagesSearchMerged::request() const { void MessagesSearchMerged::clear() { _concatedFound = {}; _migratedFirstFound = {}; + _waitingForTotal = false; + _isFull = false; } void MessagesSearchMerged::search(const Request &search) { _request = search; + _isFull = false; + _waitingForTotal = (_migratedSearch != std::nullopt); if (_migratedSearch) { - _waitingForTotal = true; _migratedSearch->searchMessages(search); } _apiSearch.searchMessages(search); From a61334247da9c708eb329ada7bfbb1feb32680e6 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 6 Mar 2026 15:11:17 +0300 Subject: [PATCH 089/415] Fixed ratio of icon in update button from dialogs widget. Related commit: c6bf905253. --- Telegram/SourceFiles/dialogs/dialogs_widget.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 295300e03b..9258c19b11 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -123,9 +123,11 @@ base::options::toggle OptionForumHideChatsList({ [[nodiscard]] QImage UpdateIcon() { const auto iconSize = st::dialogsInstallUpdateIconSize; + const auto ratio = style::DevicePixelRatio(); auto result = QImage( - Size(iconSize) * style::DevicePixelRatio(), + Size(iconSize) * ratio, QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(ratio); result.fill(Qt::transparent); { auto p = QPainter(&result); From 27080d4b9649489e4cc52eaf7b89a1b08bbb91bc Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Fri, 6 Mar 2026 13:11:28 +0000 Subject: [PATCH 090/415] Fix include in Linux translate provider --- .../SourceFiles/platform/linux/translate_provider_linux.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/platform/linux/translate_provider_linux.cpp b/Telegram/SourceFiles/platform/linux/translate_provider_linux.cpp index 81bc599c5d..45a6e77a77 100644 --- a/Telegram/SourceFiles/platform/linux/translate_provider_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/translate_provider_linux.cpp @@ -5,7 +5,7 @@ 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 "platform/mac/translate_provider_mac.h" +#include "platform/linux/translate_provider_linux.h" #include "spellcheck/platform/platform_language.h" From f0a47c8769f018103201cb3842cf3b7b17760494 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Fri, 6 Mar 2026 09:35:06 +0000 Subject: [PATCH 091/415] Allow Linux TranslateProvider re-use --- .../linux/translate_provider_linux.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Telegram/SourceFiles/platform/linux/translate_provider_linux.cpp b/Telegram/SourceFiles/platform/linux/translate_provider_linux.cpp index 45a6e77a77..c3df46fc6c 100644 --- a/Telegram/SourceFiles/platform/linux/translate_provider_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/translate_provider_linux.cpp @@ -27,7 +27,7 @@ namespace { return it != end(commands) ? *it : QString(); } -class TranslateProvider final : public Ui::TranslateProvider { +class TranslateProvider final : public QObject, public Ui::TranslateProvider { public: [[nodiscard]] bool supportsMessageId() const override { return false; @@ -38,14 +38,15 @@ public: LanguageId to, Fn done) override { const auto from = Platform::Language::Recognize(request.text.text); - _process.setProgram(Command()); - _process.setArguments( + const auto process = QPointer(new QProcess(this)); + process->setProgram(Command()); + process->setArguments( QStringList{ u"-i"_q, u"-b"_q, u"-t"_q, to.twoLetterCode() } + (from.known() ? QStringList{ u"-s"_q, from.twoLetterCode() } : QStringList())); - QObject::connect(&_process, &QProcess::finished, [=] { - _document.setHtml(_process.readAllStandardOutput()); + connect(process, &QProcess::finished, this, [=] { + _document.setHtml(process->readAllStandardOutput()); const auto text = _document.toPlainText(); done(!text.isEmpty() ? Ui::TranslateProviderResult{ @@ -55,14 +56,14 @@ public: .error = Ui::TranslateProviderError::Unknown, } ); + delete process; }); - _process.start(); - _process.write(request.text.text.toUtf8()); - _process.closeWriteChannel(); + process->start(); + process->write(request.text.text.toUtf8()); + process->closeWriteChannel(); } private: - QProcess _process; QTextDocument _document; }; From 87ef5d06dfaecb6923a3d39943be551f2124cb2f Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Fri, 6 Mar 2026 10:24:31 +0000 Subject: [PATCH 092/415] Use translation providers for chat translation with parallel requests Co-Authored-By: Codex --- .../view/history_view_translate_tracker.cpp | 92 ++++++++++--------- .../view/history_view_translate_tracker.h | 11 ++- 2 files changed, 53 insertions(+), 50 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_translate_tracker.cpp b/Telegram/SourceFiles/history/view/history_view_translate_tracker.cpp index 902cf486d6..bbb1bad658 100644 --- a/Telegram/SourceFiles/history/view/history_view_translate_tracker.cpp +++ b/Telegram/SourceFiles/history/view/history_view_translate_tracker.cpp @@ -8,7 +8,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_translate_tracker.h" #include "apiwrap.h" -#include "api/api_text_entities.h" #include "api/api_transcribes.h" #include "core/application.h" #include "core/core_settings.h" @@ -21,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/history_item_components.h" #include "history/view/history_view_element.h" +#include "lang/translate_provider.h" #include "main/main_session.h" #include "spellcheck/platform/platform_language.h" @@ -37,6 +37,7 @@ constexpr auto kRequestCountLimit = 20; TranslateTracker::TranslateTracker(not_null history) : _history(history) +, _provider(Ui::CreateTranslateProvider(&_history->session())) , _limit(kEnoughForRecognition) { setup(); } @@ -236,19 +237,20 @@ void TranslateTracker::cancelToRequest() { } void TranslateTracker::cancelSentRequest() { - if (_requestId) { + if (_requestInProcess) { const auto owner = &_history->owner(); for (const auto &id : base::take(_requested)) { if (const auto item = owner->message(id)) { item->translationShowRequiresRequest({}); } } - _history->session().api().request(base::take(_requestId)).cancel(); + ++_requestToken; + _requestInProcess = false; } } void TranslateTracker::requestSome() { - if (_requestId || _itemsToRequest.empty()) { + if (_requestInProcess || _itemsToRequest.empty()) { return; } const auto to = _history->translatedTo(); @@ -260,60 +262,60 @@ void TranslateTracker::requestSome() { _requested.reserve(_itemsToRequest.size()); const auto session = &_history->session(); const auto peerId = _itemsToRequest.back().first.peer; - auto peer = (peerId == _history->peer->id) - ? _history->peer - : session->data().peer(peerId); auto length = 0; - auto list = QVector(); - list.reserve(_itemsToRequest.size()); for (auto i = _itemsToRequest.end(); i != _itemsToRequest.begin();) { if ((--i)->first.peer != peerId) { break; } length += i->second.length; _requested.push_back(i->first); - list.push_back(MTP_int(i->first.msg)); i = _itemsToRequest.erase(i); - if (list.size() >= kRequestCountLimit + if (_requested.size() >= kRequestCountLimit || length >= kRequestLengthLimit) { break; } } - using Flag = MTPmessages_TranslateText::Flag; - _requestId = session->api().request(MTPmessages_TranslateText( - MTP_flags(Flag::f_peer | Flag::f_id), - peer->input(), - MTP_vector(list), - MTPVector(), - MTP_string(to.twoLetterCode()) - )).done([=](const MTPmessages_TranslatedText &result) { - requestDone(to, result.data().vresult().v); - }).fail([=] { - requestDone(to, {}); - }).send(); -} - -void TranslateTracker::requestDone( - LanguageId to, - const QVector &list) { - auto index = 0; - const auto session = &_history->session(); - const auto owner = &session->data(); - for (const auto &id : base::take(_requested)) { - if (const auto item = owner->message(id)) { - const auto data = (index >= list.size()) - ? nullptr - : &list[index].data(); - auto text = data ? TextWithEntities{ - qs(data->vtext()), - Api::EntitiesFromMTP(session, data->ventities().v) - } : TextWithEntities(); - item->translationDone(to, std::move(text)); - } - ++index; + if (_requested.empty()) { + return; + } + const auto owner = &session->data(); + _requestInProcess = true; + const auto requestToken = ++_requestToken; + const auto remaining = std::make_shared(_requested.size()); + for (const auto &id : _requested) { + const auto item = owner->message(id); + if (!item) { + if (!--*remaining) { + _requestInProcess = false; + _requested.clear(); + requestSome(); + } + continue; + } + auto request = Ui::PrepareTranslateProviderRequest( + _provider.get(), + session->data().peer(id.peer), + id.msg, + item->originalText()); + _provider->request( + std::move(request), + to, + [=](Ui::TranslateProviderResult result) { + if (!_requestInProcess || (_requestToken != requestToken)) { + return; + } + if (const auto item = owner->message(id)) { + item->translationDone( + to, + result.text.value_or(TextWithEntities())); + } + if (!--*remaining) { + _requestInProcess = false; + _requested.clear(); + requestSome(); + } + }); } - _requestId = 0; - requestSome(); } void TranslateTracker::applyLimit() { diff --git a/Telegram/SourceFiles/history/view/history_view_translate_tracker.h b/Telegram/SourceFiles/history/view/history_view_translate_tracker.h index 9e2ca8b69d..786b6f1bb7 100644 --- a/Telegram/SourceFiles/history/view/history_view_translate_tracker.h +++ b/Telegram/SourceFiles/history/view/history_view_translate_tracker.h @@ -11,6 +11,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class History; class HistoryItem; +namespace Ui { +class TranslateProvider; +} // namespace Ui namespace HistoryView { @@ -54,11 +57,8 @@ private: void cancelSentRequest(); void switchTranslation(not_null item, LanguageId id); - void requestDone( - LanguageId to, - const QVector &list); - const not_null _history; + const std::unique_ptr _provider; rpl::variable _trackingLanguage = false; base::flat_map _itemsForRecognize; uint64 _generation = 0; @@ -70,7 +70,8 @@ private: base::flat_map, LanguageId> _switchTranslations; base::flat_map _itemsToRequest; std::vector _requested; - mtpRequestId _requestId = 0; + uint64 _requestToken = 0; + bool _requestInProcess = false; rpl::lifetime _trackingLifetime; rpl::lifetime _lifetime; From f85da1ad12e8aa936918a87629617c5da09fe6a2 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Fri, 6 Mar 2026 10:36:59 +0000 Subject: [PATCH 093/415] Use streaming batch translation in chat tracker Co-Authored-By: Codex --- .../view/history_view_translate_tracker.cpp | 59 ++++--- .../lang/translate_mtproto_provider.cpp | 144 +++++++++++++----- .../SourceFiles/lang/translate_provider.cpp | 2 +- Telegram/lib_translate | 2 +- 4 files changed, 139 insertions(+), 68 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_translate_tracker.cpp b/Telegram/SourceFiles/history/view/history_view_translate_tracker.cpp index bbb1bad658..d090613186 100644 --- a/Telegram/SourceFiles/history/view/history_view_translate_tracker.cpp +++ b/Telegram/SourceFiles/history/view/history_view_translate_tracker.cpp @@ -279,43 +279,52 @@ void TranslateTracker::requestSome() { return; } const auto owner = &session->data(); + auto requests = std::vector(); + requests.reserve(_requested.size()); + auto ids = std::vector(); + ids.reserve(_requested.size()); + for (const auto &id : _requested) { + if (const auto item = owner->message(id)) { + requests.push_back(Ui::PrepareTranslateProviderRequest( + _provider.get(), + session->data().peer(id.peer), + id.msg, + item->originalText())); + ids.push_back(id); + } + } + _requested = std::move(ids); + if (_requested.empty()) { + requestSome(); + return; + } _requestInProcess = true; const auto requestToken = ++_requestToken; - const auto remaining = std::make_shared(_requested.size()); - for (const auto &id : _requested) { - const auto item = owner->message(id); - if (!item) { - if (!--*remaining) { - _requestInProcess = false; - _requested.clear(); - requestSome(); - } - continue; - } - auto request = Ui::PrepareTranslateProviderRequest( - _provider.get(), - session->data().peer(id.peer), - id.msg, - item->originalText()); - _provider->request( - std::move(request), - to, - [=](Ui::TranslateProviderResult result) { + _provider->requestBatch( + std::move(requests), + to, + [=](int index, Ui::TranslateProviderResult result) { if (!_requestInProcess || (_requestToken != requestToken)) { return; } + if (index < 0 || index >= _requested.size()) { + return; + } + const auto &id = _requested[index]; if (const auto item = owner->message(id)) { item->translationDone( to, result.text.value_or(TextWithEntities())); } - if (!--*remaining) { - _requestInProcess = false; - _requested.clear(); - requestSome(); + }, + [=] { + if (!_requestInProcess || (_requestToken != requestToken)) { + return; } + _requestInProcess = false; + _requested.clear(); + requestSome(); }); - } } void TranslateTracker::applyLimit() { diff --git a/Telegram/SourceFiles/lang/translate_mtproto_provider.cpp b/Telegram/SourceFiles/lang/translate_mtproto_provider.cpp index 4525425ed8..c22dbd88d9 100644 --- a/Telegram/SourceFiles/lang/translate_mtproto_provider.cpp +++ b/Telegram/SourceFiles/lang/translate_mtproto_provider.cpp @@ -32,54 +32,116 @@ public: TranslateProviderRequest request, LanguageId to, Fn done) override { - const auto msgId = MsgId(request.msgId); - const auto peerId = PeerId(PeerIdHelper(request.peerId)); - const auto peer = msgId - ? _session->data().peerLoaded(peerId) - : nullptr; + requestBatch( + { std::move(request) }, + to, + [done = std::move(done)]( + int, + TranslateProviderResult result) mutable { + done(std::move(result)); + }, + [] {}); + } + + void requestBatch( + std::vector requests, + const LanguageId &to, + Fn doneOne, + Fn doneAll) override { using Flag = MTPmessages_TranslateText::Flag; - const auto flags = msgId - ? (Flag::f_peer | Flag::f_id) - : !request.text.text.isEmpty() - ? Flag::f_text - : Flag(0); - if (!flags || (msgId && !peer)) { - done(TranslateProviderResult{ - .error = TranslateProviderError::Unknown, - }); + if (requests.empty()) { + doneAll(); return; } + + const auto failAll = [=] { + for (auto i = 0; i != requests.size(); ++i) { + doneOne(i, TranslateProviderResult{ + .error = TranslateProviderError::Unknown, + }); + } + doneAll(); + }; + const auto doneFromList = [=]( + const QVector &list) { + for (auto i = 0; i != requests.size(); ++i) { + doneOne(i, (i < list.size()) + ? TranslateProviderResult{ + .text = Api::ParseTextWithEntities(_session, list[i]), + } + : TranslateProviderResult{ + .error = TranslateProviderError::Unknown, + }); + } + doneAll(); + }; + + const auto firstPeer = PeerId(PeerIdHelper(requests.front().peerId)); + const auto allWithIds = ranges::all_of( + requests, + [&](const TranslateProviderRequest &request) { + return (PeerId(PeerIdHelper(request.peerId)) == firstPeer) + && (request.msgId != 0); + }); + if (allWithIds) { + const auto peer = _session->data().peerLoaded(firstPeer); + if (!peer) { + failAll(); + return; + } + auto ids = QVector(); + ids.reserve(requests.size()); + for (const auto &request : requests) { + ids.push_back(MTP_int(MsgId(request.msgId).bare)); + } + _api.request(MTPmessages_TranslateText( + MTP_flags(Flag::f_peer | Flag::f_id), + peer->input(), + MTP_vector(ids), + MTPVector(), + MTP_string(to.twoLetterCode()) + )).done([=](const MTPmessages_TranslatedText &result) { + doneFromList(result.data().vresult().v); + }).fail([=](const MTP::Error &) { + failAll(); + }).send(); + return; + } + + const auto allWithText = ranges::all_of( + requests, + [](const TranslateProviderRequest &request) { + return !request.text.text.isEmpty(); + }); + if (!allWithText) { + TranslateProvider::requestBatch( + std::move(requests), + to, + std::move(doneOne), + std::move(doneAll)); + return; + } + + auto text = QVector(); + text.reserve(requests.size()); + for (const auto &request : requests) { + text.push_back(MTP_textWithEntities( + MTP_string(request.text.text), + Api::EntitiesToMTP( + _session, + request.text.entities, + Api::ConvertOption::SkipLocal))); + } _api.request(MTPmessages_TranslateText( - MTP_flags(flags), - msgId ? peer->input() : MTP_inputPeerEmpty(), - (msgId - ? MTP_vector(1, MTP_int(msgId.bare)) - : MTPVector()), - (msgId - ? MTPVector() - : MTP_vector(1, MTP_textWithEntities( - MTP_string(request.text.text), - Api::EntitiesToMTP( - _session, - request.text.entities, - Api::ConvertOption::SkipLocal)))), + MTP_flags(Flag::f_text), + MTP_inputPeerEmpty(), + MTPVector(), + MTP_vector(text), MTP_string(to.twoLetterCode()) )).done([=](const MTPmessages_TranslatedText &result) { - const auto &data = result.data(); - const auto &list = data.vresult().v; - done(list.isEmpty() - ? TranslateProviderResult{ - .error = TranslateProviderError::Unknown, - } - : TranslateProviderResult{ - .text = Api::ParseTextWithEntities( - _session, - list.front()), - }); + doneFromList(result.data().vresult().v); }).fail([=](const MTP::Error &) { - done(TranslateProviderResult{ - .error = TranslateProviderError::Unknown, - }); + failAll(); }).send(); } diff --git a/Telegram/SourceFiles/lang/translate_provider.cpp b/Telegram/SourceFiles/lang/translate_provider.cpp index 5f4bba6b6f..ff6e43e1ec 100644 --- a/Telegram/SourceFiles/lang/translate_provider.cpp +++ b/Telegram/SourceFiles/lang/translate_provider.cpp @@ -51,7 +51,7 @@ TranslateProviderRequest PrepareTranslateProviderRequest( MsgId msgId, TextWithEntities text) { auto result = TranslateProviderRequest{ - .peerId = int64(peer->id.value), + .peerId = uint64(peer->id.value), .msgId = IsServerMsgId(msgId) ? msgId.bare : 0, .text = std::move(text), }; diff --git a/Telegram/lib_translate b/Telegram/lib_translate index 03534a63dc..2eb70a8cce 160000 --- a/Telegram/lib_translate +++ b/Telegram/lib_translate @@ -1 +1 @@ -Subproject commit 03534a63dcd115b05e3e1e75abfe0acdb0235eb3 +Subproject commit 2eb70a8cceb80e96076ae8cc0e79fb049bce2fa1 From e0d28a580c933d76991378bb821d24382b133e8f Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 6 Mar 2026 18:48:58 +0300 Subject: [PATCH 094/415] Slightly improved code style in mtproto translate provider. --- .../lang/translate_mtproto_provider.cpp | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Telegram/SourceFiles/lang/translate_mtproto_provider.cpp b/Telegram/SourceFiles/lang/translate_mtproto_provider.cpp index c22dbd88d9..37b151042c 100644 --- a/Telegram/SourceFiles/lang/translate_mtproto_provider.cpp +++ b/Telegram/SourceFiles/lang/translate_mtproto_provider.cpp @@ -37,7 +37,7 @@ public: to, [done = std::move(done)]( int, - TranslateProviderResult result) mutable { + TranslateProviderResult result) { done(std::move(result)); }, [] {}); @@ -62,25 +62,29 @@ public: } doneAll(); }; - const auto doneFromList = [=]( + const auto doneFromList = [=, session = _session]( const QVector &list) { for (auto i = 0; i != requests.size(); ++i) { - doneOne(i, (i < list.size()) - ? TranslateProviderResult{ - .text = Api::ParseTextWithEntities(_session, list[i]), - } - : TranslateProviderResult{ - .error = TranslateProviderError::Unknown, - }); + doneOne( + i, + (i < list.size()) + ? TranslateProviderResult{ + .text = Api::ParseTextWithEntities( + session, + list[i]), + } + : TranslateProviderResult{ + .error = TranslateProviderError::Unknown, + }); } doneAll(); }; - const auto firstPeer = PeerId(PeerIdHelper(requests.front().peerId)); + const auto firstPeer = PeerId(requests.front().peerId); const auto allWithIds = ranges::all_of( requests, [&](const TranslateProviderRequest &request) { - return (PeerId(PeerIdHelper(request.peerId)) == firstPeer) + return (PeerId(request.peerId) == firstPeer) && (request.msgId != 0); }); if (allWithIds) { @@ -92,7 +96,7 @@ public: auto ids = QVector(); ids.reserve(requests.size()); for (const auto &request : requests) { - ids.push_back(MTP_int(MsgId(request.msgId).bare)); + ids.push_back(MTP_int(MsgId(request.msgId))); } _api.request(MTPmessages_TranslateText( MTP_flags(Flag::f_peer | Flag::f_id), From ccb563eff1ef36cb648ec05ec2f235a22747c021 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 7 Mar 2026 06:34:21 +0300 Subject: [PATCH 095/415] Moved out dock menu from obj-c implementation to Qt implementation. This allows to remove the `NSApplicationDelegate::applicationDockMenu` patch for Qt. --- Telegram/CMakeLists.txt | 2 + Telegram/SourceFiles/menu/menu_dock.cpp | 83 +++++++++++ Telegram/SourceFiles/menu/menu_dock.h | 16 +++ .../platform/mac/specific_mac_p.mm | 130 ++---------------- 4 files changed, 110 insertions(+), 121 deletions(-) create mode 100644 Telegram/SourceFiles/menu/menu_dock.cpp create mode 100644 Telegram/SourceFiles/menu/menu_dock.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index f484089579..4005911faf 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1373,6 +1373,8 @@ PRIVATE media/system_media_controls_manager.cpp menu/menu_antispam_validator.cpp menu/menu_antispam_validator.h + menu/menu_dock.cpp + menu/menu_dock.h menu/menu_item_download_files.cpp menu/menu_item_download_files.h menu/menu_item_rate_transcribe_session.cpp diff --git a/Telegram/SourceFiles/menu/menu_dock.cpp b/Telegram/SourceFiles/menu/menu_dock.cpp new file mode 100644 index 0000000000..dc8704f365 --- /dev/null +++ b/Telegram/SourceFiles/menu/menu_dock.cpp @@ -0,0 +1,83 @@ +/* +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 "menu/menu_dock.h" + +#include "core/application.h" +#include "core/core_settings.h" +#include "data/data_user.h" +#include "lang/lang_keys.h" +#include "main/main_account.h" +#include "main/main_domain.h" +#include "main/main_session.h" +#include "media/audio/media_audio.h" +#include "media/player/media_player_instance.h" +#include "ui/text/text_utilities.h" + +#include + +namespace Menu { + +void RefreshDockMenu(QMenu *menu) { + menu->clear(); + if (!Core::IsAppLaunched()) { + return; + } + + const auto accounts = Core::App().domain().orderedAccounts(); + if (accounts.size() > 1) { + menu->addSeparator(); + const auto profilesHeader = menu->addAction( + tr::lng_mac_menu_profiles(tr::now)); + profilesHeader->setEnabled(false); + constexpr auto kMaxLength = 30; + for (const auto &account : accounts) { + if (account->sessionExists()) { + const auto name = account->session().user()->name(); + menu->addAction( + (name.size() > kMaxLength) + ? (name.mid(0, kMaxLength) + Ui::kQEllipsis) + : name, + [account] { + Core::App().ensureSeparateWindowFor(account); + }); + } + } + menu->addSeparator(); + } + + menu->addAction( + Core::App().settings().desktopNotify() + ? tr::lng_disable_notifications_from_tray(tr::now) + : tr::lng_enable_notifications_from_tray(tr::now), + [] { + auto &settings = Core::App().settings(); + settings.setDesktopNotify(!settings.desktopNotify()); + }); + + using namespace Media::Player; + const auto type = instance()->getActiveType(); + const auto state = instance()->getState(type); + if (!IsStoppedOrStopping(state.state)) { + menu->addSeparator(); + const auto previous = menu->addAction( + tr::lng_mac_menu_player_previous(tr::now), + [] { instance()->previous(); }); + previous->setEnabled(instance()->previousAvailable(type)); + menu->addAction( + IsPausedOrPausing(state.state) + ? tr::lng_mac_menu_player_resume(tr::now) + : tr::lng_mac_menu_player_pause(tr::now), + [] { instance()->playPause(); }); + const auto next = menu->addAction( + tr::lng_mac_menu_player_next(tr::now), + [] { instance()->next(); }); + next->setEnabled(instance()->nextAvailable(type)); + } +} + +} // namespace Menu diff --git a/Telegram/SourceFiles/menu/menu_dock.h b/Telegram/SourceFiles/menu/menu_dock.h new file mode 100644 index 0000000000..d7e2982a1f --- /dev/null +++ b/Telegram/SourceFiles/menu/menu_dock.h @@ -0,0 +1,16 @@ +/* +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 + +class QMenu; + +namespace Menu { + +void RefreshDockMenu(QMenu *menu); + +} // namespace Menu diff --git a/Telegram/SourceFiles/platform/mac/specific_mac_p.mm b/Telegram/SourceFiles/platform/mac/specific_mac_p.mm index f02c356f84..d72da3a884 100644 --- a/Telegram/SourceFiles/platform/mac/specific_mac_p.mm +++ b/Telegram/SourceFiles/platform/mac/specific_mac_p.mm @@ -9,28 +9,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwindow.h" #include "mainwidget.h" -#include "main/main_account.h" -#include "main/main_domain.h" -#include "main/main_session.h" -#include "data/data_user.h" #include "calls/calls_instance.h" #include "core/sandbox.h" #include "core/application.h" #include "core/core_settings.h" #include "core/crash_reports.h" +#include "menu/menu_dock.h" #include "storage/localstorage.h" #include "media/audio/media_audio.h" -#include "media/player/media_player_instance.h" #include "window/window_controller.h" #include "base/platform/mac/base_utilities_mac.h" #include "base/platform/base_platform_info.h" -#include "lang/lang_keys.h" #include "base/timer.h" #include "styles/style_window.h" #include "platform/platform_specific.h" #include #include +#include #if __has_include() #include #endif // __has_include() @@ -49,47 +45,16 @@ namespace { constexpr auto kIgnoreActivationTimeoutMs = 500; -NSMenuItem *CreateMenuItem( - QString title, - rpl::lifetime &lifetime, - Fn callback, - bool enabled = true) { - id block = [^{ - Core::Sandbox::Instance().customEnterFromEventLoop(callback); - } copy]; - - NSMenuItem *item = [[NSMenuItem alloc] - initWithTitle:Q2NSString(title) - action:@selector(invoke) - keyEquivalent:@""]; - [item setTarget:block]; - [item setEnabled:enabled]; - - lifetime.add([=] { - [block release]; +void SetupDockMenu() { + static const auto DockMenu = std::make_unique(); + QObject::connect(DockMenu.get(), &QMenu::aboutToShow, [] { + Menu::RefreshDockMenu(DockMenu.get()); }); - return [item autorelease]; + DockMenu->setAsDockMenu(); } } // namespace -@interface RpMenu : NSMenu { -} - -- (rpl::lifetime &) lifetime; - -@end // @interface Menu - -@implementation RpMenu { - rpl::lifetime _lifetime; -} - -- (rpl::lifetime &) lifetime { - return _lifetime; -} - -@end // @implementation Menu - @interface qVisualize : NSObject { } @@ -144,8 +109,6 @@ NSMenuItem *CreateMenuItem( - (void) ignoreApplicationActivationRightNow; -- (NSMenu *) applicationDockMenu:(NSApplication *)sender; - @end // @interface ApplicationDelegate ApplicationDelegate *_sharedDelegate = nil; @@ -220,83 +183,6 @@ ApplicationDelegate *_sharedDelegate = nil; _ignoreActivationStop.callOnce(kIgnoreActivationTimeoutMs); } -- (NSMenu *) applicationDockMenu:(NSApplication *)sender { - if (!Core::IsAppLaunched()) { - return nil; - } - RpMenu* dockMenu = [[[RpMenu alloc] initWithTitle: @""] autorelease]; - [dockMenu setAutoenablesItems:false]; - - const auto accounts = Core::App().domain().orderedAccounts(); - if (accounts.size() > 1) { - [dockMenu addItem:[NSMenuItem separatorItem]]; - NSMenuItem *profilesHeader = [[NSMenuItem alloc] - initWithTitle:@"" action:nil keyEquivalent:@""]; - NSDictionary *attributes = @{ - NSFontAttributeName: [NSFont - menuFontOfSize:[NSFont smallSystemFontSize]], - NSForegroundColorAttributeName: [NSColor secondaryLabelColor] - }; - NSAttributedString *attrTitle = [[NSAttributedString alloc] - initWithString:Q2NSString(tr::lng_mac_menu_profiles(tr::now)) - attributes:attributes]; - [profilesHeader setAttributedTitle:attrTitle]; - [attrTitle release]; - [profilesHeader setEnabled:NO]; - [dockMenu addItem:[profilesHeader autorelease]]; - constexpr auto kMaxLength = 30; - for (const auto &account : accounts) { - if (account->sessionExists()) { - auto name = account->session().user()->name(); - [dockMenu addItem:CreateMenuItem( - (name.size() > kMaxLength) - ? (name.mid(0, kMaxLength) + Ui::kQEllipsis) - : name, - [dockMenu lifetime], - [account] { - Core::App().ensureSeparateWindowFor(account); - })]; - } - } - [dockMenu addItem:[NSMenuItem separatorItem]]; - } - - auto notifyCallback = [] { - auto &settings = Core::App().settings(); - settings.setDesktopNotify(!settings.desktopNotify()); - }; - [dockMenu addItem:CreateMenuItem( - Core::App().settings().desktopNotify() - ? tr::lng_disable_notifications_from_tray(tr::now) - : tr::lng_enable_notifications_from_tray(tr::now), - [dockMenu lifetime], - std::move(notifyCallback))]; - - using namespace Media::Player; - const auto state = instance()->getState(instance()->getActiveType()); - if (!IsStoppedOrStopping(state.state)) { - [dockMenu addItem:[NSMenuItem separatorItem]]; - [dockMenu addItem:CreateMenuItem( - tr::lng_mac_menu_player_previous(tr::now), - [dockMenu lifetime], - [] { instance()->previous(); }, - instance()->previousAvailable(instance()->getActiveType()))]; - [dockMenu addItem:CreateMenuItem( - IsPausedOrPausing(state.state) - ? tr::lng_mac_menu_player_resume(tr::now) - : tr::lng_mac_menu_player_pause(tr::now), - [dockMenu lifetime], - [] { instance()->playPause(); })]; - [dockMenu addItem:CreateMenuItem( - tr::lng_mac_menu_player_next(tr::now), - [dockMenu lifetime], - [] { instance()->next(); }, - instance()->nextAvailable(instance()->getActiveType()))]; - } - - return dockMenu; -} - @end // @implementation ApplicationDelegate namespace Platform { @@ -348,6 +234,8 @@ void objc_start() { addObserver: _sharedDelegate selector: @selector(receiveWakeNote:) name: NSWorkspaceDidWakeNotification object: NULL]; + + crl::on_main([=] { SetupDockMenu(); }); } void objc_ignoreApplicationActivationRightNow() { From c86d2c69869149499beb4c86aa36978faaa189c7 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 7 Mar 2026 09:51:10 +0300 Subject: [PATCH 096/415] Added simple implementation of import/export for experimental settings. --- .../settings/settings_common_session.cpp | 4 +- .../settings/settings_experimental.cpp | 62 +++++++++++++++++-- .../settings/settings_experimental.h | 3 + Telegram/lib_base | 2 +- 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/Telegram/SourceFiles/settings/settings_common_session.cpp b/Telegram/SourceFiles/settings/settings_common_session.cpp index 98723491bd..dc6ea9f338 100644 --- a/Telegram/SourceFiles/settings/settings_common_session.cpp +++ b/Telegram/SourceFiles/settings/settings_common_session.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_common_session.h" #include "settings/cloud_password/settings_cloud_password_email_confirm.h" +#include "settings/settings_experimental.h" #include "settings/sections/settings_chat.h" #include "settings/sections/settings_main.h" @@ -16,7 +17,8 @@ namespace Settings { bool HasMenu(Type type) { return (type == ::Settings::CloudPasswordEmailConfirmId()) || (type == MainId()) - || (type == ChatId()); + || (type == ChatId()) + || (type == Experimental::Id()); } } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_experimental.cpp b/Telegram/SourceFiles/settings/settings_experimental.cpp index a57e08e413..af2e3447cc 100644 --- a/Telegram/SourceFiles/settings/settings_experimental.cpp +++ b/Telegram/SourceFiles/settings/settings_experimental.cpp @@ -10,6 +10,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/components/passkeys.h" #include "main/main_session.h" #include "ui/boxes/confirm_box.h" +#include "ui/text/text_entity.h" +#include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" #include "ui/widgets/buttons.h" @@ -42,15 +44,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/stickers_list_widget.h" #include "styles/style_settings.h" #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" + +#include +#include namespace Settings { namespace { +[[nodiscard]] bool CanImportOptionsFromText(const QString &text) { + auto error = QJsonParseError(); + const auto parsed = QJsonDocument::fromJson(text.toUtf8(), &error); + return (error.error == QJsonParseError::NoError) && parsed.isObject(); +} + void AddOption( not_null window, not_null container, base::options::option &option, - rpl::producer<> resetClicks) { + rpl::producer<> resetClicks, + rpl::producer<> reloadOptionsRequests) { auto &lifetime = container->lifetime(); const auto name = option.name().isEmpty() ? option.id() : option.name(); const auto toggles = lifetime.make_state>(); @@ -59,6 +72,9 @@ void AddOption( ) | rpl::map_to( option.defaultValue() ) | rpl::start_to_stream(*toggles, lifetime); + std::move(reloadOptionsRequests) | rpl::on_next([=, &option] { + toggles->fire_copy(option.value()); + }, lifetime); const auto button = container->add(object_ptr