From d80cd8d7aa71db04af4ecf55cf0e676c81e0a075 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 9 Feb 2026 16:13:43 +0300 Subject: [PATCH 001/179] Added virtual method to info data providers to jump to date. --- .../SourceFiles/info/downloads/info_downloads_provider.cpp | 3 +++ Telegram/SourceFiles/info/downloads/info_downloads_provider.h | 1 + .../info/global_media/info_global_media_provider.cpp | 4 ++++ .../info/global_media/info_global_media_provider.h | 1 + Telegram/SourceFiles/info/media/info_media_common.h | 1 + Telegram/SourceFiles/info/media/info_media_provider.cpp | 3 +++ Telegram/SourceFiles/info/media/info_media_provider.h | 2 ++ Telegram/SourceFiles/info/saved/info_saved_music_provider.cpp | 3 +++ Telegram/SourceFiles/info/saved/info_saved_music_provider.h | 1 + Telegram/SourceFiles/info/stories/info_stories_provider.cpp | 3 +++ Telegram/SourceFiles/info/stories/info_stories_provider.h | 1 + 11 files changed, 23 insertions(+) diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp b/Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp index 974960755c..666585bc21 100644 --- a/Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp +++ b/Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp @@ -115,6 +115,9 @@ void Provider::setSearchQuery(QString query) { _refreshed.fire({}); } +void Provider::jumpToDate(const QDate &date, Fn) { +} + void Provider::refreshViewer() { if (_started) { return; diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_provider.h b/Telegram/SourceFiles/info/downloads/info_downloads_provider.h index d2f2cc6970..d3d9d6063b 100644 --- a/Telegram/SourceFiles/info/downloads/info_downloads_provider.h +++ b/Telegram/SourceFiles/info/downloads/info_downloads_provider.h @@ -45,6 +45,7 @@ public: rpl::producer<> refreshed() override; void setSearchQuery(QString query) override; + void jumpToDate(const QDate &date, Fn callback) override; std::vector fillSections( not_null delegate) override; diff --git a/Telegram/SourceFiles/info/global_media/info_global_media_provider.cpp b/Telegram/SourceFiles/info/global_media/info_global_media_provider.cpp index debc99aaed..7718917c88 100644 --- a/Telegram/SourceFiles/info/global_media/info_global_media_provider.cpp +++ b/Telegram/SourceFiles/info/global_media/info_global_media_provider.cpp @@ -423,6 +423,10 @@ void Provider::setSearchQuery(QString query) { Unexpected("Media::Provider::setSearchQuery."); } +void Provider::jumpToDate(const QDate &date, Fn) { + Unexpected("GlobalMedia::Provider::jumpToDate."); +} + GlobalMediaKey Provider::sliceKey(Data::MessagePosition aroundId) const { return GlobalMediaKey{ aroundId }; } diff --git a/Telegram/SourceFiles/info/global_media/info_global_media_provider.h b/Telegram/SourceFiles/info/global_media/info_global_media_provider.h index bf856970be..eb8f968339 100644 --- a/Telegram/SourceFiles/info/global_media/info_global_media_provider.h +++ b/Telegram/SourceFiles/info/global_media/info_global_media_provider.h @@ -91,6 +91,7 @@ public: not_null b) override; void setSearchQuery(QString query) override; + void jumpToDate(const QDate &date, Fn callback) override; Media::ListItemSelectionData computeSelectionData( not_null item, diff --git a/Telegram/SourceFiles/info/media/info_media_common.h b/Telegram/SourceFiles/info/media/info_media_common.h index 5c48e2557e..626c4b592d 100644 --- a/Telegram/SourceFiles/info/media/info_media_common.h +++ b/Telegram/SourceFiles/info/media/info_media_common.h @@ -165,6 +165,7 @@ public: not_null document) = 0; virtual void setSearchQuery(QString query) = 0; + virtual void jumpToDate(const QDate &date, Fn callback) = 0; [[nodiscard]] virtual int64 scrollTopStatePosition( not_null item) = 0; diff --git a/Telegram/SourceFiles/info/media/info_media_provider.cpp b/Telegram/SourceFiles/info/media/info_media_provider.cpp index 25b9a4806b..9dcd472654 100644 --- a/Telegram/SourceFiles/info/media/info_media_provider.cpp +++ b/Telegram/SourceFiles/info/media/info_media_provider.cpp @@ -342,6 +342,9 @@ void Provider::setSearchQuery(QString query) { Unexpected("Media::Provider::setSearchQuery."); } +void Provider::jumpToDate(const QDate &date, Fn callback) { +} + SparseIdsMergedSlice::Key Provider::sliceKey( UniversalMsgId universalId) const { using Key = SparseIdsMergedSlice::Key; diff --git a/Telegram/SourceFiles/info/media/info_media_provider.h b/Telegram/SourceFiles/info/media/info_media_provider.h index ed09954e83..9805e0964f 100644 --- a/Telegram/SourceFiles/info/media/info_media_provider.h +++ b/Telegram/SourceFiles/info/media/info_media_provider.h @@ -48,6 +48,8 @@ public: void setSearchQuery(QString query) override; + void jumpToDate(const QDate &date, Fn callback) override; + ListItemSelectionData computeSelectionData( not_null item, TextSelection selection) override; diff --git a/Telegram/SourceFiles/info/saved/info_saved_music_provider.cpp b/Telegram/SourceFiles/info/saved/info_saved_music_provider.cpp index 1fd89502bf..e7d4ae5701 100644 --- a/Telegram/SourceFiles/info/saved/info_saved_music_provider.cpp +++ b/Telegram/SourceFiles/info/saved/info_saved_music_provider.cpp @@ -162,6 +162,9 @@ void MusicProvider::checkPreload( void MusicProvider::setSearchQuery(QString query) { } +void MusicProvider::jumpToDate(const QDate &date, Fn) { +} + void MusicProvider::refreshViewer() { _viewerLifetime.destroy(); const auto aroundId = _aroundId; diff --git a/Telegram/SourceFiles/info/saved/info_saved_music_provider.h b/Telegram/SourceFiles/info/saved/info_saved_music_provider.h index 08f2059522..0733b4b861 100644 --- a/Telegram/SourceFiles/info/saved/info_saved_music_provider.h +++ b/Telegram/SourceFiles/info/saved/info_saved_music_provider.h @@ -50,6 +50,7 @@ public: rpl::producer<> refreshed() override; void setSearchQuery(QString query) override; + void jumpToDate(const QDate &date, Fn callback) override; std::vector fillSections( not_null delegate) override; diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp index 7bf322d334..2dd55462c6 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp +++ b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp @@ -177,6 +177,9 @@ void Provider::checkPreload( void Provider::setSearchQuery(QString query) { } +void Provider::jumpToDate(const QDate &date, Fn) { +} + void Provider::refreshViewer() { _viewerLifetime.destroy(); const auto aroundId = _aroundId; diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.h b/Telegram/SourceFiles/info/stories/info_stories_provider.h index 26cd64a3ba..d1f4f78ff6 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_provider.h +++ b/Telegram/SourceFiles/info/stories/info_stories_provider.h @@ -53,6 +53,7 @@ public: rpl::producer<> refreshed() override; void setSearchQuery(QString query) override; + void jumpToDate(const QDate &date, Fn callback) override; std::vector fillSections( not_null delegate) override; From 9e434a9136ab2cc4bba0873f25f87463f2392974 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 8 Jan 2026 08:15:09 +0300 Subject: [PATCH 002/179] Added initial ability to info list widget to jump to date. --- .../data/data_search_controller.cpp | 74 +++++++++++++++++++ .../SourceFiles/data/data_search_controller.h | 18 +++++ .../info/media/info_media_inner_widget.cpp | 4 + .../info/media/info_media_inner_widget.h | 2 + .../info/media/info_media_list_widget.cpp | 14 ++++ .../info/media/info_media_list_widget.h | 2 + .../info/media/info_media_provider.cpp | 38 ++++++++++ .../info/media/info_media_widget.cpp | 4 + .../info/media/info_media_widget.h | 2 + 9 files changed, 158 insertions(+) diff --git a/Telegram/SourceFiles/data/data_search_controller.cpp b/Telegram/SourceFiles/data/data_search_controller.cpp index 2d01942c56..f5cd92180c 100644 --- a/Telegram/SourceFiles/data/data_search_controller.cpp +++ b/Telegram/SourceFiles/data/data_search_controller.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_search_controller.h" +#include "base/unixtime.h" #include "main/main_session.h" #include "data/data_session.h" #include "data/data_messages.h" @@ -334,6 +335,79 @@ HistoryResult ParseHistoryResult( data); } +std::optional PrepareSearchRequestByDate( + not_null peer, + MsgId topicRootId, + PeerId monoforumPeerId, + Storage::SharedMediaType type, + const QDate &date, + Data::LoadDirection direction) { + const auto filter = PrepareSearchFilter(type); + const auto minDate = base::unixtime::serialize( + QDateTime(date, QTime(0, 0))); + const auto maxDate = base::unixtime::serialize( + QDateTime(date.addDays(1), QTime(0, 0))) - 1; + const auto limit = kSharedMediaLimit; + const auto offsetId = 0; + const auto addOffset = 0; + const auto hash = uint64(0); + + using Flag = MTPmessages_Search::Flag; + return MTPmessages_Search( + MTP_flags((topicRootId ? Flag::f_top_msg_id : Flag(0)) + | (monoforumPeerId ? Flag::f_saved_peer_id : Flag(0))), + peer->input(), + MTP_string(""), + MTP_inputPeerEmpty(), + (monoforumPeerId + ? peer->owner().peer(monoforumPeerId)->input() + : MTPInputPeer()), + MTPVector(), + MTP_int(topicRootId), + filter, + MTP_int(minDate), + MTP_int(maxDate), + MTP_int(offsetId), + MTP_int(addOffset), + MTP_int(limit), + MTP_int(0), + MTP_int(0), + MTP_long(hash)); +} + +SearchResultByDate ParseSearchResultByDate( + not_null peer, + const QDate &date, + const SearchResult &parsed) { + const auto targetDate = base::unixtime::serialize( + QDateTime(date, QTime(0, 0))); + auto result = SearchResultByDate(); + if (parsed.messageIds.empty()) { + return result; + } + const auto firstMsgId = parsed.messageIds.front(); + result.first = Data::MessagePosition{ + .fullId = FullMsgId(peer->id, firstMsgId), + .date = TimeId(0), + }; + auto closestMsgId = firstMsgId; + auto minDiff = std::numeric_limits::max(); + for (const auto msgId : parsed.messageIds) { + if (const auto item = peer->owner().message(peer->id, msgId)) { + const auto diff = std::abs(item->date() - targetDate); + if (diff < minDiff) { + minDiff = diff; + closestMsgId = msgId; + } + } + } + result.closestToDate = Data::MessagePosition{ + .fullId = FullMsgId(peer->id, closestMsgId), + .date = TimeId(0), + }; + return result; +} + SearchController::CacheEntry::CacheEntry( not_null session, const Query &query) diff --git a/Telegram/SourceFiles/data/data_search_controller.h b/Telegram/SourceFiles/data/data_search_controller.h index e7fb3f8e9f..a4d5690360 100644 --- a/Telegram/SourceFiles/data/data_search_controller.h +++ b/Telegram/SourceFiles/data/data_search_controller.h @@ -44,6 +44,11 @@ struct GlobalMediaResult { int fullCount = 0; }; +struct SearchResultByDate { + Data::MessagePosition first; + Data::MessagePosition closestToDate; +}; + [[nodiscard]] MTPMessagesFilter PrepareSearchFilter( Storage::SharedMediaType type); @@ -74,6 +79,19 @@ struct GlobalMediaResult { Data::LoadDirection direction, const SearchRequestResult &data); +[[nodiscard]] std::optional PrepareSearchRequestByDate( + not_null peer, + MsgId topicRootId, + PeerId monoforumPeerId, + Storage::SharedMediaType type, + const QDate &date, + Data::LoadDirection direction); + +[[nodiscard]] SearchResultByDate ParseSearchResultByDate( + not_null peer, + const QDate &date, + const SearchResult &parsed); + [[nodiscard]] HistoryRequest PrepareHistoryRequest( not_null peer, MsgId messageId, diff --git a/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp b/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp index 2e6a62506a..b7c983afb4 100644 --- a/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp @@ -245,5 +245,9 @@ rpl::producer InnerWidget::scrollToRequests() const { return _scrollToRequests.events(); } +void InnerWidget::jumpToDate(const QDate &date, Fn c) { + _list->jumpToDate(date, std::move(c)); +} + } // namespace Media } // namespace Info diff --git a/Telegram/SourceFiles/info/media/info_media_inner_widget.h b/Telegram/SourceFiles/info/media/info_media_inner_widget.h index a3e4859603..ff3782f72d 100644 --- a/Telegram/SourceFiles/info/media/info_media_inner_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_inner_widget.h @@ -49,6 +49,8 @@ public: rpl::producer selectedListValue() const; void selectionAction(SelectionAction action); + void jumpToDate(const QDate &date, Fn callback); + ~InnerWidget(); protected: diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp index 61fd18a36a..6c3085562d 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp @@ -2713,5 +2713,19 @@ ListWidget::~ListWidget() { } } +void ListWidget::jumpToDate(const QDate &date, Fn c) { + _provider->jumpToDate(date, [=](FullMsgId fullId) { + const auto item = session().data().message(fullId); + if (!item) { + return; + } + _scrollTopState.position = _provider->scrollTopStatePosition(item); + _scrollTopState.item = item; + if (c) { + c(fullId); + } + }); +} + } // namespace Media } // namespace Info diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.h b/Telegram/SourceFiles/info/media/info_media_list_widget.h index e9e4798fe9..175deb9d30 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.h @@ -87,6 +87,8 @@ public: void saveState(not_null memento); void restoreState(not_null memento); + void jumpToDate(const QDate &date, Fn callback); + // Overview::Layout::Delegate void registerHeavyItem(not_null item) override; void unregisterHeavyItem(not_null item) override; diff --git a/Telegram/SourceFiles/info/media/info_media_provider.cpp b/Telegram/SourceFiles/info/media/info_media_provider.cpp index 9dcd472654..9ef64abb1a 100644 --- a/Telegram/SourceFiles/info/media/info_media_provider.cpp +++ b/Telegram/SourceFiles/info/media/info_media_provider.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/media/info_media_provider.h" +#include "apiwrap.h" #include "info/media/info_media_widget.h" #include "info/media/info_media_list_section.h" #include "info/info_controller.h" @@ -343,6 +344,43 @@ void Provider::setSearchQuery(QString query) { } void Provider::jumpToDate(const QDate &date, Fn callback) { + _viewerLifetime.destroy(); + + const auto peer = _controller->session().data().peer(_peer->id); + const auto request = Api::PrepareSearchRequestByDate( + peer, + _topicRootId, + _monoforumPeerId, + _type, + date, + Data::LoadDirection::Around); + + if (!request) { + return; + } + + _controller->session().api().request( + std::move(*request) + ).done([=](const Api::SearchRequestResult &result) { + const auto byDate = Api::ParseSearchResultByDate( + peer, + date, + Api::ParseSearchResult( + peer, + _type, + 0, + Data::LoadDirection::Around, + result)); + + if (byDate.first) { + _universalAroundId = GetUniversalId(byDate.first.fullId); + if (callback) { + callback(byDate.closestToDate.fullId); + } + _idsLimit = kMinimalIdsLimit * 2; + refreshViewer(); + } + }).send(); } SparseIdsMergedSlice::Key Provider::sliceKey( diff --git a/Telegram/SourceFiles/info/media/info_media_widget.cpp b/Telegram/SourceFiles/info/media/info_media_widget.cpp index 9197c0ae02..8db825f10e 100644 --- a/Telegram/SourceFiles/info/media/info_media_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_widget.cpp @@ -198,4 +198,8 @@ void Widget::restoreState(not_null memento) { _inner->restoreState(memento); } +void Widget::jumpToDate(const QDate &date, Fn c) { + _inner->jumpToDate(date, std::move(c)); +} + } // namespace Info::Media diff --git a/Telegram/SourceFiles/info/media/info_media_widget.h b/Telegram/SourceFiles/info/media/info_media_widget.h index 1a84139190..eb3f8de2cb 100644 --- a/Telegram/SourceFiles/info/media/info_media_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_widget.h @@ -127,6 +127,8 @@ public: rpl::producer title() override; + void jumpToDate(const QDate &date, Fn callback); + private: void saveState(not_null memento); void restoreState(not_null memento); From 09c43a0f1dd77db0dda83036708508e8a777d1c5 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 9 Feb 2026 15:39:16 +0300 Subject: [PATCH 003/179] Changed callback in calendar box to have ability to close box itself. --- Telegram/SourceFiles/export/view/export_view_settings.cpp | 8 ++++---- Telegram/SourceFiles/ui/boxes/calendar_box.cpp | 5 ++++- Telegram/SourceFiles/ui/boxes/calendar_box.h | 6 ++++-- Telegram/SourceFiles/ui/boxes/choose_date_time.cpp | 6 ++++-- Telegram/SourceFiles/window/window_session_controller.cpp | 4 ++-- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/Telegram/SourceFiles/export/view/export_view_settings.cpp b/Telegram/SourceFiles/export/view/export_view_settings.cpp index b5b0e8bd6b..85c7061cc0 100644 --- a/Telegram/SourceFiles/export/view/export_view_settings.cpp +++ b/Telegram/SourceFiles/export/view/export_view_settings.cpp @@ -617,11 +617,11 @@ void SettingsWidget::editDateLimit( } })); }; - const auto callback = crl::guard(this, [=](const QDate &date) { + const auto callback = crl::guard(this, [=]( + const QDate &date, + Fn close) { done(base::unixtime::serialize(date.startOfDay())); - if (const auto weak = shared->get()) { - weak->closeBox(); - } + close(); }); auto box = Box(Ui::CalendarBoxArgs{ .month = month, diff --git a/Telegram/SourceFiles/ui/boxes/calendar_box.cpp b/Telegram/SourceFiles/ui/boxes/calendar_box.cpp index 568690e7a7..2e4a519f6f 100644 --- a/Telegram/SourceFiles/ui/boxes/calendar_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/calendar_box.cpp @@ -1161,7 +1161,10 @@ void CalendarBox::prepare() { _previous->setClickedCallback([=] { goPreviousMonth(); }); _next->setClickedCallback([=] { goNextMonth(); }); - _inner->setDateChosenCallback(std::move(_callback)); + _inner->setDateChosenCallback([=, c = std::move(_callback)]( + const QDate &date) { + c(date, crl::guard(this, [=] { closeBox(); })); + }); _context->monthValue( ) | rpl::on_next([=](QDate month) { diff --git a/Telegram/SourceFiles/ui/boxes/calendar_box.h b/Telegram/SourceFiles/ui/boxes/calendar_box.h index 3cfdbf6221..7d147e9a37 100644 --- a/Telegram/SourceFiles/ui/boxes/calendar_box.h +++ b/Telegram/SourceFiles/ui/boxes/calendar_box.h @@ -30,13 +30,15 @@ class IconButton; class ScrollArea; class CalendarBox; +using JumpCallback = Fn close)>; + struct CalendarBoxArgs { template using required = base::required; required month; required highlighted; - required> callback; + required callback; FnMut)> finalize; const style::CalendarSizes &st = st::defaultCalendarSizes; QDate minDate; @@ -105,7 +107,7 @@ private: bool _previousEnabled = false; bool _nextEnabled = false; - Fn _callback; + JumpCallback _callback; FnMut)> _finalize; bool _watchScroll = false; diff --git a/Telegram/SourceFiles/ui/boxes/choose_date_time.cpp b/Telegram/SourceFiles/ui/boxes/choose_date_time.cpp index 19731b6f8b..b646da1018 100644 --- a/Telegram/SourceFiles/ui/boxes/choose_date_time.cpp +++ b/Telegram/SourceFiles/ui/boxes/choose_date_time.cpp @@ -172,9 +172,11 @@ ChooseDateTimeBoxDescriptor ChooseDateTimeBox( Box(Ui::CalendarBoxArgs{ .month = state->date.current(), .highlighted = state->date.current(), - .callback = crl::guard(box, [=](QDate chosen) { + .callback = crl::guard(box, [=]( + QDate chosen, + Fn close) { state->date = chosen; - (*calendar)->closeBox(); + close(); }), .minDate = minDate(), .maxDate = maxDate(), diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index fe070ff6ec..25c458ca50 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -2810,7 +2810,7 @@ void SessionController::showCalendar(Dialogs::Key chat, QDate requestedDate) { }; const auto weak = base::make_weak(this); const auto weakTopic = base::make_weak(topic); - const auto jump = [=](const QDate &date) { + const auto jump = [=](const QDate &date, Fn close) { const auto open = [=](not_null peer, MsgId id) { if (const auto strong = weak.get()) { if (!topic) { @@ -2834,7 +2834,7 @@ void SessionController::showCalendar(Dialogs::Key chat, QDate requestedDate) { show(Box(Ui::CalendarBoxArgs{ .month = highlighted, .highlighted = highlighted, - .callback = [=](const QDate &date) { jump(date); }, + .callback = jump, .minDate = minPeerDate, .maxDate = maxPeerDate, .allowsSelection = history->peer->isUser(), From d6765ab3941ffaf6b06a27b160024f2b7e419e21 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 9 Feb 2026 15:28:24 +0300 Subject: [PATCH 004/179] Replaced args for calendar show from session controller with descriptor. --- Telegram/SourceFiles/dialogs/dialogs_widget.cpp | 2 +- .../history/view/controls/history_view_compose_search.cpp | 2 +- Telegram/SourceFiles/window/window_session_controller.cpp | 8 +++++--- Telegram/SourceFiles/window/window_session_controller.h | 8 +++++--- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 7dff9162d4..7307807051 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -3717,7 +3717,7 @@ void Widget::clearSearchCache(bool clearPosts) { void Widget::showCalendar() { if (_searchState.inChat) { - controller()->showCalendar(_searchState.inChat, QDate()); + controller()->showCalendar({ _searchState.inChat }); } } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp index 7f0297b102..668bb501c9 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp @@ -1015,7 +1015,7 @@ ComposeSearch::Inner::Inner( _bottomBar->showCalendarRequests( ) | rpl::on_next([=] { hideList(); - _window->showCalendar({ _history }, QDate()); + _window->showCalendar({ Dialogs::Key(_history) }); }, _bottomBar->lifetime()); _bottomBar->showBoxFromRequests( diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 25c458ca50..ad55ed21cb 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -350,9 +350,9 @@ void DateClickHandler::onClick(ClickContext context) const { const auto my = context.other.value(); if (const auto window = my.sessionWindow.get()) { if (!_chat.topic()) { - window->showCalendar(_chat, _date); + window->showCalendar({ _chat, _date }); } else if (const auto strong = _weak.get()) { - window->showCalendar(strong, _date); + window->showCalendar({ strong, _date }); } } } @@ -2675,7 +2675,9 @@ void SessionController::startOrJoinGroupCall( Core::App().calls().startOrJoinGroupCall(uiShow(), peer, args); } -void SessionController::showCalendar(Dialogs::Key chat, QDate requestedDate) { +void SessionController::showCalendar(ShowCalendarDescriptor &&descriptor) { + const auto chat = descriptor.chat; + const auto requestedDate = descriptor.date; const auto topic = chat.topic(); const auto history = chat.owningHistory(); if (!history) { diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 1e6376da2a..854be6c660 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -542,9 +542,11 @@ public: void removeLayerBlackout(); [[nodiscard]] bool isLayerShown() const; - void showCalendar( - Dialogs::Key chat, - QDate requestedDate); + struct ShowCalendarDescriptor { + Dialogs::Key chat; + QDate date; + }; + void showCalendar(ShowCalendarDescriptor &&descriptor); void showAddContact(); void showNewGroup(); From 17b88c46b92718ce71bf937c3d5f3cebefc0fc97 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 9 Feb 2026 16:26:23 +0300 Subject: [PATCH 005/179] Added initial ability to provide dynamic image to calendar dates. --- .../SourceFiles/ui/boxes/calendar_box.cpp | 79 ++++++++++++++++++- Telegram/SourceFiles/ui/boxes/calendar_box.h | 5 ++ 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/ui/boxes/calendar_box.cpp b/Telegram/SourceFiles/ui/boxes/calendar_box.cpp index 2e4a519f6f..1a13a4f907 100644 --- a/Telegram/SourceFiles/ui/boxes/calendar_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/calendar_box.cpp @@ -16,7 +16,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/cached_round_corners.h" #include "ui/layers/generic_box.h" +#include "ui/dynamic_image.h" #include "lang/lang_keys.h" +#include "base/flat_map.h" #include "styles/style_boxes.h" #include "styles/style_chat.h" #include "styles/style_settings.h" @@ -522,10 +524,12 @@ public: QWidget *parent, not_null context, const style::CalendarSizes &st, - const style::CalendarColors &styleColors); + const style::CalendarColors &styleColors, + Fn dynamicImageForDate); [[nodiscard]] int countMaxHeight() const; void setDateChosenCallback(Fn callback); + void setDynamicImage(QDate date, std::shared_ptr image); ~Inner(); @@ -539,6 +543,7 @@ private: void monthChanged(QDate month); void setSelected(int selected); void setPressed(int pressed); + void loadDynamicImages(); int rowsLeft() const; int rowsTop() const; @@ -549,8 +554,11 @@ private: const style::CalendarColors &_styleColors; const not_null _context; bool _twoPressSelectionStarted = false; + Fn _dynamicImageForDate; std::map> _ripples; + base::flat_map> _dynamicImages; + base::flat_set _dynamicImagesRequested; Fn _dateChosenCallback; @@ -640,11 +648,13 @@ CalendarBox::Inner::Inner( QWidget *parent, not_null context, const style::CalendarSizes &st, - const style::CalendarColors &styleColors) + const style::CalendarColors &styleColors, + Fn dynamicImageForDate) : RpWidget(parent) , _st(st) , _styleColors(styleColors) -, _context(context) { +, _context(context) +, _dynamicImageForDate(std::move(dynamicImageForDate)) { setMouseTracking(true); context->monthValue( @@ -661,11 +671,45 @@ CalendarBox::Inner::Inner( void CalendarBox::Inner::monthChanged(QDate month) { setSelected(kEmptySelection); _ripples.clear(); + _dynamicImagesRequested.clear(); + loadDynamicImages(); resizeToCurrent(); update(); SendSynteticMouseEvent(this, QEvent::MouseMove, Qt::NoButton); } +void CalendarBox::Inner::loadDynamicImages() { + if (!_dynamicImageForDate) { + return; + } + const auto currentMonth = _context->month(); + const auto prevMonth = currentMonth.addMonths(-1); + const auto nextMonth = currentMonth.addMonths(1); + + const auto matchesMonth = [&](const QDate &d, const QDate &month) { + return d.year() == month.year() && d.month() == month.month(); + }; + const auto from = -_context->daysShift(); + const auto till = from + _context->rowsCount() * kDaysInWeek; + for (auto i = from; i != till; ++i) { + const auto date = _context->dateFromIndex(i); + if (matchesMonth(date, currentMonth) + || matchesMonth(date, prevMonth) + || matchesMonth(date, nextMonth)) { + if (_dynamicImages.contains(date) + || _dynamicImagesRequested.contains(date)) { + continue; + } + _dynamicImagesRequested.emplace(date); + _dynamicImageForDate( + date, + [=](QDate imageDate, std::shared_ptr image) { + setDynamicImage(imageDate, std::move(image)); + }); + } + } +} + void CalendarBox::Inner::resizeToCurrent() { const auto height = _context->rowsCount() * _st.cellSize.height(); resize(_st.width, _st.padding.top() + height + _st.padding.bottom()); @@ -742,6 +786,14 @@ void CalendarBox::Inner::paintRows(QPainter &p, QRect clip) { const auto enabled = _context->isEnabled(index); const auto innerLeft = x + innerSkipLeft; const auto innerTop = y + innerSkipTop; + const auto date = _context->dateFromIndex(index); + if (const auto it = _dynamicImages.find(date); it != end(_dynamicImages)) { + const auto image = it->second->image(_st.cellInner); + if (!image.isNull()) { + auto hq = PainterHighQualityEnabler(p); + p.drawImage(myrtlrect(innerLeft, innerTop, _st.cellInner, _st.cellInner), image); + } + } if (highlighted) { auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); @@ -904,6 +956,18 @@ void CalendarBox::Inner::setDateChosenCallback(Fn callback) { _dateChosenCallback = std::move(callback); } +void CalendarBox::Inner::setDynamicImage( + QDate date, + std::shared_ptr image) { + if (image) { + _dynamicImages[date] = std::move(image); + _dynamicImages[date]->subscribeToUpdates([=] { update(); }); + } else { + _dynamicImages.remove(date); + } + update(); +} + CalendarBox::Inner::~Inner() = default; class CalendarBox::Title final : public AbstractButton { @@ -1027,7 +1091,8 @@ CalendarBox::CalendarBox(QWidget*, CalendarBoxArgs &&args) this, _context.get(), _st, - _styleColors))) + _styleColors, + std::move(args.dynamicImageForDate)))) , _title(this, _context.get(), _st, _styleColors) , _previous(this, _styleColors.iconButtonPrevious) , _next(this, _styleColors.iconButtonNext) @@ -1131,6 +1196,12 @@ QDate CalendarBox::selectedLastDate() const { return max.has_value() ? _context->dateFromIndex(*max) : QDate(); } +void CalendarBox::setDynamicImage( + QDate date, + std::shared_ptr image) { + _inner->setDynamicImage(date, std::move(image)); +} + void CalendarBox::showJumpTooltip(not_null button) { _tooltipButton = button; Ui::Tooltip::Show(kTooltipDelay, this); diff --git a/Telegram/SourceFiles/ui/boxes/calendar_box.h b/Telegram/SourceFiles/ui/boxes/calendar_box.h index 7d147e9a37..362b9e8c96 100644 --- a/Telegram/SourceFiles/ui/boxes/calendar_box.h +++ b/Telegram/SourceFiles/ui/boxes/calendar_box.h @@ -29,7 +29,9 @@ namespace Ui { class IconButton; class ScrollArea; class CalendarBox; +class DynamicImage; +using CalendarImageSetter = Fn)>; using JumpCallback = Fn close)>; struct CalendarBoxArgs { @@ -48,6 +50,7 @@ struct CalendarBoxArgs { not_null, std::optional)> selectionChanged; const style::CalendarColors &stColors = st::defaultCalendarColors; + Fn dynamicImageForDate; }; class CalendarBox final : public BoxContent, private AbstractTooltipShower { @@ -60,6 +63,8 @@ public: [[nodiscard]] QDate selectedFirstDate() const; [[nodiscard]] QDate selectedLastDate() const; + void setDynamicImage(QDate date, std::shared_ptr image); + protected: void prepare() override; From 0134b928dca89c4b895647d4ddca83eebdc1439a Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 9 Feb 2026 12:22:06 +0300 Subject: [PATCH 006/179] Added controller for calendar search. --- Telegram/CMakeLists.txt | 2 + .../SourceFiles/data/data_search_calendar.cpp | 194 ++++++++++++++++++ .../SourceFiles/data/data_search_calendar.h | 90 ++++++++ 3 files changed, 286 insertions(+) create mode 100644 Telegram/SourceFiles/data/data_search_calendar.cpp create mode 100644 Telegram/SourceFiles/data/data_search_calendar.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index ca6cec7c8a..dad09e835a 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -119,6 +119,8 @@ PRIVATE api/api_chat_participants.h api/api_cloud_password.cpp api/api_cloud_password.h + data/data_search_calendar.h + data/data_search_calendar.cpp api/api_common.cpp api/api_common.h api/api_confirm_phone.cpp diff --git a/Telegram/SourceFiles/data/data_search_calendar.cpp b/Telegram/SourceFiles/data/data_search_calendar.cpp new file mode 100644 index 0000000000..d9f74ac352 --- /dev/null +++ b/Telegram/SourceFiles/data/data_search_calendar.cpp @@ -0,0 +1,194 @@ +/* +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 "data/data_search_calendar.h" + +#include "apiwrap.h" +#include "base/unixtime.h" +#include "data/data_document.h" +#include "data/data_media_types.h" +#include "data/data_peer.h" +#include "data/data_search_controller.h" // PrepareSearchFilter +#include "data/data_session.h" +#include "history/history_item.h" +#include "history/history.h" +#include "main/main_session.h" +#include "ui/dynamic_thumbnails.h" + +namespace Api { + +SearchCalendarController::SearchCalendarController( + not_null session, + Storage::SharedMediaType type) +: _session(session) +, _type(type) { +} + +void SearchCalendarController::monthThumbnails( + PeerId peerId, + TimeId date, + Fn)> onFinish) { + const auto parsed = base::unixtime::parse(date).date(); + const auto key = MonthKey{ + .peerId = peerId, + .year = parsed.year(), + .month = parsed.month(), + }; + + if (const auto it = _months.find(key); it != _months.end()) { + if (!it->second.cache.empty()) { + onFinish(it->second.cache); + return; + } + } + + _months[key].callbacks.push_back(std::move(onFinish)); + + if (!_months[key].requestId) { + performMonthRequest(key); + } +} + +void SearchCalendarController::performMonthRequest(const MonthKey &key) { + const auto peer = _session->data().peer(key.peerId); + const auto filter = PrepareSearchFilter(_type); + + const auto parsed = QDate(key.year, key.month, 1); + const auto endDate = base::unixtime::serialize(QDateTime( + parsed.addMonths(1).addDays(-1), + QTime(23, 59, 59))); + + auto &state = _months[key].state; + + _months[key].requestId = _session->api().request( + MTPmessages_GetSearchResultsCalendar( + MTP_flags(0), + peer->input(), + MTPInputPeer(), + filter, + MTP_int(state.offsetId), + MTP_int(state.offsetDate ? state.offsetDate : endDate) + )).done([=](const MTPmessages_SearchResultsCalendar &result) { + _months[key].requestId = 0; + const auto &data = result.data(); + _session->data().processUsers(data.vusers()); + _session->data().processChats(data.vchats()); + _session->data().processMessages( + data.vmessages(), + NewMessageType::Existing); + + auto messageIds = std::vector(); + messageIds.reserve(data.vmessages().v.size()); + for (const auto &message : data.vmessages().v) { + messageIds.push_back( + FullMsgId(key.peerId, IdFromMessage(message))); + } + + auto &monthState = _months[key].state; + const auto prevOffsetId = monthState.offsetId; + const auto prevOffsetDate = monthState.offsetDate; + monthState.offsetId = data.vmin_msg_id().v; + monthState.offsetDate = data.vmin_date().v; + + const auto noMoreData = (prevOffsetId == monthState.offsetId + && prevOffsetDate == monthState.offsetDate + && prevOffsetId != 0); + + processMonthMessages( + key, + messageIds, + data.vmin_date().v, + data.vmin_msg_id().v, + noMoreData); + }).fail([=] { + auto &data = _months[key]; + data.requestId = 0; + data.cache = {}; + for (const auto &callback : data.callbacks) { + callback({}); + } + data.callbacks.clear(); + }).send(); +} + +void SearchCalendarController::processMonthMessages( + const MonthKey &key, + const std::vector &messages, + TimeId minDate, + MsgId minMsgId, + bool noMoreData) { + auto result = std::vector(); + auto seenDays = base::flat_set(); + const auto peer = _session->data().peer(key.peerId); + const auto history = peer->owner().history(peer); + + const auto targetMonth = QDate(key.year, key.month, 1); + const auto targetStart = base::unixtime::serialize( + QDateTime(targetMonth, QTime())); + const auto targetEnd = base::unixtime::serialize(QDateTime( + targetMonth.addMonths(1).addDays(-1), + QTime(23, 59, 59))); + + for (const auto &fullId : messages) { + const auto item = _session->data().message(fullId); + if (!item) { + continue; + } + + const auto date = item->date(); + if (date < targetStart || date > targetEnd) { + continue; + } + + const auto parsed = base::unixtime::parse(date).date(); + const auto dayStart = base::unixtime::serialize( + QDateTime(parsed, QTime())); + + if (seenDays.contains(dayStart)) { + continue; + } + + const auto media = item->media(); + if (!media) { + continue; + } + + auto image = std::shared_ptr(); + + if (const auto photo = media->photo()) { + image = Ui::MakePhotoThumbnail(photo, item->fullId()); + } else if (const auto document = media->document()) { + if (document->isVideoFile()) { + image = Ui::MakeDocumentThumbnail(document, item->fullId()); + } + } + + if (image) { + seenDays.insert(dayStart); + result.push_back(DayThumbnail{ + .date = dayStart, + .image = std::move(image), + }); + } + } + + if (result.empty() + && minDate < targetStart + && !_months[key].requestId + && !noMoreData) { + performMonthRequest(key); + } else { + auto &data = _months[key]; + data.cache = result; + for (const auto &callback : data.callbacks) { + callback(result); + } + data.callbacks.clear(); + } +} + +} // namespace Api \ No newline at end of file diff --git a/Telegram/SourceFiles/data/data_search_calendar.h b/Telegram/SourceFiles/data/data_search_calendar.h new file mode 100644 index 0000000000..88304a2885 --- /dev/null +++ b/Telegram/SourceFiles/data/data_search_calendar.h @@ -0,0 +1,90 @@ +/* +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 "storage/storage_shared_media.h" + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class DynamicImage; +} // namespace Ui + +namespace Api { + +struct CalendarPeriod { + TimeId date = 0; + MsgId minMsgId = 0; + MsgId maxMsgId = 0; + int count = 0; +}; + +struct CalendarResult { + std::vector periods; + int count = 0; + TimeId minDate = 0; + MsgId minMsgId = 0; +}; + +struct DayThumbnail { + TimeId date = 0; + std::shared_ptr image; +}; + +class SearchCalendarController final { +public: + SearchCalendarController( + not_null session, + Storage::SharedMediaType type); + + void monthThumbnails( + PeerId peerId, + TimeId date, + Fn)> onFinish); + +private: + struct MonthKey { + PeerId peerId = 0; + int year = 0; + int month = 0; + + friend inline auto operator<=>( + const MonthKey &, + const MonthKey &) = default; + }; + + struct MonthState { + MsgId offsetId = 0; + TimeId offsetDate = 0; + }; + + struct MonthData { + std::vector cache; + std::vector)>> callbacks; + mtpRequestId requestId = 0; + MonthState state; + }; + + void performMonthRequest(const MonthKey &key); + void processMonthMessages( + const MonthKey &key, + const std::vector &messages, + TimeId minDate, + MsgId minMsgId, + bool noMoreData); + + const not_null _session; + const Storage::SharedMediaType _type; + + base::flat_map _months; + +}; + +} // namespace Api From 446b3941c86aaac929ded9b50a5790812cbafd6d Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 9 Feb 2026 13:28:13 +0300 Subject: [PATCH 007/179] Added simple animation for dynamic images to calendar box. --- .../SourceFiles/ui/boxes/calendar_box.cpp | 87 +++++++++++++++---- 1 file changed, 72 insertions(+), 15 deletions(-) diff --git a/Telegram/SourceFiles/ui/boxes/calendar_box.cpp b/Telegram/SourceFiles/ui/boxes/calendar_box.cpp index 1a13a4f907..4a8fb7f5a5 100644 --- a/Telegram/SourceFiles/ui/boxes/calendar_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/calendar_box.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/scroll_area.h" #include "ui/widgets/vertical_drum_picker.h" #include "ui/effects/ripple_animation.h" +#include "ui/effects/animations.h" #include "ui/chat/chat_style.h" #include "ui/ui_utility.h" #include "ui/painter.h" @@ -556,9 +557,21 @@ private: bool _twoPressSelectionStarted = false; Fn _dynamicImageForDate; + struct DynamicImageState { + std::shared_ptr image; + bool requested = false; + bool animationFinished = false; + anim::value animation; + crl::time animationStart = 0; + + [[nodiscard]] bool animating() const { + return animationStart > 0; + } + }; + std::map> _ripples; - base::flat_map> _dynamicImages; - base::flat_set _dynamicImagesRequested; + base::flat_map _dynamicImageStates; + Ui::Animations::Basic _animation; Fn _dateChosenCallback; @@ -654,7 +667,30 @@ CalendarBox::Inner::Inner( , _st(st) , _styleColors(styleColors) , _context(context) -, _dynamicImageForDate(std::move(dynamicImageForDate)) { +, _dynamicImageForDate(std::move(dynamicImageForDate)) +, _animation([=](crl::time now) { + auto animating = false; + for (auto &[date, state] : _dynamicImageStates) { + if (!state.animating()) { + continue; + } + const auto dt = std::clamp( + (now - state.animationStart) / float64(st::fadeWrapDuration), + 0., + 1.); + state.animation.update(dt, anim::linear); + if (dt >= 1.) { + state.animationStart = 0; + state.animationFinished = true; + } else { + animating = true; + } + } + if (animating) { + update(); + } + return animating; +}) { setMouseTracking(true); context->monthValue( @@ -671,7 +707,9 @@ CalendarBox::Inner::Inner( void CalendarBox::Inner::monthChanged(QDate month) { setSelected(kEmptySelection); _ripples.clear(); - _dynamicImagesRequested.clear(); + for (auto &[date, state] : _dynamicImageStates) { + state.requested = false; + } loadDynamicImages(); resizeToCurrent(); update(); @@ -696,11 +734,11 @@ void CalendarBox::Inner::loadDynamicImages() { if (matchesMonth(date, currentMonth) || matchesMonth(date, prevMonth) || matchesMonth(date, nextMonth)) { - if (_dynamicImages.contains(date) - || _dynamicImagesRequested.contains(date)) { + auto &state = _dynamicImageStates[date]; + if (state.image || state.requested) { continue; } - _dynamicImagesRequested.emplace(date); + state.requested = true; _dynamicImageForDate( date, [=](QDate imageDate, std::shared_ptr image) { @@ -787,11 +825,21 @@ void CalendarBox::Inner::paintRows(QPainter &p, QRect clip) { const auto innerLeft = x + innerSkipLeft; const auto innerTop = y + innerSkipTop; const auto date = _context->dateFromIndex(index); - if (const auto it = _dynamicImages.find(date); it != end(_dynamicImages)) { - const auto image = it->second->image(_st.cellInner); - if (!image.isNull()) { - auto hq = PainterHighQualityEnabler(p); - p.drawImage(myrtlrect(innerLeft, innerTop, _st.cellInner, _st.cellInner), image); + if (const auto it = _dynamicImageStates.find(date); it != end(_dynamicImageStates)) { + const auto &state = it->second; + if (state.image) { + const auto image = state.image->image(_st.cellInner); + if (!image.isNull()) { + const auto alpha = state.animating() + ? state.animation.current() + : 1.; + if (alpha > 0.) { + auto hq = PainterHighQualityEnabler(p); + p.setOpacity(alpha); + p.drawImage(myrtlrect(innerLeft, innerTop, _st.cellInner, _st.cellInner), image); + p.setOpacity(1.); + } + } } } if (highlighted) { @@ -959,11 +1007,20 @@ void CalendarBox::Inner::setDateChosenCallback(Fn callback) { void CalendarBox::Inner::setDynamicImage( QDate date, std::shared_ptr image) { + auto &state = _dynamicImageStates[date]; if (image) { - _dynamicImages[date] = std::move(image); - _dynamicImages[date]->subscribeToUpdates([=] { update(); }); + state.image = std::move(image); + state.image->subscribeToUpdates([=] { + auto &animState = _dynamicImageStates[date]; + if (!animState.animating() && !state.animationFinished) { + animState.animation = anim::value(0., 1.); + animState.animationStart = crl::now(); + _animation.start(); + } + update(); + }); } else { - _dynamicImages.remove(date); + _dynamicImageStates.remove(date); } update(); } From 2a48c002f9f7ada985e36b8b6c2c0ec0ba945488 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 9 Feb 2026 14:09:10 +0300 Subject: [PATCH 008/179] Slightly improved style of dynamic images in calendar box. --- .../SourceFiles/ui/boxes/calendar_box.cpp | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/Telegram/SourceFiles/ui/boxes/calendar_box.cpp b/Telegram/SourceFiles/ui/boxes/calendar_box.cpp index 4a8fb7f5a5..600fd985fb 100644 --- a/Telegram/SourceFiles/ui/boxes/calendar_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/calendar_box.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/cached_round_corners.h" #include "ui/layers/generic_box.h" #include "ui/dynamic_image.h" +#include "ui/image/image_prepare.h" #include "lang/lang_keys.h" #include "base/flat_map.h" #include "styles/style_boxes.h" @@ -825,18 +826,34 @@ void CalendarBox::Inner::paintRows(QPainter &p, QRect clip) { const auto innerLeft = x + innerSkipLeft; const auto innerTop = y + innerSkipTop; const auto date = _context->dateFromIndex(index); - if (const auto it = _dynamicImageStates.find(date); it != end(_dynamicImageStates)) { + auto dynamicImageProgress = -1.; + if (const auto it = _dynamicImageStates.find(date); + it != end(_dynamicImageStates)) { const auto &state = it->second; if (state.image) { - const auto image = state.image->image(_st.cellInner); + auto image = state.image->image(_st.cellInner); if (!image.isNull()) { + const auto opacity = grayedOut ? 0.5 : 1.; const auto alpha = state.animating() ? state.animation.current() : 1.; + dynamicImageProgress = alpha; if (alpha > 0.) { auto hq = PainterHighQualityEnabler(p); - p.setOpacity(alpha); - p.drawImage(myrtlrect(innerLeft, innerTop, _st.cellInner, _st.cellInner), image); + p.setOpacity(alpha * opacity); + const auto imgRect = myrtlrect( + innerLeft, + innerTop, + _st.cellInner, + _st.cellInner); + p.drawImage( + imgRect, + Images::Circle(std::move(image))); + p.setOpacity(1. * opacity); + p.setPen(Qt::NoPen); + p.setBrush(st::songCoverOverlayFg); + p.drawEllipse(imgRect); + p.setBrush(Qt::NoBrush); p.setOpacity(1.); } } @@ -851,7 +868,9 @@ void CalendarBox::Inner::paintRows(QPainter &p, QRect clip) { } const auto it = _ripples.find(index); if (it != _ripples.cend() && !selectionMode) { - const auto colorOverride = (!highlighted + const auto colorOverride = ((dynamicImageProgress != -1) + ? st::shadowFg + : !highlighted ? _styleColors.rippleColor : grayedOut ? _styleColors.rippleGrayedOutColor @@ -872,6 +891,15 @@ void CalendarBox::Inner::paintRows(QPainter &p, QRect clip) { ? _styleColors.dayTextGrayedOutColor : _styleColors.dayTextColor) : st::windowSubTextFg); + if (dynamicImageProgress != -1) { + auto pen = p.pen(); + pen.setColor( + anim::color( + pen.color(), + st::activeButtonFg->c, + dynamicImageProgress)); + p.setPen(std::move(pen)); + } p.drawText(rect, _context->labelFromIndex(index), style::al_center); } } From b6121a5d64bf1a7cd13d6c34c47035615ed9c9d5 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 9 Feb 2026 14:39:04 +0300 Subject: [PATCH 009/179] Added initial ability to open media calendar from info media sections. --- Telegram/Resources/langs/lang.strings | 2 ++ .../SourceFiles/info/info_wrap_widget.cpp | 2 ++ .../info/media/info_media_widget.cpp | 25 ++++++++++++++ .../info/media/info_media_widget.h | 2 ++ .../window/window_session_controller.cpp | 33 +++++++++++++++++-- .../window/window_session_controller.h | 3 ++ 6 files changed, 64 insertions(+), 3 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 14f0f8d5b4..3347e99365 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7528,6 +7528,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_summarize_header_title" = "AI summary"; "lng_summarize_header_about" = "Click to see original text"; +"lng_calendar" = "Calendar"; + // Wnd specific "lng_wnd_choose_program_menu" = "Choose Default Program..."; diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index e66eeb2816..41df09abb1 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -462,6 +462,8 @@ void WrapWidget::setupTopBarMenuToggle() { button->addClickHandler([=] { _controller->showSettings(::Settings::InformationId()); }); + } else if (section.type() == Section::Type::Media) { + addTopBarMenuButton(); } else if (section.type() == Section::Type::Downloads) { auto &manager = Core::App().downloadManager(); rpl::merge( diff --git a/Telegram/SourceFiles/info/media/info_media_widget.cpp b/Telegram/SourceFiles/info/media/info_media_widget.cpp index 8db825f10e..d6d405e1c0 100644 --- a/Telegram/SourceFiles/info/media/info_media_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_widget.cpp @@ -10,7 +10,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "info/media/info_media_inner_widget.h" #include "info/info_controller.h" +#include "data/data_session.h" #include "main/main_session.h" +#include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/scroll_area.h" #include "ui/search_field_controller.h" #include "ui/ui_utility.h" @@ -20,7 +22,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum_topic.h" #include "data/data_saved_sublist.h" #include "lang/lang_keys.h" +#include "window/window_session_controller.h" #include "styles/style_info.h" +#include "styles/style_menu_icons.h" namespace Info::Media { @@ -152,6 +156,27 @@ void Widget::selectionAction(SelectionAction action) { _inner->selectionAction(action); } +void Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { + const auto type = controller()->section().mediaType(); + if (type != Type::Photo && type != Type::Video) { + return; + } + addAction(tr::lng_calendar(tr::now), [=] { + controller()->parentController()->showCalendar({ + .chat = Dialogs::Key( + controller()->session().data().history( + controller()->key().peer())), + .date = QDate::currentDate(), + .mediaPhoto = (type == Type::Photo), + .mediaVideo = (type == Type::Video), + .customJump = [=](const QDate &date, Fn close) { + _inner->jumpToDate(date, [](auto){}); + close(); + }, + }); + }, &st::menuIconSchedule); +} + rpl::producer Widget::title() { if (controller()->key().peer()->sharedMediaInfo() && isStackBottom()) { return tr::lng_profile_shared_media(); diff --git a/Telegram/SourceFiles/info/media/info_media_widget.h b/Telegram/SourceFiles/info/media/info_media_widget.h index eb3f8de2cb..15483bbf17 100644 --- a/Telegram/SourceFiles/info/media/info_media_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_widget.h @@ -125,6 +125,8 @@ public: rpl::producer selectedListValue() const override; void selectionAction(SelectionAction action) override; + void fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override; + rpl::producer title() override; void jumpToDate(const QDate &date, Fn callback); diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index ad55ed21cb..f9ead8639a 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -61,6 +61,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer_values.h" #include "data/data_premium_limits.h" #include "data/data_web_page.h" +#include "data/data_search_calendar.h" #include "dialogs/ui/chat_search_in.h" #include "passport/passport_form_controller.h" #include "chat_helpers/tabbed_selector.h" @@ -350,9 +351,9 @@ void DateClickHandler::onClick(ClickContext context) const { const auto my = context.other.value(); if (const auto window = my.sessionWindow.get()) { if (!_chat.topic()) { - window->showCalendar({ _chat, _date }); + window->showCalendar({ _chat, _date, true, true }); } else if (const auto strong = _weak.get()) { - window->showCalendar({ strong, _date }); + window->showCalendar({ strong, _date, true, true }); } } } @@ -2810,6 +2811,31 @@ void SessionController::showCalendar(ShowCalendarDescriptor &&descriptor) { button->setPointerCursor(false); } }; + const auto dynamicImageForDate = [&, peerId = history->peer->id] { + using ReturnType = Fn; + if (!descriptor.mediaPhoto && !descriptor.mediaVideo) { + return ReturnType(nullptr); + } + const auto search = std::make_shared( + &session(), + (descriptor.mediaPhoto && descriptor.mediaVideo) + ? Storage::SharedMediaType::PhotoVideo + : descriptor.mediaPhoto + ? Storage::SharedMediaType::Photo + : Storage::SharedMediaType::Video); + return (ReturnType)[=](QDate date, Ui::CalendarImageSetter set) { + search->monthThumbnails( + peerId, + base::unixtime::serialize(QDateTime(date, QTime())), + [=](const std::vector &thumbnails) { + for (const auto &thumb : thumbnails) { + set( + base::unixtime::parse(thumb.date).date(), + thumb.image); + } + }); + }; + }(); const auto weak = base::make_weak(this); const auto weakTopic = base::make_weak(topic); const auto jump = [=](const QDate &date, Fn close) { @@ -2836,11 +2862,12 @@ void SessionController::showCalendar(ShowCalendarDescriptor &&descriptor) { show(Box(Ui::CalendarBoxArgs{ .month = highlighted, .highlighted = highlighted, - .callback = jump, + .callback = descriptor.customJump ? descriptor.customJump : jump, .minDate = minPeerDate, .maxDate = maxPeerDate, .allowsSelection = history->peer->isUser(), .selectionChanged = selectionChanged, + .dynamicImageForDate = dynamicImageForDate, })); } diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 854be6c660..46186e6069 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -545,6 +545,9 @@ public: struct ShowCalendarDescriptor { Dialogs::Key chat; QDate date; + bool mediaPhoto = false; + bool mediaVideo = false; + Fn)> customJump; }; void showCalendar(ShowCalendarDescriptor &&descriptor); From e04b6e9201d0d1a3a2287fc9a772d06672a98074 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 9 Feb 2026 18:15:37 +0300 Subject: [PATCH 010/179] Moved PeerId parameter to SearchCalendarController constructor. --- Telegram/SourceFiles/data/data_search_calendar.cpp | 5 +++-- Telegram/SourceFiles/data/data_search_calendar.h | 3 ++- Telegram/SourceFiles/window/window_session_controller.cpp | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/data/data_search_calendar.cpp b/Telegram/SourceFiles/data/data_search_calendar.cpp index d9f74ac352..31e5464ec6 100644 --- a/Telegram/SourceFiles/data/data_search_calendar.cpp +++ b/Telegram/SourceFiles/data/data_search_calendar.cpp @@ -23,18 +23,19 @@ namespace Api { SearchCalendarController::SearchCalendarController( not_null session, + PeerId peerId, Storage::SharedMediaType type) : _session(session) +, _peerId(peerId) , _type(type) { } void SearchCalendarController::monthThumbnails( - PeerId peerId, TimeId date, Fn)> onFinish) { const auto parsed = base::unixtime::parse(date).date(); const auto key = MonthKey{ - .peerId = peerId, + .peerId = _peerId, .year = parsed.year(), .month = parsed.month(), }; diff --git a/Telegram/SourceFiles/data/data_search_calendar.h b/Telegram/SourceFiles/data/data_search_calendar.h index 88304a2885..eaab6c23c9 100644 --- a/Telegram/SourceFiles/data/data_search_calendar.h +++ b/Telegram/SourceFiles/data/data_search_calendar.h @@ -42,10 +42,10 @@ class SearchCalendarController final { public: SearchCalendarController( not_null session, + PeerId peerId, Storage::SharedMediaType type); void monthThumbnails( - PeerId peerId, TimeId date, Fn)> onFinish); @@ -81,6 +81,7 @@ private: bool noMoreData); const not_null _session; + const PeerId _peerId; const Storage::SharedMediaType _type; base::flat_map _months; diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index f9ead8639a..c6f5a41096 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -2818,6 +2818,7 @@ void SessionController::showCalendar(ShowCalendarDescriptor &&descriptor) { } const auto search = std::make_shared( &session(), + history->peer->id, (descriptor.mediaPhoto && descriptor.mediaVideo) ? Storage::SharedMediaType::PhotoVideo : descriptor.mediaPhoto @@ -2825,7 +2826,6 @@ void SessionController::showCalendar(ShowCalendarDescriptor &&descriptor) { : Storage::SharedMediaType::Video); return (ReturnType)[=](QDate date, Ui::CalendarImageSetter set) { search->monthThumbnails( - peerId, base::unixtime::serialize(QDateTime(date, QTime())), [=](const std::vector &thumbnails) { for (const auto &thumb : thumbnails) { From c0852457470a64cdf3a9af7a50fd6a8f18a4d86f Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 9 Feb 2026 17:30:10 +0300 Subject: [PATCH 011/179] Replaced jump to date method with jump to message in info provider. --- .../downloads/info_downloads_provider.cpp | 2 +- .../info/downloads/info_downloads_provider.h | 2 +- .../info_global_media_provider.cpp | 4 +-- .../global_media/info_global_media_provider.h | 2 +- .../info/media/info_media_common.h | 2 +- .../info/media/info_media_list_widget.cpp | 11 ------- .../info/media/info_media_provider.cpp | 29 ++++++++++--------- .../info/media/info_media_provider.h | 2 +- .../info/saved/info_saved_music_provider.cpp | 2 +- .../info/saved/info_saved_music_provider.h | 2 +- .../info/stories/info_stories_provider.cpp | 2 +- .../info/stories/info_stories_provider.h | 2 +- 12 files changed, 26 insertions(+), 36 deletions(-) diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp b/Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp index 666585bc21..8d5817a817 100644 --- a/Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp +++ b/Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp @@ -115,7 +115,7 @@ void Provider::setSearchQuery(QString query) { _refreshed.fire({}); } -void Provider::jumpToDate(const QDate &date, Fn) { +void Provider::jumpToMessage(MsgId messageId, Fn) { } void Provider::refreshViewer() { diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_provider.h b/Telegram/SourceFiles/info/downloads/info_downloads_provider.h index d3d9d6063b..1a7840b10d 100644 --- a/Telegram/SourceFiles/info/downloads/info_downloads_provider.h +++ b/Telegram/SourceFiles/info/downloads/info_downloads_provider.h @@ -45,7 +45,7 @@ public: rpl::producer<> refreshed() override; void setSearchQuery(QString query) override; - void jumpToDate(const QDate &date, Fn callback) override; + void jumpToMessage(MsgId messageId, Fn callback) override; std::vector fillSections( not_null delegate) override; diff --git a/Telegram/SourceFiles/info/global_media/info_global_media_provider.cpp b/Telegram/SourceFiles/info/global_media/info_global_media_provider.cpp index 7718917c88..b260f7531b 100644 --- a/Telegram/SourceFiles/info/global_media/info_global_media_provider.cpp +++ b/Telegram/SourceFiles/info/global_media/info_global_media_provider.cpp @@ -423,8 +423,8 @@ void Provider::setSearchQuery(QString query) { Unexpected("Media::Provider::setSearchQuery."); } -void Provider::jumpToDate(const QDate &date, Fn) { - Unexpected("GlobalMedia::Provider::jumpToDate."); +void Provider::jumpToMessage(MsgId messageId, Fn) { + Unexpected("GlobalMedia::Provider::jumpToMessage."); } GlobalMediaKey Provider::sliceKey(Data::MessagePosition aroundId) const { diff --git a/Telegram/SourceFiles/info/global_media/info_global_media_provider.h b/Telegram/SourceFiles/info/global_media/info_global_media_provider.h index eb8f968339..7db2f02ca8 100644 --- a/Telegram/SourceFiles/info/global_media/info_global_media_provider.h +++ b/Telegram/SourceFiles/info/global_media/info_global_media_provider.h @@ -91,7 +91,7 @@ public: not_null b) override; void setSearchQuery(QString query) override; - void jumpToDate(const QDate &date, Fn callback) override; + void jumpToMessage(MsgId messageId, Fn callback) override; Media::ListItemSelectionData computeSelectionData( not_null item, diff --git a/Telegram/SourceFiles/info/media/info_media_common.h b/Telegram/SourceFiles/info/media/info_media_common.h index 626c4b592d..d96c1c4734 100644 --- a/Telegram/SourceFiles/info/media/info_media_common.h +++ b/Telegram/SourceFiles/info/media/info_media_common.h @@ -165,7 +165,7 @@ public: not_null document) = 0; virtual void setSearchQuery(QString query) = 0; - virtual void jumpToDate(const QDate &date, Fn callback) = 0; + virtual void jumpToMessage(MsgId messageId, Fn) = 0; [[nodiscard]] virtual int64 scrollTopStatePosition( not_null item) = 0; diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp index 6c3085562d..614734128d 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp @@ -2714,17 +2714,6 @@ ListWidget::~ListWidget() { } void ListWidget::jumpToDate(const QDate &date, Fn c) { - _provider->jumpToDate(date, [=](FullMsgId fullId) { - const auto item = session().data().message(fullId); - if (!item) { - return; - } - _scrollTopState.position = _provider->scrollTopStatePosition(item); - _scrollTopState.item = item; - if (c) { - c(fullId); - } - }); } } // namespace Media diff --git a/Telegram/SourceFiles/info/media/info_media_provider.cpp b/Telegram/SourceFiles/info/media/info_media_provider.cpp index 9ef64abb1a..3b4680bad4 100644 --- a/Telegram/SourceFiles/info/media/info_media_provider.cpp +++ b/Telegram/SourceFiles/info/media/info_media_provider.cpp @@ -343,16 +343,19 @@ void Provider::setSearchQuery(QString query) { Unexpected("Media::Provider::setSearchQuery."); } -void Provider::jumpToDate(const QDate &date, Fn callback) { +void Provider::jumpToMessage( + MsgId messageId, + Fn callback) { _viewerLifetime.destroy(); const auto peer = _controller->session().data().peer(_peer->id); - const auto request = Api::PrepareSearchRequestByDate( + const auto request = Api::PrepareSearchRequest( peer, _topicRootId, _monoforumPeerId, _type, - date, + QString(), + messageId, Data::LoadDirection::Around); if (!request) { @@ -362,20 +365,18 @@ void Provider::jumpToDate(const QDate &date, Fn callback) { _controller->session().api().request( std::move(*request) ).done([=](const Api::SearchRequestResult &result) { - const auto byDate = Api::ParseSearchResultByDate( + const auto parsed = Api::ParseSearchResult( peer, - date, - Api::ParseSearchResult( - peer, - _type, - 0, - Data::LoadDirection::Around, - result)); + _type, + messageId, + Data::LoadDirection::Around, + result); - if (byDate.first) { - _universalAroundId = GetUniversalId(byDate.first.fullId); + if (!parsed.messageIds.empty()) { + const auto fullId = FullMsgId(_peer->id, messageId); + _universalAroundId = GetUniversalId(fullId); if (callback) { - callback(byDate.closestToDate.fullId); + callback(fullId); } _idsLimit = kMinimalIdsLimit * 2; refreshViewer(); diff --git a/Telegram/SourceFiles/info/media/info_media_provider.h b/Telegram/SourceFiles/info/media/info_media_provider.h index 9805e0964f..2237921638 100644 --- a/Telegram/SourceFiles/info/media/info_media_provider.h +++ b/Telegram/SourceFiles/info/media/info_media_provider.h @@ -48,7 +48,7 @@ public: void setSearchQuery(QString query) override; - void jumpToDate(const QDate &date, Fn callback) override; + void jumpToMessage(MsgId, Fn done) override; ListItemSelectionData computeSelectionData( not_null item, diff --git a/Telegram/SourceFiles/info/saved/info_saved_music_provider.cpp b/Telegram/SourceFiles/info/saved/info_saved_music_provider.cpp index e7d4ae5701..e34ec4f393 100644 --- a/Telegram/SourceFiles/info/saved/info_saved_music_provider.cpp +++ b/Telegram/SourceFiles/info/saved/info_saved_music_provider.cpp @@ -162,7 +162,7 @@ void MusicProvider::checkPreload( void MusicProvider::setSearchQuery(QString query) { } -void MusicProvider::jumpToDate(const QDate &date, Fn) { +void MusicProvider::jumpToMessage(MsgId messageId, Fn) { } void MusicProvider::refreshViewer() { diff --git a/Telegram/SourceFiles/info/saved/info_saved_music_provider.h b/Telegram/SourceFiles/info/saved/info_saved_music_provider.h index 0733b4b861..67dc4af511 100644 --- a/Telegram/SourceFiles/info/saved/info_saved_music_provider.h +++ b/Telegram/SourceFiles/info/saved/info_saved_music_provider.h @@ -50,7 +50,7 @@ public: rpl::producer<> refreshed() override; void setSearchQuery(QString query) override; - void jumpToDate(const QDate &date, Fn callback) override; + void jumpToMessage(MsgId, Fn done) override; std::vector fillSections( not_null delegate) override; diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp index 2dd55462c6..aea06d76b2 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp +++ b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp @@ -177,7 +177,7 @@ void Provider::checkPreload( void Provider::setSearchQuery(QString query) { } -void Provider::jumpToDate(const QDate &date, Fn) { +void Provider::jumpToMessage(MsgId messageId, Fn) { } void Provider::refreshViewer() { diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.h b/Telegram/SourceFiles/info/stories/info_stories_provider.h index d1f4f78ff6..3dc7601f60 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_provider.h +++ b/Telegram/SourceFiles/info/stories/info_stories_provider.h @@ -53,7 +53,7 @@ public: rpl::producer<> refreshed() override; void setSearchQuery(QString query) override; - void jumpToDate(const QDate &date, Fn callback) override; + void jumpToMessage(MsgId, Fn done) override; std::vector fillSections( not_null delegate) override; From f777ad8d8ced67107a4547200e487551621b225a Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 9 Feb 2026 18:03:06 +0300 Subject: [PATCH 012/179] Replaced process of date in media calendar with offset message id. --- .../SourceFiles/data/data_search_calendar.cpp | 27 +++++++++++++++++ .../SourceFiles/data/data_search_calendar.h | 3 ++ .../info/media/info_media_inner_widget.cpp | 4 +-- .../info/media/info_media_inner_widget.h | 2 +- .../info/media/info_media_list_widget.cpp | 8 ++++- .../info/media/info_media_list_widget.h | 2 +- .../info/media/info_media_widget.cpp | 8 ++--- .../info/media/info_media_widget.h | 2 -- .../window/window_session_controller.cpp | 30 +++++++++++++++---- .../window/window_session_controller.h | 2 +- 10 files changed, 68 insertions(+), 20 deletions(-) diff --git a/Telegram/SourceFiles/data/data_search_calendar.cpp b/Telegram/SourceFiles/data/data_search_calendar.cpp index 31e5464ec6..74bcf6ff0e 100644 --- a/Telegram/SourceFiles/data/data_search_calendar.cpp +++ b/Telegram/SourceFiles/data/data_search_calendar.cpp @@ -173,6 +173,7 @@ void SearchCalendarController::processMonthMessages( result.push_back(DayThumbnail{ .date = dayStart, .image = std::move(image), + .msgId = fullId.msg, }); } } @@ -192,4 +193,30 @@ void SearchCalendarController::processMonthMessages( } } +std::optional SearchCalendarController::resolveMsgIdByDate( + TimeId date) const { + const auto parsed = base::unixtime::parse(date).date(); + const auto key = MonthKey{ + .peerId = _peerId, + .year = parsed.year(), + .month = parsed.month(), + }; + + const auto it = _months.find(key); + if (it == _months.end() || it->second.cache.empty()) { + return std::nullopt; + } + + const auto dayStart = base::unixtime::serialize( + QDateTime(parsed, QTime())); + + for (const auto &thumb : it->second.cache) { + if (thumb.date == dayStart) { + return thumb.msgId; + } + } + + return std::nullopt; +} + } // namespace Api \ No newline at end of file diff --git a/Telegram/SourceFiles/data/data_search_calendar.h b/Telegram/SourceFiles/data/data_search_calendar.h index eaab6c23c9..7648608213 100644 --- a/Telegram/SourceFiles/data/data_search_calendar.h +++ b/Telegram/SourceFiles/data/data_search_calendar.h @@ -36,6 +36,7 @@ struct CalendarResult { struct DayThumbnail { TimeId date = 0; std::shared_ptr image; + MsgId msgId = 0; }; class SearchCalendarController final { @@ -49,6 +50,8 @@ public: TimeId date, Fn)> onFinish); + [[nodiscard]] std::optional resolveMsgIdByDate(TimeId date) const; + private: struct MonthKey { PeerId peerId = 0; diff --git a/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp b/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp index b7c983afb4..bcb35fad99 100644 --- a/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp @@ -245,8 +245,8 @@ rpl::producer InnerWidget::scrollToRequests() const { return _scrollToRequests.events(); } -void InnerWidget::jumpToDate(const QDate &date, Fn c) { - _list->jumpToDate(date, std::move(c)); +void InnerWidget::jumpToMessage(MsgId msgId) { + _list->jumpToMessage(msgId); } } // namespace Media diff --git a/Telegram/SourceFiles/info/media/info_media_inner_widget.h b/Telegram/SourceFiles/info/media/info_media_inner_widget.h index ff3782f72d..14aa7be140 100644 --- a/Telegram/SourceFiles/info/media/info_media_inner_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_inner_widget.h @@ -49,7 +49,7 @@ public: rpl::producer selectedListValue() const; void selectionAction(SelectionAction action); - void jumpToDate(const QDate &date, Fn callback); + void jumpToMessage(MsgId msgId); ~InnerWidget(); diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp index 614734128d..21645e860a 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp @@ -2713,7 +2713,13 @@ ListWidget::~ListWidget() { } } -void ListWidget::jumpToDate(const QDate &date, Fn c) { +void ListWidget::jumpToMessage(MsgId msgId) { + _provider->jumpToMessage(msgId, [=](FullMsgId fullId) { + if (const auto i = session().data().message(fullId)) { + _scrollTopState.position = _provider->scrollTopStatePosition(i); + _scrollTopState.item = i; + } + }); } } // namespace Media diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.h b/Telegram/SourceFiles/info/media/info_media_list_widget.h index 175deb9d30..fbc25875ec 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.h @@ -87,7 +87,7 @@ public: void saveState(not_null memento); void restoreState(not_null memento); - void jumpToDate(const QDate &date, Fn callback); + void jumpToMessage(MsgId msgId); // Overview::Layout::Delegate void registerHeavyItem(not_null item) override; diff --git a/Telegram/SourceFiles/info/media/info_media_widget.cpp b/Telegram/SourceFiles/info/media/info_media_widget.cpp index d6d405e1c0..6b431296c3 100644 --- a/Telegram/SourceFiles/info/media/info_media_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_widget.cpp @@ -169,8 +169,8 @@ void Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { .date = QDate::currentDate(), .mediaPhoto = (type == Type::Photo), .mediaVideo = (type == Type::Video), - .customJump = [=](const QDate &date, Fn close) { - _inner->jumpToDate(date, [](auto){}); + .customJump = [=](MsgId msgId, Fn close) { + _inner->jumpToMessage(msgId); close(); }, }); @@ -223,8 +223,4 @@ void Widget::restoreState(not_null memento) { _inner->restoreState(memento); } -void Widget::jumpToDate(const QDate &date, Fn c) { - _inner->jumpToDate(date, std::move(c)); -} - } // namespace Info::Media diff --git a/Telegram/SourceFiles/info/media/info_media_widget.h b/Telegram/SourceFiles/info/media/info_media_widget.h index 15483bbf17..3274a1a0e3 100644 --- a/Telegram/SourceFiles/info/media/info_media_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_widget.h @@ -129,8 +129,6 @@ public: rpl::producer title() override; - void jumpToDate(const QDate &date, Fn callback); - private: void saveState(not_null memento); void restoreState(not_null memento); diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index c6f5a41096..f1fbe1b9d9 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -2811,10 +2811,15 @@ void SessionController::showCalendar(ShowCalendarDescriptor &&descriptor) { button->setPointerCursor(false); } }; - const auto dynamicImageForDate = [&, peerId = history->peer->id] { - using ReturnType = Fn; + struct SearchCalendarResult { + Fn factory; + Fn)> customJump; + }; + const auto searchCalendarResult = [&]() -> SearchCalendarResult { + using Factory = Fn; + using CustomJump = Fn)>; if (!descriptor.mediaPhoto && !descriptor.mediaVideo) { - return ReturnType(nullptr); + return {}; } const auto search = std::make_shared( &session(), @@ -2824,7 +2829,7 @@ void SessionController::showCalendar(ShowCalendarDescriptor &&descriptor) { : descriptor.mediaPhoto ? Storage::SharedMediaType::Photo : Storage::SharedMediaType::Video); - return (ReturnType)[=](QDate date, Ui::CalendarImageSetter set) { + const auto factory = [=](QDate date, Ui::CalendarImageSetter set) { search->monthThumbnails( base::unixtime::serialize(QDateTime(date, QTime())), [=](const std::vector &thumbnails) { @@ -2835,6 +2840,17 @@ void SessionController::showCalendar(ShowCalendarDescriptor &&descriptor) { } }); }; + auto customJump = CustomJump(nullptr); + if (const auto performJump = descriptor.customJump) { + customJump = [=](const QDate &d, Fn close) { + const auto date = base::unixtime::serialize( + QDateTime(d, QTime())); + if (const auto msgId = search->resolveMsgIdByDate(date)) { + performJump(*msgId, close); + } + }; + } + return { Factory(factory), std::move(customJump) }; }(); const auto weak = base::make_weak(this); const auto weakTopic = base::make_weak(topic); @@ -2862,12 +2878,14 @@ void SessionController::showCalendar(ShowCalendarDescriptor &&descriptor) { show(Box(Ui::CalendarBoxArgs{ .month = highlighted, .highlighted = highlighted, - .callback = descriptor.customJump ? descriptor.customJump : jump, + .callback = searchCalendarResult.customJump + ? std::move(searchCalendarResult.customJump) + : jump, .minDate = minPeerDate, .maxDate = maxPeerDate, .allowsSelection = history->peer->isUser(), .selectionChanged = selectionChanged, - .dynamicImageForDate = dynamicImageForDate, + .dynamicImageForDate = std::move(searchCalendarResult.factory), })); } diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 46186e6069..5ed494e7ad 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -547,7 +547,7 @@ public: QDate date; bool mediaPhoto = false; bool mediaVideo = false; - Fn)> customJump; + Fn)> customJump; }; void showCalendar(ShowCalendarDescriptor &&descriptor); From dfd5df4265e90ad569c32c8370fb7ecff206af3e Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 9 Feb 2026 18:06:15 +0300 Subject: [PATCH 013/179] Removed unused search functions for calendar. --- .../data/data_search_controller.cpp | 74 ------------------- .../SourceFiles/data/data_search_controller.h | 18 ----- 2 files changed, 92 deletions(-) diff --git a/Telegram/SourceFiles/data/data_search_controller.cpp b/Telegram/SourceFiles/data/data_search_controller.cpp index f5cd92180c..2d01942c56 100644 --- a/Telegram/SourceFiles/data/data_search_controller.cpp +++ b/Telegram/SourceFiles/data/data_search_controller.cpp @@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_search_controller.h" -#include "base/unixtime.h" #include "main/main_session.h" #include "data/data_session.h" #include "data/data_messages.h" @@ -335,79 +334,6 @@ HistoryResult ParseHistoryResult( data); } -std::optional PrepareSearchRequestByDate( - not_null peer, - MsgId topicRootId, - PeerId monoforumPeerId, - Storage::SharedMediaType type, - const QDate &date, - Data::LoadDirection direction) { - const auto filter = PrepareSearchFilter(type); - const auto minDate = base::unixtime::serialize( - QDateTime(date, QTime(0, 0))); - const auto maxDate = base::unixtime::serialize( - QDateTime(date.addDays(1), QTime(0, 0))) - 1; - const auto limit = kSharedMediaLimit; - const auto offsetId = 0; - const auto addOffset = 0; - const auto hash = uint64(0); - - using Flag = MTPmessages_Search::Flag; - return MTPmessages_Search( - MTP_flags((topicRootId ? Flag::f_top_msg_id : Flag(0)) - | (monoforumPeerId ? Flag::f_saved_peer_id : Flag(0))), - peer->input(), - MTP_string(""), - MTP_inputPeerEmpty(), - (monoforumPeerId - ? peer->owner().peer(monoforumPeerId)->input() - : MTPInputPeer()), - MTPVector(), - MTP_int(topicRootId), - filter, - MTP_int(minDate), - MTP_int(maxDate), - MTP_int(offsetId), - MTP_int(addOffset), - MTP_int(limit), - MTP_int(0), - MTP_int(0), - MTP_long(hash)); -} - -SearchResultByDate ParseSearchResultByDate( - not_null peer, - const QDate &date, - const SearchResult &parsed) { - const auto targetDate = base::unixtime::serialize( - QDateTime(date, QTime(0, 0))); - auto result = SearchResultByDate(); - if (parsed.messageIds.empty()) { - return result; - } - const auto firstMsgId = parsed.messageIds.front(); - result.first = Data::MessagePosition{ - .fullId = FullMsgId(peer->id, firstMsgId), - .date = TimeId(0), - }; - auto closestMsgId = firstMsgId; - auto minDiff = std::numeric_limits::max(); - for (const auto msgId : parsed.messageIds) { - if (const auto item = peer->owner().message(peer->id, msgId)) { - const auto diff = std::abs(item->date() - targetDate); - if (diff < minDiff) { - minDiff = diff; - closestMsgId = msgId; - } - } - } - result.closestToDate = Data::MessagePosition{ - .fullId = FullMsgId(peer->id, closestMsgId), - .date = TimeId(0), - }; - return result; -} - SearchController::CacheEntry::CacheEntry( not_null session, const Query &query) diff --git a/Telegram/SourceFiles/data/data_search_controller.h b/Telegram/SourceFiles/data/data_search_controller.h index a4d5690360..e7fb3f8e9f 100644 --- a/Telegram/SourceFiles/data/data_search_controller.h +++ b/Telegram/SourceFiles/data/data_search_controller.h @@ -44,11 +44,6 @@ struct GlobalMediaResult { int fullCount = 0; }; -struct SearchResultByDate { - Data::MessagePosition first; - Data::MessagePosition closestToDate; -}; - [[nodiscard]] MTPMessagesFilter PrepareSearchFilter( Storage::SharedMediaType type); @@ -79,19 +74,6 @@ struct SearchResultByDate { Data::LoadDirection direction, const SearchRequestResult &data); -[[nodiscard]] std::optional PrepareSearchRequestByDate( - not_null peer, - MsgId topicRootId, - PeerId monoforumPeerId, - Storage::SharedMediaType type, - const QDate &date, - Data::LoadDirection direction); - -[[nodiscard]] SearchResultByDate ParseSearchResultByDate( - not_null peer, - const QDate &date, - const SearchResult &parsed); - [[nodiscard]] HistoryRequest PrepareHistoryRequest( not_null peer, MsgId messageId, From 5d9271ea86d1a3ac3d11699e09de2e63e37b1157 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 9 Feb 2026 18:25:16 +0300 Subject: [PATCH 014/179] Added requireImage arg to restrict calendar clicks to dates with images. --- Telegram/SourceFiles/ui/boxes/calendar_box.cpp | 14 ++++++++++++++ Telegram/SourceFiles/ui/boxes/calendar_box.h | 1 + .../window/window_session_controller.cpp | 2 ++ 3 files changed, 17 insertions(+) diff --git a/Telegram/SourceFiles/ui/boxes/calendar_box.cpp b/Telegram/SourceFiles/ui/boxes/calendar_box.cpp index 600fd985fb..77d50a88a8 100644 --- a/Telegram/SourceFiles/ui/boxes/calendar_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/calendar_box.cpp @@ -532,6 +532,7 @@ public: [[nodiscard]] int countMaxHeight() const; void setDateChosenCallback(Fn callback); void setDynamicImage(QDate date, std::shared_ptr image); + void setRequireImage(bool require); ~Inner(); @@ -556,6 +557,7 @@ private: const style::CalendarColors &_styleColors; const not_null _context; bool _twoPressSelectionStarted = false; + bool _requireImage = false; Fn _dynamicImageForDate; struct DynamicImageState { @@ -945,6 +947,13 @@ void CalendarBox::Inner::setSelected(int selected) { if (selected != kEmptySelection && !_context->isEnabled(selected)) { selected = kEmptySelection; } + if (selected != kEmptySelection && _requireImage) { + const auto date = _context->dateFromIndex(selected); + const auto it = _dynamicImageStates.find(date); + if (it == end(_dynamicImageStates) || !it->second.image) { + selected = kEmptySelection; + } + } _selected = selected; const auto pointer = (_selected != kEmptySelection); const auto force = (_mouseMoved && _cursorSetWithoutMouseMove); @@ -1053,6 +1062,10 @@ void CalendarBox::Inner::setDynamicImage( update(); } +void CalendarBox::Inner::setRequireImage(bool require) { + _requireImage = require; +} + CalendarBox::Inner::~Inner() = default; class CalendarBox::Title final : public AbstractButton { @@ -1185,6 +1198,7 @@ CalendarBox::CalendarBox(QWidget*, CalendarBoxArgs &&args) , _finalize(std::move(args.finalize)) , _jumpTimer([=] { jump(_jumpButton); }) , _selectionChanged(std::move(args.selectionChanged)) { + _inner->setRequireImage(args.requireImage); _context->setAllowsSelection(args.allowsSelection); _context->setMinDate(args.minDate); _context->setMaxDate(args.maxDate); diff --git a/Telegram/SourceFiles/ui/boxes/calendar_box.h b/Telegram/SourceFiles/ui/boxes/calendar_box.h index 362b9e8c96..cf65aee6f6 100644 --- a/Telegram/SourceFiles/ui/boxes/calendar_box.h +++ b/Telegram/SourceFiles/ui/boxes/calendar_box.h @@ -51,6 +51,7 @@ struct CalendarBoxArgs { std::optional)> selectionChanged; const style::CalendarColors &stColors = st::defaultCalendarColors; Fn dynamicImageForDate; + bool requireImage = false; }; class CalendarBox final : public BoxContent, private AbstractTooltipShower { diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index f1fbe1b9d9..07de157154 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -2875,6 +2875,7 @@ void SessionController::showCalendar(ShowCalendarDescriptor &&descriptor) { session().api().resolveJumpToDate(chat, date, open); } }; + const auto requireImage = !!searchCalendarResult.customJump; show(Box(Ui::CalendarBoxArgs{ .month = highlighted, .highlighted = highlighted, @@ -2886,6 +2887,7 @@ void SessionController::showCalendar(ShowCalendarDescriptor &&descriptor) { .allowsSelection = history->peer->isUser(), .selectionChanged = selectionChanged, .dynamicImageForDate = std::move(searchCalendarResult.factory), + .requireImage = requireImage, })); } From c6817a60cb981bc9762a13d14098c1f6fd0f7e19 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 10 Feb 2026 17:30:17 +0300 Subject: [PATCH 015/179] Added ability to take written caption from SendFilesBox on cancel. --- Telegram/SourceFiles/boxes/send_files_box.cpp | 17 ++++++++++++++++- Telegram/SourceFiles/boxes/send_files_box.h | 6 ++++++ Telegram/SourceFiles/history/history_widget.cpp | 3 +++ .../history/view/history_view_chat_section.cpp | 3 +++ .../view/history_view_scheduled_section.cpp | 3 +++ .../business/settings_shortcut_messages.cpp | 3 +++ 6 files changed, 34 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index f3dd804918..7718db922e 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -755,7 +755,10 @@ void SendFilesBox::refreshButtons() { &_st.tabbed.menu, &_st.tabbed.icons); } - addButton(tr::lng_cancel(), [=] { closeBox(); }); + addButton(tr::lng_cancel(), [=] { + requestToTakeTextWithTags(); + closeBox(); + }); _addFile = addLeftButton( tr::lng_stickers_featured_add(), base::fn_delayed(st::historyAttach.ripple.hideDuration, this, [=] { @@ -1097,6 +1100,7 @@ void SendFilesBox::pushBlock(int from, int till) { } // Just close the box if it is the only one. if (_list.files.size() == 1) { + requestToTakeTextWithTags(); closeBox(); return; } @@ -1456,6 +1460,7 @@ void SendFilesBox::setupCaption() { }, _caption->lifetime()); _caption->cancelled( ) | rpl::on_next([=] { + requestToTakeTextWithTags(); closeBox(); }, _caption->lifetime()); _caption->setMimeDataHook([=]( @@ -1856,6 +1861,16 @@ void SendFilesBox::showFinished() { } } +rpl::producer SendFilesBox::takeTextWithTagsRequests() const { + return _textWithTagsRequests.events(); +} + +void SendFilesBox::requestToTakeTextWithTags() const { + if (_caption && !_caption->isHidden()) { + _textWithTagsRequests.fire_copy(_caption->getTextWithTags()); + } +} + void SendFilesBox::setInnerFocus() { if (_caption && !_caption->isHidden()) { _caption->setFocusFast(); diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index 92ba0e3c31..4d2db047e5 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -127,6 +127,8 @@ public: _cancelledCallback = std::move(callback); } + [[nodiscard]] rpl::producer takeTextWithTagsRequests() const; + void showFinished() override; ~SendFilesBox(); @@ -248,6 +250,8 @@ private: void checkCharsLimitation(); void refreshMessagesCount(); + void requestToTakeTextWithTags() const; + [[nodiscard]] Fn prepareSendMenuDetails( const SendFilesBoxDescriptor &descriptor); [[nodiscard]] auto prepareSendMenuCallback() @@ -307,4 +311,6 @@ private: QPointer _send; QPointer _addFile; + rpl::event_stream _textWithTagsRequests; + }; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index ed1e80a4b2..4e54c3bbf7 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -6493,6 +6493,9 @@ bool HistoryWidget::confirmSendingFiles( _field->textCursor().insertText(insertTextOnCancel); } })); + box->takeTextWithTagsRequests() | rpl::on_next([=](TextWithTags &&text) { + _field->setTextWithTags(std::move(text)); + }, box->lifetime()); Window::ActivateWindow(controller()); controller()->show(std::move(box)); diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index a2ea2b964b..fdbc38b1e1 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -1176,6 +1176,9 @@ bool ChatWidget::confirmSendingFiles( })); box->setCancelledCallback(_composeControls->restoreTextCallback( insertTextOnCancel)); + box->takeTextWithTagsRequests() | rpl::on_next([=](TextWithTags &&text) { + _composeControls->setText(std::move(text)); + }, box->lifetime()); //ActivateWindow(controller()); controller()->show(std::move(box)); diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index d6b169307d..df507c71d9 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -581,6 +581,9 @@ bool ScheduledWidget::confirmSendingFiles( })); box->setCancelledCallback(_composeControls->restoreTextCallback( insertTextOnCancel)); + box->takeTextWithTagsRequests() | rpl::on_next([=](TextWithTags &&text) { + _composeControls->setText(std::move(text)); + }, box->lifetime()); //ActivateWindow(controller()); controller()->show(std::move(box)); diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 49f0cd440b..e566df58e6 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -1374,6 +1374,9 @@ bool ShortcutMessages::confirmSendingFiles( })); box->setCancelledCallback(_composeControls->restoreTextCallback( insertTextOnCancel)); + box->takeTextWithTagsRequests() | rpl::on_next([=](TextWithTags &&text) { + _composeControls->setText(std::move(text)); + }, box->lifetime()); //ActivateWindow(_controller); _controller->show(std::move(box)); From b36d165fc30cac40a536d59f81b762f465c982aa Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 10 Feb 2026 18:26:52 +0300 Subject: [PATCH 016/179] Added ability to cancel forward in history widget with Esc key. --- Telegram/SourceFiles/history/history_widget.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 4e54c3bbf7..ea539b6bcd 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -9331,6 +9331,8 @@ void HistoryWidget::escape() { } else { cancelEdit(); } + } else if (readyToForward() && _history) { + _history->setForwardDraft(MsgId(), PeerId(), {}); } else if (_autocomplete && !_autocomplete->isHidden()) { _autocomplete->hideAnimated(); } else if ((_replyTo || _suggestOptions) From b915198205816e58388b46d52451e9c6a79f1241 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 10 Feb 2026 19:07:47 +0300 Subject: [PATCH 017/179] Added event filter to handle Esc key for reaction preview. --- .../history/view/history_view_reaction_preview.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Telegram/SourceFiles/history/view/history_view_reaction_preview.cpp b/Telegram/SourceFiles/history/view/history_view_reaction_preview.cpp index fd19a4daa7..74d7fc4935 100644 --- a/Telegram/SourceFiles/history/view/history_view_reaction_preview.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reaction_preview.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_reaction_preview.h" #include "base/call_delayed.h" +#include "base/event_filter.h" #include "boxes/sticker_set_box.h" #include "data/data_document.h" #include "data/data_message_reactions.h" @@ -76,6 +77,18 @@ bool ShowReactionPreview( })); }; state->clickable->setClickedCallback(hideAll); + base::install_event_filter(QCoreApplication::instance(), [=]( + not_null e) { + if (e->type() == QEvent::KeyPress + && state->clickable->window()->isActiveWindow()) { + const auto k = static_cast(e.get()); + if (k->key() == Qt::Key_Escape) { + hideAll(); + return base::EventFilterResult::Cancel; + } + } + return base::EventFilterResult::Continue; + }, state->clickable->lifetime()); state->mediaPreview->showPreview(origin, document); state->clickable->show(); const auto mediaPreviewRaw = state->mediaPreview.get(); From 4732d7810e8c514f60c19c675bd75d6b2a1021e7 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 11 Feb 2026 09:22:52 +0300 Subject: [PATCH 018/179] Improved fix for verified icon and gift status show both in profiles. Related commit: d51dd1cdb5. --- .../SourceFiles/info/profile/info_profile_badge.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/info/profile/info_profile_badge.cpp b/Telegram/SourceFiles/info/profile/info_profile_badge.cpp index 545787f3b0..7a997f1543 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_badge.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_badge.cpp @@ -180,9 +180,11 @@ void Badge::setContent(Content content) { p, badge->rect().marginsRemoved({ skip, skip, skip, skip }), badge->width(), - (type == Ui::TextBadgeType::Direct - ? st::windowSubTextFg - : st::attentionButtonFg)); + _overrideSt + ? _overrideSt->premiumFg + : (type == Ui::TextBadgeType::Direct + ? st::windowSubTextFg + : st::attentionButtonFg)); }, _view->lifetime()); } break; } @@ -257,7 +259,7 @@ rpl::producer BadgeContentForPeer(not_null peer) { BadgeValue(peer), EmojiStatusIdValue(peer) ) | rpl::map([=](BadgeType badge, EmojiStatusId emojiStatusId) { - if (emojiStatusId.collectible) { + if (emojiStatusId.collectible && (badge == BadgeType::Verified)) { return Badge::Content{ BadgeType::Premium, emojiStatusId }; } if (badge == BadgeType::Verified) { From b49f2a71873b1d3c7a807c64a0f847222337df56 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 11 Feb 2026 18:31:09 +0300 Subject: [PATCH 019/179] Disabled drag and drop of dialogs to folders when there are no folders. --- Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index c59f69a57e..8be11abb74 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -1701,7 +1701,7 @@ void InnerWidget::mouseMoveEvent(QMouseEvent *e) { } void InnerWidget::performDrag() { - if (!_qdragging) { + if (!_qdragging || !session().data().chatsFilters().has()) { return; } const auto history = _qdragging->history(); From 93542f89fb858a61cbe3804c35d7a35be38be1a8 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 11 Feb 2026 18:40:21 +0300 Subject: [PATCH 020/179] Used separate drag thresholds for pinned and unpinned dialogs. --- .../SourceFiles/dialogs/dialogs_inner_widget.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 8be11abb74..00b002a9a2 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -97,7 +97,8 @@ namespace { constexpr auto kHashtagResultsLimit = 5; constexpr auto kStartReorderThreshold = 30; -constexpr auto kStartDragToFilterThreshold = 35; +constexpr auto kStartDragToFilterThresholdX = kStartReorderThreshold; +constexpr auto kStartDragToFilterThresholdY = 75; constexpr auto kQueryPreviewLimit = 32; constexpr auto kPreviewPostsLimit = 3; @@ -1679,8 +1680,13 @@ void InnerWidget::mouseMoveEvent(QMouseEvent *e) { if (_pressed && (e->buttons() & Qt::LeftButton)) { const auto local = e->pos(); const auto outside = _dragging ? !rect().contains(local) : true; - const auto distanceExceeded = (local - _dragStart).manhattanLength() - >= style::ConvertScale(kStartDragToFilterThreshold); + const auto delta = local - _dragStart; + const auto thresholdY = _pressed->entry()->isPinnedDialog(_filterId) + ? kStartDragToFilterThresholdY + : kStartDragToFilterThresholdX; + const auto distanceExceeded = std::abs(delta.x()) + >= style::ConvertScale(kStartDragToFilterThresholdX) + || std::abs(delta.y()) >= style::ConvertScale(thresholdY); if (!_qdragging && outside && distanceExceeded) { if (_pressed->history()) { From 6387a1591c8b585ad39e03f0301f5ee480141aa7 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 11 Feb 2026 18:44:25 +0300 Subject: [PATCH 021/179] Added profile link as text data when dragging dialog with username. --- Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 00b002a9a2..f4a703c998 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -1727,6 +1727,10 @@ void InnerWidget::performDrag() { u"application/x-telegram-dialog"_q, std::move(byteArray)); + if (const auto u = history->peer->username(); !u.isEmpty()) { + mimeData->setText(history->peer->session().createInternalLinkFull(u)); + } + const auto &st = st::defaultDialogRow; auto pixmap = QPixmap(Size(st.height * style::DevicePixelRatio())); pixmap.setDevicePixelRatio(style::DevicePixelRatio()); From 95387d58b0a53852cb46947b8ea61bf1ee19d0c2 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 11 Feb 2026 19:13:04 +0300 Subject: [PATCH 022/179] Added ability to drag dialogs to input message fields. --- .../chat_helpers/message_field.cpp | 19 +++++++++++++++++++ .../SourceFiles/chat_helpers/message_field.h | 4 ++++ .../dialogs/dialogs_inner_widget.cpp | 3 +++ .../SourceFiles/history/history_widget.cpp | 4 ++-- .../history_view_compose_controls.cpp | 3 ++- 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index a7bed86483..dec17c47b3 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -1504,3 +1504,22 @@ void FrozenInfoBox( button->resizeToWidth(buttonWidth); }, button->lifetime()); } + +Ui::InputField::MimeDataHook WrappedMessageFieldMimeHook( + Ui::InputField::MimeDataHook original, + not_null field) { + return [field, originalHook = std::move(original)]( + not_null data, + Ui::InputField::MimeAction action) { + if (data->hasFormat(u"application/x-telegram-input-field"_q)) { + if (action == Ui::InputField::MimeAction::Check) { + return true; + } + const auto text = QString::fromUtf8( + data->data(u"application/x-telegram-input-field"_q)); + field->textCursor().insertText(text); + return true; + } + return originalHook ? originalHook(data, action) : false; + }; +} diff --git a/Telegram/SourceFiles/chat_helpers/message_field.h b/Telegram/SourceFiles/chat_helpers/message_field.h index 4d7ef52b48..5f9512b4e4 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.h +++ b/Telegram/SourceFiles/chat_helpers/message_field.h @@ -216,3 +216,7 @@ void FrozenInfoBox( not_null box, not_null session, FreezeInfoStyleOverride st); + +[[nodiscard]] Ui::InputField::MimeDataHook WrappedMessageFieldMimeHook( + Ui::InputField::MimeDataHook original, + not_null field); diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index f4a703c998..eab59fda66 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -1729,6 +1729,9 @@ void InnerWidget::performDrag() { if (const auto u = history->peer->username(); !u.isEmpty()) { mimeData->setText(history->peer->session().createInternalLinkFull(u)); + mimeData->setData( + u"application/x-telegram-input-field"_q, + ('@' + u).toUtf8()); } const auto &st = st::defaultDialogRow; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index ea539b6bcd..9d004743eb 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -528,7 +528,7 @@ HistoryWidget::HistoryWidget( supportInitAutocomplete(); } _field->rawTextEdit()->installEventFilter(this); - _field->setMimeDataHook([=]( + _field->setMimeDataHook(WrappedMessageFieldMimeHook([=]( not_null data, Ui::InputField::MimeAction action) { if (action == Ui::InputField::MimeAction::Check) { @@ -540,7 +540,7 @@ HistoryWidget::HistoryWidget( Core::ReadMimeText(data)); } Unexpected("action in MimeData hook."); - }); + }, _field)); updateFieldSubmitSettings(); 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 cbcb886d08..3be1f9896a 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -1803,7 +1803,8 @@ rpl::producer> ComposeControls::attachRequests() const { } void ComposeControls::setMimeDataHook(MimeDataHook hook) { - _field->setMimeDataHook(std::move(hook)); + _field->setMimeDataHook( + WrappedMessageFieldMimeHook(std::move(hook), _field)); } bool ComposeControls::confirmMediaEdit(Ui::PreparedList &list) { From 359675b44642f3ffb2bc3e5d35fcc5e16278d2bf Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 11 Feb 2026 19:29:51 +0300 Subject: [PATCH 023/179] Added ability to drag dialogs to search field. --- .../SourceFiles/boxes/choose_filter_box.cpp | 37 ++++++++++--------- .../SourceFiles/boxes/choose_filter_box.h | 4 ++ .../SourceFiles/dialogs/dialogs_widget.cpp | 15 ++++++++ 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.cpp b/Telegram/SourceFiles/boxes/choose_filter_box.cpp index e7842c0e69..28b71c15e1 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/choose_filter_box.cpp @@ -470,28 +470,29 @@ bool FillChooseFilterWithAdminedGroupsMenu( return added; } +History *HistoryFromMimeData( + const QMimeData *mime, + not_null session) { + const auto mimeFormat = u"application/x-telegram-dialog"_q; + if (!mime->hasFormat(mimeFormat)) { + return nullptr; + } + auto peerId = int64(-1); + auto isTestMode = false; + auto stream = QDataStream(mime->data(mimeFormat)); + stream >> peerId; + stream >> isTestMode; + if (isTestMode != session->isTestMode()) { + return nullptr; + } + return session->data().historyLoaded(PeerId(peerId)); +} + void SetupFilterDragAndDrop( not_null outer, not_null session, Fn(QPoint)> filterIdAtPosition, Fn activeFilterId) { - const auto mimeFormat = u"application/x-telegram-dialog"_q; - const auto peerIdFromMime = [=](const QMimeData *mimeData) { - auto peerId = int64(-1); - auto isTestMode = false; - if (mimeData->hasFormat(mimeFormat)) { - auto stream = QDataStream(mimeData->data(mimeFormat)); - stream >> peerId; - stream >> isTestMode; - if (isTestMode != session->isTestMode()) { - return int64(-1); - } - } - return peerId; - }; - const auto historyFromMime = [=](const QMimeData *mime) { - return session->data().historyLoaded(PeerId(peerIdFromMime(mime))); - }; const auto hasAction = [=](not_null drop, bool perform) { const auto mimeData = drop->mimeData(); const auto filterId = filterIdAtPosition( @@ -500,7 +501,7 @@ void SetupFilterDragAndDrop( return false; } const auto id = *filterId; - if (const auto h = historyFromMime(mimeData)) { + if (const auto h = HistoryFromMimeData(mimeData, session)) { auto v = ChooseFilterValidator(h); if (id) { if (v.canAdd(id)) { diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.h b/Telegram/SourceFiles/boxes/choose_filter_box.h index 99492c6ba6..4bcfd3ddcc 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.h +++ b/Telegram/SourceFiles/boxes/choose_filter_box.h @@ -63,3 +63,7 @@ void SetupFilterDragAndDrop( not_null session, Fn(QPoint)> filterIdAtPosition, Fn activeFilterId); + +[[nodiscard]] History *HistoryFromMimeData( + const QMimeData *mime, + not_null session); diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 7307807051..7250b50c3d 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_requests_bar.h" #include "history/view/history_view_top_bar_widget.h" #include "boxes/peers/edit_peer_requests_box.h" +#include "boxes/choose_filter_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" #include "ui/widgets/chat_filters_tabs_strip.h" @@ -550,6 +551,20 @@ Widget::Widget( _search->submits( ) | rpl::on_next([=] { submit(); }, _search->lifetime()); + _search->setMimeDataHook([=]( + not_null data, + Ui::InputField::MimeAction action) { + if (data->hasFormat(u"application/x-telegram-dialog"_q)) { + if (const auto history = HistoryFromMimeData(data, &session())) { + if (action != Ui::InputField::MimeAction::Check) { + controller->searchInChat(history); + } + return true; + } + } + return false; + }); + QObject::connect( _search->rawTextEdit().get(), &QTextEdit::cursorPositionChanged, From 51178d7dd76f51e286f3ed783a1e74844acf171c Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 11 Feb 2026 19:39:00 +0300 Subject: [PATCH 024/179] Added ability to add peer to filter by link or username. --- .../SourceFiles/boxes/choose_filter_box.cpp | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.cpp b/Telegram/SourceFiles/boxes/choose_filter_box.cpp index 28b71c15e1..2f68bdaca6 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/choose_filter_box.cpp @@ -474,18 +474,31 @@ History *HistoryFromMimeData( const QMimeData *mime, not_null session) { const auto mimeFormat = u"application/x-telegram-dialog"_q; - if (!mime->hasFormat(mimeFormat)) { - return nullptr; + if (mime->hasFormat(mimeFormat)) { + auto peerId = int64(-1); + auto isTestMode = false; + auto stream = QDataStream(mime->data(mimeFormat)); + stream >> peerId; + stream >> isTestMode; + if (isTestMode != session->isTestMode()) { + return nullptr; + } + return session->data().historyLoaded(PeerId(peerId)); } - auto peerId = int64(-1); - auto isTestMode = false; - auto stream = QDataStream(mime->data(mimeFormat)); - stream >> peerId; - stream >> isTestMode; - if (isTestMode != session->isTestMode()) { - return nullptr; + if (mime->hasText()) { + auto text = mime->text().trimmed(); + if (text.startsWith('@')) { + text = text.mid(1); + } else if (text.startsWith(u"https://t.me/"_q)) { + text = text.mid(13); + } else { + return nullptr; + } + if (const auto peer = session->data().peerByUsername(text)) { + return session->data().historyLoaded(peer->id); + } } - return session->data().historyLoaded(PeerId(peerId)); + return nullptr; } void SetupFilterDragAndDrop( From cb136366222d121d7774bfaaae01a134999bb320 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 12 Feb 2026 07:34:30 +0300 Subject: [PATCH 025/179] Fixed profile gifts button showing empty before recent gifts loaded. --- Telegram/SourceFiles/info/media/info_media_buttons.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.cpp b/Telegram/SourceFiles/info/media/info_media_buttons.cpp index a4ad07931e..ef01a3a1f5 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.cpp +++ b/Telegram/SourceFiles/info/media/info_media_buttons.cpp @@ -324,6 +324,7 @@ not_null AddPeerGiftsButton( rpl::event_stream<> textRefreshed; QPointer button; rpl::lifetime appearedLifetime; + bool giftsLoaded = false; }; const auto state = parent->lifetime().make_state(); @@ -364,7 +365,13 @@ not_null AddPeerGiftsButton( .customEmojiLoopLimit = 1, })))); wrap->setDuration(st::infoSlideDuration); - wrap->toggleOn(rpl::duplicate(forked) | rpl::map(rpl::mappers::_1 > 0)); + wrap->toggleOn( + rpl::combine( + rpl::duplicate(forked), + state->textRefreshed.events_starting_with({}) + ) | rpl::map([=](int count, auto) { + return count > 0 && state->giftsLoaded; + })); tracker.track(wrap); rpl::duplicate(forked) | rpl::filter( @@ -380,6 +387,7 @@ not_null AddPeerGiftsButton( gift.info.document->id, refresh)); } + state->giftsLoaded = true; state->textRefreshed.fire({}); }); navigation->session().recentSharedGifts().request(peer, requestDone); From 0543491bab413b47dc800852f5e6daa8133fb683 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 12 Feb 2026 07:43:10 +0300 Subject: [PATCH 026/179] Guarded api requests in search calendar controller. --- Telegram/SourceFiles/data/data_search_calendar.cpp | 5 +++-- Telegram/SourceFiles/data/data_search_calendar.h | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/data/data_search_calendar.cpp b/Telegram/SourceFiles/data/data_search_calendar.cpp index 74bcf6ff0e..513683fa00 100644 --- a/Telegram/SourceFiles/data/data_search_calendar.cpp +++ b/Telegram/SourceFiles/data/data_search_calendar.cpp @@ -27,7 +27,8 @@ SearchCalendarController::SearchCalendarController( Storage::SharedMediaType type) : _session(session) , _peerId(peerId) -, _type(type) { +, _type(type) +, _api(&session->mtp()) { } void SearchCalendarController::monthThumbnails( @@ -65,7 +66,7 @@ void SearchCalendarController::performMonthRequest(const MonthKey &key) { auto &state = _months[key].state; - _months[key].requestId = _session->api().request( + _months[key].requestId = _api.request( MTPmessages_GetSearchResultsCalendar( MTP_flags(0), peer->input(), diff --git a/Telegram/SourceFiles/data/data_search_calendar.h b/Telegram/SourceFiles/data/data_search_calendar.h index 7648608213..3fcd4758c8 100644 --- a/Telegram/SourceFiles/data/data_search_calendar.h +++ b/Telegram/SourceFiles/data/data_search_calendar.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "mtproto/sender.h" #include "storage/storage_shared_media.h" namespace Main { @@ -87,6 +88,8 @@ private: const PeerId _peerId; const Storage::SharedMediaType _type; + MTP::Sender _api; + base::flat_map _months; }; From c66c670ce3b3301f22f0a02d6afe45c1c9be7caf Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 14 Feb 2026 08:51:27 +0300 Subject: [PATCH 027/179] Added initial support of touch scroll to history admin log section. --- .../admin_log/history_admin_log_inner.cpp | 241 +++++++++++++++++- .../admin_log/history_admin_log_inner.h | 26 ++ 2 files changed, 266 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp index e2fea82f4e..5c47c4976f 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/translate_box.h" #include "ui/boxes/confirm_box.h" #include "base/platform/base_platform_info.h" +#include "base/qt/qt_common_adapters.h" #include "base/qt/qt_key_modifiers.h" #include "base/unixtime.h" #include "base/call_delayed.h" @@ -262,7 +263,9 @@ InnerWidget::InnerWidget( st::historyAdminLogEmptyWidth - st::historyAdminLogEmptyPadding.left() - st::historyAdminLogEmptyPadding.left()) -, _antiSpamValidator(_controller, _channel) { +, _antiSpamValidator(_controller, _channel) +, _touchSelectTimer([=] { onTouchSelect(); }) +, _touchScrollTimer([=] { onTouchScrollTimer(); }) { Window::ChatThemeValueFromPeer( controller, channel @@ -271,6 +274,7 @@ InnerWidget::InnerWidget( controller->setChatStyleTheme(_theme); }, lifetime()); + setAttribute(Qt::WA_AcceptTouchEvents); setMouseTracking(true); _scrollDateHideTimer.setCallback([=] { scrollDateHideByTimer(); }); session().data().viewRepaintRequest( @@ -2182,6 +2186,241 @@ QPoint InnerWidget::mapPointToItem(QPoint point, const Element *view) const { return point - QPoint(0, itemTop(view)); } +bool InnerWidget::eventHook(QEvent *e) { + if (e->type() == QEvent::TouchBegin + || e->type() == QEvent::TouchUpdate + || e->type() == QEvent::TouchEnd + || e->type() == QEvent::TouchCancel) { + QTouchEvent *ev = static_cast(e); + if (ev->device()->type() == base::TouchDevice::TouchScreen) { + touchEvent(ev); + return true; + } + } + return RpWidget::eventHook(e); +} + +void InnerWidget::onTouchSelect() { + _touchSelect = true; + mouseActionStart(_touchPos, Qt::LeftButton); +} + +void InnerWidget::onTouchScrollTimer() { + auto nowTime = crl::now(); + if (_touchScrollState == Ui::TouchScrollState::Acceleration + && _touchWaitingAcceleration + && (nowTime - _touchAccelerationTime) > 40) { + _touchScrollState = Ui::TouchScrollState::Manual; + touchResetSpeed(); + } else if (_touchScrollState == Ui::TouchScrollState::Auto + || _touchScrollState == Ui::TouchScrollState::Acceleration) { + int32 elapsed = int32(nowTime - _touchTime); + QPoint delta = _touchSpeed * elapsed / 1000; + bool hasScrolled = !delta.isNull(); + + if (_touchSpeed.isNull() || !hasScrolled) { + _touchScrollState = Ui::TouchScrollState::Manual; + _touchScroll = false; + _touchScrollTimer.cancel(); + } else { + _touchTime = nowTime; + } + touchDeaccelerate(elapsed); + } +} + +void InnerWidget::touchUpdateSpeed() { + const auto nowTime = crl::now(); + if (_touchPrevPosValid) { + const int elapsed = nowTime - _touchSpeedTime; + if (elapsed) { + const QPoint newPixelDiff = (_touchPos - _touchPrevPos); + const QPoint pixelsPerSecond = newPixelDiff * (1000 / elapsed); + + const int newSpeedY = (qAbs(pixelsPerSecond.y()) + > Ui::kFingerAccuracyThreshold) + ? pixelsPerSecond.y() + : 0; + const int newSpeedX = (qAbs(pixelsPerSecond.x()) + > Ui::kFingerAccuracyThreshold) + ? pixelsPerSecond.x() + : 0; + if (_touchScrollState == Ui::TouchScrollState::Auto) { + const int oldSpeedY = _touchSpeed.y(); + const int oldSpeedX = _touchSpeed.x(); + if ((oldSpeedY <= 0 && newSpeedY <= 0) || ((oldSpeedY >= 0 && newSpeedY >= 0) + && (oldSpeedX <= 0 && newSpeedX <= 0)) || (oldSpeedX >= 0 && newSpeedX >= 0)) { + _touchSpeed.setY(std::clamp( + (oldSpeedY + (newSpeedY / 4)), + -Ui::kMaxScrollAccelerated, + +Ui::kMaxScrollAccelerated)); + _touchSpeed.setX(std::clamp( + (oldSpeedX + (newSpeedX / 4)), + -Ui::kMaxScrollAccelerated, + +Ui::kMaxScrollAccelerated)); + } else { + _touchSpeed = QPoint(); + } + } else { + if (!_touchSpeed.isNull()) { + _touchSpeed.setX(std::clamp( + (_touchSpeed.x() / 4) + (newSpeedX * 3 / 4), + -Ui::kMaxScrollFlick, + +Ui::kMaxScrollFlick)); + _touchSpeed.setY(std::clamp( + (_touchSpeed.y() / 4) + (newSpeedY * 3 / 4), + -Ui::kMaxScrollFlick, + +Ui::kMaxScrollFlick)); + } else { + _touchSpeed = QPoint(newSpeedX, newSpeedY); + } + } + } + } else { + _touchPrevPosValid = true; + } + _touchSpeedTime = nowTime; + _touchPrevPos = _touchPos; +} + +void InnerWidget::touchResetSpeed() { + _touchSpeed = QPoint(); + _touchPrevPosValid = false; +} + +void InnerWidget::touchDeaccelerate(int32 elapsed) { + int32 x = _touchSpeed.x(); + int32 y = _touchSpeed.y(); + _touchSpeed.setX((x == 0) + ? x + : (x > 0) + ? qMax(0, x - elapsed) + : qMin(0, x + elapsed)); + _touchSpeed.setY((y == 0) + ? y + : (y > 0) + ? qMax(0, y - elapsed) + : qMin(0, y + elapsed)); +} + +void InnerWidget::touchEvent(QTouchEvent *e) { + if (e->type() == QEvent::TouchCancel) { + if (!_touchInProgress) { + return; + } + _touchInProgress = false; + _touchSelectTimer.cancel(); + _touchScroll = _touchSelect = false; + _touchScrollState = Ui::TouchScrollState::Manual; + mouseActionCancel(); + return; + } + + if (!e->touchPoints().isEmpty()) { + _touchPrevPos = _touchPos; + _touchPos = e->touchPoints().cbegin()->screenPos().toPoint(); + } + + switch (e->type()) { + case QEvent::TouchBegin: { + if (_menu) { + e->accept(); + return; + } + if (_touchInProgress || e->touchPoints().isEmpty()) { + return; + } + + _touchInProgress = true; + if (_touchScrollState == Ui::TouchScrollState::Auto) { + _touchScrollState = Ui::TouchScrollState::Acceleration; + _touchWaitingAcceleration = true; + _touchAccelerationTime = crl::now(); + touchUpdateSpeed(); + _touchStart = _touchPos; + } else { + _touchScroll = false; + _touchSelectTimer.callOnce(QApplication::startDragTime()); + } + _touchSelect = false; + _touchStart = _touchPrevPos = _touchPos; + } break; + + case QEvent::TouchUpdate: { + if (!_touchInProgress) { + return; + } else if (_touchSelect) { + mouseActionUpdate(_touchPos); + } else if (!_touchScroll + && (_touchPos - _touchStart).manhattanLength() + >= QApplication::startDragDistance()) { + _touchSelectTimer.cancel(); + _touchScroll = true; + touchUpdateSpeed(); + } + if (_touchScroll) { + if (_touchScrollState == Ui::TouchScrollState::Manual) { + touchScrollUpdated(_touchPos); + } else if (_touchScrollState == Ui::TouchScrollState::Acceleration) { + touchUpdateSpeed(); + _touchAccelerationTime = crl::now(); + if (_touchSpeed.isNull()) { + _touchScrollState = Ui::TouchScrollState::Manual; + } + } + } + } break; + + case QEvent::TouchEnd: { + if (!_touchInProgress) { + return; + } + _touchInProgress = false; + const auto notMoved = (_touchPos - _touchStart).manhattanLength() + < QApplication::startDragDistance(); + auto weak = base::make_weak(this); + if (_touchSelect) { + if (notMoved) { + mouseActionFinish(_touchPos, Qt::RightButton); + auto contextMenu = QContextMenuEvent( + QContextMenuEvent::Mouse, + mapFromGlobal(_touchPos), + _touchPos); + showContextMenu(&contextMenu, true); + } + _touchScroll = false; + } else if (_touchScroll) { + if (_touchScrollState == Ui::TouchScrollState::Manual) { + _touchScrollState = Ui::TouchScrollState::Auto; + _touchPrevPosValid = false; + _touchScrollTimer.callEach(15); + _touchTime = crl::now(); + } else if (_touchScrollState == Ui::TouchScrollState::Auto) { + _touchScrollState = Ui::TouchScrollState::Manual; + _touchScroll = false; + touchResetSpeed(); + } else if (_touchScrollState == Ui::TouchScrollState::Acceleration) { + _touchScrollState = Ui::TouchScrollState::Auto; + _touchWaitingAcceleration = false; + _touchPrevPosValid = false; + } + } else if (notMoved) { + mouseActionStart(_touchPos, Qt::LeftButton); + mouseActionFinish(_touchPos, Qt::LeftButton); + } + if (weak) { + _touchSelectTimer.cancel(); + _touchSelect = false; + } + } break; + } +} + +void InnerWidget::touchScrollUpdated(const QPoint &screenPos) { + _touchPos = screenPos; + touchUpdateSpeed(); +} + InnerWidget::~InnerWidget() = default; } // namespace AdminLog diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h index 15d5b5f02d..028b05c9c2 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/rp_widget.h" #include "ui/effects/animations.h" #include "ui/widgets/tooltip.h" +#include "ui/widgets/scroll_area.h" #include "mtproto/sender.h" #include "base/timer.h" @@ -165,6 +166,7 @@ protected: void enterEventHook(QEnterEvent *e) override; void leaveEventHook(QEvent *e) override; void contextMenuEvent(QContextMenuEvent *e) override; + bool eventHook(QEvent *e) override; // Resizes content and counts natural widget height for the desired width. int resizeGetHeight(int newWidth) override; @@ -275,6 +277,14 @@ private: template void enumerateDates(Method method); + void touchEvent(QTouchEvent *e); + void touchScrollUpdated(const QPoint &screenPos); + void touchResetSpeed(); + void touchUpdateSpeed(); + void touchDeaccelerate(int32 elapsed); + void onTouchSelect(); + void onTouchScrollTimer(); + const not_null _controller; const not_null _channel; const not_null _history; @@ -341,6 +351,22 @@ private: QPoint _trippleClickPoint; base::Timer _trippleClickTimer; + base::Timer _touchSelectTimer; + base::Timer _touchScrollTimer; + + // Touch scroll support. + bool _touchScroll = false; + bool _touchSelect = false; + bool _touchInProgress = false; + QPoint _touchStart, _touchPrevPos, _touchPos; + Ui::TouchScrollState _touchScrollState = Ui::TouchScrollState::Manual; + bool _touchPrevPosValid = false; + bool _touchWaitingAcceleration = false; + QPoint _touchSpeed; + crl::time _touchSpeedTime = 0; + crl::time _touchAccelerationTime = 0; + crl::time _touchTime = 0; + FilterValue _filter; QString _searchQuery; std::vector> _admins; From 8c86a6e55ebd09750f5001a6d2fd74cc9dee33de Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sun, 15 Feb 2026 09:59:05 +0300 Subject: [PATCH 028/179] Disabled dialogs drag to filters while dragging among pinned dialogs. --- Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index eab59fda66..baa3cb7ed5 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -1679,7 +1679,7 @@ void InnerWidget::mouseMoveEvent(QMouseEvent *e) { if (_pressed && (e->buttons() & Qt::LeftButton)) { const auto local = e->pos(); - const auto outside = _dragging ? !rect().contains(local) : true; + const auto outside = _dragging ? false : true; const auto delta = local - _dragStart; const auto thresholdY = _pressed->entry()->isPinnedDialog(_filterId) ? kStartDragToFilterThresholdY From 4c5ca97e755dd2806f40a0f7bb41a42645728af4 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sun, 15 Feb 2026 10:51:39 +0300 Subject: [PATCH 029/179] Slightly improved media animation in calendar box. --- .../SourceFiles/ui/boxes/calendar_box.cpp | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/Telegram/SourceFiles/ui/boxes/calendar_box.cpp b/Telegram/SourceFiles/ui/boxes/calendar_box.cpp index 77d50a88a8..9948901691 100644 --- a/Telegram/SourceFiles/ui/boxes/calendar_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/calendar_box.cpp @@ -689,9 +689,7 @@ CalendarBox::Inner::Inner( animating = true; } } - if (animating) { - update(); - } + update(); return animating; }) { setMouseTracking(true); @@ -831,8 +829,15 @@ void CalendarBox::Inner::paintRows(QPainter &p, QRect clip) { auto dynamicImageProgress = -1.; if (const auto it = _dynamicImageStates.find(date); it != end(_dynamicImageStates)) { - const auto &state = it->second; + auto &state = it->second; if (state.image) { + if (!state.animating() && !state.animationFinished) { + state.animation = anim::value(0., 1.); + state.animationStart = crl::now(); + if (!_animation.animating()) { + _animation.start(); + } + } auto image = state.image->image(_st.cellInner); if (!image.isNull()) { const auto opacity = grayedOut ? 0.5 : 1.; @@ -851,7 +856,6 @@ void CalendarBox::Inner::paintRows(QPainter &p, QRect clip) { p.drawImage( imgRect, Images::Circle(std::move(image))); - p.setOpacity(1. * opacity); p.setPen(Qt::NoPen); p.setBrush(st::songCoverOverlayFg); p.drawEllipse(imgRect); @@ -1047,15 +1051,7 @@ void CalendarBox::Inner::setDynamicImage( auto &state = _dynamicImageStates[date]; if (image) { state.image = std::move(image); - state.image->subscribeToUpdates([=] { - auto &animState = _dynamicImageStates[date]; - if (!animState.animating() && !state.animationFinished) { - animState.animation = anim::value(0., 1.); - animState.animationStart = crl::now(); - _animation.start(); - } - update(); - }); + state.image->subscribeToUpdates([=] { update(); }); } else { _dynamicImageStates.remove(date); } From b9f1c21b5e515fc11d887d87674ebac815db9817 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sun, 15 Feb 2026 10:52:13 +0300 Subject: [PATCH 030/179] Fixed position of popup menu from More button in shared media info. --- Telegram/SourceFiles/info/info.style | 2 +- Telegram/SourceFiles/info/info_wrap_widget.cpp | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index e686949527..49da24caf3 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -354,7 +354,7 @@ infoLayerTopBar: InfoTopBar(infoTopBar) { radius: boxRadius; } -infoLayerTopBarMenuPosition: point(40px, 37px); +infoLayerTopBarMenuPosition: point(48px, 37px); infoTopBarColoredBack: IconButton(infoTopBarBack) { icon: icon {{ "info/info_back", groupCallMembersFg }}; diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 41df09abb1..99730a1f00 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -611,8 +611,9 @@ void WrapWidget::showTopBarMenu(bool check) { } _topBarMenu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); _topBarMenuToggle->setForceRippled(true); - _topBarMenu->popup(_topBarMenuToggle->mapToGlobal( - st::infoLayerTopBarMenuPosition)); + _topBarMenu->popup(Ui::PopupMenu::ConstrainToParentScreen( + _topBarMenu, + _topBarMenuToggle->mapToGlobal(st::infoLayerTopBarMenuPosition))); } bool WrapWidget::requireTopBarSearch() const { From a8a3c1555b1d95e242d6990054edf346bb8ff31e Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sun, 15 Feb 2026 11:47:58 +0300 Subject: [PATCH 031/179] Added highlight filter button from filters container on dialog drag. --- .../SourceFiles/boxes/choose_filter_box.cpp | 7 ++++++- .../SourceFiles/boxes/choose_filter_box.h | 3 ++- .../ui/widgets/chat_filters_tabs_strip.cpp | 15 +++++++++++-- .../ui/widgets/discrete_sliders.cpp | 21 +++++++++++++++++++ .../SourceFiles/ui/widgets/discrete_sliders.h | 1 + .../window/window_filters_menu.cpp | 7 ++++++- 6 files changed, 49 insertions(+), 5 deletions(-) diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.cpp b/Telegram/SourceFiles/boxes/choose_filter_box.cpp index 2f68bdaca6..304d6d5248 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/choose_filter_box.cpp @@ -505,7 +505,8 @@ void SetupFilterDragAndDrop( not_null outer, not_null session, Fn(QPoint)> filterIdAtPosition, - Fn activeFilterId) { + Fn activeFilterId, + Fn selectByFilterId) { const auto hasAction = [=](not_null drop, bool perform) { const auto mimeData = drop->mimeData(); const auto filterId = filterIdAtPosition( @@ -522,6 +523,7 @@ void SetupFilterDragAndDrop( if (perform) { v.add(id); } + selectByFilterId(perform ? FilterId(-1) : id); return true; } } @@ -531,10 +533,12 @@ void SetupFilterDragAndDrop( if (perform) { v.remove(active); } + selectByFilterId(perform ? FilterId(-1) : active); return true; } } } + selectByFilterId(-1); return false; }; outer->setAcceptDrops(true); @@ -560,6 +564,7 @@ void SetupFilterDragAndDrop( dm->ignore(); } } else if (e->type() == QEvent::DragLeave) { + selectByFilterId(-1); } else if (e->type() == QEvent::Drop) { const auto drop = static_cast(e.get()); if (hasAction(drop, true)) { diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.h b/Telegram/SourceFiles/boxes/choose_filter_box.h index 4bcfd3ddcc..59cff96333 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.h +++ b/Telegram/SourceFiles/boxes/choose_filter_box.h @@ -62,7 +62,8 @@ void SetupFilterDragAndDrop( not_null outer, not_null session, Fn(QPoint)> filterIdAtPosition, - Fn activeFilterId); + Fn activeFilterId, + Fn selectByFilterId); [[nodiscard]] History *HistoryFromMimeData( const QMimeData *mime, diff --git a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.cpp b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.cpp index 74f93e835d..001d7a9a9e 100644 --- a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.cpp +++ b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.cpp @@ -310,7 +310,8 @@ not_null AddChatFiltersTabsStrip( ? slider->lookupSectionLeft(i + 1) : slider->width(); if (x >= left && x < right) { - const auto &list = session->data().chatsFilters().list(); + const auto &list + = session->data().chatsFilters().list(); return (i < list.size()) ? list[i].id() : FilterId(); @@ -318,7 +319,17 @@ not_null AddChatFiltersTabsStrip( } return std::nullopt; }, - [=] { return state->lastFilterId.value_or(FilterId()); }); + [=] { return state->lastFilterId.value_or(FilterId()); }, + [=](FilterId id) { + const auto &list = session->data().chatsFilters().list(); + for (auto i = 0; i < list.size(); i++) { + if (list[i].id() == id) { + slider->selectSection(i); + return; + } + } + slider->selectSection(-1); + }); } wrap->toggle(false, anim::type::instant); scroll->setCustomWheelProcess([=](not_null e) { diff --git a/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp b/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp index 3c80be1720..702d760e33 100644 --- a/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp +++ b/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp @@ -58,6 +58,27 @@ void DiscreteSlider::finishAnimating() { } } +void DiscreteSlider::selectSection(int index) { + if (index < 0 || index >= _sections.size()) { + for (auto &other : _sections) { + if (other.ripple) { + other.ripple->lastStop(); + } + } + return; + } + auto §ion = _sections[index]; + if (section.ripple && !section.ripple->empty()) { + return; + } + for (auto &other : _sections) { + if (other.ripple) { + other.ripple->lastStop(); + } + } + startRipple(index); +} + void DiscreteSlider::setAdditionalContentWidthToSection(int index, int w) { if (index >= 0 && index < _sections.size()) { auto §ion = _sections[index]; diff --git a/Telegram/SourceFiles/ui/widgets/discrete_sliders.h b/Telegram/SourceFiles/ui/widgets/discrete_sliders.h index 6c7e6405bb..bda6cc7735 100644 --- a/Telegram/SourceFiles/ui/widgets/discrete_sliders.h +++ b/Telegram/SourceFiles/ui/widgets/discrete_sliders.h @@ -45,6 +45,7 @@ public: void setActiveSection(int index); void setActiveSectionFast(int index); void finishAnimating(); + void selectSection(int index); void setAdditionalContentWidthToSection(int index, int width); diff --git a/Telegram/SourceFiles/window/window_filters_menu.cpp b/Telegram/SourceFiles/window/window_filters_menu.cpp index 3cf6af2e40..8dbbc40f83 100644 --- a/Telegram/SourceFiles/window/window_filters_menu.cpp +++ b/Telegram/SourceFiles/window/window_filters_menu.cpp @@ -145,7 +145,12 @@ void FiltersMenu::setupDragAndDrop() { } return std::nullopt; }, - [=] { return _activeFilterId; }); + [=] { return _activeFilterId; }, + [=](FilterId filterId) { + for (const auto &[id, button] : _filters) { + button->setForceRippled(id == filterId); + } + }); } void FiltersMenu::setupMainMenuIcon() { From 4cdea65aa5b631fcd188fb2039ff22c01a9127b7 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sun, 15 Feb 2026 12:10:35 +0300 Subject: [PATCH 032/179] Added support of rpl name to color sample for your color buttons. --- .../SourceFiles/boxes/peers/edit_peer_color_box.cpp | 13 +++++++++---- Telegram/SourceFiles/ui/peer/color_sample.cpp | 10 ++++++---- Telegram/SourceFiles/ui/peer/color_sample.h | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index e41de5d728..06afcfee6c 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -2637,7 +2637,10 @@ void SetupPeerColorSample( ) | rpl::map([=] { return peer->emojiStatusId(); }); - const auto name = peer->shortName(); + auto name = peer->session().changes().peerFlagsValue( + peer, + Data::PeerUpdate::Flag::Name + ) | rpl::map([=] { return peer->shortName(); }); const auto sampleSize = st::settingsColorSampleSize; @@ -2648,7 +2651,7 @@ void SetupPeerColorSample( style, rpl::duplicate(colorIndexValue), rpl::duplicate(colorCollectibleValue), - name); + rpl::duplicate(name)); sample->show(); struct ProfileSampleState { @@ -2681,13 +2684,15 @@ void SetupPeerColorSample( rpl::duplicate(label), rpl::duplicate(colorIndexValue), rpl::duplicate(colorProfileIndexValue), - rpl::duplicate(emojiStatusIdValue) + rpl::duplicate(emojiStatusIdValue), + rpl::duplicate(name) ) | rpl::on_next([=]( int width, const QString &buttonText, int colorIndex, std::optional profileIndex, - EmojiStatusId emojiStatusId) { + EmojiStatusId emojiStatusId, + const QString &name) { const auto available = width - st::settingsButton.padding.left() - (st::settingsColorButton.padding.right() - sampleSize) diff --git a/Telegram/SourceFiles/ui/peer/color_sample.cpp b/Telegram/SourceFiles/ui/peer/color_sample.cpp index 8819c2e9f2..ffbcce68fd 100644 --- a/Telegram/SourceFiles/ui/peer/color_sample.cpp +++ b/Telegram/SourceFiles/ui/peer/color_sample.cpp @@ -33,15 +33,17 @@ ColorSample::ColorSample( std::shared_ptr style, rpl::producer colorIndex, rpl::producer> collectible, - const QString &name) + rpl::producer name) : AbstractButton(parent) , _style(style) { rpl::combine( std::move(colorIndex), - std::move(collectible) + std::move(collectible), + std::move(name) ) | rpl::on_next([=]( uint8 index, - std::shared_ptr collectible) { + std::shared_ptr collectible, + const QString &nameValue) { _index = index; _collectible = std::move(collectible); if (const auto raw = _collectible.get()) { @@ -53,7 +55,7 @@ ColorSample::ColorSample( kMarkupTextOptions, std::move(context)); } else { - _name.setText(st::semiboldTextStyle, name); + _name.setText(st::semiboldTextStyle, nameValue); } setNaturalWidth([&] { if (_name.isEmpty() || _style->colorPatternIndex(_index)) { diff --git a/Telegram/SourceFiles/ui/peer/color_sample.h b/Telegram/SourceFiles/ui/peer/color_sample.h index 26c69f16bc..360d281456 100644 --- a/Telegram/SourceFiles/ui/peer/color_sample.h +++ b/Telegram/SourceFiles/ui/peer/color_sample.h @@ -29,7 +29,7 @@ public: std::shared_ptr style, rpl::producer colorIndex, rpl::producer> collectible, - const QString &name); + rpl::producer name); ColorSample( not_null parent, std::shared_ptr style, From a22f6c28d6abd67bea966f7d698c88650103ca7e Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sun, 15 Feb 2026 12:34:05 +0300 Subject: [PATCH 033/179] Replaced sticker set box with foreground preview on custom emoji click. --- Telegram/Resources/langs/lang.strings | 1 + .../history/view/history_view_reaction_preview.cpp | 13 ++++++++----- .../history/view/history_view_reaction_preview.h | 5 +++-- .../history/view/history_view_text_helper.cpp | 9 ++++++--- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 3347e99365..62abc5f3b8 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4989,6 +4989,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_animated_emoji_many#one" = "This message contains emoji from **{count} pack**."; "lng_context_animated_emoji_many#other" = "This message contains emoji from **{count} packs**."; "lng_context_animated_reaction" = "This reaction is from the **{name}** pack."; +"lng_context_animated_emoji_preview" = "This emoji is from the **{name}** pack."; "lng_context_animated_tag" = "This tag is from **{name} pack**."; "lng_context_animated_reactions" = "Reactions contain emoji from **{name} pack**."; "lng_context_animated_reactions_many#one" = "Reactions contain emoji from **{count} pack**."; diff --git a/Telegram/SourceFiles/history/view/history_view_reaction_preview.cpp b/Telegram/SourceFiles/history/view/history_view_reaction_preview.cpp index 74d7fc4935..e987cc7703 100644 --- a/Telegram/SourceFiles/history/view/history_view_reaction_preview.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reaction_preview.cpp @@ -35,7 +35,8 @@ namespace HistoryView { bool ShowReactionPreview( not_null controller, FullMsgId origin, - Data::ReactionId reactionId) { + Data::ReactionId reactionId, + bool emojiPreview) { auto document = (DocumentData*)(nullptr); if (const auto custom = reactionId.custom()) { document = controller->session().data().document(custom); @@ -112,10 +113,12 @@ bool ShowReactionPreview( }); state->label = base::make_unique_q( state->background.get(), - tr::lng_context_animated_reaction( - lt_name, - rpl::single(Ui::Text::Colorized(packName)), - tr::rich)); + (emojiPreview + ? tr::lng_context_animated_emoji_preview + : tr::lng_context_animated_reaction)( + lt_name, + rpl::single(Ui::Text::Colorized(packName)), + tr::rich)); state->label->setAttribute(Qt::WA_TransparentForMouseEvents); const auto backgroundRaw = state->background.get(); const auto labelRaw = state->label.get(); diff --git a/Telegram/SourceFiles/history/view/history_view_reaction_preview.h b/Telegram/SourceFiles/history/view/history_view_reaction_preview.h index daf6664ccb..fc7ad5d8fd 100644 --- a/Telegram/SourceFiles/history/view/history_view_reaction_preview.h +++ b/Telegram/SourceFiles/history/view/history_view_reaction_preview.h @@ -17,9 +17,10 @@ class SessionController; namespace HistoryView { -[[nodiscard]] bool ShowReactionPreview( +bool ShowReactionPreview( not_null controller, FullMsgId origin, - Data::ReactionId reactionId); + Data::ReactionId reactionId, + bool emojiPreview = false); } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_text_helper.cpp b/Telegram/SourceFiles/history/view/history_view_text_helper.cpp index 4fe5af1c39..5465ddf487 100644 --- a/Telegram/SourceFiles/history/view/history_view_text_helper.cpp +++ b/Telegram/SourceFiles/history/view/history_view_text_helper.cpp @@ -7,12 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/history_view_text_helper.h" -#include "boxes/sticker_set_box.h" #include "core/click_handler_types.h" #include "data/data_document.h" #include "data/data_session.h" #include "data/stickers/data_custom_emoji.h" #include "history/view/history_view_element.h" +#include "history/view/history_view_reaction_preview.h" #include "history/history.h" #include "main/main_session.h" #include "window/window_session_controller.h" @@ -57,8 +57,11 @@ void InitElementTextPart(not_null view, Ui::Text::String &text) { if (const auto controller = my.sessionWindow.get()) { const auto documentId = Data::ParseCustomEmojiData(entityData); if (documentId) { - const auto document = controller->session().data().document(documentId); - StickerSetBox::Show(controller->uiShow(), document, documentId); + ShowReactionPreview( + controller, + my.itemId, + Data::ReactionId{ documentId }, + true); } } }); From 2fc23dc8fb203dd6c22c81cf7669e6ece0f6ff0d Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sun, 15 Feb 2026 13:13:37 +0300 Subject: [PATCH 034/179] Added simple tooltip to shared media buttons in profile. Related commit: d6ba6ac41e. --- Telegram/Resources/langs/lang.strings | 3 +++ Telegram/SourceFiles/info/media/info_media_buttons.cpp | 6 ++++++ Telegram/lib_ui | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 62abc5f3b8..859d24d020 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7531,6 +7531,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_calendar" = "Calendar"; +"lng_new_window_tooltip_ctrl" = "Use Ctrl+Click to open in New Window."; +"lng_new_window_tooltip_cmd" = "Use Cmd+Click to open in New Window."; + // Wnd specific "lng_wnd_choose_program_menu" = "Choose Default Program..."; diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.cpp b/Telegram/SourceFiles/info/media/info_media_buttons.cpp index ef01a3a1f5..a0df1d0f79 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.cpp +++ b/Telegram/SourceFiles/info/media/info_media_buttons.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/media/info_media_buttons.h" #include "base/call_delayed.h" +#include "base/platform/base_platform_info.h" #include "base/qt/qt_key_modifiers.h" #include "core/application.h" #include "core/ui_integration.h" @@ -173,6 +174,11 @@ not_null AddButton( const auto openInWindow = separateId ? [=] { navigation->parentController()->showInNewWindow(separateId); } : Fn(nullptr); + Ui::InstallTooltip(result, [=] { + return Platform::IsMac() + ? tr::lng_new_window_tooltip_cmd(tr::now) + : tr::lng_new_window_tooltip_ctrl(tr::now); + }); AddContextMenuToButton(result, openInWindow); result->addClickHandler([=](Qt::MouseButton mouse) { if (mouse == Qt::RightButton) { diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 2eec13dfc5..46c5593ef6 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 2eec13dfc59be8722ba120fca2a8c5c5692ea7b8 +Subproject commit 46c5593ef628f660705418b5c11c415b81e5b821 From 8d258d013a164fe154e32d1b2139f93b67991812 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 16 Feb 2026 07:46:57 +0300 Subject: [PATCH 035/179] Added simple small mark to clickable title in calendar box. --- Telegram/SourceFiles/ui/boxes/calendar_box.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/ui/boxes/calendar_box.cpp b/Telegram/SourceFiles/ui/boxes/calendar_box.cpp index 9948901691..b63bd5930a 100644 --- a/Telegram/SourceFiles/ui/boxes/calendar_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/calendar_box.cpp @@ -1144,10 +1144,24 @@ void CalendarBox::Title::paintEvent(QPaintEvent *e) { const auto clip = e->rect(); + const auto triangleSize = st::lineWidth * 6; + const auto triangleX = _textLeft; + const auto triangleY = (st::calendarTitleHeight + - st::calendarTitleFont->height) / 2 + + st::calendarTitleFont->height / 2; + auto triangle = QPainterPath(); + triangle.moveTo(triangleX, triangleY - triangleSize / 2); + triangle.lineTo(triangleX + triangleSize, triangleY); + triangle.lineTo(triangleX, triangleY + triangleSize / 2); + triangle.closeSubpath(); + p.setPen(Qt::NoPen); + p.setBrush(st::windowSubTextFg); + p.drawPath(triangle); + p.setFont(st::calendarTitleFont); p.setPen(_styleColors.titleTextColor); p.drawTextLeft( - _textLeft, + _textLeft + triangleSize * 2, (st::calendarTitleHeight - st::calendarTitleFont->height) / 2, width(), _text, From 491f569ca3601b62d339016567eade25674d1892 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 16 Feb 2026 09:22:21 +0300 Subject: [PATCH 036/179] Added simple loading placeholder while jumping to date in shared media. --- .../info/media/info_media_empty_widget.cpp | 37 +++++++++++++++---- .../info/media/info_media_empty_widget.h | 3 ++ .../info/media/info_media_inner_widget.cpp | 12 +++++- .../info/media/info_media_inner_widget.h | 3 ++ 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/Telegram/SourceFiles/info/media/info_media_empty_widget.cpp b/Telegram/SourceFiles/info/media/info_media_empty_widget.cpp index a6d1db394d..0186086178 100644 --- a/Telegram/SourceFiles/info/media/info_media_empty_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_empty_widget.cpp @@ -78,12 +78,33 @@ void EmptyWidget::setSearchQuery(const QString &query) { resizeToWidth(width()); } +void EmptyWidget::setLoading(bool loading) { + if (_loading == loading) { + return; + } + _loading = loading; + resizeToWidth(width()); + update(); +} + +bool EmptyWidget::loading() const { + return _loading; +} + void EmptyWidget::paintEvent(QPaintEvent *e) { - if (!_icon) { + auto p = QPainter(this); + + if (_loading) { + p.setFont(st::normalFont); + p.setPen(st::windowSubTextFg); + p.drawText(rect(), tr::lng_contacts_loading(tr::now), + QTextOption(Qt::AlignCenter)); return; } - auto p = QPainter(this); + if (!_icon) { + return; + } auto iconLeft = (width() - _icon->width()) / 2; auto iconTop = height() - st::infoEmptyIconTop; @@ -91,12 +112,14 @@ void EmptyWidget::paintEvent(QPaintEvent *e) { } int EmptyWidget::resizeGetHeight(int newWidth) { - auto labelTop = _height - st::infoEmptyLabelTop; - auto labelWidth = newWidth - 2 * st::infoEmptyLabelSkip; - _text->resizeToNaturalWidth(labelWidth); + if (!_loading) { + auto labelTop = _height - st::infoEmptyLabelTop; + auto labelWidth = newWidth - 2 * st::infoEmptyLabelSkip; + _text->resizeToNaturalWidth(labelWidth); - auto labelLeft = (newWidth - _text->width()) / 2; - _text->moveToLeft(labelLeft, labelTop, newWidth); + auto labelLeft = (newWidth - _text->width()) / 2; + _text->moveToLeft(labelLeft, labelTop, newWidth); + } update(); return _height; diff --git a/Telegram/SourceFiles/info/media/info_media_empty_widget.h b/Telegram/SourceFiles/info/media/info_media_empty_widget.h index c998464664..9b60a38ef7 100644 --- a/Telegram/SourceFiles/info/media/info_media_empty_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_empty_widget.h @@ -24,6 +24,8 @@ public: void setFullHeight(rpl::producer fullHeightValue); void setType(Type type); void setSearchQuery(const QString &query); + void setLoading(bool loading); + [[nodiscard]] bool loading() const; protected: void paintEvent(QPaintEvent *e) override; @@ -35,6 +37,7 @@ private: Type _type = Type::kCount; const style::icon *_icon = nullptr; int _height = 0; + bool _loading = false; }; diff --git a/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp b/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp index bcb35fad99..7735e50cf6 100644 --- a/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp @@ -221,7 +221,7 @@ int InnerWidget::recountHeight() { listHeight = _list->heightNoMargins(); top += listHeight; } - if (listHeight > 0) { + if (listHeight > _emptyHeightThreshold && !_empty->loading()) { _empty->hide(); } else { _empty->show(); @@ -246,7 +246,17 @@ rpl::producer InnerWidget::scrollToRequests() const { } void InnerWidget::jumpToMessage(MsgId msgId) { + _empty->setLoading(true); + _emptyHeightThreshold = st::semiboldFont->height; _list->jumpToMessage(msgId); + _emptyLoadingLifetime = _list->heightValue( + ) | rpl::skip(1) | rpl::filter( + rpl::mappers::_1 > _emptyHeightThreshold + ) | rpl::take(1) | rpl::on_next([=](int height) { + _empty->setLoading(false); + _emptyHeightThreshold = 0; + recountHeight(); + }); } } // namespace Media diff --git a/Telegram/SourceFiles/info/media/info_media_inner_widget.h b/Telegram/SourceFiles/info/media/info_media_inner_widget.h index 14aa7be140..744a4adb87 100644 --- a/Telegram/SourceFiles/info/media/info_media_inner_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_inner_widget.h @@ -85,6 +85,9 @@ private: rpl::event_stream> _selectedLists; rpl::event_stream> _listTops; + int _emptyHeightThreshold = 0; + rpl::lifetime _emptyLoadingLifetime; + }; } // namespace Info::Media From f7098efc36d839e6d59078f69f4596509c7a8e53 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 16 Feb 2026 09:40:49 +0300 Subject: [PATCH 037/179] Fixed icons in transcribe button while loading with disabled animation. --- .../view/history_view_transcribe_button.cpp | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp b/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp index da82a5f728..48471c82bc 100644 --- a/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp +++ b/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp @@ -125,6 +125,11 @@ void TranscribeButton::paint( } } + const auto state = _animation + ? _animation->computeState() + : Ui::RadialState(); + const auto staticLoading = anim::Disabled() && state.shown > 0; + auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(context.st->msgServiceBg()); @@ -159,13 +164,15 @@ void TranscribeButton::paint( } }); } - (_item->history()->session().api().transcribes().summary( - _item).shown - ? st::mediaviewFullScreenButton.icon - : st::mediaviewFullScreenOutIcon).paintInCenter( - p, - r, - st::msgServiceFg->c); + if (!staticLoading) [[likely]] { + (_item->history()->session().api().transcribes().summary( + _item).shown + ? st::mediaviewFullScreenButton.icon + : st::mediaviewFullScreenOutIcon).paintInCenter( + p, + r, + st::msgServiceFg->c); + } } else if (!_loading && hasLock()) { ClipPainterForLock(p, true, r); context.st->historyFastTranscribeIcon().paintInCenter(p, r); @@ -178,14 +185,10 @@ void TranscribeButton::paint( context.st->historyFastTranscribeIcon().paintInCenter(p, r); } - const auto state = _animation - ? _animation->computeState() - : Ui::RadialState(); - auto pen = QPen(st::msgServiceFg); pen.setCapStyle(Qt::RoundCap); p.setPen(pen); - if (_animation && state.shown > 0 && anim::Disabled()) { + if (staticLoading) [[unlikely]] { const auto _st = &st::defaultRadio; anim::DrawStaticLoading( p, From b2531b588ffaf5d8cee6046f48d654c8a15c12bf Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 16 Feb 2026 13:52:25 +0300 Subject: [PATCH 038/179] Fixed possible crash from rection preview. --- .../view/history_view_reaction_preview.cpp | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_reaction_preview.cpp b/Telegram/SourceFiles/history/view/history_view_reaction_preview.cpp index e987cc7703..e646309bb3 100644 --- a/Telegram/SourceFiles/history/view/history_view_reaction_preview.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reaction_preview.cpp @@ -53,8 +53,16 @@ bool ShowReactionPreview( base::unique_qptr clickable; base::unique_qptr background; base::unique_qptr label; + + void clear() { + mediaPreview.reset(); + clickable.reset(); + background.reset(); + label.reset(); + }; + }; - const auto state = std::make_shared(); + const auto state = std::make_shared>(); const auto mainwidget = controller->widget(); state->mediaPreview = base::make_unique_q( @@ -73,9 +81,7 @@ bool ShowReactionPreview( } base::call_delayed( st::defaultToggle.duration, - crl::guard(state->clickable.get(), [=] { - state->clickable.reset(); - })); + [=] { state->clear(); }); }; state->clickable->setClickedCallback(hideAll); base::install_event_filter(QCoreApplication::instance(), [=]( @@ -150,8 +156,6 @@ bool ShowReactionPreview( mainwidget->sizeValue() | rpl::on_next([=](QSize size) { mediaPreviewRaw->setGeometry(Rect(size)); - clickableRaw->setGeometry(Rect(size)); - clickableRaw->raise(); if (backgroundRaw && labelRaw) { const auto maxLabelWidth = labelRaw->textMaxWidth() / 2; @@ -174,6 +178,11 @@ bool ShowReactionPreview( backgroundRaw->raise(); } }, mediaPreviewRaw->lifetime()); + + mainwidget->sizeValue() | rpl::on_next([=](QSize size) { + clickableRaw->setGeometry(Rect(size)); + clickableRaw->raise(); + }, clickableRaw->lifetime()); return true; } From 770c39677d4ad28031ee7de27c52ba716747960e Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Mon, 16 Feb 2026 08:43:50 +0400 Subject: [PATCH 039/179] Install 2x icons on Linux --- Telegram/CMakeLists.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index dad09e835a..a034d7a130 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -2222,12 +2222,19 @@ if (LINUX AND DESKTOP_APP_USE_PACKAGED) generate_appstream_changelog(Telegram "${CMAKE_SOURCE_DIR}/changelog.txt" "${CMAKE_CURRENT_BINARY_DIR}/org.telegram.desktop.metainfo.xml") install(TARGETS Telegram RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" BUNDLE DESTINATION "${CMAKE_INSTALL_BINDIR}") install(FILES "Resources/art/icon16.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps" RENAME "org.telegram.desktop.png") + install(FILES "Resources/art/icon16@2x.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16@2/apps" RENAME "org.telegram.desktop.png") install(FILES "Resources/art/icon32.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps" RENAME "org.telegram.desktop.png") + install(FILES "Resources/art/icon32@2x.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32@2/apps" RENAME "org.telegram.desktop.png") install(FILES "Resources/art/icon48.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/apps" RENAME "org.telegram.desktop.png") + install(FILES "Resources/art/icon48@2x.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48@2/apps" RENAME "org.telegram.desktop.png") install(FILES "Resources/art/icon64.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/64x64/apps" RENAME "org.telegram.desktop.png") + install(FILES "Resources/art/icon64@2x.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/64x64@2/apps" RENAME "org.telegram.desktop.png") install(FILES "Resources/art/icon128.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/128x128/apps" RENAME "org.telegram.desktop.png") + install(FILES "Resources/art/icon128@2x.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/128x128@2/apps" RENAME "org.telegram.desktop.png") install(FILES "Resources/art/icon256.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256/apps" RENAME "org.telegram.desktop.png") + install(FILES "Resources/art/icon256@2x.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256@2/apps" RENAME "org.telegram.desktop.png") install(FILES "Resources/art/icon512.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/512x512/apps" RENAME "org.telegram.desktop.png") + install(FILES "Resources/art/icon512@2x.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/512x512@2/apps" RENAME "org.telegram.desktop.png") install(FILES "Resources/icons/tray_monochrome.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/symbolic/apps" RENAME "org.telegram.desktop-symbolic.svg") install(FILES "Resources/icons/tray_monochrome_attention.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/symbolic/apps" RENAME "org.telegram.desktop-attention-symbolic.svg") install(FILES "Resources/icons/tray_monochrome_mute.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/symbolic/apps" RENAME "org.telegram.desktop-mute-symbolic.svg") From 7201f8a86e47dcf17036b1364baccf84cc8f06ee Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 13 Feb 2026 09:23:50 +0400 Subject: [PATCH 040/179] [ai] Ask /task command to record work time. --- .claude/commands/task.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.claude/commands/task.md b/.claude/commands/task.md index 6d4b147f45..85ddee36fb 100644 --- a/.claude/commands/task.md +++ b/.claude/commands/task.md @@ -38,6 +38,8 @@ Project structure: ## Phase 0: Setup +**Record the current time now** (using `Get-Date` in PowerShell or equivalent) and store it as `$START_TIME`. You will use this at the end to display total elapsed time. + ⚠️ **CRITICAL: Follow-up detection MUST happen FIRST, before anything else.** ### Step 0a: Follow-up detection (MANDATORY — do this BEFORE understanding the task) @@ -493,7 +495,8 @@ When all phases including build verification and code review are done: 2. Show which files were modified/created. 3. Note any issues encountered during implementation. 4. Summarize code review iterations: how many rounds, what was found and fixed, or if it was approved on first pass. -5. Remind the user of the project name so they can use `/task ` for follow-up changes. +5. Calculate and display the total elapsed time since `$START_TIME` (format as `Xh Ym Zs`, omitting zero components — e.g. `12m 34s` or `1h 5m 12s`). +6. Remind the user of the project name so they can use `/task ` for follow-up changes. ## Error Handling From d98011ccdc9dd584474a190f6b9a1f52d80db14e Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 13 Feb 2026 13:22:33 +0400 Subject: [PATCH 041/179] [ai] Move codex skill to correct place. --- .agents/skills/task-think/PROMPTS.md | 454 ++++++++++++++++++ .../skills/task-think/SKILL.md | 62 ++- .claude/commands/task.md | 22 +- .codex/skills/task-think/PROMPTS.md | 227 --------- .cursor/rules/api_usage.mdc | 105 ---- .cursor/rules/localization.mdc | 164 ------- .cursor/rules/rpl_guide.mdc | 216 --------- .cursor/rules/styling.mdc | 154 ------ 8 files changed, 504 insertions(+), 900 deletions(-) create mode 100644 .agents/skills/task-think/PROMPTS.md rename {.codex => .agents}/skills/task-think/SKILL.md (52%) delete mode 100644 .codex/skills/task-think/PROMPTS.md delete mode 100644 .cursor/rules/api_usage.mdc delete mode 100644 .cursor/rules/localization.mdc delete mode 100644 .cursor/rules/rpl_guide.mdc delete mode 100644 .cursor/rules/styling.mdc diff --git a/.agents/skills/task-think/PROMPTS.md b/.agents/skills/task-think/PROMPTS.md new file mode 100644 index 0000000000..ad804219a4 --- /dev/null +++ b/.agents/skills/task-think/PROMPTS.md @@ -0,0 +1,454 @@ +# Phase Prompts + +Use these templates for `codex exec --json` child runs. Replace ``, ``, ``, and ``. + +## Phase 0: Setup + +**Record the current time now** and store it as `$START_TIME`. You will use this at the end to display total elapsed time. + +Before running any phase prompts, the orchestrator must determine whether this is a new project or a follow-up task. + +**Follow-up detection (MANDATORY — do this BEFORE anything else):** +1. Extract the first word/token from the task description. Call it `FIRST_TOKEN`. +2. Run these two checks IN PARALLEL: + - `ls .ai/` — to see all existing project names + - `ls .ai//about.md` — to check if this specific project exists +3. If check 2 **succeeds** (the file exists): this is a **follow-up task**. The project name is `FIRST_TOKEN`. The task description is everything after `FIRST_TOKEN`. +4. If check 2 **fails** (file not found): this is a **new project**. The full input is the task description. + +**Do NOT proceed until you have run these checks and determined follow-up vs new.** + +**For new projects:** +- Using the list from check 1, pick a unique short name (1-2 lowercase words, hyphen-separated) that doesn't collide with existing projects. +- Create `.ai//` and `.ai//a/` and `logs/`. +- Set `` = `a`. + +**For follow-up tasks:** +- Scan `.ai//` for existing task folders (`a/`, `b/`, ...). Find the latest one (highest letter). +- The previous task letter = that highest letter. +- The new task letter = next letter in sequence. +- Create `.ai///` and `logs/`. + +Then proceed to Phase 1. Follow-up tasks do NOT skip context gathering — they go through a modified version of it. + +## Phase 1: Context (New Project, letter = `a`) + +```text +You are a context-gathering agent for a large C++ codebase (Telegram Desktop). + +TASK: + +YOUR JOB: Read AGENTS.md, inspect the codebase, find ALL files and code relevant to this task, and write two documents. + +Steps: +1. Read AGENTS.md for project conventions and build instructions. +2. Search the codebase for files, classes, functions, and patterns related to the task. +3. Read all potentially relevant files. Be thorough - read more rather than less. +4. For each relevant file, note: + - File path + - Relevant line ranges + - What the code does and how it relates to the task + - Key data structures, function signatures, patterns used +5. Look for similar existing features that could serve as a reference implementation. +6. Check api.tl if the task involves Telegram API. +7. Check .style files if the task involves UI. +8. Check lang.strings if the task involves user-visible text. + +Write TWO files: + +### File 1: .ai//about.md + +NOTE: This file is NOT used by any agent in the current task. It exists solely as a starting point for a FUTURE follow-up task's context gatherer. No planning, implementation, or review agent will ever read it. Only the context-gathering agent of the next follow-up task reads about.md (together with the latest context.md) to produce a fresh context.md for that next task. + +Write it as if the project is already fully implemented and working. It should contain: +- **Project**: What this project does (feature description, goals, scope) +- **Architecture**: High-level architectural decisions, which modules are involved, how they interact +- **Key Design Decisions**: Important choices made about the approach +- **Relevant Codebase Areas**: Which parts of the codebase this project touches, key types and APIs involved + +Do NOT include temporal state like "Current State", "Pending Changes", "Not yet implemented", "TODO", or any other framing that distinguishes between "done" and "not done". Describe the project as a complete, coherent whole — as if everything is already working. This is a project overview, not a status tracker. Task-specific work belongs exclusively in context.md. + +### File 2: .ai//a/context.md + +This is the task-specific implementation context. This is the PRIMARY document — all downstream agents (planning, implementation, review) will read ONLY this file. It must be completely self-contained. It should contain: +- **Task Description**: The full task restated clearly +- **Relevant Files**: Every file path with line ranges and descriptions of what's there +- **Key Code Patterns**: How similar things are done in the codebase (with code snippets) +- **Data Structures**: Relevant types, structs, classes +- **API Methods**: Any TL schema methods involved (copied from api.tl) +- **UI Styles**: Any relevant style definitions +- **Localization**: Any relevant string keys +- **Build Info**: Build command and any special notes +- **Reference Implementations**: Similar features that can serve as templates + +Be extremely thorough. Another agent with NO prior context will read this file and must be able to understand everything needed to implement the task. + +Do not implement code in this phase. +``` + +## Phase 1F: Context (Follow-up Task, letter = `b`, `c`, ...) + +```text +You are a context-gathering agent for a follow-up task on an existing project in a large C++ codebase (Telegram Desktop). + +NEW TASK: + +YOUR JOB: Read the existing project state, gather any additional context needed, and produce fresh documents for the new task. + +Steps: +1. Read AGENTS.md for project conventions and build instructions. +2. Read .ai//about.md — this is the project-level blueprint describing everything done so far. +3. Read .ai///context.md — this is the previous task's gathered context. +4. Understand what has already been implemented by reading the actual source files referenced in about.md and the previous context. +5. Based on the NEW TASK description, search the codebase for any ADDITIONAL files, classes, functions, and patterns that are relevant to the new task but not already covered. +6. Read all newly relevant files thoroughly. + +Write TWO files: + +### File 1: .ai//about.md (REWRITE) + +NOTE: This file is NOT used by any agent in the current task. It exists solely as a starting point for a FUTURE follow-up task's context gatherer. No planning, implementation, or review agent will ever read it. You are rewriting it now so that the next follow-up has an accurate project overview to start from. + +REWRITE this file (not append). The new about.md must be a single coherent document that describes the project as if everything — including this new task's changes — is already fully implemented and working. + +It should incorporate: +- Everything from the old about.md that is still accurate and relevant +- The new task's functionality described as part of the project (not as "changes to make") +- Any changed design decisions or architectural updates from the new task requirements + +It should NOT contain: +- Any temporal state: "Current State", "Pending Changes", "TODO", "Not yet implemented" +- History of how requirements changed between tasks +- References to "the old approach" vs "the new approach" +- Task-by-task changelog or timeline +- Any distinction between "what was done before" and "what this task adds" +- Information that contradicts the new task requirements (if the new task changes direction, the about.md should reflect the NEW direction as if it was always the plan) + +### File 2: .ai///context.md + +This is the PRIMARY document — all downstream agents (planning, implementation, review) will read ONLY this file. It must be completely self-contained. about.md will NOT be available to them. + +It should contain: +- **Task Description**: The new task restated clearly, with enough project background (from about.md and previous context.md) that an implementation agent can understand it without reading any other .ai/ files +- **Relevant Files**: Every file path with line ranges relevant to THIS task (including files modified by previous tasks and any newly relevant files) +- **Key Code Patterns**: How similar things are done in the codebase +- **Data Structures**: Relevant types, structs, classes +- **API Methods**: Any TL schema methods involved +- **UI Styles**: Any relevant style definitions +- **Localization**: Any relevant string keys +- **Build Info**: Build command and any special notes +- **Reference Implementations**: Similar features that can serve as templates + +Be extremely thorough. Another agent with NO prior context will read ONLY this file and must be able to understand everything needed to implement the new task. Do NOT assume the reader has seen about.md or any previous task files. The context.md is the single source of truth for all downstream agents — it must include all relevant project background, not just the delta. + +Do not implement code in this phase. +``` + +## Phase 2: Plan + +```text +You are a planning agent. You must create a detailed implementation plan. + +Read these files: +- .ai///context.md - Contains all gathered context for this task +- Then read the specific source files referenced in context.md to understand the code deeply. + +Create a detailed plan in: .ai///plan.md + +The plan.md should contain: + +## Task + + +## Approach + + +## Files to Modify + + +## Files to Create + + +## Implementation Steps + +Each step must be specific enough that an agent can execute it without ambiguity: +- Exact file paths +- Exact function names +- What code to add/modify/remove +- Where exactly in the file (after which function, in which class, etc.) + +Number every step. Group steps into phases if there are more than ~8 steps. + +### Phase 1: +1. +2. +... + +### Phase 2: (if needed) +... + +## Build Verification +- Build command to run +- Expected outcome + +## Status +- [ ] Phase 1: +- [ ] Phase 2: (if applicable) +- [ ] Build verification +- [ ] Code review + +Do not implement code in this phase. +``` + +## Phase 3: Plan Assessment + +```text +You are a plan assessment agent. Review and refine an implementation plan. + +Read these files: +- .ai///context.md +- .ai///plan.md +- Then read the actual source files referenced to verify the plan makes sense. + +Assess the plan: + +1. **Correctness**: Are the file paths and line references accurate? Does the plan reference real functions and types? +2. **Completeness**: Are there missing steps? Edge cases not handled? +3. **Code quality**: Will the plan minimize code duplication? Does it follow existing codebase patterns from AGENTS.md? +4. **Design**: Could the approach be improved? Are there better patterns already used in the codebase? +5. **Phase sizing**: Each phase should be implementable by a single agent in one session. If a phase has more than ~8-10 substantive code changes, split it further. + +Update plan.md with your refinements. Keep the same structure but: +- Fix any inaccuracies +- Add missing steps +- Improve the approach if you found better patterns +- Ensure phases are properly sized for single-agent execution +- Add a line at the top of the Status section: `Phases: ` indicating how many implementation phases there are +- Add `Assessed: yes` at the bottom of the file + +If the plan is small enough for a single agent (roughly <=8 steps), mark it as a single phase. + +Do not implement code in this phase. +``` + +## Phase 4: Implementation + +For each phase in the plan that is not yet marked as done, run a separate child session: + +```text +You are an implementation agent working on phase of an implementation plan. + +Read these files first: +- .ai///context.md - Full codebase context +- .ai///plan.md - Implementation plan + +Then read the source files you'll be modifying. + +YOUR TASK: Implement ONLY Phase from the plan: + + +Rules: +- Follow the plan precisely +- Follow AGENTS.md coding conventions (no comments except complex algorithms, use auto, empty line before closing brace, etc.) +- Do NOT modify .ai/ files except to update the Status section in plan.md +- When done, update plan.md Status section: change `- [ ] Phase : ...` to `- [x] Phase : ...` +- Do NOT work on other phases + +When finished, report what you did and any issues encountered. +``` + +After each implementation agent returns: +1. Read `plan.md` to check the status was updated. +2. If more phases remain, run the next implementation child session. +3. If all phases are done, proceed to build verification. + +## Phase 5: Build Verification + +Only run this phase if the task involved modifying project source code (not just docs or config). + +```text +You are a build verification agent. + +Read these files: +- .ai///context.md +- .ai///plan.md + +The implementation is complete. Your job is to build the project and fix any build errors. + +Steps: +1. Run (from repository root): cmake --build ./out --config Debug --target Telegram +2. If the build succeeds, update plan.md: change `- [ ] Build verification` to `- [x] Build verification` +3. If the build fails: + a. Read the error messages carefully + b. Read the relevant source files + c. Fix the errors in accordance with the plan and AGENTS.md conventions + d. Rebuild and repeat until the build passes + e. Update plan.md status when done + +Rules: +- Only fix build errors, do not refactor or improve code +- Follow AGENTS.md conventions +- If build fails with file-locked errors (C1041, LNK1104), STOP and report - do not retry + +When finished, report the build result. +``` + +## Phase 6: Code Review Loop + +After build verification passes, run up to 3 review-fix iterations to improve code quality. Set iteration counter `R = 1`. + +### Review Loop + +``` +LOOP: + 1. Run review agent (Step 6a) with iteration R + 2. Read review.md verdict: + - "APPROVED" → go to FINISH + - Has improvement suggestions → run fix agent (Step 6b) + 3. After fix agent completes and build passes: + R = R + 1 + If R > 3 → go to FINISH (stop iterating, accept current state) + Otherwise → go to step 1 + +FINISH: + - Update plan.md: change `- [ ] Code review` to `- [x] Code review` + - Proceed to Completion +``` + +### Step 6a: Code Review Agent + +```text +You are a code review agent for Telegram Desktop (C++ / Qt). + +Read these files: +- .ai///context.md - Codebase context +- .ai///plan.md - Implementation plan +- REVIEW.md - Style and formatting rules to enforce + 1, also read:> +- .ai///review.md - Previous review (to see what was already addressed) + +Then run `git diff` to see all uncommitted changes made by the implementation. Implementation agents do not commit, so `git diff` shows exactly the current feature's changes. + +Then read the modified source files in full to understand changes in context. + +Perform a thorough code review. + +REVIEW CRITERIA (in order of importance): + +1. **Correctness and safety**: Obvious logic errors, missing null checks at API boundaries, potential crashes, use-after-free, dangling references, race conditions. This is the highest priority — bugs and safety issues must be caught first. Do NOT nitpick internal code that relies on framework guarantees. + +2. **Dead code**: Any code added or left behind that is never called or used, within the scope of the changes. Unused variables, unreachable branches, leftover scaffolding. + +3. **Redundant changes**: Changes in the diff that have no functional effect — moving declarations or code blocks to a different location without reason, reformatting untouched code, reordering includes or fields with no purpose. Every line in the diff should serve the feature. If a file appears in `git diff` but contains only no-op rearrangements, flag it for revert. + +4. **Code duplication**: Unnecessary repetition of logic that should be shared. Look for near-identical blocks that differ only in minor details and could be unified. + +5. **Wrong placement**: Code added to a module where it doesn't logically belong. If another existing module is a clearly better fit for the new code, flag it. Consider the existing module boundaries and responsibilities visible in context.md. + +6. **Function decomposition**: For longer functions (roughly 50+ lines), consider whether a logical sub-task could be cleanly extracted into a separate function. This is NOT a hard rule — a 100-line function that flows naturally and isn't easily divisible is perfectly fine. But sometimes even a 20-line function contains a clear isolated subtask that reads better as two 10-line functions. The key is to think about it each time: does extracting improve readability and reduce cognitive load, or does it just scatter logic across call sites for no real benefit? Only suggest extraction when there's a genuinely self-contained piece of logic with a clear name and purpose. + +7. **Module structure**: Only in exceptional cases — if a large amount of newly added code (hundreds of lines) is logically distinct from the rest of its host module, suggest extracting it into a new module. But do NOT suggest new modules lightly: every module adds significant build overhead due to PCH and heavy template usage. Only suggest this when the new code is both large enough AND logically separated enough to justify it. At the same time, don't let modules grow into multi-thousand-line monoliths either. + +8. **Style compliance**: Verify adherence to REVIEW.md rules (empty line before closing brace, operators at start of continuation lines, minimize type checks with direct cast instead of is+as, no if-with-initializer when simpler alternatives exist) and AGENTS.md conventions (no unnecessary comments, `auto` usage, no hardcoded sizes — must use .style definitions), etc. + +IMPORTANT GUIDELINES: +- Review ONLY the changes made, not pre-existing code in the repository. +- Be pragmatic. Don't suggest changes for the sake of it. Each suggestion should have a clear, concrete benefit. +- Don't suggest adding comments, docstrings, or type annotations — the codebase style avoids these. +- Don't suggest error handling for impossible scenarios or over-engineering. + +Write your review to: .ai///review.md + +The review document should contain: + +## Code Review - Iteration + +## Summary +<1-2 sentence overall assessment> + +## Verdict: + + + + + +## Changes Required + +### +- **Category**: +- **File(s)**: +- **Problem**: +- **Fix**: + +### +... + +Keep the list focused. Only include issues that genuinely improve the code. If you find yourself listing more than ~5-6 issues, prioritize the most impactful ones. + +When finished, report your verdict clearly as: APPROVED or NEEDS_CHANGES. +``` + +### Step 6b: Review Fix Agent + +```text +You are a review fix agent. You implement improvements identified during code review. + +Read these files: +- .ai///context.md - Codebase context +- .ai///plan.md - Original implementation plan +- .ai///review.md - Code review with required changes + +Then read the source files mentioned in the review. + +YOUR TASK: Implement ALL changes listed in review.md. + +For each issue in the review: +1. Read the relevant source file(s). +2. Make the specified change. +3. Verify the change makes sense in context. + +After all changes are made: +1. Build (from repository root): cmake --build ./out --config Debug --target Telegram +2. If the build fails, fix build errors and rebuild until it passes. +3. If build fails with file-locked errors (C1041, LNK1104), STOP and report - do not retry. + +Rules: +- Implement exactly the changes from the review, nothing more. +- Follow AGENTS.md coding conventions. +- Do NOT modify .ai/ files. + +When finished, report what changes were made. +``` + +## Completion + +When all phases including build verification and code review are done: +1. Read the final `plan.md` and report the summary to the user. +2. Show which files were modified/created. +3. Note any issues encountered during implementation. +4. Summarize code review iterations: how many rounds, what was found and fixed, or if it was approved on first pass. +5. Calculate and display the total elapsed time since `$START_TIME` (format as `Xh Ym Zs`, omitting zero components — e.g. `12m 34s` or `1h 5m 12s`). +6. Remind the user of the project name so they can request follow-up tasks within the same project. + +## Error Handling + +- If any phase fails or gets stuck, report the issue to the user and ask how to proceed. +- If context.md or plan.md is not written properly by a phase, re-run that phase with more specific instructions. +- If build errors persist after the build phase's attempts, report the remaining errors to the user. +- If a review fix phase introduces new build errors that it cannot resolve, report to the user. + +## Reasoning Effort + +Phases 2 (Plan), 3 (Assessment), and 6a (Review) require elevated reasoning. Pass `-c model_reasoning_effort="xhigh"` on those `codex exec` invocations. All other phases use the default reasoning effort. + +## Example Runner Commands + +```powershell +codex exec --json -C "" | Tee-Object .ai///logs/phase-1-context.jsonl +codex exec --json -C -c model_reasoning_effort="xhigh" "" | Tee-Object .ai///logs/phase-2-plan.jsonl +codex exec --json -C -c model_reasoning_effort="xhigh" "" | Tee-Object .ai///logs/phase-3-assess.jsonl +codex exec --json -C "" | Tee-Object .ai///logs/phase-4-impl-N.jsonl +codex exec --json -C "" | Tee-Object .ai///logs/phase-5-build.jsonl +codex exec --json -C -c model_reasoning_effort="xhigh" "" | Tee-Object .ai///logs/phase-6a-review-R.jsonl +codex exec --json -C "" | Tee-Object .ai///logs/phase-6b-fix-R.jsonl +``` diff --git a/.codex/skills/task-think/SKILL.md b/.agents/skills/task-think/SKILL.md similarity index 52% rename from .codex/skills/task-think/SKILL.md rename to .agents/skills/task-think/SKILL.md index 19f1260a33..3f332e7887 100644 --- a/.codex/skills/task-think/SKILL.md +++ b/.agents/skills/task-think/SKILL.md @@ -1,6 +1,6 @@ --- name: task-think -description: Orchestrate a multi-phase implementation workflow for this repository with artifact files under .ai/// and optional fresh codex exec child runs per phase. Use when the user wants one prompt to drive context gathering, planning, implementation, verification, and review iterations while keeping the main session context clean. +description: Orchestrate a multi-phase implementation workflow for this repository with artifact files under .ai/// and fresh codex exec child runs per phase. Use when the user wants one prompt to drive context gathering, planning, plan assessment, implementation, build verification, and review iterations while keeping the main session context clean. --- # Task Pipeline @@ -12,7 +12,7 @@ Run a full implementation workflow with repository artifacts and clear phase bou Collect: - task description - optional project name (if missing, derive a short kebab-case name) -- optional constraints (files, architecture, deadlines, risk tolerance) +- optional constraints (files, architecture, risk tolerance) - optional screenshot paths If screenshots are attached in UI but not present as files, write a brief textual summary in `.ai//about.md` so child runs can consume the requirements. @@ -28,11 +28,13 @@ Project structure: a/ # First task context.md # Gathered codebase context for this task plan.md # Implementation plan - review.md # Code review document + review1.md # Code review documents (up to 3 iterations) + review2.md + review3.md b/ # Follow-up task context.md plan.md - review.md + review1.md c/ # Another follow-up task ... ``` @@ -46,41 +48,55 @@ Create and maintain: - `.ai//about.md` - `.ai///context.md` - `.ai///plan.md` -- `.ai///implementation.md` -- `.ai///review.md` +- `.ai///review.md` (up to 3 review iterations) - `.ai///logs/phase-*.jsonl` (when running child `codex exec`) -## Execution Mode +## Phases -Run `codex exec --json` child sessions for each phase. +The workflow runs these phases sequentially via `codex exec --json` child sessions: -## Fresh-Run Mode Procedure - -1. Detect follow-up vs new project (check if first token of task description matches an existing project name with `about.md`). -2. For new projects: pick unique short name, create `.ai//` and `.ai//a/`. -3. For follow-up tasks: find latest task letter, create `.ai///`. -4. Run child phase sessions sequentially, waiting for each to finish. -5. After each phase, validate artifact file exists and has substantive content. -6. Summarize status in the parent session after each phase. -7. Stop immediately on blocking errors and report exact blocker. +1. **Phase 0: Setup** — Record start time, detect follow-up vs new project, create directories. +2. **Phase 1: Context Gathering** — Read codebase, write `about.md` and `context.md`. (Phase 1F for follow-ups.) +3. **Phase 2: Planning** — Read context, write detailed `plan.md` with numbered steps grouped into phases. +4. **Phase 3: Plan Assessment** — Review and refine the plan for correctness, completeness, code quality, and phase sizing. +5. **Phase 4: Implementation** — One child session per plan phase. Each implements only its assigned phase and updates `plan.md` status. +6. **Phase 5: Build Verification** — Build the project, fix any build errors. Skip if no source code was modified. +7. **Phase 6: Code Review Loop** — Up to 3 review-fix iterations: + - 6a: Review agent writes `review.md` with verdict (APPROVED or NEEDS_CHANGES). + - 6b: Fix agent implements review changes and rebuilds. + - Loop until APPROVED or R > 3. Use the phase prompt templates in `PROMPTS.md`. +## Execution Mode + +Run `codex exec --json` child sessions for each phase. Wait for each to finish before starting the next. After each phase, validate that the expected artifact file exists and has substantive content. + +Phases that require elevated reasoning (Planning, Plan Assessment, Code Review) must use `-c model_reasoning_effort="xhigh"`. See example commands in `PROMPTS.md`. + ## Verification Rules -- If build or test commands fail due to file locks or access-denied outputs, stop and ask the user to close locking processes before retrying. +- If build or test commands fail due to file locks or access-denied outputs (C1041, LNK1104), stop and ask the user to close locking processes before retrying. - Never claim completion without: - implemented code changes present - - build/test attempt results recorded + - build attempt results recorded - review pass documented with any follow-up fixes ## Completion Criteria Mark complete only when: -- plan phases are done -- verification results are recorded -- review issues are addressed or explicitly deferred with rationale -- Remind the user of the project name so they can request follow-up tasks within the same project. +- All plan phases are done +- Build verification is recorded +- Review issues are addressed or explicitly deferred with rationale +- Display total elapsed time since start (format: `Xh Ym Zs`, omitting zero components) +- Remind the user of the project name so they can request follow-up tasks within the same project + +## Error Handling + +- If any phase fails or gets stuck, report the issue to the user and ask how to proceed. +- If context.md or plan.md is not written properly by a phase, re-run that phase with more specific instructions. +- If build errors persist after the build phase's attempts, report the remaining errors to the user. +- If a review fix phase introduces new build errors that it cannot resolve, report to the user. ## User Invocation diff --git a/.claude/commands/task.md b/.claude/commands/task.md index 85ddee36fb..30149f1bf0 100644 --- a/.claude/commands/task.md +++ b/.claude/commands/task.md @@ -82,10 +82,10 @@ You are a context-gathering agent for a large C++ codebase (Telegram Desktop). TASK: -YOUR JOB: Read CLAUDE.md, inspect the codebase, find ALL files and code relevant to this task, and write two documents. +YOUR JOB: Read AGENTS.md, inspect the codebase, find ALL files and code relevant to this task, and write two documents. Steps: -1. Read CLAUDE.md for project conventions and build instructions. +1. Read AGENTS.md for project conventions and build instructions. 2. Search the codebase for files, classes, functions, and patterns related to the task. 3. Read all potentially relevant files. Be thorough - read more rather than less. 4. For each relevant file, note: @@ -142,7 +142,7 @@ NEW TASK: YOUR JOB: Read the existing project state, gather any additional context needed, and produce fresh documents for the new task. Steps: -1. Read CLAUDE.md for project conventions and build instructions. +1. Read AGENTS.md for project conventions and build instructions. 2. Read .ai//about.md — this is the project-level blueprint describing everything done so far. 3. Read .ai///context.md — this is the previous task's gathered context. 4. Understand what has already been implemented by reading the actual source files referenced in about.md and the previous context. @@ -268,7 +268,7 @@ Use /ultrathink to assess the plan: 1. **Correctness**: Are the file paths and line references accurate? Does the plan reference real functions and types? 2. **Completeness**: Are there missing steps? Edge cases not handled? -3. **Code quality**: Will the plan minimize code duplication? Does it follow existing codebase patterns from CLAUDE.md? +3. **Code quality**: Will the plan minimize code duplication? Does it follow existing codebase patterns from AGENTS.md? 4. **Design**: Could the approach be improved? Are there better patterns already used in the codebase? 5. **Phase sizing**: Each phase should be implementable by a single agent in one session. If a phase has more than ~8-10 substantive code changes, split it further. @@ -305,7 +305,7 @@ YOUR TASK: Implement ONLY Phase from the plan: Rules: - Follow the plan precisely -- Follow CLAUDE.md coding conventions (no comments except complex algorithms, use auto, empty line before closing brace, etc.) +- Follow AGENTS.md coding conventions (no comments except complex algorithms, use auto, empty line before closing brace, etc.) - Do NOT modify .ai/ files except to update the Status section in plan.md - When done, update plan.md Status section: change `- [ ] Phase : ...` to `- [x] Phase : ...` - Do NOT work on other phases @@ -334,18 +334,18 @@ Read these files: The implementation is complete. Your job is to build the project and fix any build errors. Steps: -1. Run: cmake --build "c:\Telegram\tdesktop\out" --config Debug --target Telegram +1. Run (from repository root): cmake --build ./out --config Debug --target Telegram 2. If the build succeeds, update plan.md: change `- [ ] Build verification` to `- [x] Build verification` 3. If the build fails: a. Read the error messages carefully b. Read the relevant source files - c. Fix the errors in accordance with the plan and CLAUDE.md conventions + c. Fix the errors in accordance with the plan and AGENTS.md conventions d. Rebuild and repeat until the build passes e. Update plan.md status when done Rules: - Only fix build errors, do not refactor or improve code -- Follow CLAUDE.md conventions +- Follow AGENTS.md conventions - If build fails with file-locked errors (C1041, LNK1104), STOP and report - do not retry When finished, report the build result. @@ -411,7 +411,7 @@ REVIEW CRITERIA (in order of importance): 7. **Module structure**: Only in exceptional cases — if a large amount of newly added code (hundreds of lines) is logically distinct from the rest of its host module, suggest extracting it into a new module. But do NOT suggest new modules lightly: every module adds significant build overhead due to PCH and heavy template usage. Only suggest this when the new code is both large enough AND logically separated enough to justify it. At the same time, don't let modules grow into multi-thousand-line monoliths either. -8. **Style compliance**: Verify adherence to REVIEW.md rules (empty line before closing brace, operators at start of continuation lines, minimize type checks with direct cast instead of is+as, no if-with-initializer when simpler alternatives exist) and CLAUDE.md conventions (no unnecessary comments, `auto` usage, no hardcoded sizes — must use .style definitions), etc. +8. **Style compliance**: Verify adherence to REVIEW.md rules (empty line before closing brace, operators at start of continuation lines, minimize type checks with direct cast instead of is+as, no if-with-initializer when simpler alternatives exist) and AGENTS.md conventions (no unnecessary comments, `auto` usage, no hardcoded sizes — must use .style definitions), etc. IMPORTANT GUIDELINES: - Review ONLY the changes made, not pre-existing code in the repository. @@ -474,13 +474,13 @@ For each issue in the review: 3. Verify the change makes sense in context. After all changes are made: -1. Build: cmake --build "c:\Telegram\tdesktop\out" --config Debug --target Telegram +1. Build (from repository root): cmake --build ./out --config Debug --target Telegram 2. If the build fails, fix build errors and rebuild until it passes. 3. If build fails with file-locked errors (C1041, LNK1104), STOP and report - do not retry. Rules: - Implement exactly the changes from the review, nothing more. -- Follow CLAUDE.md coding conventions. +- Follow AGENTS.md coding conventions. - Do NOT modify .ai/ files. When finished, report what changes were made. diff --git a/.codex/skills/task-think/PROMPTS.md b/.codex/skills/task-think/PROMPTS.md deleted file mode 100644 index f86d962849..0000000000 --- a/.codex/skills/task-think/PROMPTS.md +++ /dev/null @@ -1,227 +0,0 @@ -# Phase Prompts - -Use these templates for `codex exec --json` child runs. Replace ``, ``, ``, and ``. - -## Phase 0: Setup - -Before running any phase prompts, the orchestrator must determine whether this is a new project or a follow-up task. - -**Follow-up detection:** -1. Extract the first word/token from the task description. Call it `FIRST_TOKEN`. -2. Check if `.ai//about.md` exists. -3. If it exists: this is a **follow-up task**. The project name is `FIRST_TOKEN`. The task description is everything after `FIRST_TOKEN`. -4. If it does not exist: this is a **new project**. The full input is the task description. - -**For new projects:** -- List existing `.ai/` folders to pick a unique short name (1-2 lowercase words, hyphen-separated). -- Create `.ai//` and `.ai//a/` and `logs/`. -- Set `` = `a`. - -**For follow-up tasks:** -- Scan `.ai//` for existing task folders (`a/`, `b/`, ...). Find the latest one (highest letter). -- The new task letter = next letter in sequence. -- Create `.ai///` and `logs/`. - -Then proceed to Phase 1. Follow-up tasks do NOT skip context gathering — they go through a modified version of it. - -## Phase 1: Context (New Project, letter = `a`) - -```text -You are the context phase for task "" in repository . - -Read CLAUDE.md for the basic coding rules and guidelines. - -Read AGENTS.md and all relevant source files. Write TWO documents: - -### File 1: .ai//about.md - -NOTE: This file is NOT used by any agent in the current task. It exists solely as a starting point for a FUTURE follow-up task's context gatherer. No planning, implementation, or review agent will ever read it. - -Write it as if the project is already fully implemented and working. It should contain: -- **Project**: What this project does (feature description, goals, scope) -- **Architecture**: High-level architectural decisions, which modules are involved, how they interact -- **Key Design Decisions**: Important choices made about the approach -- **Relevant Codebase Areas**: Which parts of the codebase this project touches, key types and APIs involved - -Do NOT include temporal state like "Current State", "Pending Changes", "Not yet implemented", "TODO", or any other framing that distinguishes between "done" and "not done". Describe the project as a complete, coherent whole. - -### File 2: .ai//a/context.md - -This is the PRIMARY document — all downstream agents (planning, implementation, review) will read ONLY this file. It must be completely self-contained. Include: -1. Task description restated clearly -2. Relevant files with line ranges and why they matter -3. Existing patterns to follow (with code snippets) -4. Data structures, types, classes -5. API methods (from api.tl if applicable) -6. UI styles (from .style files if applicable) -7. Localization (from lang.strings if applicable) -8. Build info and verification hooks -9. Reference implementations of similar features -10. Risks and unknowns - -Be extremely thorough. Another agent with NO prior context will read this file and must be able to understand everything needed to implement the task. - -Do not implement code in this phase. -``` - -## Phase 1F: Context (Follow-up Task, letter = `b`, `c`, ...) - -```text -You are the context phase for a follow-up task on an existing project in repository . - -NEW TASK: - -Read CLAUDE.md for the basic coding rules and guidelines. - -Steps: -1. Read AGENTS.md for project conventions. -2. Read .ai//about.md — the project-level blueprint describing everything done so far. -3. Read .ai///context.md — the previous task's gathered context. -4. Understand what has already been implemented by reading the actual source files referenced in about.md and the previous context. -5. Search the codebase for any ADDITIONAL files, classes, functions, and patterns relevant to the new task but not already covered. -6. Read all newly relevant files thoroughly. - -Write TWO files: - -### File 1: .ai//about.md (REWRITE) - -NOTE: This file is NOT used by any agent in the current task. It exists solely as a starting point for a FUTURE follow-up task's context gatherer. You are rewriting it now so that the next follow-up has an accurate project overview to start from. - -REWRITE this file (not append). The new about.md must be a single coherent document that describes the project as if everything — including this new task's changes — is already fully implemented and working. - -It should incorporate: -- Everything from the old about.md that is still accurate and relevant -- The new task's functionality described as part of the project (not as "changes to make") -- Any changed design decisions or architectural updates from the new task requirements - -It should NOT contain: -- Any temporal state: "Current State", "Pending Changes", "TODO", "Not yet implemented" -- History of how requirements changed between tasks -- References to "the old approach" vs "the new approach" -- Task-by-task changelog or timeline -- Any distinction between "what was done before" and "what this task adds" -- Information that contradicts the new task requirements - -### File 2: .ai///context.md - -This is the PRIMARY document — all downstream agents (planning, implementation, review) will read ONLY this file. It must be completely self-contained. about.md will NOT be available to them. - -It should contain: -- **Task Description**: The new task restated clearly, with enough project background that an implementation agent can understand it without reading any other .ai/ files -- **Relevant Files**: Every file path with line ranges relevant to THIS task (including files modified by previous tasks and any newly relevant files) -- **Key Code Patterns**: How similar things are done in the codebase -- **Data Structures**: Relevant types, structs, classes -- **API Methods**: Any TL schema methods involved -- **UI Styles**: Any relevant style definitions -- **Localization**: Any relevant string keys -- **Build Info**: Build command and any special notes -- **Reference Implementations**: Similar features that can serve as templates - -Be extremely thorough. Another agent with NO prior context will read ONLY this file and must be able to understand everything needed to implement the new task. Do NOT assume the reader has seen about.md or any previous task files. - -Do not implement code in this phase. -``` - -## Phase 2: Plan - -```text -You are the planning phase for task "" in repository . - -Read CLAUDE.md for the basic coding rules and guidelines. - -Read: -- .ai///context.md - -Then read the specific source files referenced in context.md to understand the code deeply. - -Create: -- .ai///plan.md - -Plan requirements: -1. Concrete file-level edits -2. Ordered phases (each phase implementable by a single agent, roughly <=8 steps) -3. Verification commands -4. Rollback/risk notes -5. Status section with checklist of phases, build verification, and code review -``` - -## Phase 3: Implement - -```text -You are the implementation phase for task "" in repository . - -Read CLAUDE.md for the basic coding rules and guidelines. - -Read: -- .ai///context.md -- .ai///plan.md - -Implement the plan in code. Then write: -- .ai///implementation.md - -Include: -1. Files changed -2. What was implemented -3. Any deviations from plan and why -4. Update plan.md Status section to mark completed phases -``` - -## Phase 4: Verify - -```text -You are the verification phase for task "" in repository . - -Read CLAUDE.md for the basic coding rules and guidelines. - -Read: -- .ai///plan.md -- .ai///implementation.md - -Run the relevant build/test commands from AGENTS.md and plan.md. -Append results to: -- .ai///implementation.md - -If blocked by locked files or access errors, stop and report exact blocker. -``` - -## Phase 5: Review - -```text -You are the review phase for task "" in repository . - -Read AGENTS.md for the basic coding rules and guidelines. -Read REVIEW.md for the style and formatting rules you must enforce. - -Read: -- .ai///context.md -- .ai///plan.md -- .ai///implementation.md - -Run `git diff` to see all uncommitted changes made by the implementation. Implementation phases do not commit, so `git diff` shows exactly the current feature's changes. Then read the modified source files in full. - -Perform a code review using these criteria (in order of importance): - -1. Correctness and safety: logic errors, null-check gaps at API boundaries, crashes, use-after-free, dangling references, race conditions. -2. Dead code: code added or left behind that is never called or used. Unused variables, unreachable branches, leftover scaffolding. -3. Redundant changes: changes in the diff with no functional effect — moving declarations or code blocks without reason, reformatting untouched code, reordering includes or fields with no purpose. Every line in the diff should serve the feature. If a file appears in `git diff` but contains only no-op rearrangements, flag it for revert. -4. Code duplication: unnecessary repetition of logic that should be shared. -5. Wrong placement: code added to a module where it doesn't logically belong. -6. Function decomposition: for longer functions (~50+ lines), consider whether a sub-task could be cleanly extracted. Only suggest when there is a genuinely self-contained piece of logic. -7. Module structure: only flag if a large amount of new code (hundreds of lines) is logically distinct from its host module. -8. Style compliance: verify adherence to REVIEW.md rules and AGENTS.md conventions. - -Write: -- .ai///review.md - -If issues are found, implement fixes and update implementation.md/review.md with final status. -``` - -## Example Runner Commands - -```powershell -codex exec --json -C "" | Tee-Object .ai///logs/phase-1-context.jsonl -codex exec --json -C "" | Tee-Object .ai///logs/phase-2-plan.jsonl -codex exec --json -C "" | Tee-Object .ai///logs/phase-3-implement.jsonl -codex exec --json -C "" | Tee-Object .ai///logs/phase-4-verify.jsonl -codex exec --json -C "" | Tee-Object .ai///logs/phase-5-review.jsonl -``` diff --git a/.cursor/rules/api_usage.mdc b/.cursor/rules/api_usage.mdc deleted file mode 100644 index bf6e55bbdd..0000000000 --- a/.cursor/rules/api_usage.mdc +++ /dev/null @@ -1,105 +0,0 @@ ---- -description: For tasks requiring sending Telegram server API requests or working with generated API types. -globs: -alwaysApply: false ---- -# Telegram Desktop API Usage - -## API Schema - -The API definitions are described using [TL Language]\(https:/core.telegram.org/mtproto/TL) in two main schema files: - -1. **`Telegram/SourceFiles/mtproto/scheme/mtproto.tl`** - * Defines the core MTProto protocol types and methods used for basic communication, encryption, authorization, service messages, etc. - * Some fundamental types and methods from this schema (like basic types, RPC calls, containers) are often implemented directly in the C++ MTProto core (`SourceFiles/mtproto/`) and may be skipped during the C++ code generation phase. - * Other parts of `mtproto.tl` might still be processed by the code generator. - -2. **`Telegram/SourceFiles/mtproto/scheme/api.tl`** - * Defines the higher-level Telegram API layer, including all the methods and types related to chat functionality, user profiles, messages, channels, stickers, etc. - * This is the primary schema used when making functional API requests within the application. - -Both files use the same TL syntax to describe API methods (functions) and types (constructors). - -## Code Generation - -A custom code generation tool processes `api.tl` (and parts of `mtproto.tl`) to create corresponding C++ classes and types. These generated headers are typically included via the Precompiled Header (PCH) for the main `Telegram` project. - -Generated types often follow the pattern `MTP[Type]` (e.g., `MTPUser`, `MTPMessage`) and methods correspond to functions within the `MTP` namespace or related classes (e.g., `MTPmessages_SendMessage`). - -## Making API Requests - -API requests are made using a standard pattern involving the `api()` object (providing access to the `MTP::Instance`), the generated `MTP...` request object, callback handlers for success (`.done()`) and failure (`.fail()`), and the `.send()` method. - -Here's the general structure: - -```cpp -// Include necessary headers if not already in PCH - -// Obtain the API instance (usually via api() or MTP::Instance::Get()) -api().request(MTPnamespace_MethodName( - // Constructor arguments based on the api.tl definition for the method - MTP_flags(flags_value), // Use MTP_flags if the method has flags - MTP_inputPeer(peer), // Use MTP_... types for parameters - MTP_string(messageText), - MTP_long(randomId), - // ... other arguments matching the TL definition - MTP_vector() // Example for a vector argument -)).done([=]\(const MTPResponseType &result) { - // Handle the successful response (result). - // 'result' will be of the C++ type corresponding to the TL type - // specified after the '=' in the api.tl method definition. - // How to access data depends on whether the TL type has one or multiple constructors: - - // 1. Multiple Constructors (e.g., User = User | UserEmpty): - // Use .match() with lambdas for each constructor: - result.match([&]\(const MTPDuser &data) { - /* use data.vfirst_name().v, etc. */ - }, [&]\(const MTPDuserEmpty &data) { - /* handle empty user */ - }); - - // Alternatively, check the type explicitly and use the constructor getter: - if (result.type() == mtpc_user) { - const auto &data = result.c_user(); // Asserts if type is not mtpc_user! - // use data.vfirst_name().v - } else if (result.type() == mtpc_userEmpty) { - const auto &data = result.c_userEmpty(); - // handle empty user - } - - // 2. Single Constructor (e.g., Messages = messages { msgs: vector }): - // Use .match() with a single lambda: - result.match([&]\(const MTPDmessages &data) { /* use data.vmessages().v */ }); - - // Or check the type explicitly and use the constructor getter: - if (result.type() == mtpc_messages) { - const auto &data = result.c_messages(); // Asserts if type is not mtpc_messages! - // use data.vmessages().v - } - - // Or use the shortcut .data() for single-constructor types: - const auto &data = result.data(); // Only works for single-constructor types! - // use data.vmessages().v - -}).fail([=]\(const MTP::Error &error) { - // Handle the API error (error). - // 'error' is an MTP::Error object containing the error code (error.type()) - // and description (error.description()). Check for specific error strings. - if (error.type() == u"FLOOD_WAIT_X"_q) { - // Handle flood wait - } else { - Ui::show(Box(Lang::Hard::ServerError())); // Example generic error handling - } -}).handleFloodErrors().send(); // handleFloodErrors() is common, then send() -``` - -**Key Points:** - -* Always refer to `Telegram/SourceFiles/mtproto/scheme/api.tl` for the correct method names, parameters (names and types), and response types. -* Use the generated `MTP...` types/classes for request parameters (e.g., `MTP_int`, `MTP_string`, `MTP_bool`, `MTP_vector`, `MTPInputUser`, etc.) and response handling. -* The `.done()` lambda receives the specific C++ `MTP...` type corresponding to the TL return type. - * For types with **multiple constructors** (e.g., `User = User | UserEmpty`), use `result.match([&]\(const MTPDuser &d){ ... }, [&]\(const MTPDuserEmpty &d){ ... })` to handle each case, or check `result.type() == mtpc_user` / `mtpc_userEmpty` and call the specific `result.c_user()` / `result.c_userEmpty()` getter (which asserts on type mismatch). - * For types with a **single constructor** (e.g., `Messages = messages{...}`), you can use `result.match([&]\(const MTPDmessages &d){ ... })` with one lambda, or check `type()` and call `c_messages()`, or use the shortcut `result.data()` to access the fields directly. -* The `.fail()` lambda receives an `MTP::Error` object. Check `error.type()` against known error strings (often defined as constants or using `u"..."_q` literals). -* Directly construct the `MTPnamespace_MethodName(...)` object inside `request()`. -* Include `.handleFloodErrors()` before `.send()` for standard flood wait handling. diff --git a/.cursor/rules/localization.mdc b/.cursor/rules/localization.mdc deleted file mode 100644 index 85e7a32c7a..0000000000 --- a/.cursor/rules/localization.mdc +++ /dev/null @@ -1,164 +0,0 @@ ---- -description: For tasks requiring changing or adding user facing phrases and text parts. -globs: -alwaysApply: false ---- -# Telegram Desktop Localization - -## Coding Style Note - -**Use `auto`:** In the actual codebase, variable types are almost always deduced using `auto` (or `const auto`, `const auto &`) rather than being written out explicitly. Examples in this guide may use explicit types for clarity, but prefer `auto` in practice. - -```cpp -// Prefer this: -auto currentTitle = tr::lng_settings_title(tr::now); -auto nameProducer = GetNameProducer(); // Returns rpl::producer<...> - -// Instead of this: -QString currentTitle = tr::lng_settings_title(tr::now); -rpl::producer nameProducer = GetNameProducer(); -``` - -## String Resource File - -Base user-facing English strings are defined in the `lang.strings` file: - -`Telegram/Resources/langs/lang.strings` - -This file uses a key-value format with named placeholders: - -``` -"lng_settings_title" = "Settings"; -"lng_confirm_delete_item" = "Are you sure you want to delete {item_name}?"; -"lng_files_selected" = "{count} files selected"; // Simple count example (see Pluralization) -``` - -Placeholders are enclosed in curly braces, e.g., `{name}`, `{user}`. A special placeholder `{count}` is used for pluralization rules. - -### Pluralization - -For keys that depend on a number (using the `{count}` placeholder), English typically requires two forms: singular and plural. These are defined in `lang.strings` using `#one` and `#other` suffixes: - -``` -"lng_files_selected#one" = "{count} file selected"; -"lng_files_selected#other" = "{count} files selected"; -``` - -While only `#one` and `#other` are defined in the base `lang.strings`, the code generation process creates C++ accessors for all six CLDR plural categories (`#zero`, `#one`, `#two`, `#few`, `#many`, `#other`) to support languages with more complex pluralization rules. - -## Translation Process - -While `lang.strings` provides the base English text and the keys, the actual translations are managed via Telegram's translations platform (translations.telegram.org) and loaded dynamically at runtime from the API. The keys from `lang.strings` (including the `#one`/`#other` variants) are used on the platform. - -## Code Generation - -A code generation tool processes `lang.strings` to create C++ structures and accessors within the `tr` namespace. These allow type-safe access to strings and handling of placeholders and pluralization. Generated keys typically follow the pattern `tr::lng_key_name`. - -## String Usage in Code - -Strings are accessed in C++ code using the generated objects within the `tr::` namespace. There are two main ways to use them: reactively (returning an `rpl::producer`) or immediately (returning the current value). - -### 1. Reactive Usage (rpl::producer) - -Calling a generated string function directly returns a reactive producer, typically `rpl::producer`. This producer automatically updates its value whenever the application language changes. - -```cpp -// Key: "settings_title" = "Settings"; -auto titleProducer = tr::lng_settings_title(); // Type: rpl::producer - -// Key: "confirm_delete_item" = "Are you sure you want to delete {item_name}?"; -auto itemNameProducer = /* ... */; // Type: rpl::producer -auto confirmationProducer = tr::lng_confirm_delete_item( // Type: rpl::producer - tr::now, // NOTE: tr::now is NOT passed here for reactive result - lt_item_name, - std::move(itemNameProducer)); // Placeholder producers should be moved -``` - -### 2. Immediate Usage (Current Value) - -Passing `tr::now` as the first argument retrieves the string's current value in the active language (typically as a `QString`). - -```cpp -// Key: "settings_title" = "Settings"; -auto currentTitle = tr::lng_settings_title(tr::now); // Type: QString - -// Key: "confirm_delete_item" = "Are you sure you want to delete {item_name}?"; -const auto currentItemName = QString("My Document"); // Type: QString -auto currentConfirmation = tr::lng_confirm_delete_item( // Type: QString - tr::now, // Pass tr::now for immediate value - lt_item_name, currentItemName); // Placeholder value is a direct QString (or convertible) -``` - -### 3. Placeholders (`{tag}`) - -Placeholders like `{item_name}` are replaced by providing arguments after `tr::now` (for immediate) or as the initial arguments (for reactive). A corresponding `lt_tag_name` constant is passed before the value. - -* **Immediate:** Pass the direct value (e.g., `QString`, `int`). -* **Reactive:** Pass an `rpl::producer` of the corresponding type (e.g., `rpl::producer`). Remember to `std::move` the producer or use `rpl::duplicate` if you need to reuse the original producer afterwards. - -### 4. Pluralization (`{count}`) - -Keys using `{count}` require a numeric value for the `lt_count` placeholder. The correct plural form (`#zero`, `#one`, ..., `#other`) is automatically selected based on this value and the current language rules. - -* **Immediate (`tr::now`):** Pass a `float64` or `int` (which is auto-converted to `float64`). - ```cpp - int count = 1; - auto filesText = tr::lng_files_selected(tr::now, lt_count, count); // Type: QString - count = 5; - filesText = tr::lng_files_selected(tr::now, lt_count, count); // Uses "files_selected#other" - ``` - -* **Reactive:** Pass an `rpl::producer`. Use the `tr::to_count()` helper to convert an `rpl::producer` or wrap a single value. - ```cpp - // From an existing int producer: - auto countProducer = /* ... */; // Type: rpl::producer - auto filesTextProducer = tr::lng_files_selected( // Type: rpl::producer - lt_count, - countProducer | tr::to_count()); // Use tr::to_count() for conversion - - // From a single int value wrapped reactively: - int currentCount = 5; - auto filesTextProducerSingle = tr::lng_files_selected( // Type: rpl::producer - lt_count, - rpl::single(currentCount) | tr::to_count()); - // Alternative for single values (less common): rpl::single(currentCount * 1.) - ``` - -### 5. Custom Projectors - -An optional final argument can be a projector function (like `Ui::Text::Upper` or `Ui::Text::WithEntities`) to transform the output. - -* If the projector returns `OutputType`, the string function returns `OutputType` (immediate) or `rpl::producer` (reactive). -* Placeholder values must match the projector's *input* requirements. For `Ui::Text::WithEntities`, placeholders expect `TextWithEntities` (immediate) or `rpl::producer` (reactive). - -```cpp -// Immediate with Ui::Text::WithEntities projector -// Key: "user_posted_photo" = "{user} posted a photo"; -const auto userName = TextWithEntities{ /* ... */ }; // Type: TextWithEntities -auto message = tr::lng_user_posted_photo( // Type: TextWithEntities - tr::now, - lt_user, - userName, // Must be TextWithEntities - Ui::Text::WithEntities); // Projector - -// Reactive with Ui::Text::WithEntities projector -auto userNameProducer = /* ... */; // Type: rpl::producer -auto messageProducer = tr::lng_user_posted_photo( // Type: rpl::producer - lt_user, - std::move(userNameProducer), // Move placeholder producers - Ui::Text::WithEntities); // Projector -``` - -## Key Summary - -* Keys are defined in `Resources/langs/lang.strings` using `{tag}` placeholders. -* Plural keys use `{count}` and have `#one`/`#other` variants in `lang.strings`. -* Access keys via `tr::lng_key_name(...)` in C++. -* Call with `tr::now` as the first argument for the immediate `QString` (or projected type). -* Call without `tr::now` for the reactive `rpl::producer` (or projected type). -* Provide placeholder values (`lt_tag_name, value`) matching the usage (direct value for immediate, `rpl::producer` for reactive). Producers should typically be moved via `std::move`. -* For `{count}`: - * Immediate: Pass `int` or `float64`. - * Reactive: Pass `rpl::producer`, typically by converting an `int` producer using `| tr::to_count()`. -* Optional projector function as the last argument modifies the output type and required placeholder types. -* Actual translations are loaded at runtime from the API. diff --git a/.cursor/rules/rpl_guide.mdc b/.cursor/rules/rpl_guide.mdc deleted file mode 100644 index dea6beba47..0000000000 --- a/.cursor/rules/rpl_guide.mdc +++ /dev/null @@ -1,216 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- -# RPL (Reactive Programming Library) Guide - -## Coding Style Note - -**Use `auto`:** In the actual codebase, variable types are almost always deduced using `auto` (or `const auto`, `const auto &`) rather than being written out explicitly. Examples in this guide may use explicit types for clarity, but prefer `auto` in practice. - -```cpp -// Prefer this: -auto intProducer = rpl::single(123); -const auto &lifetime = existingLifetime; - -// Instead of this: -rpl::producer intProducer = rpl::single(123); -const rpl::lifetime &lifetime = existingLifetime; - -// Sometimes needed if deduction is ambiguous or needs help: -auto user = std::make_shared(); -auto data = QByteArray::fromHex("..."); -``` - -## Introduction - -RPL is the reactive programming library used in this project, residing in the `rpl::` namespace. It allows handling asynchronous streams of data over time. - -The core concept is the `rpl::producer`, which represents a stream of values that can be generated over a certain lifetime. - -## Producers: `rpl::producer` - -The fundamental building block is `rpl::producer`. It produces values of `Type` and can optionally signal an error of type `Error`. By default, `Error` is `rpl::no_error`, indicating that the producer does not explicitly handle error signaling through this mechanism. - -```cpp -// A producer that emits integers. -auto intProducer = /* ... */; // Type: rpl::producer - -// A producer that emits strings and can potentially emit a CustomError. -auto stringProducerWithError = /* ... */; // Type: rpl::producer -``` - -Producers are typically lazy; they don't start emitting values until someone subscribes to them. - -## Lifetime Management: `rpl::lifetime` - -Reactive pipelines have a limited duration, managed by `rpl::lifetime`. An `rpl::lifetime` object essentially holds a collection of cleanup callbacks. When the lifetime ends (either explicitly destroyed or goes out of scope), these callbacks are executed, tearing down the associated pipeline and freeing resources. - -```cpp -rpl::lifetime myLifetime; -// ... later ... -// myLifetime is destroyed, cleanup happens. - -// Or, pass a lifetime instance to manage a pipeline's duration. -rpl::lifetime &parentLifetime = /* ... get lifetime from context ... */; -``` - -## Starting a Pipeline: `rpl::start_...` - -To consume values from a producer, you start a pipeline using one of the `rpl::start_...` methods. These methods subscribe to the producer and execute callbacks for the events they handle. - -The most common method is `rpl::on_next`: - -```cpp -auto counter = /* ... */; // Type: rpl::producer -rpl::lifetime lifetime; - -// Counter is consumed here, use std::move if it's an l-value. -std::move( - counter -) | rpl::on_next([=]\(int nextValue) { - // Process the next integer value emitted by the producer. - qDebug() << "Received: " << nextValue; -}, lifetime); // Pass the lifetime to manage the subscription. -// Note: `counter` is now in a moved-from state and likely invalid. - -// If you need to start the same producer multiple times, duplicate it: -// rpl::duplicate(counter) | rpl::on_next(...); - -// If you DON'T pass a lifetime to a start_... method: -auto counter2 = /* ... */; // Type: rpl::producer -rpl::lifetime subscriptionLifetime = std::move( - counter2 -) | rpl::on_next([=]\(int nextValue) { /* ... */ }); -// The returned lifetime MUST be stored. If it's discarded immediately, -// the subscription stops instantly. -// `counter2` is also moved-from here. -``` - -Other variants allow handling errors (`_error`) and completion (`_done`): - -```cpp -auto dataStream = /* ... */; // Type: rpl::producer -rpl::lifetime lifetime; - -// Assuming dataStream might be used again, we duplicate it for the first start. -// If it's the only use, std::move(dataStream) would be preferred. -rpl::duplicate( - dataStream -) | rpl::on_error([=]\(Error &&error) { - // Handle the error signaled by the producer. - qDebug() << "Error: " << error.text(); -}, lifetime); - -// Using dataStream again, perhaps duplicated again or moved if last use. -rpl::duplicate( - dataStream -) | rpl::on_done([=] { - // Execute when the producer signals it's finished emitting values. - qDebug() << "Stream finished."; -}, lifetime); - -// Last use of dataStream, so we move it. -std::move( - dataStream -) | rpl::on_next_error_done( - [=]\(QString &&value) { /* handle next value */ }, - [=]\(Error &&error) { /* handle error */ }, - [=] { /* handle done */ }, - lifetime); -``` - -## Transforming Producers - -RPL provides functions to create new producers by transforming existing ones: - -* `rpl::map`: Transforms each value emitted by a producer. - ```cpp - auto ints = /* ... */; // Type: rpl::producer - // The pipe operator often handles the move implicitly for chained transformations. - auto strings = std::move( - ints // Explicit move is safer - ) | rpl::map([](int value) { - return QString::number(value * 2); - }); // Emits strings like "0", "2", "4", ... - ``` - -* `rpl::filter`: Emits only the values from a producer that satisfy a condition. - ```cpp - auto ints = /* ... */; // Type: rpl::producer - auto evenInts = std::move( - ints // Explicit move is safer - ) | rpl::filter([](int value) { - return (value % 2 == 0); - }); // Emits only even numbers. - ``` - -## Combining Producers - -You can combine multiple producers into one: - -* `rpl::combine`: Combines the latest values from multiple producers whenever *any* of them emits a new value. Requires all producers to have emitted at least one value initially. - While it produces a `std::tuple`, subsequent operators like `map`, `filter`, and `on_next` can automatically unpack this tuple into separate lambda arguments. - ```cpp - auto countProducer = rpl::single(1); // Type: rpl::producer - auto textProducer = rpl::single(u"hello"_q); // Type: rpl::producer - rpl::lifetime lifetime; - - // rpl::combine takes producers by const-ref internally and duplicates, - // so move/duplicate is usually not strictly needed here unless you - // want to signal intent or manage the lifetime of p1/p2 explicitly. - auto combined = rpl::combine( - countProducer, // or rpl::duplicate(countProducer) - textProducer // or rpl::duplicate(textProducer) - ); - - // Starting the combined producer consumes it. - // The lambda receives unpacked arguments, not the tuple itself. - std::move( - combined - ) | rpl::on_next([=]\(int count, const QString &text) { - // No need for std::get<0>(latest), etc. - qDebug() << "Combined: Count=" << count << ", Text=" << text; - }, lifetime); - - // This also works with map, filter, etc. - std::move( - combined - ) | rpl::filter([=]\(int count, const QString &text) { - return count > 0 && !text.isEmpty(); - }) | rpl::map([=]\(int count, const QString &text) { - return text.repeated(count); - }) | rpl::on_next([=]\(const QString &result) { - qDebug() << "Mapped & Filtered: " << result; - }, lifetime); - ``` - -* `rpl::merge`: Merges the output of multiple producers of the *same type* into a single producer. It emits a value whenever *any* of the source producers emits a value. - ```cpp - auto sourceA = /* ... */; // Type: rpl::producer - auto sourceB = /* ... */; // Type: rpl::producer - - // rpl::merge also duplicates internally. - auto merged = rpl::merge(sourceA, sourceB); - - // Starting the merged producer consumes it. - std::move( - merged - ) | rpl::on_next([=]\(QString &&value) { - // Receives values from either sourceA or sourceB as they arrive. - qDebug() << "Merged value: " << value; - }, lifetime); - ``` - -## Key Concepts Summary - -* Use `rpl::producer` to represent streams of values. -* Manage subscription duration using `rpl::lifetime`. -* Pass `rpl::lifetime` to `rpl::start_...` methods. -* If `rpl::lifetime` is not passed, **store the returned lifetime** to keep the subscription active. -* Use operators like `| rpl::map`, `| rpl::filter` to transform streams. -* Use `rpl::combine` or `rpl::merge` to combine streams. -* When starting a chain (`std::move(producer) | rpl::map(...)`), explicitly move the initial producer. -* These functions typically duplicate their input producers internally. -* Starting a pipeline consumes the producer; use ` diff --git a/.cursor/rules/styling.mdc b/.cursor/rules/styling.mdc deleted file mode 100644 index 42729f096c..0000000000 --- a/.cursor/rules/styling.mdc +++ /dev/null @@ -1,154 +0,0 @@ ---- -description: For tasks requiring working with user facing UI components. -globs: -alwaysApply: false ---- -# Telegram Desktop UI Styling - -## Style Definition Files - -UI element styles (colors, fonts, paddings, margins, icons, etc.) are defined in `.style` files using a custom syntax. These files are located alongside the C++ source files they correspond to within specific UI component directories (e.g., `Telegram/SourceFiles/ui/chat/chat.style`). - -Definitions from other `.style` files can be included using the `using` directive at the top of the file: -```style -using "ui/basic.style"; -using "ui/widgets/widgets.style"; -``` - -The central definition of named colors happens in `Telegram/SourceFiles/ui/colors.palette`. This file allows for theme generation and loading colors from various sources. - -### Syntax Overview - -1. **Built-in Types:** The syntax recognizes several base types inferred from the value assigned: - * `int`: Integer numbers (e.g., `lineHeight: 20;`) - * `bool`: Boolean values (e.g., `useShadow: true;`) - * `pixels`: Pixel values, ending with `px` (e.g., `borderWidth: 1px;`). Generated as `int` in C++. - * `color`: Named colors defined in `colors.palette` (e.g., `background: windowBg;`) - * `icon`: Defined inline using a specific syntax (see below). Generates `style::icon`. - * `margins`: Four pixel values for margins or padding. Requires `margins(top, right, bottom, left)` syntax (e.g., `margin: margins(10px, 5px, 10px, 5px);` or `padding: margins(8px, 8px, 8px, 8px);`). Generates `style::margins` (an alias for `QMargins`). - * `size`: Two pixel values for width and height (e.g., `iconSize: size(16px, 16px);`). Generates `style::size`. - * `point`: Two pixel values for x and y coordinates (e.g., `textPos: point(5px, 2px);`). Generates `style::point`. - * `align`: Alignment keywords (e.g., `textAlign: align(center);` or `iconAlign: align(left);`). Generates `style::align`. - * `font`: Font definitions (e.g., `font: font(14px semibold);`). Generates `style::font`. - * `double`: Floating point numbers (e.g., `disabledOpacity: 0.5;`) - - *Note on Borders:* Borders are typically defined using multiple fields like `border: pixels;` (for width) and `borderFg: color;` (for color), rather than a single CSS-like property. - -2. **Structure Definition:** You can define complex data structures directly within the `.style` file: - ```style - MyButtonStyle { // Defines a structure named 'MyButtonStyle' - textPadding: margins; // Field 'textPadding' expects margins type - icon: icon; // Field 'icon' of type icon - height: pixels; // Field 'height' of type pixels - } - ``` - This generates a `struct MyButtonStyle { ... };` inside the `namespace style`. Fields will have corresponding C++ types (`style::margins`, `style::icon`, `int`). - -3. **Variable Definition & Inheritance:** Variables are defined using `name: value;` or `groupName { ... }`. They can be of built-in types or custom structures. Structures can be initialized inline or inherit from existing variables. - - **Icon Definition Syntax:** Icons are defined inline using the `icon{...}` syntax. The generator probes for `.svg` files or `.png` files (including `@2x`, `@3x` variants) based on the provided path stem. - ```style - // Single-part icon definition: - myIconSearch: icon{{ "gui/icons/search", iconColor }}; - // Multi-part icon definition (layers drawn bottom-up): - myComplexIcon: icon{ - { "gui/icons/background", iconBgColor }, - { "gui/icons/foreground", iconFgColor } - }; - // Icon with path modifiers (PNG only for flips, SVG only for size): - myFlippedIcon: icon{{ "gui/icons/arrow-flip_horizontal", arrowColor }}; - myResizedIcon: icon{{ "gui/icons/logo-128x128", logoColor }}; // Forces 128x128 for SVG - ``` - - **Other Variable Examples:** - ```style - // Simple variables - buttonHeight: 30px; - activeButtonColor: buttonBgActive; // Named color from colors.palette - - // Variable of a custom structure type, initialized inline - defaultButton: MyButtonStyle { - textPadding: margins(10px, 15px, 10px, 15px); // Use margins(...) syntax - icon: myIconSearch; // Assign the previously defined icon variable - height: buttonHeight; // Reference another variable - } - - // Another variable inheriting from 'defaultButton' and overriding/adding fields - primaryButton: MyButtonStyle(defaultButton) { - icon: myComplexIcon; // Override icon with the multi-part one - backgroundColor: activeButtonColor; // Add a field not in MyButtonStyle definition - } - - // Style group (often used for specific UI elements) - chatInput { // Example using separate border properties and explicit padding - border: 1px; // Border width - borderFg: defaultInputFieldBorder; // Border color (named color) - padding: margins(5px, 10px, 5px, 10px); // Use margins(...) syntax for padding field - backgroundColor: defaultChatBg; // Background color - } - ``` - -## Code Generation - -A code generation tool processes these `.style` files and `colors.palette` to create C++ objects. -- The `using` directives resolve dependencies between `.style` files. -- Custom structure definitions (like `MyButtonStyle`) generate corresponding `struct MyButtonStyle { ... };` within the `namespace style`. -- Style variables/groups (like `defaultButton`, `primaryButton`, `chatInput`) are generated as objects/structs within the `st` namespace (e.g., `st::defaultButton`, `st::primaryButton`, `st::chatInput`). These generated structs contain members corresponding to the fields defined in the `.style` file. -- Color objects are generated into the `st` namespace as well, based on their names in `colors.palette` (e.g., `st::windowBg`, `st::buttonBgActive`). -- The generated header files for styles are placed in the `Telegram/SourceFiles/styles/` directory with a `style_` prefix (e.g., `styles/style_widgets.h` for `ui/widgets/widgets.style`). You include them like `#include "styles/style_widgets.h"`. - -Generated C++ types correspond to the `.style` types: `style::color`, `style::font`, `style::margins` (used for both `margin:` and `padding:` fields), `style::icon`, `style::size`, `style::point`, `style::align`, and `int` or `bool` for simple types. - -## Style Usage in Code - -Styles are applied in C++ code by referencing the generated `st::...` objects and their members. - -```cpp -// Example: Including the generated style header -#include "styles/style_widgets.h" // For styles defined in ui/widgets/widgets.style - -// ... inside some UI class code ... - -// Accessing members of a generated style struct -int height = st::primaryButton.height; // Accessing the 'height' field (pixels -> int) -const style::icon &icon = st::primaryButton.icon; // Accessing the 'icon' field (st::myComplexIcon) -style::margins padding = st::primaryButton.textPadding; // Accessing 'textPadding' -style::color bgColor = st::primaryButton.backgroundColor; // Accessing the color (st::activeButtonColor) - -// Applying styles (conceptual examples) -myButton->setIcon(st::primaryButton.icon); -myButton->setHeight(st::primaryButton.height); -myButton->setPadding(st::primaryButton.textPadding); -myButton->setBackgroundColor(st::primaryButton.backgroundColor); - -// Using styles directly in painting -void MyWidget::paintEvent(QPaintEvent *e) { - Painter p(this); - p.fillRect(rect(), st::chatInput.backgroundColor); // Use color from chatInput style - - // Border painting requires width and color - int borderWidth = st::chatInput.border; // Access border width (pixels -> int) - style::color borderColor = st::chatInput.borderFg; // Access border color - if (borderWidth > 0) { - p.setPen(QPen(borderColor, borderWidth)); - // Adjust rect for pen width if needed before drawing - p.drawRect(rect().adjusted(borderWidth / 2, borderWidth / 2, -borderWidth / 2, -borderWidth / 2)); - } - - // Access padding (style::margins) - style::margins inputPadding = st::chatInput.padding; - // ... use inputPadding.top(), inputPadding.left() etc. for content layout ... -} -``` - -**Key Points:** - -* Styles are defined in `.style` files next to their corresponding C++ source files. -* `using "path/to/other.style";` includes definitions from other style files. -* Named colors are defined centrally in `ui/colors.palette`. -* `.style` syntax supports built-in types (like `pixels`, `color`, `margins`, `point`, `size`, `align`, `font`, `double`), custom structure definitions (`Name { field: type; ... }`), variable definitions (`name: value;`), and inheritance (`child: Name(parent) { ... }`). -* Values must match the expected type (e.g., fields declared as `margins` type, like `margin:` or `padding:`, require `margins(...)` syntax). Borders are typically set via separate `border: pixels;` and `borderFg: color;` fields. -* Icons are defined inline using `name: icon{{ "path_stem", color }};` or `name: icon{ { "path1", c1 }, ... };` syntax, with optional path modifiers. -* Code generation creates `struct` definitions in the `style` namespace for custom types and objects/structs in the `st` namespace for defined variables/groups. -* Generated headers are in `styles/` with a `style_` prefix and must be included. -* Access style properties via the generated `st::` objects (e.g., `st::primaryButton.height`, `st::chatInput.backgroundColor`). From 3134f6a08f9d308f5cffb0076f7d22a836605f9b Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 16 Feb 2026 14:10:25 +0400 Subject: [PATCH 042/179] [ai] Improve style rule for trailing newlines. --- REVIEW.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/REVIEW.md b/REVIEW.md index 33345a7cb2..5900278677 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -4,7 +4,7 @@ This file contains style and formatting rules that the review subagent must chec ## Empty line before closing brace -Always add an empty line before the closing brace of a class (after all private fields): +Always add an empty line before the closing brace of a **class** (which has one or more sections like `public:` / `private:`). Plain **structs** with just data members do NOT get a trailing empty line — they are compact: `struct Foo { data lines; };`. ```cpp // BAD: From f9e2b5f9a246df8de6d3cdd62605e774314783ab Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 18 Feb 2026 14:11:40 +0400 Subject: [PATCH 043/179] [ai] Add uninitialized variables review rule. --- REVIEW.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/REVIEW.md b/REVIEW.md index 5900278677..e28252d5d4 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -94,6 +94,26 @@ if (const auto peer = session().data().peerLoaded(peerId) if (const auto peer = session().data().peerLoaded(peerId)) { if (const auto user = peer->asUser()) { +## Always initialize variables of basic types + +Never leave variables of basic types (`int`, `float`, `bool`, pointers, etc.) uninitialized. Custom types with constructors are fine — they initialize themselves. But for any basic type, always provide a default value (`= 0`, `= false`, `= nullptr`, etc.). This applies especially to class fields, where uninitialized members are a persistent source of bugs. + +The only exception is performance-critical hot paths where you can prove no read-from-uninitialized-memory occurs. For class fields there is no such exception — always initialize. + +```cpp +// BAD: +int _bulletLeft; +int _bulletTop; +bool _expanded; +SomeType *_pointer; + +// GOOD: +int _bulletLeft = 0; +int _bulletTop = 0; +bool _expanded = false; +SomeType *_pointer = nullptr; +``` + ## std::optional access — avoid value() Do not call `std::optional::value()` because it throws an exception that is not available on older macOS targets. Use `has_value()`, `value_or()`, `operator bool()`, or `operator*` instead. From 3d6b1feee0d910ac3b71dbfe2e91e8ee34f377f3 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 19 Feb 2026 09:34:44 +0300 Subject: [PATCH 044/179] Fixed emoji display in top bar from admin log. Related commit: 29ca797b3b. --- .../profile/profile_back_button.cpp | 37 ++++++++++--------- .../SourceFiles/profile/profile_back_button.h | 8 ++-- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/Telegram/SourceFiles/profile/profile_back_button.cpp b/Telegram/SourceFiles/profile/profile_back_button.cpp index 410d00469a..ecdfed7f2f 100644 --- a/Telegram/SourceFiles/profile/profile_back_button.cpp +++ b/Telegram/SourceFiles/profile/profile_back_button.cpp @@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "profile/profile_back_button.h" -#include "ui/painter.h" +#include "ui/text/text.h" #include "styles/style_widgets.h" #include "styles/style_window.h" #include "styles/style_profile.h" @@ -24,13 +24,13 @@ BackButton::BackButton(QWidget *parent) : Ui::AbstractButton(parent) { } void BackButton::setText(const QString &text) { - _text = text; + _text.setText(st::semiboldTextStyle, text); _cachedWidth = -1; update(); } void BackButton::setSubtext(const QString &subtext) { - _subtext = subtext; + _subtext.setText(st::defaultTextStyle, subtext); _cachedWidth = -1; update(); } @@ -80,18 +80,13 @@ void BackButton::updateCache() { const auto availableWidth = width() - st::historyAdminLogTopBarLeft - widgetWidth; - _cachedElidedText = st::semiboldFont->elided( - _text, - availableWidth); - _cachedElidedSubtext = st::dialogsTextFont->elided( - _subtext, - availableWidth); + _elisionWidth = availableWidth; } void BackButton::paintEvent(QPaintEvent *e) { updateCache(); - auto p = Painter(this); + auto p = QPainter(this); p.fillRect(e->rect(), st::profileBg); st::topBarBack.paint( @@ -112,18 +107,24 @@ void BackButton::paintEvent(QPaintEvent *e) { : 0; const auto textX = st::historyAdminLogTopBarLeft + widgetWidth; - p.setFont(st::semiboldFont); + const auto context = Ui::Text::PaintContext{ + .position = QPoint{ textX, startY }, + .outerWidth = width(), + .availableWidth = _elisionWidth, + .elisionLines = 1, + }; p.setPen(st::dialogsNameFg); - p.drawTextLeft(textX, startY, width(), _cachedElidedText); + _text.draw(p, context); if (!_subtext.isEmpty()) { - p.setFont(st::dialogsTextFont); + const auto subtextContext = Ui::Text::PaintContext{ + .position = { textX, startY + textHeight + st::lineWidth * 2 }, + .outerWidth = width(), + .availableWidth = _elisionWidth, + .elisionLines = 1, + }; p.setPen(st::historyStatusFg); - p.drawTextLeft( - textX, - startY + textHeight + st::lineWidth * 2, - width(), - _cachedElidedSubtext); + _subtext.draw(p, subtextContext); } } diff --git a/Telegram/SourceFiles/profile/profile_back_button.h b/Telegram/SourceFiles/profile/profile_back_button.h index f8df0a0bd8..b55163294d 100644 --- a/Telegram/SourceFiles/profile/profile_back_button.h +++ b/Telegram/SourceFiles/profile/profile_back_button.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "ui/abstract_button.h" +#include "ui/text/text.h" class QGraphicsOpacityEffect; @@ -32,13 +33,12 @@ private: void updateCache(); rpl::lifetime _unreadBadgeLifetime; - QString _text; - QString _subtext; + Ui::Text::String _text; + Ui::Text::String _subtext; Ui::RpWidget *_widget = nullptr; int _cachedWidth = -1; - QString _cachedElidedText; - QString _cachedElidedSubtext; + int _elisionWidth = 0; float64 _opacity = 1.0; QGraphicsOpacityEffect *_opacityEffect = nullptr; From ca8959fc67b9c0f0d9b87824f333064049507a66 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 19 Feb 2026 09:43:01 +0300 Subject: [PATCH 045/179] Added auto-close for moderate box when all messages are deleted. --- .../SourceFiles/boxes/moderate_messages_box.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index 59ab073528..09f9bc3f3b 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -381,6 +381,20 @@ void CreateModerateMessagesBox( const auto historyPeerId = history->peer->id; const auto ids = session->data().itemsToIds(items); + { + const auto remainingIds + = box->lifetime().make_state>( + ids.begin(), + ids.end()); + session->data().itemRemoved( + ) | rpl::on_next([=](not_null item) { + remainingIds->erase(item->fullId()); + if (remainingIds->empty()) { + box->closeBox(); + } + }, box->lifetime()); + } + if (ModerateCommonGroups.value() || session->supportMode()) { ProccessCommonGroups( items, From dff3698432c60b8a1a1885a7ee1f1cee80adf46d Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 19 Feb 2026 10:59:54 +0300 Subject: [PATCH 046/179] Replaced FileLoadTask constructor parameters with structs. --- Telegram/SourceFiles/apiwrap.cpp | 145 ++++++++++-------- .../chat_helpers/stickers_lottie.cpp | 20 +-- .../SourceFiles/storage/localimageloader.cpp | 75 ++++----- .../SourceFiles/storage/localimageloader.h | 52 ++++--- 4 files changed, 145 insertions(+), 147 deletions(-) diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 47e59c7a1a..af074e8a75 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3741,14 +3741,16 @@ void ApiWrap::sendVoiceMessage( const SendAction &action) { const auto caption = TextWithTags(); const auto to = FileLoadTaskOptions(action); - _fileLoader->addTask(std::make_unique( - &session(), - result, - duration, - waveform, - video, - to, - caption)); + _fileLoader->addTask( + std::make_unique(FileLoadTask::VoiceArgs{ + .session = &session(), + .voice = result, + .duration = duration, + .waveform = waveform, + .video = video, + .to = to, + .caption = caption, + })); } void ApiWrap::editMedia( @@ -3774,29 +3776,35 @@ void ApiWrap::editMedia( } const auto forceFile = (type == SendMediaType::File) && (file.type == Ui::PreparedFile::Type::Video); - _fileLoader->addTask(std::make_unique( - &session(), - file.path, - file.content, - std::move(file.information), - (file.videoCover - ? std::make_unique( - &session(), - file.videoCover->path, - file.videoCover->content, - std::move(file.videoCover->information), - nullptr, - SendMediaType::Photo, - to, - TextWithTags(), - false) + _fileLoader->addTask(std::make_unique(FileLoadTask::Args{ + .session = &session(), + .filepath = file.path, + .content = file.content, + .information = std::move(file.information), + .videoCover = (file.videoCover + ? std::make_unique(FileLoadTask::Args{ + .session = &session(), + .filepath = file.videoCover->path, + .content = file.videoCover->content, + .information = std::move(file.videoCover->information), + .videoCover = nullptr, + .type = SendMediaType::Photo, + .to = to, + .caption = TextWithTags(), + .spoiler = false, + .album = nullptr, + .forceFile = false, + .idOverride = 0, + }) : nullptr), - type, - to, - caption, - file.spoiler, - nullptr, - forceFile)); + .type = type, + .to = to, + .caption = caption, + .spoiler = file.spoiler, + .album = nullptr, + .forceFile = forceFile, + .idOverride = 0, + })); } void ApiWrap::sendFiles( @@ -3831,30 +3839,35 @@ void ApiWrap::sendFiles( : SendMediaType::File; const auto forceFile = (type == SendMediaType::File) && (file.type == Ui::PreparedFile::Type::Video); - tasks.push_back(std::make_unique( - &session(), - file.path, - file.content, - std::move(file.information), - (file.videoCover - ? std::make_unique( - &session(), - file.videoCover->path, - file.videoCover->content, - std::move(file.videoCover->information), - nullptr, - SendMediaType::Photo, - to, - TextWithTags(), - false, - nullptr) + tasks.push_back(std::make_unique(FileLoadTask::Args{ + .session = &session(), + .filepath = file.path, + .content = file.content, + .information = std::move(file.information), + .videoCover = (file.videoCover + ? std::make_unique(FileLoadTask::Args{ + .session = &session(), + .filepath = file.videoCover->path, + .content = file.videoCover->content, + .information = std::move(file.videoCover->information), + .videoCover = nullptr, + .type = SendMediaType::Photo, + .to = to, + .caption = TextWithTags(), + .spoiler = false, + .album = nullptr, + .forceFile = false, + .idOverride = 0, + }) : nullptr), - uploadWithType, - to, - caption, - file.spoiler, - album, - forceFile)); + .type = uploadWithType, + .to = to, + .caption = caption, + .spoiler = file.spoiler, + .album = album, + .forceFile = forceFile, + .idOverride = 0, + })); caption = TextWithTags(); } if (album) { @@ -3874,18 +3887,20 @@ void ApiWrap::sendFile( const auto to = FileLoadTaskOptions(action); auto caption = TextWithTags(); const auto spoiler = false; - const auto information = nullptr; - const auto videoCover = nullptr; - _fileLoader->addTask(std::make_unique( - &session(), - QString(), - fileContent, - information, - videoCover, - type, - to, - caption, - spoiler)); + _fileLoader->addTask(std::make_unique(FileLoadTask::Args{ + .session = &session(), + .filepath = QString(), + .content = fileContent, + .information = nullptr, + .videoCover = nullptr, + .type = type, + .to = to, + .caption = caption, + .spoiler = spoiler, + .album = nullptr, + .forceFile = false, + .idOverride = 0 + })); } void ApiWrap::sendUploadedPhoto( diff --git a/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp b/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp index 0c62f7680a..e2b9cd80e7 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp @@ -324,18 +324,14 @@ QSize ComputeStickerSize(not_null document, QSize box) { not_null GenerateLocalSticker( not_null session, const QString &path) { - auto task = FileLoadTask( - session, - path, - QByteArray(), - nullptr, - nullptr, - SendMediaType::File, - FileLoadTo(0, {}, {}, 0), - {}, - false, - nullptr, - LocalStickerId(path)); + auto task = FileLoadTask(FileLoadTask::Args{ + .session = session, + .filepath = path, + .type = SendMediaType::File, + .to = FileLoadTo(0, {}, {}, 0), + .caption = {}, + .idOverride = LocalStickerId(path), + }); task.process({ .generateGoodThumbnail = false }); const auto result = task.peekResult(); Assert(result != nullptr); diff --git a/Telegram/SourceFiles/storage/localimageloader.cpp b/Telegram/SourceFiles/storage/localimageloader.cpp index 8c143c2a10..34be9f435d 100644 --- a/Telegram/SourceFiles/storage/localimageloader.cpp +++ b/Telegram/SourceFiles/storage/localimageloader.cpp @@ -466,57 +466,38 @@ std::shared_ptr MakePreparedFile( return std::make_shared(std::move(descriptor)); } -FileLoadTask::FileLoadTask( - not_null session, - const QString &filepath, - const QByteArray &content, - std::unique_ptr information, - std::unique_ptr videoCover, - SendMediaType type, - const FileLoadTo &to, - const TextWithTags &caption, - bool spoiler, - std::shared_ptr album, - bool forceFile, - uint64 idOverride) -: _id(idOverride ? idOverride : base::RandomValue()) -, _session(session) -, _dcId(session->mainDcId()) -, _to(to) -, _album(std::move(album)) -, _filepath(filepath) -, _content(content) -, _videoCover(std::move(videoCover)) -, _information(std::move(information)) -, _type(type) -, _caption(caption) -, _spoiler(spoiler) -, _forceFile(forceFile) { - Expects(to.options.scheduled - || to.options.shortcutId - || !to.replaceMediaOf - || IsServerMsgId(to.replaceMediaOf)); +FileLoadTask::FileLoadTask(Args &&args) +: _id(args.idOverride ? args.idOverride : base::RandomValue()) +, _session(args.session) +, _dcId(args.session->mainDcId()) +, _to(std::move(args.to)) +, _album(std::move(args.album)) +, _filepath(std::move(args.filepath)) +, _content(std::move(args.content)) +, _videoCover(std::move(args.videoCover)) +, _information(std::move(args.information)) +, _type(args.type) +, _caption(std::move(args.caption)) +, _spoiler(args.spoiler) +, _forceFile(args.forceFile) { + Expects(_to.options.scheduled + || _to.options.shortcutId + || !_to.replaceMediaOf + || IsServerMsgId(_to.replaceMediaOf)); SendLargePhotosAtomic = SendLargePhotos.value(); } -FileLoadTask::FileLoadTask( - not_null session, - const QByteArray &voice, - crl::time duration, - const VoiceWaveform &waveform, - bool video, - const FileLoadTo &to, - const TextWithTags &caption) +FileLoadTask::FileLoadTask(VoiceArgs &&args) : _id(base::RandomValue()) -, _session(session) -, _dcId(session->mainDcId()) -, _to(to) -, _content(voice) -, _duration(duration) -, _waveform(waveform) -, _type(video ? SendMediaType::Round : SendMediaType::Audio) -, _caption(caption) { +, _session(args.session) +, _dcId(args.session->mainDcId()) +, _to(std::move(args.to)) +, _content(std::move(args.voice)) +, _duration(args.duration) +, _waveform(std::move(args.waveform)) +, _type(args.video ? SendMediaType::Round : SendMediaType::Audio) +, _caption(std::move(args.caption)) { } FileLoadTask::~FileLoadTask() = default; @@ -686,7 +667,7 @@ bool FileLoadTask::FillImageInformation( return true; } -void FileLoadTask::process(Args &&args) { +void FileLoadTask::process(ProcessArgs &&args) { _result = MakePreparedFile({ .taskId = id(), .id = _id, diff --git a/Telegram/SourceFiles/storage/localimageloader.h b/Telegram/SourceFiles/storage/localimageloader.h index a5f38e46b4..2c011f45fd 100644 --- a/Telegram/SourceFiles/storage/localimageloader.h +++ b/Telegram/SourceFiles/storage/localimageloader.h @@ -220,37 +220,43 @@ public: QByteArray content = {}, QByteArray format = {}); - FileLoadTask( - not_null session, - const QString &filepath, - const QByteArray &content, - std::unique_ptr information, - std::unique_ptr videoCover, - SendMediaType type, - const FileLoadTo &to, - const TextWithTags &caption, - bool spoiler, - std::shared_ptr album = nullptr, - bool forceFile = false, - uint64 idOverride = 0); - FileLoadTask( - not_null session, - const QByteArray &voice, - crl::time duration, - const VoiceWaveform &waveform, - bool video, - const FileLoadTo &to, - const TextWithTags &caption); + struct Args { + not_null session; + QString filepath; + QByteArray content; + std::unique_ptr information; + std::unique_ptr videoCover; + SendMediaType type; + FileLoadTo to; + TextWithTags caption; + bool spoiler = false; + std::shared_ptr album; + bool forceFile = false; + uint64 idOverride = 0; + }; + + struct VoiceArgs { + not_null session; + QByteArray voice; + crl::time duration = 0; + VoiceWaveform waveform; + bool video = false; + FileLoadTo to; + TextWithTags caption; + }; + + explicit FileLoadTask(Args &&args); + explicit FileLoadTask(VoiceArgs &&args); ~FileLoadTask(); uint64 fileid() const { return _id; } - struct Args { + struct ProcessArgs { bool generateGoodThumbnail = true; }; - void process(Args &&args); + void process(ProcessArgs &&args); void process() override { process({}); From 7ffebafb60003ab0a3f9e4180f1f2225523e2005 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 19 Feb 2026 10:13:14 +0300 Subject: [PATCH 047/179] Added ability rename files from send files box. --- Telegram/Resources/langs/lang.strings | 2 + Telegram/SourceFiles/apiwrap.cpp | 2 + Telegram/SourceFiles/boxes/send_files_box.cpp | 108 ++++++++++++++++++ Telegram/SourceFiles/boxes/send_files_box.h | 5 + .../SourceFiles/storage/localimageloader.cpp | 7 +- .../SourceFiles/storage/localimageloader.h | 2 + .../attach_abstract_single_file_preview.cpp | 7 ++ .../attach_abstract_single_file_preview.h | 1 + .../ui/chat/attach/attach_prepare.cpp | 4 + .../ui/chat/attach/attach_prepare.h | 1 + .../attach/attach_single_file_preview.cpp | 15 ++- .../chat/attach/attach_single_file_preview.h | 1 + 12 files changed, 151 insertions(+), 4 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 859d24d020..38ac04e215 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7600,4 +7600,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_mac_hold_to_quit" = "Hold {text} to Quit"; +"lng_rename_file" = "Rename file"; + // Keys finished diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index af074e8a75..e295b5eb25 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3804,6 +3804,7 @@ void ApiWrap::editMedia( .album = nullptr, .forceFile = forceFile, .idOverride = 0, + .displayName = file.displayName, })); } @@ -3867,6 +3868,7 @@ void ApiWrap::sendFiles( .album = album, .forceFile = forceFile, .idOverride = 0, + .displayName = file.displayName, })); caption = TextWithTags(); } diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index 7718db922e..b8d7e9a34c 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -62,12 +62,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" #include "styles/style_layers.h" +#include "styles/style_settings.h" +#include "styles/style_menu_icons.h" #include namespace { constexpr auto kMaxMessageLength = 4096; +constexpr auto kMaxDisplayNameLength = 64; using Ui::SendFilesWay; @@ -113,6 +116,55 @@ rpl::producer FieldPlaceholder( : tr::lng_photos_comment(); } +void RenameFileBox( + not_null box, + const QString ¤tName, + Fn apply) { + box->setTitle(tr::lng_rename_file()); + const auto field = box->addRow(object_ptr( + box, + st::settingsDeviceName, + rpl::single(QString()), + currentName)); + const auto extension = [&] { + const auto dot = currentName.lastIndexOf('.'); + return (dot >= 0) ? currentName.mid(dot) : QString(); + }(); + const auto nameWithoutExt = extension.isEmpty() + ? currentName + : currentName.left(currentName.size() - extension.size()); + const auto maxNameLength = kMaxDisplayNameLength - extension.size(); + field->setMaxLength((maxNameLength > 0) ? maxNameLength : 0); + field->setText(nameWithoutExt); + field->selectAll(); + box->setFocusCallback([=] { + field->setFocusFast(); + }); + const auto save = [=] { + const auto newName = field->getLastText().trimmed(); + if (newName.isEmpty()) { + field->showError(); + return; + } + if ((newName.size() + extension.size()) > kMaxDisplayNameLength) { + field->showError(); + return; + } + const auto weak = base::make_weak(box); + apply(newName + extension); + if (const auto strong = weak.get()) { + strong->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, @@ -469,6 +521,16 @@ QImage SendFilesBox::Block::generatePriceTagBackground() const { return QImage(); } +bool SendFilesBox::Block::setSingleFileDisplayName( + const QString &displayName) { + if (_isAlbum || _isSingleMedia) { + return false; + } + const auto single = static_cast(_preview.get()); + single->setDisplayName(displayName); + return true; +} + SendFilesBox::SendFilesBox( QWidget*, not_null controller, @@ -689,6 +751,18 @@ void SendFilesBox::refreshAllAfterChanges(int fromItem, Fn perform) { captionResized(); } +bool SendFilesBox::setDisplayNameInSingleFilePreview( + int fileIndex, + const QString &displayName) { + for (auto &block : _blocks) { + if (fileIndex < block.fromIndex() || fileIndex >= block.tillIndex()) { + continue; + } + return block.setSingleFileDisplayName(displayName); + } + return false; +} + void SendFilesBox::openDialogToAddFileToAlbum() { const auto show = uiShow(); const auto checkResult = [=](const Ui::PreparedList &list) { @@ -1283,6 +1357,40 @@ void SendFilesBox::pushBlock(int from, int till) { _priceTag->update(); } }, widget->lifetime()); + + struct State { + base::unique_qptr menu; + }; + const auto state = widget->lifetime().make_state(); + base::install_event_filter(widget, [=, from = from, till = till]( + not_null e) { + if (e->type() == QEvent::ContextMenu) { + const auto mouse = static_cast(e.get()); + if (from >= till || from >= _list.files.size()) { + return base::EventFilterResult::Continue; + } + const auto fileIndex = from; + 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); + state->menu->popup(mouse->globalPos()); + return base::EventFilterResult::Cancel; + } + return base::EventFilterResult::Continue; + }, widget->lifetime()); } void SendFilesBox::refreshControls(bool initial) { diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index 4d2db047e5..0e47358424 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -177,6 +177,8 @@ private: void applyChanges(); [[nodiscard]] QImage generatePriceTagBackground() const; + [[nodiscard]] bool setSingleFileDisplayName( + const QString &displayName); private: base::unique_qptr _preview; @@ -243,6 +245,9 @@ private: void openDialogToAddFileToAlbum(); void refreshAllAfterChanges(int fromItem, Fn perform = nullptr); + [[nodiscard]] bool setDisplayNameInSingleFilePreview( + int fileIndex, + const QString &displayName); void enqueueNextPrepare(); void addPreparedAsyncFile(Ui::PreparedFile &&file); diff --git a/Telegram/SourceFiles/storage/localimageloader.cpp b/Telegram/SourceFiles/storage/localimageloader.cpp index 34be9f435d..5ecabb50a9 100644 --- a/Telegram/SourceFiles/storage/localimageloader.cpp +++ b/Telegram/SourceFiles/storage/localimageloader.cpp @@ -473,6 +473,7 @@ FileLoadTask::FileLoadTask(Args &&args) , _to(std::move(args.to)) , _album(std::move(args.album)) , _filepath(std::move(args.filepath)) +, _displayName(std::move(args.displayName)) , _content(std::move(args.content)) , _videoCover(std::move(args.videoCover)) , _information(std::move(args.information)) @@ -810,7 +811,11 @@ void FileLoadTask::process(ProcessArgs &&args) { QImage goodThumbnail; QByteArray goodThumbnailBytes; - QVector attributes(1, MTP_documentAttributeFilename(MTP_string(filename))); + auto attributes = QVector( + 1, + MTP_documentAttributeFilename(MTP_string(_displayName.isEmpty() + ? filename + : _displayName))); auto thumbnail = PreparedFileThumbnail(); diff --git a/Telegram/SourceFiles/storage/localimageloader.h b/Telegram/SourceFiles/storage/localimageloader.h index 2c011f45fd..e2c3832b8b 100644 --- a/Telegram/SourceFiles/storage/localimageloader.h +++ b/Telegram/SourceFiles/storage/localimageloader.h @@ -233,6 +233,7 @@ public: std::shared_ptr album; bool forceFile = false; uint64 idOverride = 0; + QString displayName; }; struct VoiceArgs { @@ -292,6 +293,7 @@ private: FileLoadTo _to; const std::shared_ptr _album; QString _filepath; + QString _displayName; QByteArray _content; std::unique_ptr _videoCover; std::unique_ptr _information; 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 77e8bfd538..5ad8e15d18 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 @@ -68,6 +68,13 @@ rpl::producer<> AbstractSingleFilePreview::clearCoverRequests() const { return rpl::never<>(); } +void AbstractSingleFilePreview::setDisplayName(const QString &displayName) { + auto data = _data; + data.name = displayName; + setData(data); + update(); +} + void AbstractSingleFilePreview::prepareThumbFor( Data &data, const QImage &preview) { 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 eef735f265..5fd830725a 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 @@ -32,6 +32,7 @@ public: [[nodiscard]] rpl::producer<> modifyRequests() const override; [[nodiscard]] rpl::producer<> editCoverRequests() const override; [[nodiscard]] rpl::producer<> clearCoverRequests() const override; + virtual void setDisplayName(const QString &displayName); protected: struct Data { diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp index 3cdae65132..73a13eeb80 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp @@ -15,6 +15,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/ui_utility.h" #include "core/mime_type.h" +#include + namespace Ui { namespace { @@ -23,6 +25,8 @@ constexpr auto kMaxAlbumCount = 10; } // namespace PreparedFile::PreparedFile(const QString &path) : path(path) { + const auto fileInfo = QFileInfo(path); + displayName = fileInfo.fileName(); } PreparedFile::PreparedFile(PreparedFile &&other) = default; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h index 311903a00f..635be179ad 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h @@ -79,6 +79,7 @@ struct PreparedFile { [[nodiscard]] bool isGifv() const; QString path; + QString displayName; 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 344b30dd2c..c7ad3d0bef 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.cpp @@ -27,6 +27,10 @@ SingleFilePreview::SingleFilePreview( preparePreview(file); } +void SingleFilePreview::setDisplayName(const QString &displayName) { + AbstractSingleFilePreview::setDisplayName(displayName); +} + void SingleFilePreview::preparePreview(const PreparedFile &file) { AbstractSingleFilePreview::Data data; @@ -41,13 +45,18 @@ void SingleFilePreview::preparePreview(const PreparedFile &file) { prepareThumbFor(data, preview); const auto filepath = file.path; if (filepath.isEmpty()) { - auto filename = "image.png"; - data.name = filename; + const auto fallbackName = u"image.png"_q; + const auto displayName = file.displayName.isEmpty() + ? fallbackName + : file.displayName; + data.name = displayName; data.statusText = FormatImageSizeText(file.originalDimensions); data.fileIsImage = true; } else { auto fileinfo = QFileInfo(filepath); - auto filename = fileinfo.fileName(); + auto filename = file.displayName.isEmpty() + ? fileinfo.fileName() + : file.displayName; data.fileIsImage = Core::FileIsImage( filename, Core::MimeTypeForFile(fileinfo).name()); 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 24647c9174..4fc1f3b53a 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.h @@ -20,6 +20,7 @@ public: const style::ComposeControls &st, const PreparedFile &file, AttachControls::Type type = AttachControls::Type::Full); + void setDisplayName(const QString &displayName) override; private: void preparePreview(const PreparedFile &file); From ce8682e0eefa91600b094c467f1767b84c64c678 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 20 Feb 2026 11:22:54 +0400 Subject: [PATCH 048/179] Generate HTML changelog from text version. --- .github/scripts/generate_changelog.py | 328 ++++++++++++++++++++++++++ .github/workflows/changelog.yml | 47 ++++ .gitignore | 3 + 3 files changed, 378 insertions(+) create mode 100644 .github/scripts/generate_changelog.py create mode 100644 .github/workflows/changelog.yml diff --git a/.github/scripts/generate_changelog.py b/.github/scripts/generate_changelog.py new file mode 100644 index 0000000000..6cdd12652d --- /dev/null +++ b/.github/scripts/generate_changelog.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python3 +"""Convert changelog.txt to a static HTML page for GitHub Pages.""" + +import re +import shutil +import sys +import html +from pathlib import Path + +MONTHS = [ + "", "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December", +] + +VERSION_RE = re.compile( + r"^(\d+\.\d+(?:\.\d+)?)\s*" # version number + r"(?:(alpha|beta|dev|stable)\s*)?" # optional tag + r"\((\d{2})\.(\d{2})\.(\d{2,4})\)$" # date (DD.MM.YY or DD.MM.YYYY) +) + + +def parse_date(day: str, month: str, year: str) -> tuple[str, str, str]: + """Return (sort_key, raw_display, full_display) from DD, MM, YY strings.""" + y = int(year) + if y < 100: + y += 2000 + m = int(month) + d = int(day) + sort_key = f"{y:04d}-{m:02d}-{d:02d}" + raw_display = f"{day}.{month}.{year}" + full_display = f"{d} {MONTHS[m]} {y}" + return sort_key, raw_display, full_display + + +def parse_changelog(text: str) -> list[dict]: + entries = [] + current = None + + for raw_line in text.splitlines(): + line = raw_line.rstrip() + m = VERSION_RE.match(line) + if m: + if current: + entries.append(current) + version, tag, day, month, year = m.groups() + sort_key, raw_date, full_date = parse_date(day, month, year) + current = { + "version": version, + "tag": tag or "", + "date": raw_date, + "full_date": full_date, + "sort_key": sort_key, + "lines": [], + } + elif current is not None: + # Skip blank lines at the start + if not line and not current["lines"]: + continue + # Skip stray artifact lines + if line.strip() in ("),", "),"): + continue + current["lines"].append(line) + + if current: + entries.append(current) + + # Trim trailing blank lines from each entry + for entry in entries: + while entry["lines"] and not entry["lines"][-1]: + entry["lines"].pop() + + return entries + + +def render_entry(entry: dict) -> str: + version = html.escape(entry["version"]) + tag = entry["tag"] + date = html.escape(entry["date"]) + anchor = f"v{version}" + + tag_html = "" + if tag and tag not in ("stable",): + tag_html = f' {html.escape(tag)}' + + parts = [ + f'
', + f'

' + f'{version}{tag_html}' + f'

', + ] + + in_list = False + for line in entry["lines"]: + stripped = line.lstrip() + if stripped.startswith("- ") or stripped.startswith("\u2014 "): + # Bullet point (- or em dash) + if not in_list: + parts.append("
    ") + in_list = True + bullet_text = stripped[2:] + parts.append(f"
  • {html.escape(bullet_text)}
  • ") + else: + if in_list: + parts.append("
") + in_list = False + if stripped: + parts.append(f"

{html.escape(stripped)}

") + + if in_list: + parts.append(" ") + + parts.append("
") + return "\n".join(parts) + + +def build_html(entries: list[dict]) -> str: + count = len(entries) + first_date = entries[-1]["full_date"] if entries else "" + latest_version = entries[0]["version"] if entries else "" + + entries_html = "\n\n".join(render_entry(e) for e in entries) + + return f""" + + + + +Version history + + + + + + +
+

Version history

+

{count} releases since {first_date} · latest: {latest_version}

+
+ +
+ + +
+{entries_html} +
+
+ + + + + + +""" + + +def main(): + repo = Path(__file__).resolve().parent.parent.parent + src = repo / "changelog.txt" + if len(sys.argv) > 1: + src = Path(sys.argv[1]) + + out = repo / "docs" / "changelog" / "index.html" + if len(sys.argv) > 2: + out = Path(sys.argv[2]) + + text = src.read_text(encoding="utf-8") + entries = parse_changelog(text) + html_content = build_html(entries) + + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(html_content, encoding="utf-8") + + # Copy favicon files from resources + icons_src = repo / "Telegram" / "Resources" / "art" + for name in ("icon16.png", "icon32.png"): + icon = icons_src / name + if icon.exists(): + shutil.copy2(icon, out.parent / name) + + print(f"Generated {out} ({len(entries)} entries, {out.stat().st_size:,} bytes)") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000000..fc00881413 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,47 @@ +name: Changelog + +on: + push: + branches: [dev] + paths: [changelog.txt] + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Generate HTML + run: python .github/scripts/generate_changelog.py + + - name: Upload pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/changelog + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deploy + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 99141c5f44..8547c2aa0b 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ settings.local.json # AI work folder (session-specific, not for version control) .ai + +# Generated changelog page (built by CI, deployed to GitHub Pages) +/docs/changelog/ From 5a6a38da2933042b58c6905839661b44a170734c Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 20 Feb 2026 11:27:46 +0400 Subject: [PATCH 049/179] Fix path for changelog. --- .github/workflows/changelog.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index fc00881413..b38bd62525 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -33,7 +33,7 @@ jobs: - name: Upload pages artifact uses: actions/upload-pages-artifact@v3 with: - path: docs/changelog + path: docs deploy: needs: build From 0cddac16fb7c810017fe94c5b8569f4858dc7cb7 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Thu, 26 Feb 2026 22:00:25 +0000 Subject: [PATCH 050/179] Add MinSizeRel support on Linux Allows to build Telegram itself with multi-config generator as well as all dependencies with -Os optimizations --- CMakeLists.txt | 10 ++++++++++ Telegram/build/docker/centos_env/Dockerfile | 5 ++++- Telegram/build/docker/centos_env/gen_dockerfile.py | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fc7b239b03..e762a3cd59 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,10 @@ include(cmake/validate_special_target.cmake) include(cmake/version.cmake) desktop_app_parse_version(Telegram/build/version) +if (NOT DEFINED CMAKE_CONFIGURATION_TYPES) + set(configuration_types_init 1) +endif() + project(Telegram LANGUAGES C CXX VERSION ${desktop_app_version_cmake} @@ -23,6 +27,12 @@ if (APPLE) enable_language(OBJC OBJCXX) endif() +if (configuration_types_init + AND CMAKE_CONFIGURATION_TYPES + AND NOT MinSizeRel IN_LIST CMAKE_CONFIGURATION_TYPES) + set(CMAKE_CONFIGURATION_TYPES "${CMAKE_CONFIGURATION_TYPES};MinSizeRel" CACHE STRING "" FORCE) +endif() + set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT Telegram) get_filename_component(third_party_loc "Telegram/ThirdParty" REALPATH) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index e5ce8f7fe8..40bb23e1d4 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -44,7 +44,7 @@ WORKDIR /usr/src ENV AR=gcc-ar ENV RANLIB=gcc-ranlib ENV NM=gcc-nm -ENV CFLAGS='{% if DEBUG %}-g{% endif %} -O3 {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fexceptions -fasynchronous-unwind-tables -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fhardened -Wno-hardened' +ENV CFLAGS='{% if DEBUG %}-g{% endif %} {% if MINSIZE %}-Os{% else %}-O3{% endif %} {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fexceptions -fasynchronous-unwind-tables -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fhardened -Wno-hardened' ENV CXXFLAGS=$CFLAGS ENV LDFLAGS='-static-libstdc++ -static-libgcc -static-libasan -pthread -Wl,--push-state,--no-as-needed,-ldl,--pop-state -Wl,--as-needed -Wl,-z,muldefs' @@ -527,6 +527,9 @@ RUN git clone -b n6.1.1 --depth=1 https://github.com/FFmpeg/FFmpeg.git \ --disable-network \ --disable-autodetect \ --disable-everything \ +{%- if MINSIZE %} + --enable-small \ +{%- endif %} --enable-libdav1d \ --enable-libopenh264 \ --enable-libopus \ diff --git a/Telegram/build/docker/centos_env/gen_dockerfile.py b/Telegram/build/docker/centos_env/gen_dockerfile.py index 0e40b08340..86d27c96e1 100755 --- a/Telegram/build/docker/centos_env/gen_dockerfile.py +++ b/Telegram/build/docker/centos_env/gen_dockerfile.py @@ -11,6 +11,7 @@ def checkEnv(envName, defaultValue): def main(): print(Environment(loader=FileSystemLoader(dirname(__file__))).get_template("Dockerfile").render( DEBUG=checkEnv("DEBUG", True), + MINSIZE=checkEnv("MINSIZE", False), LTO=checkEnv("LTO", True), ASAN=checkEnv("ASAN", False), JOBS=checkEnv("JOBS", ""), From 9dc236b6850a4e38e09607dd1e3c6a70982930c6 Mon Sep 17 00:00:00 2001 From: futpib Date: Sat, 21 Feb 2026 22:04:34 +0000 Subject: [PATCH 051/179] Convert https://t.me/ URLs to tg:// scheme in start URL handling checkStartUrls() passed URLs directly to openLocalUrl() which only handles tg:// scheme URLs. When https://t.me/ links were passed as command-line arguments, they were silently ignored. Now URLs are converted via TryConvertUrlToLocal() before processing, matching the behavior used when clicking links inside the app. --- Telegram/SourceFiles/core/application.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 2b99c608da..1e9ecf178b 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -1094,7 +1094,8 @@ void Application::checkStartUrls() { iv().showTonSite(url.toString(), {}); return false; } else if (_lastActivePrimaryWindow) { - return !openLocalUrl(url.toString(), {}); + const auto local = TryConvertUrlToLocal(url.toString()); + return !openLocalUrl(local, {}); } return true; }) | ranges::to>; From 827bd7c9a9cb8aebc84f077312bda3e1b1525f46 Mon Sep 17 00:00:00 2001 From: Reza Bakhshi Laktasaraei Date: Thu, 26 Feb 2026 17:38:23 +0330 Subject: [PATCH 052/179] feat(accessibility): add accessible name to search button in narrow layout The search icon button shown when the filters sidebar is active had no accessible name, making it appear as an unnamed button to screen readers. --- Telegram/SourceFiles/dialogs/dialogs_widget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 7250b50c3d..f89c8a1ad7 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -609,6 +609,7 @@ Widget::Widget( setupStories(); } + _searchForNarrowLayout->setAccessibleName(tr::lng_dlg_filter(tr::now)); _searchForNarrowLayout->setClickedCallback([=] { _search->setFocusFast(); if (_childList) { From 47bf9f2429f3adcdc67daca49190153982ed5ed9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:22:41 +0000 Subject: [PATCH 053/179] Bump actions/setup-python from 5 to 6 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/changelog.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index b38bd62525..9b883bf7d8 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -23,7 +23,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.12" From 3030a80f373eda26b14c0f8abccf0d8fac2f08a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:22:36 +0000 Subject: [PATCH 054/179] Bump actions/upload-pages-artifact from 3 to 4 Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 3 to 4. - [Release notes](https://github.com/actions/upload-pages-artifact/releases) - [Commits](https://github.com/actions/upload-pages-artifact/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/upload-pages-artifact dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/changelog.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 9b883bf7d8..f9575c27d1 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -31,7 +31,7 @@ jobs: run: python .github/scripts/generate_changelog.py - name: Upload pages artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v4 with: path: docs From d285b2409b2bbf1d631d23246b92e0a739d847ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:22:33 +0000 Subject: [PATCH 055/179] Bump actions/checkout from 4 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/changelog.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index f9575c27d1..a94c13542b 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -21,7 +21,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: From 0bcec5f10c69d063a08e3bf02ac6013040f08a8a Mon Sep 17 00:00:00 2001 From: Artem Lytkin Date: Tue, 6 Jan 2026 14:57:46 +0300 Subject: [PATCH 056/179] Fixed: GitHub Issue #29530 - Missing Expandable Quote Button in Caption Field --- Telegram/SourceFiles/chat_helpers/chat_helpers.style | 1 + 1 file changed, 1 insertion(+) diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 8dc53ebdf0..29e350ce15 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1421,6 +1421,7 @@ defaultComposeFilesMenu: IconButton(defaultIconButton) { defaultComposeFilesField: InputField(defaultInputField) { textMargins: margins(1px, 26px, 31px, 4px); heightMax: 158px; + style: historyTextStyle; } defaultComposeFiles: ComposeFiles { check: defaultCheck; From dbeebd2efdb9861639ce5a8e56a1a0e6093c663c Mon Sep 17 00:00:00 2001 From: mukthar777 Date: Thu, 5 Feb 2026 12:46:44 +0530 Subject: [PATCH 057/179] Labeling in media player --- Telegram/Resources/langs/lang.strings | 3 ++- .../media/player/media_player_widget.cpp | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 38ac04e215..620367a846 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -5960,7 +5960,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_confcall_e2e_about" = "These four emoji represent the call's encryption key. They must match for all participants and change when someone joins or leaves."; "lng_no_mic_permission" = "Telegram needs microphone access so that you can make calls and record voice messages."; - "lng_player_message_today" = "today at {time}"; "lng_player_message_yesterday" = "yesterday at {time}"; "lng_player_message_date" = "{date} at {time}"; @@ -7602,4 +7601,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_rename_file" = "Rename file"; +"lng_sr_playback_order" = "Playback order"; +"lng_sr_player_close" = "Close media player"; // Keys finished diff --git a/Telegram/SourceFiles/media/player/media_player_widget.cpp b/Telegram/SourceFiles/media/player/media_player_widget.cpp index 018f1110ac..ec466ef183 100644 --- a/Telegram/SourceFiles/media/player/media_player_widget.cpp +++ b/Telegram/SourceFiles/media/player/media_player_widget.cpp @@ -81,6 +81,10 @@ Widget::Widget( _speedController->realtimeValue( ) | rpl::on_next([=](float64 speed) { _speedToggle->setSpeed(speed); + _speedToggle->setAccessibleName(tr::lng_mediaview_playback_speed( + tr::now, + lt_speed, + QString::number(base::SafeRound(speed * 10) / 10.) + "x")); }, _speedToggle->lifetime()); _speedToggle->finishAnimating(); @@ -90,6 +94,11 @@ Widget::Widget( setupRightControls(); + _volumeToggle->setAccessibleName(tr::lng_ringtones_box_volume(tr::now)); + _repeatToggle->setAccessibleName(tr::lng_schedule_repeat_label(tr::now)); + _orderToggle->setAccessibleName(tr::lng_sr_playback_order(tr::now)); + _close->setAccessibleName(tr::lng_sr_player_close(tr::now)); + _nameLabel->setAttribute(Qt::WA_TransparentForMouseEvents); _timeLabel->setAttribute(Qt::WA_TransparentForMouseEvents); @@ -633,6 +642,9 @@ void Widget::handleSongUpdate(const TrackState &state) { : showPause ? &st::mediaPlayerPauseIcon : nullptr); + _playPause->setAccessibleName(showPause + ? tr::lng_shortcuts_media_pause(tr::now) + : tr::lng_shortcuts_media_play(tr::now)); updateTimeText(state); } @@ -728,11 +740,13 @@ void Widget::createPrevNextButtons() { _previousTrack->setClickedCallback([=]() { instance()->previous(_type); }); + _previousTrack->setAccessibleName(tr::lng_shortcuts_media_previous(tr::now)); _nextTrack.create(this, st::mediaPlayerNextButton); _nextTrack->show(); _nextTrack->setClickedCallback([=]() { instance()->next(_type); }); + _nextTrack->setAccessibleName(tr::lng_shortcuts_media_next(tr::now)); hidePlaylistOn(_previousTrack); hidePlaylistOn(_nextTrack); updatePlayPrevNextPositions(); From d5634d5aa468f0c96a89266fe08b58ba91dafb75 Mon Sep 17 00:00:00 2001 From: mukthar777 Date: Thu, 5 Feb 2026 17:11:24 +0530 Subject: [PATCH 058/179] Accessibility: Add labeling to calls panel --- Telegram/SourceFiles/calls/calls_panel.cpp | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp index 8f57effdfb..597bf0fbf1 100644 --- a/Telegram/SourceFiles/calls/calls_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -145,11 +145,17 @@ Panel::Panel(not_null call) , _controlsShownForceTimer([=] { controlsShownForce(false); }) { _decline->setDuration(st::callPanelDuration); _decline->entity()->setText(tr::lng_call_decline()); + _decline->entity()->setAccessibleName(tr::lng_call_decline(tr::now)); _cancel->setDuration(st::callPanelDuration); _cancel->entity()->setText(tr::lng_call_cancel()); + _cancel->entity()->setAccessibleName(tr::lng_call_cancel(tr::now)); _screencast->setDuration(st::callPanelDuration); + _screencast->entity()->setAccessibleName(tr::lng_call_screencast(tr::now)); _addPeople->setDuration(st::callPanelDuration); _addPeople->entity()->setText(tr::lng_call_add_people()); + _addPeople->entity()->setAccessibleName(tr::lng_call_add_people(tr::now)); + _camera->setAccessibleName(tr::lng_call_start_video(tr::now)); + _mute->entity()->setAccessibleName(tr::lng_call_mute_audio(tr::now)); initWindow(); initWidget(); @@ -732,6 +738,9 @@ void Panel::reinitWithCall(Call *call) { _mute->entity()->setText(mute ? tr::lng_call_unmute_audio() : tr::lng_call_mute_audio()); + _mute->entity()->setAccessibleName(mute + ? tr::lng_call_unmute_audio(tr::now) + : tr::lng_call_mute_audio(tr::now)); }, _callLifetime); _call->videoOutgoing()->stateValue( @@ -742,6 +751,9 @@ void Panel::reinitWithCall(Call *call) { _camera->setText(active ? tr::lng_call_stop_video() : tr::lng_call_start_video()); + _camera->setAccessibleName(active + ? tr::lng_call_stop_video(tr::now) + : tr::lng_call_start_video(tr::now)); } { const auto active = _call->isSharingScreen(); @@ -1030,12 +1042,14 @@ void Panel::initMediaDeviceToggles() { { Webrtc::DeviceType::Camera, _call->cameraDeviceIdValue() }, }); }); + _cameraDeviceToggle->setAccessibleName(tr::lng_settings_call_camera(tr::now)); _audioDeviceToggle->setClickedCallback([=] { showDevicesMenu(_audioDeviceToggle, { { Webrtc::DeviceType::Playback, _call->playbackDeviceIdValue() }, { Webrtc::DeviceType::Capture, _call->captureDeviceIdValue() }, }); }); + _audioDeviceToggle->setAccessibleName(tr::lng_settings_call_section_output(tr::now)); } void Panel::showDevicesMenu( @@ -1381,6 +1395,7 @@ void Panel::stateChanged(State state) { st::callStartVideo); _startVideo->show(); _startVideo->setText(tr::lng_call_start_video()); + _startVideo->setAccessibleName(tr::lng_call_start_video(tr::now)); _startVideo->clicks() | rpl::map_to(true) | rpl::start_to_stream( _startOutgoingRequests, _startVideo->lifetime()); @@ -1447,6 +1462,15 @@ void Panel::refreshAnswerHangupRedialLabel() { } Unexpected("AnswerHangupRedialState value."); }()); + _answerHangupRedial->setAccessibleName([&] { + switch (*_answerHangupRedialState) { + case AnswerHangupRedialState::Answer: return tr::lng_call_accept(tr::now); + case AnswerHangupRedialState::Hangup: return tr::lng_call_end_call(tr::now); + case AnswerHangupRedialState::Redial: return tr::lng_call_redial(tr::now); + case AnswerHangupRedialState::StartCall: return tr::lng_call_start(tr::now); + } + Unexpected("AnswerHangupRedialState value."); + }()); } void Panel::updateStatusText(State state) { From 91fb6d405db2620dd65a730bd91738ea9af10dd1 Mon Sep 17 00:00:00 2001 From: mukthar777 Date: Thu, 5 Feb 2026 23:03:02 +0530 Subject: [PATCH 059/179] Accessibility: add labeling for group call panel buttons --- Telegram/Resources/langs/lang.strings | 1 + .../calls/group/calls_group_panel.cpp | 22 ++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 620367a846..fca715c78e 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7603,4 +7603,5 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_sr_playback_order" = "Playback order"; "lng_sr_player_close" = "Close media player"; +"lng_sr_group_call_menu" = "Video chat menu"; // Keys finished diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index 02329beaeb..4c8fb805d3 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -616,6 +616,7 @@ void Panel::initControls() { }, lifetime()); _hangup->setClickedCallback([=] { endCall(); }); + _hangup->setAccessibleName(tr::lng_group_call_leave(tr::now)); const auto scheduleDate = _call->scheduleDate(); if (scheduleDate) { @@ -701,12 +702,14 @@ void Panel::refreshLeftButton() { _settings.destroy(); _callShare.create(widget(), st::groupCallShare); _callShare->setClickedCallback(_callShareLinkCallback); + _callShare->setAccessibleName(tr::lng_group_call_invite(tr::now)); } else { _callShare.destroy(); _settings.create(widget(), st::groupCallSettings); _settings->setClickedCallback([=] { uiShow()->showBox(Box(SettingsBox, _call)); }); + _settings->setAccessibleName(tr::lng_group_call_settings_title(tr::now)); trackControls(_trackControls, true); } const auto raw = _callShare ? _callShare.data() : _settings.data(); @@ -754,6 +757,7 @@ void Panel::refreshVideoButtons(std::optional overrideWideMode) { StickedTooltipHide::Activated); _call->toggleVideo(!_call->isSharingCamera()); }); + _video->setAccessibleName(tr::lng_call_start_video(tr::now)); _video->setColorOverrides( toggleableOverrides(_call->isSharingCameraValue())); _call->isSharingCameraValue( @@ -764,6 +768,9 @@ void Panel::refreshVideoButtons(std::optional overrideWideMode) { StickedTooltipHide::Activated); } _video->setProgress(sharing ? 1. : 0.); + _video->setAccessibleName(sharing + ? tr::lng_call_stop_video(tr::now) + : tr::lng_call_start_video(tr::now)); }, _video->lifetime()); } if (!_screenShare) { @@ -772,17 +779,22 @@ void Panel::refreshVideoButtons(std::optional overrideWideMode) { _screenShare->setClickedCallback([=] { chooseShareScreenSource(); }); + _screenShare->setAccessibleName(tr::lng_group_call_screen_share_start(tr::now)); _screenShare->setColorOverrides( toggleableOverrides(_call->isSharingScreenValue())); _call->isSharingScreenValue( ) | rpl::on_next([=](bool sharing) { _screenShare->setProgress(sharing ? 1. : 0.); + _screenShare->setAccessibleName(sharing + ? tr::lng_group_call_screen_share_stop(tr::now) + : tr::lng_group_call_screen_share_start(tr::now)); }, _screenShare->lifetime()); } if (!_wideMenu) { _wideMenu.create(widget(), st::groupCallMenuToggleSmall); _wideMenu->show(); _wideMenu->setClickedCallback([=] { showMainMenu(); }); + _wideMenu->setAccessibleName(tr::lng_sr_group_call_menu(tr::now)); _wideMenu->setColorOverrides( toggleableOverrides(_wideMenuShown.value())); } @@ -799,6 +811,7 @@ void Panel::createMessageButton() { &st::groupCallMessageActiveSmall); _message->show(); _message->setClickedCallback([=] { toggleMessageTyping(); }); + _message->setAccessibleName(tr::lng_group_call_message(tr::now)); _message->setColorOverrides( toggleableOverrides(_messageTyping.value())); } @@ -880,8 +893,7 @@ void Panel::setupRealMuteButtonState(not_null real) { const auto wide = (mode == PanelMode::Wide); using Type = Ui::CallMuteButtonType; using ExpandType = Ui::CallMuteButtonExpandType; - _mute->setState(Ui::CallMuteButtonState{ - .text = (wide + const auto text = (wide ? QString() : scheduleDate ? (canManage @@ -897,7 +909,10 @@ void Panel::setupRealMuteButtonState(not_null real) { ? tr::lng_group_call_raised_hand(tr::now) : mute == MuteState::Muted ? tr::lng_group_call_unmute(tr::now) - : tr::lng_group_call_you_are_live(tr::now)), + : tr::lng_group_call_you_are_live(tr::now)); + _mute->outer()->setAccessibleName(text); + _mute->setState(Ui::CallMuteButtonState{ + .text = text, .tooltip = ((!scheduleDate && mute == MuteState::Muted) ? tr::lng_group_call_unmute_sub(tr::now) : QString()), @@ -1480,6 +1495,7 @@ void Panel::refreshTopButton() { if (!_menuToggle) { _menuToggle.create(widget(), st::groupCallMenuToggle); _menuToggle->show(); + _menuToggle->setAccessibleName(tr::lng_sr_group_call_menu(tr::now)); _menuToggle->setClickedCallback([=] { showMainMenu(); }); updateControlsGeometry(); raiseControls(); From 17db3361d374a80b5f76187801e452cead7c1826 Mon Sep 17 00:00:00 2001 From: mukthar777 Date: Tue, 10 Feb 2026 18:23:40 +0530 Subject: [PATCH 060/179] Add labels to buttons in dialog widget --- Telegram/Resources/langs/lang.strings | 5 +++++ Telegram/SourceFiles/dialogs/dialogs_widget.cpp | 13 +++++++++++++ Telegram/SourceFiles/dialogs/ui/chat_search_in.cpp | 1 + 3 files changed, 19 insertions(+) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index fca715c78e..2a1fe55aad 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7604,4 +7604,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_sr_playback_order" = "Playback order"; "lng_sr_player_close" = "Close media player"; "lng_sr_group_call_menu" = "Video chat menu"; +"lng_sr_search_date" = "Search by date"; +"lng_sr_cancel_search" = "Cancel search"; +"lng_sr_clear_search" = "Clear search"; +"lng_sr_scroll_to_top" = "Scroll to top"; + // Keys finished diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index f89c8a1ad7..3ea3626233 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -587,14 +587,20 @@ Widget::Widget( _cancelSearch->setClickedCallback([=] { cancelSearch({ .jumpBackToSearchedChat = true }); }); + _cancelSearch->setAccessibleName(tr::lng_sr_cancel_search(tr::now)); _jumpToDate->entity()->setClickedCallback([=] { showCalendar(); }); + _jumpToDate->entity()->setAccessibleName( + tr::lng_sr_search_date(tr::now)); _chooseFromUser->entity()->setClickedCallback([=] { showSearchFrom(); }); + _chooseFromUser->entity()->setAccessibleName( + tr::lng_search_messages_from(tr::now)); rpl::single(rpl::empty) | rpl::then( session().domain().local().localPasscodeChanged() ) | rpl::on_next([=] { updateLockUnlockVisibility(); }, lifetime()); const auto lockUnlock = _lockUnlock->entity(); + lockUnlock->setAccessibleName(tr::lng_shortcuts_lock(tr::now)); lockUnlock->setClickedCallback([=] { lockUnlock->setIconOverride( &st::dialogsUnlockIcon, @@ -616,6 +622,7 @@ Widget::Widget( controller->closeForum(); } }); + _searchForNarrowLayout->setAccessibleName(tr::lng_dlg_filter(tr::now)); setAcceptDrops(true); @@ -1048,6 +1055,7 @@ void Widget::scrollToDefaultChecked(bool verytop) { void Widget::setupScrollUpButton() { _scrollToTop->setClickedCallback([=] { scrollToDefaultChecked(); }); + _scrollToTop->setAccessibleName(tr::lng_sr_scroll_to_top(tr::now)); trackScroll(_scrollToTop); trackScroll(this); updateScrollUpVisibility(); @@ -3336,6 +3344,11 @@ void Widget::updateCancelSearch() { || (!_searchState.inChat && (_searchHasFocus || _searchSuggestionsLocked)); _cancelSearch->toggle(shown, anim::type::normal); + if (_searchState.inChat) { + _cancelSearch->setAccessibleName(shown + ? tr::lng_sr_clear_search(tr::now) + : tr::lng_sr_cancel_search(tr::now)); + } } QString Widget::validateSearchQuery() { diff --git a/Telegram/SourceFiles/dialogs/ui/chat_search_in.cpp b/Telegram/SourceFiles/dialogs/ui/chat_search_in.cpp index e426f453b9..f82ebe8baf 100644 --- a/Telegram/SourceFiles/dialogs/ui/chat_search_in.cpp +++ b/Telegram/SourceFiles/dialogs/ui/chat_search_in.cpp @@ -449,6 +449,7 @@ void ChatSearchIn::updateSection( const auto st = &st::dialogsCancelSearchInPeer; section->cancel = std::make_unique(raw, *st); + section->cancel->setAccessibleName(tr::lng_cancel(tr::now)); section->cancel->show(); raw->sizeValue() | rpl::on_next([=](QSize size) { const auto left = size.width() - section->cancel->width(); From 215303cc693f8a8d64ac29213ef6b837cb019fe7 Mon Sep 17 00:00:00 2001 From: mukthar777 Date: Mon, 9 Feb 2026 09:02:55 +0530 Subject: [PATCH 061/179] accessibility: implement button labeling across info sections --- Telegram/Resources/langs/lang.strings | 4 +++ Telegram/SourceFiles/info/info_top_bar.cpp | 31 +++++++++++++++++ .../SourceFiles/info/info_wrap_widget.cpp | 5 +++ .../info/profile/info_profile_actions.cpp | 31 +++++++++++++++++ .../info/profile/info_profile_badge.cpp | 20 +++++++++++ .../info/profile/info_profile_members.cpp | 21 ++++++++---- .../profile/info_profile_music_button.cpp | 1 + .../info/profile/info_profile_top_bar.cpp | 33 +++++++++++++++++-- .../SourceFiles/ui/controls/stars_rating.cpp | 1 + 9 files changed, 138 insertions(+), 9 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 2a1fe55aad..ee75d5e9c9 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7608,5 +7608,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_sr_cancel_search" = "Cancel search"; "lng_sr_clear_search" = "Clear search"; "lng_sr_scroll_to_top" = "Scroll to top"; +"lng_sr_verified_badge" = "Verified"; +"lng_sr_bot_verified_badge" = "Verified Bot"; +"lng_sr_profile_menu" = "Profile menu"; +"lng_sr_close_panel" = "Close panel"; // Keys finished diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp index 3ebf9a613b..f89dceea3c 100644 --- a/Telegram/SourceFiles/info/info_top_bar.cpp +++ b/Telegram/SourceFiles/info/info_top_bar.cpp @@ -134,6 +134,7 @@ void TopBar::enableBackButton() { st::infoTopBarScale); _back->setDuration(st::infoTopBarDuration); _back->toggle(!selectionMode(), anim::type::instant); + _back->entity()->setAccessibleName(tr::lng_go_back(tr::now)); _back->entity()->clicks( ) | rpl::to_empty | rpl::start_to_stream(_backClicks, _back->lifetime()); @@ -265,6 +266,7 @@ void TopBar::createSearchView( auto button = base::make_unique_q(this, _st.search); auto search = button.get(); + search->setAccessibleName(tr::lng_dlg_filter(tr::now)); search->addClickHandler([=] { showSearch(); }); auto searchWrap = pushButton(std::move(button)); registerToggleControlCallback(searchWrap, [=] { @@ -276,10 +278,21 @@ void TopBar::createSearchView( auto cancel = Ui::CreateChild( wrap, _st.searchRow.fieldCancel); + cancel->setAccessibleName(tr::lng_sr_cancel_search(tr::now)); registerToggleControlCallback(cancel, [=] { return !selectionMode() && searchMode(); }); + const auto updateCancelName = [=] { + const auto hasText = !field->getLastText().isEmpty(); + cancel->setAccessibleName(hasText + ? tr::lng_sr_clear_search(tr::now) + : tr::lng_sr_cancel_search(tr::now)); + }; + field->changes( + ) | rpl::on_next(updateCancelName, cancel->lifetime()); + updateCancelName(); + cancel->addClickHandler([=] { if (!field->getLastText().isEmpty()) { field->setText(QString()); @@ -637,6 +650,12 @@ void TopBar::updateSelectionState() { (_allStoriesInProfile ? &_st.storiesArchive.iconOver : &_st.storiesSave.iconOver)); + _toggleStoryInProfile->entity()->setAccessibleName(_allStoriesInProfile + ? tr::lng_mediaview_archive_story(tr::now) + : tr::lng_mediaview_save_to_profile(tr::now)); + _toggleStoryPin->entity()->setAccessibleName(_canUnpinStories + ? tr::lng_context_unpin_from_top(tr::now) + : tr::lng_context_pin_to_top(tr::now)); _toggleStoryPin->toggle(_canToggleStoryPin, anim::type::instant); _toggleStoryPin->entity()->setIconOverride( _canUnpinStories ? &_st.storiesUnpin.icon : nullptr, @@ -663,6 +682,8 @@ void TopBar::createSelectionControls() { object_ptr(this, _st.mediaCancel), st::infoTopBarScale)); _cancelSelection->setDuration(st::infoTopBarDuration); + _cancelSelection->entity()->setAccessibleName( + tr::lng_context_clear_selection(tr::now)); _cancelSelection->entity()->clicks( ) | rpl::map_to( SelectionAction::Clear @@ -693,6 +714,8 @@ void TopBar::createSelectionControls() { _forward.data(), [this] { return selectionMode() && _canForward; }); _forward->setDuration(st::infoTopBarDuration); + _forward->entity()->setAccessibleName( + tr::lng_context_forward_selected(tr::now)); _forward->entity()->clicks( ) | rpl::map_to( SelectionAction::Forward @@ -709,6 +732,8 @@ void TopBar::createSelectionControls() { _delete.data(), [this] { return selectionMode() && _canDelete; }); _delete->setDuration(st::infoTopBarDuration); + _delete->entity()->setAccessibleName( + tr::lng_context_delete_selected(tr::now)); _delete->entity()->clicks( ) | rpl::map_to( SelectionAction::Delete @@ -728,6 +753,9 @@ void TopBar::createSelectionControls() { _toggleStoryInProfile.data(), [this] { return selectionMode() && _canToggleStoryPin; }); _toggleStoryInProfile->setDuration(st::infoTopBarDuration); + _toggleStoryInProfile->entity()->setAccessibleName(_allStoriesInProfile + ? tr::lng_mediaview_archive_story(tr::now) + : tr::lng_mediaview_save_to_profile(tr::now)); _toggleStoryInProfile->entity()->clicks( ) | rpl::map([=] { return _allStoriesInProfile @@ -754,6 +782,9 @@ void TopBar::createSelectionControls() { _toggleStoryPin.data(), [this] { return selectionMode() && _canToggleStoryPin; }); _toggleStoryPin->setDuration(st::infoTopBarDuration); + _toggleStoryPin->entity()->setAccessibleName(_canUnpinStories + ? tr::lng_context_unpin_from_top(tr::now) + : tr::lng_context_pin_to_top(tr::now)); _toggleStoryPin->entity()->clicks( ) | rpl::map_to( SelectionAction::ToggleStoryPin diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 99730a1f00..4cc5413e7c 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -378,6 +378,7 @@ void WrapWidget::createTopBar() { base::make_unique_q( _topBar, st::infoTopBarClose)); + close->setAccessibleName(tr::lng_sr_close_panel(tr::now)); close->addClickHandler([this] { _controller->parentController()->closeThirdSection(); }); @@ -392,6 +393,7 @@ void WrapWidget::createTopBar() { base::make_unique_q( _topBar, st::infoLayerTopBarClose)); + close->setAccessibleName(tr::lng_sr_close_panel(tr::now)); close->addClickHandler([this] { checkBeforeClose([=] { _controller->parentController()->hideSpecialLayer(); @@ -432,6 +434,7 @@ void WrapWidget::setupTopBarMenuToggle() { : st::infoTopBarSearch; const auto button = _topBar->addButton( base::make_unique_q(_topBar, st)); + button->setAccessibleName(tr::lng_dlg_filter(tr::now)); button->addClickHandler([=] { _controller->showSettings(::Settings::Search::Id()); }); @@ -445,6 +448,7 @@ void WrapWidget::setupTopBarMenuToggle() { : st::infoTopBarQr; const auto button = _topBar->addButton( base::make_unique_q(_topBar, st)); + button->setAccessibleName(tr::lng_group_invite_context_qr(tr::now)); button->addClickHandler([show, self] { Ui::DefaultShowFillPeerQrBoxCallback(show, self); }); @@ -531,6 +535,7 @@ void WrapWidget::addTopBarMenuButton() { (wrap() == Wrap::Layer ? st::infoLayerTopBarMenu : st::infoTopBarMenu)))); + _topBarMenuToggle->setAccessibleName(tr::lng_sr_profile_menu(tr::now)); _topBarMenuToggle->addClickHandler([this] { showTopBarMenu(false); }); diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 1aac712d38..8ba8b99835 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -578,6 +578,33 @@ base::options::toggle ShowChannelJoinedBelowAbout({ openedWrap->resize(width, std::max(h1, size.height()) - added); }, openedWrap->lifetime()); + rpl::combine( + state->opened.value(), + state->opensIn.value(), + state->expanded.value(), + dayHoursTextValue(state->day.value()) + ) | rpl::on_next([=]( + bool opened, + TimeId opensIn, + bool expanded, + const QString &timing) { + const auto status = (opened + ? tr::lng_info_work_open + : tr::lng_info_work_closed)(tr::now); + const auto when = (!opensIn || expanded) + ? timing + : (opensIn >= 86400) + ? tr::lng_info_hours_opens_in_days(tr::now, lt_count, opensIn / 86400) + : (opensIn >= 3600) + ? tr::lng_info_hours_opens_in_hours(tr::now, lt_count, opensIn / 3600) + : tr::lng_info_hours_opens_in_minutes( + tr::now, + lt_count, + std::max(opensIn / 60, 1)); + button->setAccessibleName( + tr::lng_info_hours_label(tr::now) + ": " + status + ", " + when); + }, inner->lifetime()); + const auto labelWrap = inner->add(object_ptr(inner)); const auto label = Ui::CreateChild( labelWrap, @@ -1653,6 +1680,7 @@ object_ptr DetailsFiller::setupInfo() { const auto qrButton = Ui::CreateChild( usernameLine.text->parentWidget(), st::infoProfileLabeledButtonQr); + qrButton->setAccessibleName(tr::lng_group_invite_context_qr(tr::now)); UsernamesValue(_peer) | rpl::on_next([=](const auto &u) { qrButton->setVisible(!u.empty()); }, qrButton->lifetime()); @@ -1733,6 +1761,7 @@ object_ptr DetailsFiller::setupInfo() { const auto qr = Ui::CreateChild( linkLine.text->parentWidget(), st::infoProfileLabeledButtonQr); + qr->setAccessibleName(tr::lng_group_invite_context_qr(tr::now)); UsernamesValue(_peer) | rpl::on_next([=](const auto &u) { qr->setVisible(!u.empty()); }, qr->lifetime()); @@ -2045,6 +2074,7 @@ object_ptr DetailsFiller::setupPersonalChannel( button->lower(); inner->lifetime().make_state>( button); + button->setAccessibleName(tr::lng_profile_view_channel(tr::now)); } inner->setAttribute(Qt::WA_TransparentForMouseEvents); Ui::AddSkip(messageChannelWrap->entity()); @@ -2868,6 +2898,7 @@ void SetupAddChannelMember( auto add = Ui::CreateChild( parent.get(), st::infoMembersAddMember); + add->setAccessibleName(tr::lng_channel_add_members(tr::now)); add->showOn(CanAddMemberValue(channel)); add->addClickHandler([=] { Window::PeerMenuAddChannelMembers(navigation, channel); diff --git a/Telegram/SourceFiles/info/profile/info_profile_badge.cpp b/Telegram/SourceFiles/info/profile/info_profile_badge.cpp index 7a997f1543..496a0e1357 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_badge.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_badge.cpp @@ -81,6 +81,26 @@ void Badge::setContent(Content content) { return; } _view.create(_parent); + _view->setAccessibleName([&] { + switch (_content.badge) { + case BadgeType::Verified: + return tr::lng_sr_verified_badge(tr::now); + case BadgeType::BotVerified: + return tr::lng_sr_bot_verified_badge(tr::now); + case BadgeType::Premium: + if (_content.emojiStatusId) { + return tr::lng_profile_bot_emoji_status_access(tr::now); + } + return tr::lng_premium_summary_title(tr::now); + case BadgeType::Scam: + return tr::lng_scam_badge(tr::now); + case BadgeType::Fake: + return tr::lng_fake_badge(tr::now); + case BadgeType::Direct: + return tr::lng_direct_badge(tr::now); + } + Unexpected("badge type"); + }()); _view->show(); switch (_content.badge) { case BadgeType::Verified: diff --git a/Telegram/SourceFiles/info/profile/info_profile_members.cpp b/Telegram/SourceFiles/info/profile/info_profile_members.cpp index 25900c1ec4..1f3cc86cc7 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_members.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_members.cpp @@ -131,6 +131,7 @@ void Members::setupHeader() { _openMembers = Ui::CreateChild( parent, rpl::single(QString())); + // _openMembers->setAccessibleName(tr::lng_manage_peer_members(tr::now)); object_ptr( parent, @@ -142,12 +143,14 @@ void Members::setupHeader() { _addMember = Ui::CreateChild( _openMembers, st::infoMembersAddMember); + _addMember->setAccessibleName(tr::lng_channel_add_members(tr::now)); //_searchField = _controller->searchFieldController()->createField( // parent, // st::infoMembersSearchField); _search = Ui::CreateChild( _openMembers, st::infoMembersSearch); + _search->setAccessibleName(tr::lng_participant_filter(tr::now)); //_cancelSearch = Ui::CreateChild( // parent, // st::infoMembersCancelSearch); @@ -169,15 +172,19 @@ object_ptr Members::setupTitle() { auto visible = _peer->isMegagroup() ? CanViewParticipantsValue(_peer->asMegagroup()) : rpl::single(true); + auto text = rpl::conditional( + std::move(visible), + tr::lng_chat_status_members( + lt_count_decimal, + MembersCountValue(_peer) | tr::to_count(), + tr::upper), + tr::lng_channel_admins(tr::upper)); + rpl::duplicate(text) | rpl::on_next([=](const QString &v) { + _openMembers->setAccessibleName(v); + }, _openMembers->lifetime()); auto result = object_ptr( _titleWrap, - rpl::conditional( - std::move(visible), - tr::lng_chat_status_members( - lt_count_decimal, - MembersCountValue(_peer) | tr::to_count(), - tr::upper), - tr::lng_channel_admins(tr::upper)), + std::move(text), st::infoBlockHeaderLabel); result->setAttribute(Qt::WA_TransparentForMouseEvents); return result; diff --git a/Telegram/SourceFiles/info/profile/info_profile_music_button.cpp b/Telegram/SourceFiles/info/profile/info_profile_music_button.cpp index 93ec22e280..ff6d94f5df 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_music_button.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_music_button.cpp @@ -41,6 +41,7 @@ void MusicButton::updateData(MusicButtonData data) { _title.setText( st::defaultTextStyle, result.text.mid(performerLength, result.text.size())); + setAccessibleName(result.text); update(); } diff --git a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp index 2f902339ee..c681ffa5c9 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp @@ -116,6 +116,7 @@ public: : Ui::AbstractButton(parent) , _hasStories(std::move(hasStories)) { installEventFilter(this); + setAccessibleName(tr::lng_mediaview_profile_photo(tr::now)); } QString tooltipText() const override { @@ -701,6 +702,7 @@ void TopBar::setupActions(not_null controller) { moreButton->setClickedCallback([=] { showTopBarMenu(controller, false); }); + moreButton->setAccessibleName(tr::lng_profile_action_short_more(tr::now)); _actionMore = moreButton; _actions->add(moreButton); buttons.push_back(moreButton); @@ -751,6 +753,7 @@ void TopBar::setupActions(not_null controller) { peer->id, Window::SectionShow::Way::Forward); }); + message->setAccessibleName(tr::lng_profile_action_short_message(tr::now)); buttons.push_back(message); _actions->add(message); } @@ -763,6 +766,7 @@ void TopBar::setupActions(not_null controller) { join->setClickedCallback([=] { channel->session().api().joinChannel(channel); }); + join->setAccessibleName(tr::lng_profile_action_short_join(tr::now)); buttons.push_back(join); _actions->add(join); } else if (const auto channel = peer->monoforumBroadcast()) { @@ -775,6 +779,7 @@ void TopBar::setupActions(not_null controller) { channel, Window::SectionShow::Way::Forward); }); + message->setAccessibleName(tr::lng_profile_action_short_channel(tr::now)); buttons.push_back(message); _actions->add(message); } @@ -783,6 +788,7 @@ void TopBar::setupActions(not_null controller) { this, tr::lng_profile_action_short_mute(tr::now), st::infoProfileTopBarActionMessage); + notifications->setAccessibleName(tr::lng_profile_action_short_mute(tr::now)); notifications->convertToToggle( st::infoProfileTopBarActionUnmute, st::infoProfileTopBarActionMute, @@ -800,9 +806,11 @@ void TopBar::setupActions(not_null controller) { : NotificationsEnabledValue(peer) ) | rpl::on_next([=](bool enabled) { notifications->toggle(enabled); - notifications->setText(enabled + const auto text = enabled ? tr::lng_profile_action_short_mute(tr::now) - : tr::lng_profile_action_short_unmute(tr::now)); + : tr::lng_profile_action_short_unmute(tr::now); + notifications->setText(text); + notifications->setAccessibleName(text); }, notifications->lifetime()); notifications->finishAnimating(); @@ -863,6 +871,7 @@ void TopBar::setupActions(not_null controller) { call->setClickedCallback([=] { Core::App().calls().startOutgoingCall(user, {}); }); + call->setAccessibleName(tr::lng_profile_action_short_call(tr::now)); buttons.push_back(call); _actions->add(call); } @@ -885,6 +894,7 @@ void TopBar::setupActions(not_null controller) { chat, Window::SectionShow::Way::Forward); }); + discuss->setAccessibleName(tr::lng_profile_action_short_discuss(tr::now)); _actions->add(discuss); buttons.push_back(discuss); } @@ -907,6 +917,7 @@ void TopBar::setupActions(not_null controller) { window->showEditPeerBox(peer); } }); + manage->setAccessibleName(tr::lng_profile_action_short_manage(tr::now)); buttons.push_back(manage); _actions->add(manage); } @@ -935,6 +946,7 @@ void TopBar::setupActions(not_null controller) { giftButton->setClickedCallback([=] { Ui::ShowStarGiftBox(controller, peer); }); + giftButton->setAccessibleName(tr::lng_profile_action_short_gift(tr::now)); _actions->add(giftButton); buttons.push_back(giftButton); } @@ -956,6 +968,7 @@ void TopBar::setupActions(not_null controller) { reportButton->setClickedCallback([=] { ShowReportMessageBox(show, peer, {}, {}); }); + reportButton->setAccessibleName(tr::lng_profile_action_short_report(tr::now)); _actions->add(reportButton); buttons.push_back(reportButton); } @@ -969,6 +982,7 @@ void TopBar::setupActions(not_null controller) { st::infoProfileTopBarActionLeave); leaveButton->setClickedCallback( Window::DeleteAndLeaveHandler(controller, peer)); + leaveButton->setAccessibleName(tr::lng_profile_action_short_leave(tr::now)); _actions->add(leaveButton); buttons.push_back(leaveButton); } @@ -1863,6 +1877,7 @@ void TopBar::setupButtons( st::infoTopBarScale); _back->QWidget::show(); _back->setDuration(0); + _back->entity()->setAccessibleName(tr::lng_go_back(tr::now)); _back->toggleOn(isLayer || isSide ? (_backToggles.value() | rpl::type_erased) : rpl::single(wrap == Wrap::Narrow)); @@ -1878,6 +1893,7 @@ void TopBar::setupButtons( shouldUseColored ? st::infoTopBarColoredClose : st::infoTopBarBlackClose); + _close->setAccessibleName(tr::lng_sr_close_panel(tr::now)); _close->show(); _close->addClickHandler(isSide ? Fn ([=] { controller->closeThirdSection(); }) @@ -1915,6 +1931,7 @@ void TopBar::addTopBarEditButton( _topBarButton->addClickHandler([=] { controller->showSettings(::Settings::InformationId()); }); + _topBarButton->setAccessibleName(tr::lng_settings_information(tr::now)); widthValue() | rpl::on_next([=] { if (_close) { @@ -2309,6 +2326,18 @@ void TopBar::setupNewGifts( } entry.position = positions[i]; entry.button = base::make_unique_q(this); + entry.button->setAccessibleName([&] { + const auto base = tr::lng_profile_action_short_gift(tr::now); + + if (const auto &unique = gift.info.unique) { + const auto name = Data::UniqueGiftName(*unique); + if (!name.isEmpty()) { + return base + ": " + name; + } + } + + return base; + }()); entry.button->show(); entry.button->setClickedCallback([=, giftData = gift, peer = _peer] { diff --git a/Telegram/SourceFiles/ui/controls/stars_rating.cpp b/Telegram/SourceFiles/ui/controls/stars_rating.cpp index fb3c20353b..7fa17b8ba1 100644 --- a/Telegram/SourceFiles/ui/controls/stars_rating.cpp +++ b/Telegram/SourceFiles/ui/controls/stars_rating.cpp @@ -467,6 +467,7 @@ void StarsRating::updateData(Data::StarsRating rating) { _currentLevel = rating.level; } updateWidth(); + _widget->setAccessibleName(tr::lng_boost_level(tr::now, lt_count, rating.level)); } void StarsRating::updateWidth() { From eabeae20b6b3e2ffc6e30776e285b6edea7505c4 Mon Sep 17 00:00:00 2001 From: mukthar777 Date: Fri, 13 Feb 2026 12:01:05 +0530 Subject: [PATCH 062/179] Add lableing to birthday in profile action --- Telegram/SourceFiles/data/data_birthday.cpp | 10 +++++++--- Telegram/SourceFiles/data/data_birthday.h | 2 +- .../SourceFiles/info/profile/info_profile_actions.cpp | 8 ++++++++ .../SourceFiles/info/profile/info_profile_values.cpp | 7 ++++--- .../SourceFiles/info/profile/info_profile_values.h | 3 ++- 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/Telegram/SourceFiles/data/data_birthday.cpp b/Telegram/SourceFiles/data/data_birthday.cpp index 34a2b07374..408c731429 100644 --- a/Telegram/SourceFiles/data/data_birthday.cpp +++ b/Telegram/SourceFiles/data/data_birthday.cpp @@ -69,12 +69,16 @@ int Birthday::year() const { return _value / 10000; } -QString BirthdayText(Birthday date) { +QString BirthdayText(Birthday date, bool fullMonth) { + const auto monthText = (fullMonth + ? Lang::MonthDay(date.month()) + : Lang::MonthSmall(date.month()))(tr::now); + if (const auto year = date.year()) { return tr::lng_month_day_year( tr::now, lt_month, - Lang::MonthSmall(date.month())(tr::now), + monthText, lt_day, QString::number(date.day()), lt_year, @@ -83,7 +87,7 @@ QString BirthdayText(Birthday date) { return tr::lng_month_day( tr::now, lt_month, - Lang::MonthSmall(date.month())(tr::now), + monthText, lt_day, QString::number(date.day())); } diff --git a/Telegram/SourceFiles/data/data_birthday.h b/Telegram/SourceFiles/data/data_birthday.h index e728cce420..15858b6e01 100644 --- a/Telegram/SourceFiles/data/data_birthday.h +++ b/Telegram/SourceFiles/data/data_birthday.h @@ -38,7 +38,7 @@ private: }; -[[nodiscard]] QString BirthdayText(Birthday date); +[[nodiscard]] QString BirthdayText(Birthday date, bool fullMonth = false); [[nodiscard]] QString BirthdayCake(); [[nodiscard]] int BirthdayAge(Birthday date); [[nodiscard]] bool IsBirthdayToday(Birthday date); diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 8ba8b99835..07eed0b292 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -928,6 +928,14 @@ void DeleteContactNote( giftIcon->setVisible(!disable); }, result->lifetime()); + BirthdayValueText( + rpl::duplicate(birthday), + true + ) | rpl::on_next([=](const QString &accessibleText) { + button->setAccessibleName( + tr::lng_info_birthday_label(tr::now) + ": " + accessibleText); + }, button->lifetime()); + auto nonEmptyText = std::move( text ) | rpl::before_next([slide = result.data()]( diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.cpp b/Telegram/SourceFiles/info/profile/info_profile_values.cpp index a303b418c2..19c194b8b9 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_values.cpp @@ -735,17 +735,18 @@ rpl::producer BirthdayLabelText( } rpl::producer BirthdayValueText( - rpl::producer birthday) { + rpl::producer birthday, + bool fullMonth) { return std::move( birthday - ) | rpl::map([](Data::Birthday value) -> rpl::producer { + ) | rpl::map([=](Data::Birthday value) -> rpl::producer { if (!value) { return rpl::single(QString()); } return Data::IsBirthdayTodayValue( value ) | rpl::map([=](bool today) { - auto text = Data::BirthdayText(value); + auto text = Data::BirthdayText(value, fullMonth); if (const auto age = Data::BirthdayAge(value)) { text = (today ? tr::lng_info_birthday_today_years diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.h b/Telegram/SourceFiles/info/profile/info_profile_values.h index 220d93cd37..3ca1bd1cce 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.h +++ b/Telegram/SourceFiles/info/profile/info_profile_values.h @@ -143,6 +143,7 @@ enum class BadgeType : uchar; [[nodiscard]] rpl::producer BirthdayLabelText( rpl::producer birthday); [[nodiscard]] rpl::producer BirthdayValueText( - rpl::producer birthday); + rpl::producer birthday, + bool fullMonth = false); } // namespace Info::Profile From d9d0410797931f541059ab75502020295b3597ec Mon Sep 17 00:00:00 2001 From: mukthar777 Date: Sat, 14 Feb 2026 12:59:12 +0530 Subject: [PATCH 063/179] accessibility: add label to clear search button in search field --- Telegram/SourceFiles/ui/search_field_controller.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Telegram/SourceFiles/ui/search_field_controller.cpp b/Telegram/SourceFiles/ui/search_field_controller.cpp index 279dbb1c74..a490972ad5 100644 --- a/Telegram/SourceFiles/ui/search_field_controller.cpp +++ b/Telegram/SourceFiles/ui/search_field_controller.cpp @@ -42,6 +42,7 @@ auto SearchFieldController::createRowView( return !value.isEmpty(); }) | rpl::on_next([cancel](bool shown) { cancel->toggle(shown, anim::type::normal); + cancel->setAccessibleName(tr::lng_sr_clear_search(tr::now)); }, cancel->lifetime()); cancel->finishAnimating(); From 234e0b247b83cd331c3bd60c5f705cd431cac1b3 Mon Sep 17 00:00:00 2001 From: cumdev1337 Date: Sat, 28 Feb 2026 02:48:48 +0200 Subject: [PATCH 064/179] Save webauthn resident key --- Telegram/SourceFiles/platform/win/webauthn_win.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/platform/win/webauthn_win.cpp b/Telegram/SourceFiles/platform/win/webauthn_win.cpp index 657d2cbd02..a121a4df16 100644 --- a/Telegram/SourceFiles/platform/win/webauthn_win.cpp +++ b/Telegram/SourceFiles/platform/win/webauthn_win.cpp @@ -160,7 +160,7 @@ void RegisterKey( || defined(WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_7) \ || defined(WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_8) \ || defined(WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_9) - options.bPreferResidentKey = FALSE; + options.bPreferResidentKey = TRUE; #endif auto hwnd = (HWND)(nullptr); From 1292bbd95470df4ca6d03271dea8c5fb18a49a8d Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Sat, 28 Feb 2026 16:03:53 +0400 Subject: [PATCH 065/179] Mention by name with Ctrl-Click --- Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 13a1aa583d..b80e9c354a 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -43,7 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/cached_round_corners.h" #include "base/unixtime.h" #include "base/random.h" -#include "base/qt/qt_common_adapters.h" +#include "base/qt/qt_key_modifiers.h" #include "boxes/sticker_set_box.h" #include "window/window_adaptive.h" #include "window/window_session_controller.h" @@ -1666,7 +1666,9 @@ void InitFieldAutocomplete( raw->mentionChosen( ) | rpl::on_next([=](FieldAutocomplete::MentionChosen data) { const auto user = data.user; - if (data.mention.isEmpty()) { + const auto ctrlClick = base::IsCtrlPressed() + && data.method == FieldAutocomplete::ChooseMethod::ByClick; + if (data.mention.isEmpty() || ctrlClick) { field->insertTag( user->firstName.isEmpty() ? user->name() : user->firstName, PrepareMentionTag(user)); From 46758a82a7f8bff1b58f49ee2ad6c9a32812d81e Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Sat, 28 Feb 2026 06:57:21 +0400 Subject: [PATCH 066/179] Add unlimited recent stickers experimental option --- .../chat_helpers/stickers_list_widget.cpp | 12 +++++++++++- .../SourceFiles/chat_helpers/stickers_list_widget.h | 2 ++ .../SourceFiles/settings/settings_experimental.cpp | 2 ++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 91b1e3b90a..78002e3ee0 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "chat_helpers/stickers_list_widget.h" +#include "base/options.h" #include "base/timer_rpl.h" #include "core/application.h" #include "data/data_document.h" @@ -70,12 +71,20 @@ using Data::StickersPack; using Data::StickersSetThumbnailView; using SetFlag = Data::StickersSetFlag; +base::options::toggle OptionUnlimitedRecentStickers({ + .id = kOptionUnlimitedRecentStickers, + .name = "Unlimited recent stickers", + .description = "Display as much recent stickers as the server provides", +}); + [[nodiscard]] bool SetInMyList(Data::StickersSetFlags flags) { return (flags & SetFlag::Installed) && !(flags & SetFlag::Archived); } } // namespace +const char kOptionUnlimitedRecentStickers[] = "unlimited-recent-stickers"; + struct StickersListWidget::Sticker { not_null document; std::shared_ptr documentMedia; @@ -2549,7 +2558,8 @@ auto StickersListWidget::collectRecentStickers() -> std::vector { _custom.reserve(cloudCount + recent.size() + customCount); auto add = [&](not_null document, bool custom) { - if (result.size() >= kRecentDisplayLimit) { + if (result.size() >= kRecentDisplayLimit + && !OptionUnlimitedRecentStickers.value()) { return; } const auto i = ranges::find(result, document, &Sticker::document); diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h index 87fc1a6733..2a10e629b6 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h @@ -56,6 +56,8 @@ struct FlatLabel; namespace ChatHelpers { +extern const char kOptionUnlimitedRecentStickers[]; + struct StickerIcon; enum class ValidateIconAnimations; class StickersListFooter; diff --git a/Telegram/SourceFiles/settings/settings_experimental.cpp b/Telegram/SourceFiles/settings/settings_experimental.cpp index 800d462a60..07a48ffe70 100644 --- a/Telegram/SourceFiles/settings/settings_experimental.cpp +++ b/Telegram/SourceFiles/settings/settings_experimental.cpp @@ -40,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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" #include "styles/style_layers.h" @@ -177,6 +178,7 @@ void SetupExperimental( addToggle(Info::kAlternativeScrollProcessing); addToggle(kModerateCommonGroups); addToggle(kForceComposeSearchOneColumn); + addToggle(ChatHelpers::kOptionUnlimitedRecentStickers); } } // namespace From d62f29449410e02e6bd3031fc0a45fcbf276d9ef Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 24 Feb 2026 12:30:13 +0300 Subject: [PATCH 067/179] Added initial support of auto-scroll with middle click to history view. Fixed #24518. --- .../history/history_inner_widget.cpp | 33 +++ .../history/history_inner_widget.h | 2 + .../history/view/history_view_list_widget.cpp | 35 ++- .../history/view/history_view_list_widget.h | 2 + .../ui/widgets/middle_click_autoscroll.cpp | 228 ++++++++++++++++++ .../ui/widgets/middle_click_autoscroll.h | 53 ++++ Telegram/cmake/td_ui.cmake | 2 + 7 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 Telegram/SourceFiles/ui/widgets/middle_click_autoscroll.cpp create mode 100644 Telegram/SourceFiles/ui/widgets/middle_click_autoscroll.h diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index dc01301f7d..114d45fc4c 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -333,6 +333,11 @@ HistoryInner::HistoryInner( [=](QRect updated) { update(updated); })) , _touchSelectTimer([=] { onTouchSelect(); }) , _touchScrollTimer([=] { onTouchScrollTimer(); }) +, _middleClickAutoscroll( + [=](int d) { _scroll->scrollToY(_scroll->scrollTop() + d); }, + [=](const QCursor &cursor) { setCursor(cursor); }, + [=] { mouseActionUpdate(QCursor::pos()); setCursor(_cursor); }, + [=] { return window()->isActiveWindow(); }) , _scrollDateCheck([this] { scrollDateCheck(); }) , _scrollDateHideTimer([this] { scrollDateHideByTimer(); }) { _history->delegateMixin()->setCurrent(this); @@ -1734,6 +1739,14 @@ void HistoryInner::touchEvent(QTouchEvent *e) { void HistoryInner::mouseMoveEvent(QMouseEvent *e) { static auto lastGlobalPosition = e->globalPos(); auto reallyMoved = (lastGlobalPosition != e->globalPos()); + if (_middleClickAutoscroll.active()) { + if (reallyMoved) { + _mouseActive = true; + lastGlobalPosition = e->globalPos(); + } + e->accept(); + return; + } auto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton)); if (!buttonsPressed && _mouseAction != MouseAction::None) { mouseReleaseEvent(e); @@ -1786,6 +1799,18 @@ void HistoryInner::mousePressEvent(QMouseEvent *e) { e->accept(); return; // ignore mouse press, that was hiding context menu } + if (_middleClickAutoscroll.active()) { + _middleClickAutoscroll.stop(); + e->accept(); + return; + } + if (e->button() == Qt::MiddleButton) { + mouseActionCancel(); + ClickHandler::unpressed(); + _middleClickAutoscroll.start(e->globalPos()); + e->accept(); + return; + } _mouseActive = true; mouseActionStart(e->globalPos(), e->button()); } @@ -2196,6 +2221,10 @@ void HistoryInner::mouseActionFinish( } void HistoryInner::mouseReleaseEvent(QMouseEvent *e) { + if (_middleClickAutoscroll.active()) { + e->accept(); + return; + } mouseActionFinish(e->globalPos(), e->button()); if (!rect().contains(e->pos())) { leaveEvent(e); @@ -3503,6 +3532,10 @@ TextForMimeData HistoryInner::getSelectedText() const { } void HistoryInner::keyPressEvent(QKeyEvent *e) { + if (_middleClickAutoscroll.active() && e->key() == Qt::Key_Escape) { + _middleClickAutoscroll.stop(); + return; + } if (e->key() == Qt::Key_Escape) { _widget->escape(); } else if (e == QKeySequence::Copy && !_selected.empty()) { diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 7debbea883..4141fb5b67 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/swipe_handler_data.h" #include "ui/effects/animations.h" #include "ui/dragging_scroll_manager.h" +#include "ui/widgets/middle_click_autoscroll.h" #include "ui/widgets/tooltip.h" #include "ui/widgets/scroll_area.h" #include "ui/userpic_view.h" @@ -560,6 +561,7 @@ private: crl::time _touchAccelerationTime = 0; crl::time _touchTime = 0; base::Timer _touchScrollTimer; + Ui::MiddleClickAutoscroll _middleClickAutoscroll; Ui::Controls::SwipeContextData _gestureHorizontal; Ui::Controls::SwipeBackResult _swipeBackData; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index a8fd3ea847..57de39bce0 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -413,7 +413,12 @@ ListWidget::ListWidget( [=](const HistoryItem *item) { return viewForItem(item); }, [=](const Element *view) { repaintItem(view); }) , _touchSelectTimer([=] { onTouchSelect(); }) -, _touchScrollTimer([=] { onTouchScrollTimer(); }) { +, _touchScrollTimer([=] { onTouchScrollTimer(); }) +, _middleClickAutoscroll( + [=](int d) { _delegate->listScrollTo(_visibleTop + d, false); }, + [=](const QCursor &cursor) { setCursor(cursor); }, + [=] { mouseActionUpdate(QCursor::pos()); setCursor(_cursor); }, + [=] { return window()->isActiveWindow(); }) { setAttribute(Qt::WA_AcceptTouchEvents); setMouseTracking(true); _scrollDateHideTimer.setCallback([this] { scrollDateHideByTimer(); }); @@ -2699,6 +2704,10 @@ void ListWidget::keyPressEvent(QKeyEvent *e) { const auto hasModifiers = (Qt::NoModifier != (e->modifiers() & ~(Qt::KeypadModifier | Qt::GroupSwitchModifier))); + if (_middleClickAutoscroll.active() && key == Qt::Key_Escape) { + _middleClickAutoscroll.stop(); + return; + } if (key == Qt::Key_Escape || key == Qt::Key_Back) { if (hasSelectedText() || hasSelectedItems()) { cancelSelection(); @@ -3003,6 +3012,18 @@ void ListWidget::mousePressEvent(QMouseEvent *e) { e->accept(); return; // ignore mouse press, that was hiding context menu } + if (_middleClickAutoscroll.active()) { + _middleClickAutoscroll.stop(); + e->accept(); + return; + } + if (e->button() == Qt::MiddleButton) { + mouseActionCancel(); + ClickHandler::unpressed(); + _middleClickAutoscroll.start(e->globalPos()); + e->accept(); + return; + } _mouseActive = true; mouseActionStart(e->globalPos(), e->button()); } @@ -3206,6 +3227,14 @@ void ListWidget::touchEvent(QTouchEvent *e) { void ListWidget::mouseMoveEvent(QMouseEvent *e) { static auto lastGlobalPosition = e->globalPos(); auto reallyMoved = (lastGlobalPosition != e->globalPos()); + if (_middleClickAutoscroll.active()) { + if (reallyMoved) { + _mouseActive = true; + lastGlobalPosition = e->globalPos(); + } + e->accept(); + return; + } auto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton)); if (!buttonsPressed && _mouseAction != MouseAction::None) { mouseReleaseEvent(e); @@ -3223,6 +3252,10 @@ void ListWidget::mouseMoveEvent(QMouseEvent *e) { } void ListWidget::mouseReleaseEvent(QMouseEvent *e) { + if (_middleClickAutoscroll.active()) { + e->accept(); + return; + } mouseActionFinish(e->globalPos(), e->button()); if (!rect().contains(e->pos())) { leaveEvent(e); diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index 4caeb5a855..a0d0bec38f 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/rp_widget.h" #include "ui/effects/animations.h" #include "ui/dragging_scroll_manager.h" +#include "ui/widgets/middle_click_autoscroll.h" #include "ui/widgets/tooltip.h" #include "mtproto/sender.h" #include "data/data_messages.h" @@ -867,6 +868,7 @@ private: crl::time _touchAccelerationTime = 0; crl::time _touchTime = 0; base::Timer _touchScrollTimer; + Ui::MiddleClickAutoscroll _middleClickAutoscroll; rpl::event_stream _requestedToEditMessage; rpl::event_stream _requestedToReplyToMessage; diff --git a/Telegram/SourceFiles/ui/widgets/middle_click_autoscroll.cpp b/Telegram/SourceFiles/ui/widgets/middle_click_autoscroll.cpp new file mode 100644 index 0000000000..5dfe2889cd --- /dev/null +++ b/Telegram/SourceFiles/ui/widgets/middle_click_autoscroll.cpp @@ -0,0 +1,228 @@ +/* +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 "ui/widgets/middle_click_autoscroll.h" + +#include "styles/style_widgets.h" +#include "ui/effects/animation_value.h" +#include "ui/rect.h" + +namespace Ui { +namespace { + +constexpr auto kDelay = 15; + +[[nodiscard]] int Deadzone() { + return std::max(1, int(std::lround(style::ConvertFloatScale(6.)))); +} + +[[nodiscard]] float64 SpeedScale() { + return style::ConvertFloatScale(30.); +} + +[[nodiscard]] float64 MaxSpeed() { + return style::ConvertFloatScale(3600.); +} + +[[nodiscard]] QImage PaintCursorImage( + int size, + int factor, + bool drawOuter, + bool drawTopArrow, + bool drawBottomArrow, + const QColor &inner, + const QColor &indicator, + const QColor &topArrow, + const QColor &bottomArrow) { + auto image = QImage( + QSize(size, size) * factor, + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(factor); + image.fill(Qt::transparent); + auto p = QPainter(&image); + p.setRenderHint(QPainter::Antialiasing); + p.setPen(Qt::NoPen); + + const auto outerRect = QRectF(QPointF(0.5, 0.5), Size(size - 1.)); + if (drawOuter) { + const auto outerStroke = std::max(1., style::ConvertFloatScale(1.5)); + auto pen = QPen(indicator); + pen.setWidthF(outerStroke); + p.setPen(pen); + p.setBrush(inner); + p.drawEllipse(outerRect.marginsRemoved(Margins(outerStroke * 0.5))); + } + p.setPen(Qt::NoPen); + const auto centerSkip = style::ConvertFloatScale(10.5); + const auto centerRect = outerRect - Margins(centerSkip); + p.setBrush(indicator); + p.drawEllipse(centerRect); + + const auto middle = size * 0.5; + const auto arrowGap = style::ConvertFloatScale(5.); + const auto headHalfWidth = style::ConvertFloatScale(4.5); + const auto headHeight = style::ConvertFloatScale(3.5); + const auto stroke = std::max(1., style::ConvertFloatScale(2.)); + const auto drawArrow = [&](bool up, const QColor &color) { + auto pen = QPen(color); + pen.setWidthF(stroke); + pen.setCapStyle(Qt::RoundCap); + pen.setJoinStyle(Qt::RoundJoin); + p.setPen(pen); + const auto dir = up ? -1. : 1.; + const auto tipY = middle + dir * (arrowGap + headHeight); + const auto neckY = tipY - dir * headHeight; + p.drawLine( + QPointF(middle, tipY), + QPointF(middle - headHalfWidth, neckY)); + p.drawLine( + QPointF(middle, tipY), + QPointF(middle + headHalfWidth, neckY)); + }; + if (drawTopArrow) { + drawArrow(true, topArrow); + } + if (drawBottomArrow) { + drawArrow(false, bottomArrow); + } + return image; +} + +} // namespace + +MiddleClickAutoscroll::MiddleClickAutoscroll( + Fn scrollBy, + Fn applyCursor, + Fn restoreCursor, + Fn shouldContinue) +: _scrollBy(std::move(scrollBy)) +, _applyCursor(std::move(applyCursor)) +, _restoreCursor(std::move(restoreCursor)) +, _shouldContinue(std::move(shouldContinue)) +, _timer([=] { onTimer(); }) { +} + +void MiddleClickAutoscroll::start(const QPoint &globalPosition) { + if (_active) { + stop(); + } + _active = true; + _startPosition = globalPosition; + _time = crl::now(); + updateCursor(QCursor::pos().y() - _startPosition.y()); + _timer.callEach(kDelay); +} + +void MiddleClickAutoscroll::stop() { + if (!_active) { + return; + } + _active = false; + _timer.cancel(); + if (_restoreCursor) { + _restoreCursor(); + } +} + +void MiddleClickAutoscroll::updateCursor(int delta) { + if (!_applyCursor) { + return; + } + const auto mode = (std::abs(delta) <= Deadzone()) + ? CursorMode::Neutral + : (delta < 0) + ? CursorMode::Up + : CursorMode::Down; + _applyCursor(makeCursor(mode)); +} + +QCursor MiddleClickAutoscroll::makeCursor(CursorMode mode) { + struct CachedCursor { + CursorMode mode = CursorMode::Neutral; + int size = 0; + int factor = 0; + QColor inner; + QColor indicator; + QCursor cursor; + }; + const auto size = std::max( + 1, + int(std::lround(style::ConvertFloatScale(27.)))); + const auto factor = style::DevicePixelRatio(); + const auto inner = anim::with_alpha(st::windowBg->c, (240. / 255.)); + const auto active = anim::with_alpha(st::windowFg->c, (210. / 255.)); + const auto passive = anim::with_alpha( + st::windowSubTextFg->c, + (210. / 255.)); + const auto neutral = (mode == CursorMode::Neutral); + const auto indicator = neutral ? passive : active; + static auto cache = std::vector(); + if (const auto i = ranges::find_if( + cache, + [&](const CachedCursor &entry) { + return (entry.mode == mode) + && (entry.size == size) + && (entry.factor == factor) + && (entry.inner == inner) + && (entry.indicator == indicator); + }); + i != cache.end()) { + return i->cursor; + } + const auto image = PaintCursorImage( + size, + factor, + neutral, + mode != CursorMode::Down, + mode != CursorMode::Up, + inner, + indicator, + indicator, + indicator); + const auto hot = int(std::floor(size * 0.5)); + auto cursor = QCursor(QPixmap::fromImage(image), hot, hot); + cache.push_back(CachedCursor{ + .mode = mode, + .size = size, + .factor = factor, + .inner = inner, + .indicator = indicator, + .cursor = cursor, + }); + return cursor; +} + +void MiddleClickAutoscroll::onTimer() { + if (!_active) { + return; + } else if (_shouldContinue && !_shouldContinue()) { + stop(); + return; + } + const auto now = crl::now(); + const auto elapsed = std::max(now - _time, 1LL); + _time = now; + const auto delta = QCursor::pos().y() - _startPosition.y(); + updateCursor(delta); + const auto absolute = std::abs(delta) - Deadzone(); + if (absolute <= 0) { + return; + } + const auto speed = std::min( + absolute * SpeedScale(), + MaxSpeed()); + auto scroll = int(std::lround((speed * elapsed) / 1000.)); + if (scroll <= 0) { + scroll = 1; + } + if (delta < 0) { + scroll = -scroll; + } + _scrollBy(scroll); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/widgets/middle_click_autoscroll.h b/Telegram/SourceFiles/ui/widgets/middle_click_autoscroll.h new file mode 100644 index 0000000000..62702ce10a --- /dev/null +++ b/Telegram/SourceFiles/ui/widgets/middle_click_autoscroll.h @@ -0,0 +1,53 @@ +/* +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 "base/timer.h" + +class QCursor; + +namespace Ui { + +class MiddleClickAutoscroll final { +public: + explicit MiddleClickAutoscroll( + Fn scrollBy, + Fn applyCursor = nullptr, + Fn restoreCursor = nullptr, + Fn shouldContinue = nullptr); + + [[nodiscard]] bool active() const { + return _active; + } + + void start(const QPoint &globalPosition); + void stop(); + +private: + enum class CursorMode { + Neutral, + Up, + Down, + }; + + void updateCursor(int delta); + void onTimer(); + [[nodiscard]] static QCursor makeCursor(CursorMode mode); + + Fn _scrollBy; + Fn _applyCursor; + Fn _restoreCursor; + Fn _shouldContinue; + bool _active = false; + QPoint _startPosition; + crl::time _time = 0; + base::Timer _timer; + +}; + +} // namespace Ui diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 0d3377c3a0..2aa6bbc804 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -513,6 +513,8 @@ PRIVATE ui/widgets/horizontal_fit_container.h ui/widgets/level_meter.cpp ui/widgets/level_meter.h + ui/widgets/middle_click_autoscroll.cpp + ui/widgets/middle_click_autoscroll.h ui/widgets/multi_select.cpp ui/widgets/multi_select.h ui/widgets/sent_code_field.cpp From ff3b0fe2d92f83a069096ac78eaa7d1551ed28b2 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 27 Feb 2026 08:38:58 +0300 Subject: [PATCH 068/179] Updated Windows build docs for VS 2026. --- .github/workflows/win.yml | 1 - README.md | 5 ++-- docs/building-win-x64.md | 52 --------------------------------------- docs/building-win.md | 32 ++++++++++++++++++++---- 4 files changed, 29 insertions(+), 61 deletions(-) delete mode 100644 docs/building-win-x64.md diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index 2e12fde5ee..cbdff19e04 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -86,7 +86,6 @@ jobs: shell: bash run: | DOCPATH=$TBUILD/$REPO_NAME/docs/building-win.md - [ "${{ matrix.arch }}" = x64 ] && DOCPATH=$TBUILD/$REPO_NAME/docs/building-win-x64.md SDK="$(grep "SDK version" $DOCPATH | sed -r 's/.*\*\*(.*)\*\* SDK version.*/\1/')" echo "SDK=$SDK" >> $GITHUB_ENV diff --git a/README.md b/README.md index 02fd87697d..b5cabb358e 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Version **1.8.15** was the last that supports older systems ## Build instructions -* Windows [(32-bit)][win32] [(64-bit)][win64] +* [Windows (32-bit and 64-bit)][win] * [macOS][mac] * [GNU/Linux using Docker][linux] @@ -78,8 +78,7 @@ Version **1.8.15** was the last that supports older systems [telegram_api]: https://core.telegram.org [telegram_proto]: https://core.telegram.org/mtproto [license]: LICENSE -[win32]: docs/building-win.md -[win64]: docs/building-win-x64.md +[win]: docs/building-win.md [mac]: docs/building-mac.md [linux]: docs/building-linux.md [preview_image]: https://github.com/telegramdesktop/tdesktop/blob/dev/docs/assets/preview.png "Preview of Telegram Desktop" diff --git a/docs/building-win-x64.md b/docs/building-win-x64.md deleted file mode 100644 index 403c346b80..0000000000 --- a/docs/building-win-x64.md +++ /dev/null @@ -1,52 +0,0 @@ -# Build instructions for Windows 64-bit - -- [Prepare folder](#prepare-folder) -- [Install third party software](#install-third-party-software) -- [Clone source code and prepare libraries](#clone-source-code-and-prepare-libraries) -- [Build the project](#build-the-project) -- [Qt Visual Studio Tools](#qt-visual-studio-tools) - -## Prepare folder - -The build is done in **Visual Studio 2022** with **10.0.26100.0** SDK version. - -Choose an empty folder for the future build, for example **D:\\TBuild**. It will be named ***BuildPath*** in the rest of this document. Create two folders there, ***BuildPath*\\ThirdParty** and ***BuildPath*\\Libraries**. - -All commands (if not stated otherwise) will be launched from **x64 Native Tools Command Prompt for VS 2022.bat** (should be in **Start Menu > Visual Studio 2022** menu folder). Pay attention not to use any other Command Prompt. - -### Obtain your API credentials - -You will require **api_id** and **api_hash** to access the Telegram API servers. To learn how to obtain them [click here][api_credentials]. - -## Install third party software - -* Download **Python 3.10** installer from [https://www.python.org/downloads/](https://www.python.org/downloads/) and install it with adding to PATH. -* Download **Git** installer from [https://git-scm.com/download/win](https://git-scm.com/download/win) and install it. - -## Clone source code and prepare libraries - -Open **x64 Native Tools Command Prompt for VS 2022.bat**, go to ***BuildPath*** and run - - git clone --recursive https://github.com/telegramdesktop/tdesktop.git - tdesktop\Telegram\build\prepare\win.bat - -## Build the project - -Go to ***BuildPath*\\tdesktop\\Telegram** and run (using [your **api_id** and **api_hash**](#obtain-your-api-credentials)) - - 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 2022 -* 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**) - -### Qt Visual Studio Tools - -For better debugging you may want to install Qt Visual Studio Tools: - -* Open **Extensions** -> **Manage Extensions** -* Go to **Online** tab -* Search for **Qt** -* Install **Qt Visual Studio Tools** extension - -[api_credentials]: api_credentials.md diff --git a/docs/building-win.md b/docs/building-win.md index be79bfcfc5..1e9f7df47b 100644 --- a/docs/building-win.md +++ b/docs/building-win.md @@ -2,17 +2,18 @@ - [Prepare folder](#prepare-folder) - [Install third party software](#install-third-party-software) +- [Choose architecture and initialize terminal](#choose-architecture-and-initialize-terminal) - [Clone source code and prepare libraries](#clone-source-code-and-prepare-libraries) - [Build the project](#build-the-project) - [Qt Visual Studio Tools](#qt-visual-studio-tools) ## Prepare folder -The build is done in **Visual Studio 2022** with **10.0.26100.0** SDK version. +The build is done in **Visual Studio 2026** with **10.0.26100.0** SDK version. Choose an empty folder for the future build, for example **D:\\TBuild**. It will be named ***BuildPath*** in the rest of this document. Create two folders there, ***BuildPath*\\ThirdParty** and ***BuildPath*\\Libraries**. -All commands (if not stated otherwise) will be launched from **x86 Native Tools Command Prompt for VS 2022.bat** (should be in **Start Menu > Visual Studio 2022** menu folder). Pay attention not to use any other Command Prompt. +The default modern toolset from Visual Studio 2026 (`v145`) does not support Windows 7, so for Telegram Desktop you must use `-vcvars_ver=14.44` (`v144.4`, based on `v143` with Windows 7 support). ### Obtain your API credentials @@ -23,20 +24,41 @@ You will require **api_id** and **api_hash** to access the Telegram API servers. * Download **Python 3.10** installer from [https://www.python.org/downloads/](https://www.python.org/downloads/) and install it with adding to PATH. * Download **Git** installer from [https://git-scm.com/download/win](https://git-scm.com/download/win) and install it. +## Choose architecture and initialize terminal + +Before preparing libraries and running build commands, initialize the Visual Studio environment for your target architecture. +The default modern toolset from Visual Studio 2026 (`v145`) does not support Windows 7, so for Telegram Desktop you must use `-vcvars_ver=14.44` (`v144.4`, based on `v143` with Windows 7 support). + +For `win` (32-bit): + + %comspec% /k "C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvars32.bat" -vcvars_ver=14.44 + +For `win64` (64-bit): + + %comspec% /k "C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvars64.bat" -vcvars_ver=14.44 + +Run both `Clone source code and prepare libraries` and `Build the project` sections in the terminal initialized with one of the commands above. + ## Clone source code and prepare libraries -Open **x86 Native Tools Command Prompt for VS 2022.bat**, go to ***BuildPath*** and run +In the initialized terminal, go to ***BuildPath*** and run git clone --recursive https://github.com/telegramdesktop/tdesktop.git tdesktop\Telegram\build\prepare\win.bat ## Build the project -Go to ***BuildPath*\\tdesktop\\Telegram** and run (using [your **api_id** and **api_hash**](#obtain-your-api-credentials)) +Go to ***BuildPath*\\tdesktop\\Telegram** and run (using [your **api_id** and **api_hash**](#obtain-your-api-credentials)): + +For `win` (32-bit): configure.bat -D TDESKTOP_API_ID=YOUR_API_ID -D TDESKTOP_API_HASH=YOUR_API_HASH -* Open ***BuildPath*\\tdesktop\\out\\Telegram.sln** in Visual Studio 2022 +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 * 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 3ebec4db75b60692e53ea89985061b4531f0b931 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 27 Feb 2026 12:03:28 +0300 Subject: [PATCH 069/179] Changed behavior to send text as caption to first file in files album. --- Telegram/SourceFiles/apiwrap.cpp | 18 +- Telegram/SourceFiles/boxes/send_files_box.cpp | 17 +- .../view/history_view_scheduled_section.cpp | 4 +- .../business/settings_shortcut_messages.cpp | 4 +- .../ui/chat/attach/attach_prepare.cpp | 165 +++++++++++++----- .../ui/chat/attach/attach_prepare.h | 6 + 6 files changed, 157 insertions(+), 57 deletions(-) diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index e295b5eb25..c4c97630e6 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3815,10 +3815,22 @@ void ApiWrap::sendFiles( std::shared_ptr album, const SendAction &action) { const auto haveCaption = !caption.text.isEmpty(); - if (haveCaption - && !list.canAddCaption( + const auto captionAttached = !haveCaption + ? false + : (list.files.size() == 1) + ? list.canAddCaption( album != nullptr, - type == SendMediaType::Photo)) { + 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(caption); message.action.clearDraft = false; diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index b8d7e9a34c..e2aafdc72d 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -108,10 +108,9 @@ void FileDialogCallback( rpl::producer FieldPlaceholder( const Ui::PreparedList &list, - SendFilesWay way) { - return list.canAddCaption( - way.groupFiles() && way.sendImagesAsPhotos(), - way.sendImagesAsPhotos()) + SendFilesWay way, + bool slowmode) { + return Ui::CaptionWillBeAttached(list, way, slowmode) ? tr::lng_photo_caption() : tr::lng_photos_comment(); } @@ -793,9 +792,9 @@ void SendFilesBox::openDialogToAddFileToAlbum() { void SendFilesBox::refreshMessagesCount() { const auto way = _sendWay.current(); - const auto withCaption = _list.canAddCaption( - way.groupFiles() && way.sendImagesAsPhotos(), - way.sendImagesAsPhotos()); + const auto slowmode = (_limits & SendFilesAllow::OnlyOne) + && (_list.files.size() > 1); + const auto withCaption = Ui::CaptionWillBeAttached(_list, way, slowmode); const auto withComment = !withCaption && _caption && !_caption->isHidden() @@ -1082,7 +1081,9 @@ void SendFilesBox::updateCaptionPlaceholder() { _emojiToggle->hide(); } } else { - _caption->setPlaceholder(FieldPlaceholder(_list, way)); + const auto slowmode = (_limits & SendFilesAllow::OnlyOne) + && (_list.files.size() > 1); + _caption->setPlaceholder(FieldPlaceholder(_list, way, slowmode)); _caption->show(); if (_emojiToggle) { _emojiToggle->show(); diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index df507c71d9..201bf390c4 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -603,13 +603,13 @@ void ScheduledWidget::sendingFilesConfirmed( return; } auto groups = DivideByGroups(std::move(list), way, false); + const auto captionAttached = CaptionWillBeAttached(groups); const auto type = way.sendImagesAsPhotos() ? SendMediaType::Photo : SendMediaType::File; auto action = prepareSendAction(options); action.clearDraft = false; - if ((groups.size() != 1 || !groups.front().sentWithCaption()) - && !caption.text.isEmpty()) { + if (!captionAttached && !caption.text.isEmpty()) { auto message = Api::MessageToSend(action); message.textWithTags = base::take(caption); session().api().sendMessage(std::move(message)); diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index e566df58e6..0c833f8701 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -1416,13 +1416,13 @@ void ShortcutMessages::sendingFilesConfirmed( std::move(list), way, _history->peer->slowmodeApplied()); + const auto captionAttached = CaptionWillBeAttached(groups); const auto type = way.sendImagesAsPhotos() ? SendMediaType::Photo : SendMediaType::File; auto action = prepareSendAction(options); action.clearDraft = false; - if ((groups.size() != 1 || !groups.front().sentWithCaption()) - && !caption.text.isEmpty()) { + if (!captionAttached && !caption.text.isEmpty()) { auto message = Api::MessageToSend(action); message.textWithTags = base::take(caption); _session->api().sendMessage(std::move(message)); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp index 73a13eeb80..dc1a745c3c 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp @@ -22,6 +22,89 @@ namespace { constexpr auto kMaxAlbumCount = 10; +struct GroupRange { + int from = 0; + int till = 0; + AlbumType type = AlbumType::None; + + [[nodiscard]] int size() const { + return till - from; + } + [[nodiscard]] bool sentWithCaption() const { + return (size() == 1) || (type == AlbumType::PhotoVideo); + } +}; + +[[nodiscard]] AlbumType GroupTypeForFile( + PreparedFile::Type type, + bool groupFiles, + bool sendImagesAsPhotos) { + using Type = PreparedFile::Type; + return (type == Type::Music) + ? (groupFiles ? AlbumType::Music : AlbumType::None) + : (type == Type::Video) + ? (groupFiles ? AlbumType::PhotoVideo : AlbumType::None) + : (type == Type::Photo) + ? ((groupFiles && sendImagesAsPhotos) + ? AlbumType::PhotoVideo + : (groupFiles && !sendImagesAsPhotos) + ? AlbumType::File + : AlbumType::None) + : (type == Type::File) + ? (groupFiles ? AlbumType::File : AlbumType::None) + : AlbumType::None; +} + +[[nodiscard]] std::vector GroupRanges( + const std::vector &files, + SendFilesWay way, + bool slowmode) { + const auto sendImagesAsPhotos = way.sendImagesAsPhotos(); + const auto groupFiles = way.groupFiles() || slowmode; + + auto result = std::vector(); + if (files.empty()) { + return result; + } + auto from = 0; + auto groupType = AlbumType::None; + for (auto i = 0; i != int(files.size()); ++i) { + const auto fileGroupType = GroupTypeForFile( + files[i].type, + groupFiles, + sendImagesAsPhotos); + const auto count = (i - from); + if ((i > from && groupType != fileGroupType) + || ((groupType != AlbumType::None) && (count == kMaxAlbumCount))) { + result.push_back(GroupRange{ + .from = from, + .till = i, + .type = (count > 1) ? groupType : AlbumType::None, + }); + from = i; + } + groupType = fileGroupType; + } + const auto till = int(files.size()); + const auto count = (till - from); + result.push_back(GroupRange{ + .from = from, + .till = till, + .type = (count > 1) ? groupType : AlbumType::None, + }); + return result; +} + +[[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()); +} + } // namespace PreparedFile::PreparedFile(const QString &path) : path(path) { @@ -274,6 +357,33 @@ bool PreparedList::hasSpoilerMenu(bool compress) const { return allAreVideo || (allAreMedia && compress); } +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; +} + +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, @@ -283,8 +393,9 @@ std::shared_ptr PrepareFilesBundle( for (const auto &group : groups) { totalCount += group.list.files.size(); } + const auto captionAttached = CaptionWillBeAttached(groups); const auto sendComment = !caption.text.isEmpty() - && (groups.size() != 1 || !groups.front().sentWithCaption()); + && !captionAttached; return std::make_shared(PreparedBundle{ .groups = std::move(groups), .way = way, @@ -310,49 +421,19 @@ std::vector DivideByGroups( PreparedList &&list, SendFilesWay way, bool slowmode) { - const auto sendImagesAsPhotos = way.sendImagesAsPhotos(); - const auto groupFiles = way.groupFiles() || slowmode; - - auto group = Ui::PreparedList(); - - using Type = Ui::PreparedFile::Type; - auto groupType = AlbumType::None; - + const auto ranges = GroupRanges(list.files, way, slowmode); auto result = std::vector(); - auto pushGroup = [&] { - const auto type = (group.files.size() > 1) - ? groupType - : AlbumType::None; - result.push_back(PreparedGroup{ - .list = base::take(group), - .type = type, - }); - }; - for (auto i = 0; i != list.files.size(); ++i) { - auto &file = list.files[i]; - const auto fileGroupType = (file.type == Type::Music) - ? (groupFiles ? AlbumType::Music : AlbumType::None) - : (file.type == Type::Video) - ? (groupFiles ? AlbumType::PhotoVideo : AlbumType::None) - : (file.type == Type::Photo) - ? ((groupFiles && sendImagesAsPhotos) - ? AlbumType::PhotoVideo - : (groupFiles && !sendImagesAsPhotos) - ? AlbumType::File - : AlbumType::None) - : (file.type == Type::File) - ? (groupFiles ? AlbumType::File : AlbumType::None) - : AlbumType::None; - if ((!group.files.empty() && groupType != fileGroupType) - || ((groupType != AlbumType::None) - && (group.files.size() == Ui::MaxAlbumItems()))) { - pushGroup(); + result.reserve(ranges.size()); + for (const auto &range : ranges) { + auto grouped = Ui::PreparedList(); + grouped.files.reserve(range.size()); + for (auto i = range.from; i != range.till; ++i) { + grouped.files.push_back(std::move(list.files[i])); } - group.files.push_back(std::move(file)); - groupType = fileGroupType; - } - if (!group.files.empty()) { - pushGroup(); + result.push_back(PreparedGroup{ + .list = std::move(grouped), + .type = range.type, + }); } return result; } diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h index 635be179ad..5eddcf8ad1 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h @@ -154,6 +154,12 @@ struct PreparedGroup { 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; From 04dbbab4a1f6ea9fe11231a01016b1ce8438ec7c Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 27 Feb 2026 19:26:12 +0300 Subject: [PATCH 070/179] Replaced vector container for SendFilesBox::Block with deque. SendFilesBox::Block (aka Block) passes action callbacks to AlbumPreview / SingleMediaPreview. These callbacks access Block::_items through a captured "this" pointer. With std::vector, _blocks.emplace_back() may reallocate and move existing Block objects. Callbacks already stored inside previews then keep a dangling Block*, and a later context-menu action can crash when reading this->_items. Switched _blocks to std::deque so existing element addresses stay stable on std::deque::push_back, keeping callback captures valid for previews. --- Telegram/SourceFiles/boxes/send_files_box.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index 0e47358424..5582f6ea2c 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -307,7 +307,7 @@ private: object_ptr _scroll; QPointer _inner; - std::vector _blocks; + std::deque _blocks; Fn _whenReadySend; bool _preparing = false; From 18487481185369a04c95bcc593c703d296af9747 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 28 Feb 2026 08:17:00 +0300 Subject: [PATCH 071/179] Fixed build. --- Telegram/SourceFiles/data/data_search_calendar.cpp | 2 -- .../SourceFiles/history/view/history_view_reaction_preview.cpp | 2 +- Telegram/SourceFiles/settings/sections/settings_advanced.cpp | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/data/data_search_calendar.cpp b/Telegram/SourceFiles/data/data_search_calendar.cpp index 513683fa00..1d4cc94fcd 100644 --- a/Telegram/SourceFiles/data/data_search_calendar.cpp +++ b/Telegram/SourceFiles/data/data_search_calendar.cpp @@ -125,8 +125,6 @@ void SearchCalendarController::processMonthMessages( bool noMoreData) { auto result = std::vector(); auto seenDays = base::flat_set(); - const auto peer = _session->data().peer(key.peerId); - const auto history = peer->owner().history(peer); const auto targetMonth = QDate(key.year, key.month, 1); const auto targetStart = base::unixtime::serialize( diff --git a/Telegram/SourceFiles/history/view/history_view_reaction_preview.cpp b/Telegram/SourceFiles/history/view/history_view_reaction_preview.cpp index e646309bb3..0393cbe7b8 100644 --- a/Telegram/SourceFiles/history/view/history_view_reaction_preview.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reaction_preview.cpp @@ -62,7 +62,7 @@ bool ShowReactionPreview( }; }; - const auto state = std::make_shared>(); + const auto state = std::make_shared(); const auto mainwidget = controller->widget(); state->mediaPreview = base::make_unique_q( diff --git a/Telegram/SourceFiles/settings/sections/settings_advanced.cpp b/Telegram/SourceFiles/settings/sections/settings_advanced.cpp index aa4a152a27..a72f977b65 100644 --- a/Telegram/SourceFiles/settings/sections/settings_advanced.cpp +++ b/Telegram/SourceFiles/settings/sections/settings_advanced.cpp @@ -798,7 +798,6 @@ void BuildSpellcheckerSection(SectionBuilder &builder) { const auto session = builder.session(); const auto settings = &Core::App().settings(); const auto isSystem = Platform::Spellchecker::IsSystemSpellchecker(); - const auto container = builder.container(); builder.addDivider(); builder.addSkip(); From 7899e4943bde172b34a8a606c0379711cedb4606 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 28 Feb 2026 09:31:40 +0300 Subject: [PATCH 072/179] Changed behavior to open context menu with right-click on macOS tray. --- Telegram/SourceFiles/platform/mac/tray_mac.mm | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/Telegram/SourceFiles/platform/mac/tray_mac.mm b/Telegram/SourceFiles/platform/mac/tray_mac.mm index 91de70b1e4..a900ec444c 100644 --- a/Telegram/SourceFiles/platform/mac/tray_mac.mm +++ b/Telegram/SourceFiles/platform/mac/tray_mac.mm @@ -82,6 +82,11 @@ namespace Platform { namespace { +enum class TrayClickType { + Left, + Right, +}; + [[nodiscard]] bool IsAnyActiveForTrayMenu() { for (const NSWindow *w in [[NSApplication sharedApplication] windows]) { if (w.isKeyWindow) { @@ -233,14 +238,14 @@ public: void showMenu(not_null menu); void deactivateButton(); - [[nodiscard]] rpl::producer<> clicks() const; + [[nodiscard]] rpl::producer clicks() const; [[nodiscard]] rpl::producer<> aboutToShowRequests() const; private: CommonDelegate *_delegate; NSStatusItem *_status; - rpl::event_stream<> _clicks; + rpl::event_stream _clicks; rpl::lifetime _lifetime; @@ -276,12 +281,17 @@ NativeIcon::NativeIcon() [_status.button sendActionOn:masks]; id buttonCallback = [^{ - const auto type = NSApp.currentEvent.type; + const auto event = NSApp.currentEvent; + const auto type = event.type; - if ((type == NSEventTypeLeftMouseDown) - || (type == NSEventTypeRightMouseDown)) { + if (type == NSEventTypeLeftMouseDown) { Core::Sandbox::Instance().customEnterFromEventLoop([=] { - _clicks.fire({}); + _clicks.fire(TrayClickType::Left); + }); + } else if (type == NSEventTypeRightMouseDown + || type == NSEventTypeRightMouseUp) { + Core::Sandbox::Instance().customEnterFromEventLoop([=] { + _clicks.fire(TrayClickType::Right); }); } } copy]; @@ -319,7 +329,7 @@ void NativeIcon::deactivateButton() { [_status.button highlight:false]; } -rpl::producer<> NativeIcon::clicks() const { +rpl::producer NativeIcon::clicks() const { return _clicks.events(); } @@ -336,8 +346,13 @@ void Tray::createIcon() { // On macOS we are activating the window on click // instead of showing the menu, when the window is not activated. _nativeIcon->clicks( - ) | rpl::on_next([=] { - if (IsAnyActiveForTrayMenu()) { + ) | rpl::on_next([=](TrayClickType type) { + if (!_menu) { + return; + } + if (type == TrayClickType::Right) { + _nativeIcon->showMenu(_menu.get()); + } else if (IsAnyActiveForTrayMenu()) { _nativeIcon->showMenu(_menu.get()); } else { _nativeIcon->deactivateButton(); From 7d310e56cc7fcb121154b1ecc4f15737175b5912 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 28 Feb 2026 09:44:31 +0300 Subject: [PATCH 073/179] Added accounts list to tray menu on macOS. Related commit: 57b22d2ad2. --- Telegram/CMakeLists.txt | 13 +++ Telegram/SourceFiles/platform/mac/tray_mac.h | 15 ++++ Telegram/SourceFiles/platform/mac/tray_mac.mm | 47 +++++++++++ Telegram/SourceFiles/tray.cpp | 7 ++ Telegram/SourceFiles/tray_accounts_menu.cpp | 81 +++++++++++++++++++ Telegram/SourceFiles/tray_accounts_menu.h | 19 +++++ .../SourceFiles/tray_accounts_menu_dummy.cpp | 20 +++++ 7 files changed, 202 insertions(+) create mode 100644 Telegram/SourceFiles/tray_accounts_menu.cpp create mode 100644 Telegram/SourceFiles/tray_accounts_menu.h create mode 100644 Telegram/SourceFiles/tray_accounts_menu_dummy.cpp diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index a034d7a130..9a2a48ea9e 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1814,10 +1814,23 @@ PRIVATE settings.cpp settings.h stdafx.h + tray_accounts_menu.h tray.cpp tray.h ) +if (APPLE) + nice_target_sources(Telegram ${src_loc} + PRIVATE + tray_accounts_menu.cpp + ) +else() + nice_target_sources(Telegram ${src_loc} + PRIVATE + tray_accounts_menu_dummy.cpp + ) +endif() + if (NOT build_winstore) remove_target_sources(Telegram ${src_loc} platform/win/windows_start_task.cpp diff --git a/Telegram/SourceFiles/platform/mac/tray_mac.h b/Telegram/SourceFiles/platform/mac/tray_mac.h index 6667d3cb12..1e472ce3b1 100644 --- a/Telegram/SourceFiles/platform/mac/tray_mac.h +++ b/Telegram/SourceFiles/platform/mac/tray_mac.h @@ -12,6 +12,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unique_qptr.h" class QMenu; +class QIcon; + +namespace Ui { +class DynamicImage; +} // namespace Ui namespace Platform { @@ -38,6 +43,16 @@ public: void destroyMenu(); void addAction(rpl::producer text, Fn &&callback); + void addAction( + rpl::producer text, + Fn &&callback, + const QIcon &icon); + void addAction( + rpl::producer text, + Fn &&callback, + std::shared_ptr icon, + int size); + void addSeparator(); void showTrayMessage() const; [[nodiscard]] bool hasTrayMessageSupport() const; diff --git a/Telegram/SourceFiles/platform/mac/tray_mac.mm b/Telegram/SourceFiles/platform/mac/tray_mac.mm index a900ec444c..6f2d565826 100644 --- a/Telegram/SourceFiles/platform/mac/tray_mac.mm +++ b/Telegram/SourceFiles/platform/mac/tray_mac.mm @@ -13,9 +13,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_controller.h" #include "window/window_session_controller.h" #include "ui/painter.h" +#include "ui/dynamic_image.h" #include "styles/style_window.h" #include +#include #import #import @@ -387,11 +389,19 @@ void Tray::destroyMenu() { } void Tray::addAction(rpl::producer text, Fn &&callback) { + addAction(std::move(text), std::move(callback), QIcon()); +} + +void Tray::addAction( + rpl::producer text, + Fn &&callback, + const QIcon &icon) { if (!_menu) { return; } const auto action = _menu->addAction(QString(), std::move(callback)); + action->setIcon(icon); std::move( text ) | rpl::on_next([=](const QString &text) { @@ -399,6 +409,43 @@ void Tray::addAction(rpl::producer text, Fn &&callback) { }, _actionsLifetime); } +void Tray::addAction( + rpl::producer text, + Fn &&callback, + std::shared_ptr icon, + int size) { + if (!_menu) { + return; + } + + const auto action = _menu->addAction(QString(), std::move(callback)); + if (icon) { + const auto updateIcon = crl::guard(action, [=] { + action->setIcon(QIcon(QPixmap::fromImage(icon->image(size)))); + }); + icon->subscribeToUpdates([=] { + Core::Sandbox::Instance().customEnterFromEventLoop([=] { + updateIcon(); + }); + }); + updateIcon(); + _actionsLifetime.add([icon = std::move(icon)] { + icon->subscribeToUpdates(nullptr); + }); + } + std::move( + text + ) | rpl::on_next([=](const QString &text) { + action->setText(text); + }, _actionsLifetime); +} + +void Tray::addSeparator() { + if (_menu) { + _menu->addSeparator(); + } +} + void Tray::showTrayMessage() const { } diff --git a/Telegram/SourceFiles/tray.cpp b/Telegram/SourceFiles/tray.cpp index 521886ee7e..f6a6214800 100644 --- a/Telegram/SourceFiles/tray.cpp +++ b/Telegram/SourceFiles/tray.cpp @@ -6,6 +6,7 @@ For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "tray.h" +#include "tray_accounts_menu.h" #include "core/application.h" #include "core/core_settings.h" @@ -51,6 +52,10 @@ void Tray::create() { rebuildMenu(); }, _tray.lifetime()); + TrayAccountsMenu::SetupChangesSubscription( + [=] { rebuildMenu(); }, + _tray.lifetime()); + _tray.iconClicks( ) | rpl::on_next([=] { const auto skipTrayClick = (_lastTrayClickTime > 0) @@ -97,6 +102,8 @@ void Tray::rebuildMenu() { _tray.addAction(tr::lng_quit_from_tray(), [] { Core::Quit(); }); + TrayAccountsMenu::Fill(_tray); + updateMenuText(); } diff --git a/Telegram/SourceFiles/tray_accounts_menu.cpp b/Telegram/SourceFiles/tray_accounts_menu.cpp new file mode 100644 index 0000000000..760ecfd425 --- /dev/null +++ b/Telegram/SourceFiles/tray_accounts_menu.cpp @@ -0,0 +1,81 @@ +/* +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 "tray_accounts_menu.h" + +#include "base/weak_ptr.h" +#include "base/qt/qt_key_modifiers.h" +#include "core/application.h" +#include "data/data_peer.h" +#include "data/data_user.h" +#include "info/profile/info_profile_values.h" +#include "main/main_account.h" +#include "main/main_domain.h" +#include "main/main_session.h" +#include "styles/style_window.h" +#include "ui/dynamic_thumbnails.h" + +namespace Core::TrayAccountsMenu { + +void SetupChangesSubscription(Fn callback, rpl::lifetime &lifetime) { + const auto accountSessionsLifetime = lifetime.make_state(); + const auto watchAccountSessions = [=] { + accountSessionsLifetime->destroy(); + for (const auto &[index, account] : Core::App().domain().accounts()) { + account->sessionChanges( + ) | rpl::on_next([=](Main::Session*) { + callback(); + }, *accountSessionsLifetime); + } + }; + Core::App().domain().accountsChanges() | rpl::on_next([=] { + watchAccountSessions(); + callback(); + }, lifetime); + watchAccountSessions(); +} + +void Fill(Platform::Tray &tray) { + auto accounts = std::vector>(); + for (const auto &account : Core::App().domain().orderedAccounts()) { + if (account->sessionExists()) { + accounts.push_back(account); + } + } + if (accounts.size() <= 1) { + return; + } + tray.addSeparator(); + constexpr auto kMaxLength = 30; + for (const auto account : accounts) { + const auto user = account->session().user(); + const auto weak = base::make_weak(account); + tray.addAction( + Info::Profile::NameValue( + user + ) | rpl::map([=](const QString &name) { + return (name.size() > kMaxLength) + ? (name.mid(0, kMaxLength) + Ui::kQEllipsis) + : name; + }), + [weak] { + const auto strong = weak.get(); + if (!strong || !strong->sessionExists()) { + return; + } + if (base::IsCtrlPressed()) { + Core::App().ensureSeparateWindowFor({ strong }); + } else { + Core::App().domain().maybeActivate(strong); + } + }, + Ui::MakeUserpicThumbnail(user, true), + st::notifyMacPhotoSize); + } +} + +} // namespace Core::TrayAccountsMenu diff --git a/Telegram/SourceFiles/tray_accounts_menu.h b/Telegram/SourceFiles/tray_accounts_menu.h new file mode 100644 index 0000000000..2d24931dfb --- /dev/null +++ b/Telegram/SourceFiles/tray_accounts_menu.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 "platform/platform_tray.h" + +namespace Core::TrayAccountsMenu { + +void SetupChangesSubscription( + Fn callback, + rpl::lifetime &lifetime); +void Fill(Platform::Tray &tray); + +} // namespace Core::TrayAccountsMenu diff --git a/Telegram/SourceFiles/tray_accounts_menu_dummy.cpp b/Telegram/SourceFiles/tray_accounts_menu_dummy.cpp new file mode 100644 index 0000000000..1e135ed6fc --- /dev/null +++ b/Telegram/SourceFiles/tray_accounts_menu_dummy.cpp @@ -0,0 +1,20 @@ +/* +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 "tray_accounts_menu.h" + +namespace Core::TrayAccountsMenu { + +void SetupChangesSubscription( + [[maybe_unused]] Fn callback, + [[maybe_unused]] rpl::lifetime &lifetime) { +} + +void Fill([[maybe_unused]] Platform::Tray &tray) { +} + +} // namespace Core::TrayAccountsMenu From 385f05b21b2d7c3820b0451cd2109cf11c7dd484 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 28 Feb 2026 17:04:05 +0300 Subject: [PATCH 074/179] Added ability to show moderate box with preset of options. --- Telegram/SourceFiles/boxes/moderate_messages_box.cpp | 9 +++++---- Telegram/SourceFiles/boxes/moderate_messages_box.h | 9 ++++++++- Telegram/SourceFiles/history/history_inner_widget.cpp | 6 ++++-- Telegram/SourceFiles/history/history_widget.cpp | 3 ++- .../history/view/history_view_context_menu.cpp | 3 ++- .../history/view/history_view_list_widget.cpp | 3 ++- 6 files changed, 23 insertions(+), 10 deletions(-) diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index 09f9bc3f3b..b01cff5045 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -350,7 +350,8 @@ void ProccessCommonGroups( void CreateModerateMessagesBox( not_null box, const HistoryItemsList &items, - Fn confirmed) { + Fn confirmed, + ModerateMessagesBoxOptions options) { Expects(!items.empty()); using Controller = Ui::ExpandablePeerListController; @@ -582,7 +583,7 @@ void CreateModerateMessagesBox( object_ptr( box, tr::lng_report_spam(tr::now), - false, + options.reportSpam, st::defaultBoxCheckbox), st::boxRowPadding + buttonPadding); const auto controller = box->lifetime().make_state( @@ -613,7 +614,7 @@ void CreateModerateMessagesBox( lt_user, tr::bold(firstItem->from()->name()), tr::marked), - false, + options.deleteAll, st::defaultBoxCheckbox), st::boxRowPadding + buttonPadding); auto messagesCounts = MessagesCountValue(history, participants); @@ -729,7 +730,7 @@ void CreateModerateMessagesBox( rpl::single(isSingle), tr::lng_ban_user(), tr::lng_ban_users())), - false, + options.banUser, st::defaultBoxCheckbox), st::boxRowPadding + buttonPadding); const auto controller = box->lifetime().make_state( diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.h b/Telegram/SourceFiles/boxes/moderate_messages_box.h index 92307ce347..4b3d120ad6 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.h +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.h @@ -19,10 +19,17 @@ class GenericBox; extern const char kModerateCommonGroups[]; +struct ModerateMessagesBoxOptions final { + bool reportSpam = false; + bool deleteAll = false; + bool banUser = false; +}; + void CreateModerateMessagesBox( not_null box, const HistoryItemsList &items, - Fn confirmed); + Fn confirmed, + ModerateMessagesBoxOptions options); [[nodiscard]] bool CanCreateModerateMessagesBox(const HistoryItemsList &); diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 114d45fc4c..88b51b7e81 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -4949,7 +4949,8 @@ void HistoryInner::deleteItem(not_null item) { } const auto list = HistoryItemsList{ item }; if (CanCreateModerateMessagesBox(list)) { - _controller->show(Box(CreateModerateMessagesBox, list, nullptr)); + const auto opt = ModerateMessagesBoxOptions(); + _controller->show(Box(CreateModerateMessagesBox, list, nullptr, opt)); } else { const auto suggestModerate = false; _controller->show(Box(item, suggestModerate)); @@ -4970,7 +4971,8 @@ void HistoryInner::deleteAsGroup(FullMsgId itemId) { _controller->show(Box( CreateModerateMessagesBox, group->items, - nullptr)); + nullptr, + ModerateMessagesBoxOptions{})); } else { _controller->show(Box( &session(), diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 9d004743eb..9d365e7c0a 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -9291,7 +9291,8 @@ void HistoryWidget::confirmDeleteSelected() { controller()->show(Box( CreateModerateMessagesBox, items, - crl::guard(this, [=] { clearSelected(); }))); + crl::guard(this, [=] { clearSelected(); }), + ModerateMessagesBoxOptions{})); } else { auto box = Box(&session(), std::move(ids)); box->setDeleteConfirmedCallback(crl::guard(this, [=] { diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 4c145238db..018ce5a1c7 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -893,8 +893,9 @@ bool AddDeleteMessageAction( } const auto list = HistoryItemsList{ item }; if (CanCreateModerateMessagesBox(list)) { + const auto opt = ModerateMessagesBoxOptions(); controller->show( - Box(CreateModerateMessagesBox, list, nullptr)); + Box(CreateModerateMessagesBox, list, nullptr, opt)); } else { const auto suggestModerateActions = false; controller->show( diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 57de39bce0..fa66c458c5 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -4427,8 +4427,9 @@ void ConfirmDeleteSelectedItems(not_null widget) { widget->cancelSelection(); }); if (CanCreateModerateMessagesBox(historyItems)) { + const auto opt = ModerateMessagesBoxOptions(); controller->show( - Box(CreateModerateMessagesBox, historyItems, confirmed)); + Box(CreateModerateMessagesBox, historyItems, confirmed, opt)); } else { auto box = Box( &widget->session(), From 9b483c390d73b5a56dc9ca922d2c56efdb78ad7c Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 28 Feb 2026 17:04:23 +0300 Subject: [PATCH 075/179] Added ability to open full checked moderate box from left userpic menu. --- Telegram/Resources/langs/lang.strings | 1 + .../history/history_inner_widget.cpp | 21 ++++++++++ .../history/view/history_view_list_widget.cpp | 10 +++++ .../SourceFiles/window/window_peer_menu.cpp | 38 +++++++++++++++++++ .../SourceFiles/window/window_peer_menu.h | 6 +++ 5 files changed, 76 insertions(+) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index ee75d5e9c9..eae8c30d18 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4864,6 +4864,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_edit_permissions" = "Edit admin rights"; "lng_context_restrict_user" = "Restrict user"; "lng_context_ban_user" = "Ban"; +"lng_context_delete_and_ban" = "Delete and ban"; "lng_context_remove_from_group" = "Remove from group"; "lng_context_add_to_group" = "Add to group"; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 88b51b7e81..0b62173dcb 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -2459,6 +2459,27 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { _widget->fillSenderUserpicMenu( _menu.get(), session->data().peer(PeerId(linkUserpicPeerId))); + Window::AddSenderUserpicModerateAction( + _controller, + [&] { + auto moderateItem = _dragStateItem; + const auto contextY = mapFromGlobal(e->globalPos()).y(); + enumerateItems( + [&](not_null view, int top, int bottom) { + if (bottom <= contextY) { + return true; + } else if (top > contextY) { + return false; + } + const auto item = view->data(); + if (item->from()->id == PeerId(linkUserpicPeerId)) { + moderateItem = item; + } + return false; + }); + return moderateItem; + }(), + Ui::Menu::CreateAddActionCallback(_menu.get())); if (_menu->empty()) { _menu = nullptr; } else { diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index fa66c458c5..9a22de5626 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_peer_menu.h" #include "main/main_session.h" #include "ui/layers/generic_box.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/scroll_area.h" #include "ui/toast/toast.h" @@ -2914,6 +2915,15 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { } else if (linkUserpicPeerId) { _menu = _delegate->listFillSenderUserpicMenu(linkUserpicPeerId); if (_menu) { + Window::AddSenderUserpicModerateAction( + controller(), + [&] { + const auto contextY = _visibleTop + + mapFromGlobal(e->globalPos()).y(); + const auto contextView = strictFindItemByY(contextY); + return contextView ? contextView->data().get() : overItem; + }(), + Ui::Menu::CreateAddActionCallback(_menu.get())); _menu->popup(e->globalPos()); e->accept(); return; diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 5fd8557386..a6ad45c90b 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -3798,6 +3798,44 @@ void FillSenderUserpicMenu( } } +void AddSenderUserpicModerateAction( + not_null controller, + HistoryItem *moderateItem, + const PeerMenuCallback &addAction) { + const auto moderateChannel = moderateItem + ? moderateItem->history()->peer->asChannel() + : nullptr; + const auto moderateUser = moderateItem + ? moderateItem->from()->asUser() + : nullptr; + const auto canDeleteAndBan = moderateItem + && moderateChannel + && moderateChannel->isMegagroup() + && moderateUser + && !moderateChannel->isGroupAdmin(moderateUser) + && moderateItem->suggestBanReport() + && moderateItem->suggestDeleteAllReport(); + if (canDeleteAndBan) { + addAction({ .isSeparator = true }); + addAction({ + .text = tr::lng_context_delete_and_ban(tr::now), + .handler = [=] { + controller->show(Box( + CreateModerateMessagesBox, + HistoryItemsList{ not_null(moderateItem) }, + nullptr, + ModerateMessagesBoxOptions{ + .reportSpam = true, + .deleteAll = true, + .banUser = true, + })); + }, + .icon = &st::menuIconBlockAttention, + .isAttention = true, + }); + } +} + bool IsUnreadThread(not_null thread) { return thread->chatListBadgesState().unread; } diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index d2afbcc4d6..4a08e99f49 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/menu/menu_add_action_callback.h" class History; +class HistoryItem; namespace Api { struct SendOptions; @@ -84,6 +85,11 @@ void FillSenderUserpicMenu( Dialogs::Key searchInEntry, const PeerMenuCallback &addAction); +void AddSenderUserpicModerateAction( + not_null controller, + HistoryItem *moderateItem, + const PeerMenuCallback &addAction); + void MenuAddMarkAsReadAllChatsAction( not_null session, std::shared_ptr show, From 7fb0e3900fd82a4b028c2d9d84698eebbbdbcfae Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 28 Feb 2026 17:28:33 +0300 Subject: [PATCH 076/179] Added ability to open full checked moderate box with pressed Ctrl. --- Telegram/SourceFiles/boxes/moderate_messages_box.cpp | 10 ++++++++++ Telegram/SourceFiles/boxes/moderate_messages_box.h | 2 ++ Telegram/SourceFiles/history/history_inner_widget.cpp | 2 +- Telegram/SourceFiles/history/history_widget.cpp | 3 ++- .../history/view/history_view_context_menu.cpp | 2 +- .../history/view/history_view_list_widget.cpp | 2 +- 6 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index b01cff5045..b0b4fa0bde 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -1170,3 +1170,13 @@ void DeleteSublistBox( }, st::attentionBoxButton); box->addButton(tr::lng_cancel(), close); } + +ModerateMessagesBoxOptions DefaultModerateMessagesBoxOptions() { + return base::IsCtrlPressed() + ? ModerateMessagesBoxOptions{ + .reportSpam = true, + .deleteAll = true, + .banUser = true, + } + : ModerateMessagesBoxOptions{}; +} diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.h b/Telegram/SourceFiles/boxes/moderate_messages_box.h index 4b3d120ad6..2d5f2cec1c 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.h +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.h @@ -25,6 +25,8 @@ struct ModerateMessagesBoxOptions final { bool banUser = false; }; +[[nodiscard]] ModerateMessagesBoxOptions DefaultModerateMessagesBoxOptions(); + void CreateModerateMessagesBox( not_null box, const HistoryItemsList &items, diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 0b62173dcb..6a6c584ba4 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -4970,7 +4970,7 @@ void HistoryInner::deleteItem(not_null item) { } const auto list = HistoryItemsList{ item }; if (CanCreateModerateMessagesBox(list)) { - const auto opt = ModerateMessagesBoxOptions(); + const auto opt = DefaultModerateMessagesBoxOptions(); _controller->show(Box(CreateModerateMessagesBox, list, nullptr, opt)); } else { const auto suggestModerate = false; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 9d365e7c0a..10f1946401 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -9288,11 +9288,12 @@ void HistoryWidget::confirmDeleteSelected() { } const auto items = session().data().idsToItems(ids); if (CanCreateModerateMessagesBox(items)) { + const auto opt = DefaultModerateMessagesBoxOptions(); controller()->show(Box( CreateModerateMessagesBox, items, crl::guard(this, [=] { clearSelected(); }), - ModerateMessagesBoxOptions{})); + opt)); } else { auto box = Box(&session(), std::move(ids)); box->setDeleteConfirmedCallback(crl::guard(this, [=] { diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 018ce5a1c7..6967d2b66c 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -893,7 +893,7 @@ bool AddDeleteMessageAction( } const auto list = HistoryItemsList{ item }; if (CanCreateModerateMessagesBox(list)) { - const auto opt = ModerateMessagesBoxOptions(); + const auto opt = DefaultModerateMessagesBoxOptions(); controller->show( Box(CreateModerateMessagesBox, list, nullptr, opt)); } else { diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 9a22de5626..eeb3667bd4 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -4437,7 +4437,7 @@ void ConfirmDeleteSelectedItems(not_null widget) { widget->cancelSelection(); }); if (CanCreateModerateMessagesBox(historyItems)) { - const auto opt = ModerateMessagesBoxOptions(); + const auto opt = DefaultModerateMessagesBoxOptions(); controller->show( Box(CreateModerateMessagesBox, historyItems, confirmed, opt)); } else { From 03e78536f0b7000889a7f13042d445d63a0f609c Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 18 Feb 2026 22:21:03 +0400 Subject: [PATCH 077/179] Update lib_base with zlib-ng fix. --- Telegram/lib_base | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/lib_base b/Telegram/lib_base index cca87bd2fb..c53d239144 160000 --- a/Telegram/lib_base +++ b/Telegram/lib_base @@ -1 +1 @@ -Subproject commit cca87bd2fb4f83bb3fc45d8b59ed8f1d9429d78b +Subproject commit c53d239144139d7d273f025182853dde46b9d4ea From 18ab2dd0d16c657445fd773a9389240c059c8a5c Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 20 Feb 2026 11:43:48 +0400 Subject: [PATCH 078/179] [ai] Remove mention of ultrathink for Claude. --- .claude/commands/planner.md | 12 ++---------- .claude/commands/task.md | 6 +++--- .claude/commands/withtest.md | 10 +++++----- .claude/iterate.ps1 | 2 +- 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/.claude/commands/planner.md b/.claude/commands/planner.md index ec10090dc4..0a0e2e7c3b 100644 --- a/.claude/commands/planner.md +++ b/.claude/commands/planner.md @@ -37,16 +37,8 @@ ls .ai/ Suggest a name to the user or let them specify one directly via $ARGUMENTS. -### 3. Use /ultrathink for Planning -Before writing the prompt, use `/ultrathink` to carefully plan: -- The structure of the prompt -- What context the autonomous agent needs -- How tasks should be broken down -- What patterns/examples to include -- Edge cases and error handling - -### 4. Create the Folder and Files +### 3. Create the Folder and Files Create `.ai//`: @@ -91,7 +83,7 @@ IMPORTANT: Never try to commit files in .ai/ } ``` -### 5. Iterate with the User +### 4. Iterate with the User After creating initial files, the user may want to: - Add more tasks to tasks.json diff --git a/.claude/commands/task.md b/.claude/commands/task.md index 30149f1bf0..31988405bb 100644 --- a/.claude/commands/task.md +++ b/.claude/commands/task.md @@ -203,7 +203,7 @@ Read these files: - .ai///context.md - Contains all gathered context for this task - Then read the specific source files referenced in context.md to understand the code deeply. -Use /ultrathink to reason carefully about the implementation approach. +Think carefully about the implementation approach. Create a detailed plan in: .ai///plan.md @@ -264,7 +264,7 @@ Read these files: - .ai///plan.md - Then read the actual source files referenced to verify the plan makes sense. -Use /ultrathink to assess the plan: +Carefully assess the plan: 1. **Correctness**: Are the file paths and line references accurate? Does the plan reference real functions and types? 2. **Completeness**: Are there missing steps? Edge cases not handled? @@ -393,7 +393,7 @@ Then run `git diff` to see all uncommitted changes made by the implementation. I Then read the modified source files in full to understand changes in context. -Use /ultrathink to perform a thorough code review. +Perform a thorough code review. REVIEW CRITERIA (in order of importance): diff --git a/.claude/commands/withtest.md b/.claude/commands/withtest.md index 739c583be8..d6f3c72f30 100644 --- a/.claude/commands/withtest.md +++ b/.claude/commands/withtest.md @@ -100,7 +100,7 @@ YOUR JOB: - [ ] Testing Assessed: yes -Use /ultrathink to reason carefully. The follow-up plan should be self-contained enough that an implementation agent can execute it by reading context.md and the updated plan.md. +Reason carefully. The follow-up plan should be self-contained enough that an implementation agent can execute it by reading context.md and the updated plan.md. ``` After this agent completes, read `plan.md` to verify the follow-up plan was written. Then proceed to Phase 4 (Implementation), using the follow-up phases (F1, F2, etc.) instead of the original phases. After implementation and build verification, proceed to Stage 2 (Testing Loop) as normal. @@ -163,7 +163,7 @@ Read these files: - .ai//context.md - Contains all gathered context - Then read the specific source files referenced in context.md to understand the code deeply. -Use /ultrathink to reason carefully about the implementation approach. +Think carefully about the implementation approach. Create a detailed plan in: .ai//plan.md @@ -224,7 +224,7 @@ Read these files: - .ai//plan.md - Then read the actual source files referenced to verify the plan makes sense. -Use /ultrathink to assess the plan: +Carefully assess the plan: 1. **Correctness**: Are the file paths and line references accurate? Does the plan reference real functions and types? 2. **Completeness**: Are there missing steps? Edge cases not handled? @@ -367,7 +367,7 @@ YOUR TASKS: - Decide: reuse/modify existing test code or start fresh. 3. **Plan the test code.** - Use /ultrathink to design test code that will verify the implementation works correctly. + Carefully design test code that will verify the implementation works correctly. The test code must: - Be wrapped in `#ifdef _DEBUG` blocks so it only runs in Debug builds @@ -530,7 +530,7 @@ Read these files: - .ai//result.md 1, also read previous test/result pairs for history> -Use /ultrathink to carefully analyze the test results. +Carefully analyze the test results. DECIDE one of three outcomes: diff --git a/.claude/iterate.ps1 b/.claude/iterate.ps1 index be038920b7..195c1be6ac 100644 --- a/.claude/iterate.ps1 +++ b/.claude/iterate.ps1 @@ -173,7 +173,7 @@ Do exactly ONE task per iteration. ## Steps 1. Read tasks.json and find the most suitable task to implement (it can be first uncompleted task or it can be some task in the middle, if it is better suited to be implemented right now, respecting dependencies) -2. Use /ultrathink to plan the implementation carefully +2. Plan the implementation carefully 3. Implement that ONE task only 4. After successful implementation: $AfterImplementation From 17e8fd56366d07352d84b8e8ef8fe81166a57fa3 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 20 Feb 2026 12:00:16 +0400 Subject: [PATCH 079/179] Update changelog link to generated page. --- Telegram/SourceFiles/core/application.cpp | 31 +---------------------- 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 1e9ecf178b..4a0d319454 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -1131,36 +1131,7 @@ bool Application::openInternalUrl(const QString &url, QVariant context) { } QString Application::changelogLink() const { - const auto base = u"https://desktop.telegram.org/changelog"_q; - const auto languages = { - "id", - "de", - "fr", - "nl", - "pl", - "tr", - "uk", - "fa", - "ru", - "ms", - "es", - "it", - "uz", - "pt-br", - "be", - "ar", - "ko", - }; - const auto current = _langpack->id().replace("-raw", ""); - if (current.isEmpty()) { - return base; - } - for (const auto language : languages) { - if (current == language || current.split(u'-')[0] == language) { - return base + "?setln=" + language; - } - } - return base; + return u"https://telegramdesktop.github.io/tdesktop/changelog/"_q; } bool Application::openCustomUrl( From 5c7d2723edd6f41e489d7448872c6bbb956534ff Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 6 Feb 2026 13:24:04 +0400 Subject: [PATCH 080/179] Update API scheme to layer 223. Owners. --- Telegram/Resources/langs/lang.strings | 2 + .../SourceFiles/api/api_text_entities.cpp | 1 + .../peers/channel_ownership_transfer.cpp | 40 ++-- .../boxes/peers/channel_ownership_transfer.h | 2 - .../boxes/select_future_owner_box.cpp | 216 +++++++++++++----- .../boxes/select_future_owner_box.h | 3 +- .../export/data/export_data_types.cpp | 3 +- Telegram/SourceFiles/mtproto/scheme/api.tl | 14 +- .../SourceFiles/window/window_peer_menu.cpp | 17 +- 9 files changed, 192 insertions(+), 106 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index eae8c30d18..a14ec5350e 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1655,6 +1655,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_leave_next_owner_box_title_group" = "Leave Group?"; "lng_leave_next_owner_box_about" = "If you leave, **{user}** will become the new owner of **{chat}** in **1 week**."; "lng_leave_next_owner_box_about_admin" = "If you leave, **{user}** will become the new admin of **{chat}** in **1 week**."; +"lng_leave_next_owner_box_about_legacy" = "If you leave, **{user}** will immediately become the new owner of **{chat}**."; +"lng_leave_next_owner_box_about_admin_legacy" = "If you leave, **{user}** will immediately become the new admin of **{chat}**."; "lng_select_next_owner_box" = "Appoint Another Owner"; "lng_select_next_owner_box_admin" = "Appoint Another Admin"; "lng_select_next_owner_box_title" = "Appoint New Owner"; diff --git a/Telegram/SourceFiles/api/api_text_entities.cpp b/Telegram/SourceFiles/api/api_text_entities.cpp index f3d6db8b40..ae9e30c22e 100644 --- a/Telegram/SourceFiles/api/api_text_entities.cpp +++ b/Telegram/SourceFiles/api/api_text_entities.cpp @@ -238,6 +238,7 @@ EntitiesInText EntitiesFromMTP( d.vlength().v, d.is_collapsed() ? u"1"_q : QString(), }); + }, [&](const MTPDmessageEntityFormattedDate &d) { }); } return result; diff --git a/Telegram/SourceFiles/boxes/peers/channel_ownership_transfer.cpp b/Telegram/SourceFiles/boxes/peers/channel_ownership_transfer.cpp index 990636ed77..2153db1df7 100644 --- a/Telegram/SourceFiles/boxes/peers/channel_ownership_transfer.cpp +++ b/Telegram/SourceFiles/boxes/peers/channel_ownership_transfer.cpp @@ -11,7 +11,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "boxes/passcode_box.h" #include "core/core_cloud_password.h" -#include "data/data_channel.h" #include "data/data_user.h" #include "lang/lang_keys.h" #include "main/main_session.h" @@ -47,21 +46,10 @@ bool ChannelOwnershipTransfer::handleTransferPasswordError( } void ChannelOwnershipTransfer::start() { - if (const auto chat = _peer->asChatNotMigrated()) { - _peer->session().api().migrateChat(chat, [=]( - not_null channel) { - startTransfer(channel); - }); - } else if (const auto channel = _peer->asChannelOrMigrated()) { - startTransfer(channel); - } -} - -void ChannelOwnershipTransfer::startTransfer(not_null channel) { - const auto api = &channel->session().api(); + const auto api = &_peer->session().api(); api->cloudPassword().reload(); - api->request(MTPchannels_EditCreator( - channel->inputChannel(), + api->request(MTPmessages_EditChatCreator( + _peer->input(), MTP_inputUserEmpty(), MTP_inputCheckPasswordEmpty() )).fail([=](const MTP::Error &error) { @@ -78,7 +66,7 @@ void ChannelOwnershipTransfer::startTransfer(not_null channel) { .text = tr::lng_rights_transfer_about( tr::now, lt_group, - tr::bold(channel->name()), + tr::bold(_peer->name()), lt_user, tr::bold(_selectedUser->shortName()), tr::rich), @@ -110,20 +98,16 @@ void ChannelOwnershipTransfer::requestPassword() { void ChannelOwnershipTransfer::sendRequest( base::weak_qptr box, const Core::CloudPasswordResult &result) { - const auto channel = _peer->asChannelOrMigrated(); - if (!channel) { - return; - } - const auto api = &channel->session().api(); - api->request(MTPchannels_EditCreator( - channel->inputChannel(), + const auto api = &_peer->session().api(); + api->request(MTPmessages_EditChatCreator( + _peer->input(), _selectedUser->inputUser(), result.result )).done([=](const MTPUpdates &result) { api->applyUpdates(result); const auto currentShow = box ? box->uiShow() : _show; currentShow->showToast( - (channel->isBroadcast() + (_peer->isBroadcast() ? tr::lng_rights_transfer_done_channel : tr::lng_rights_transfer_done_group)( tr::now, @@ -143,11 +127,13 @@ void ChannelOwnershipTransfer::sendRequest( } else if (type == u"CHANNELS_ADMIN_LOCATED_TOO_MUCH"_q) { return tr::lng_channels_too_much_located_other(tr::now); } else if (type == u"ADMINS_TOO_MUCH"_q) { - return (channel->isBroadcast() + return (_peer->isBroadcast() ? tr::lng_error_admin_limit_channel : tr::lng_error_admin_limit)(tr::now); - } else if (type == u"CHANNEL_INVALID"_q) { - return (channel->isBroadcast() + } else if (type == u"CHANNEL_INVALID"_q + || type == u"CHAT_CREATOR_REQUIRED"_q + || type == u"PARTICIPANT_MISSING"_q) { + return (_peer->isBroadcast() ? tr::lng_channel_not_accessible : tr::lng_group_not_accessible)(tr::now); } diff --git a/Telegram/SourceFiles/boxes/peers/channel_ownership_transfer.h b/Telegram/SourceFiles/boxes/peers/channel_ownership_transfer.h index 73c7709cc9..2fca0750fd 100644 --- a/Telegram/SourceFiles/boxes/peers/channel_ownership_transfer.h +++ b/Telegram/SourceFiles/boxes/peers/channel_ownership_transfer.h @@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -class ChannelData; class UserData; namespace Ui { @@ -36,7 +35,6 @@ private: void sendRequest( base::weak_qptr box, const Core::CloudPasswordResult &result); - void startTransfer(not_null channel); const not_null _peer; const not_null _selectedUser; diff --git a/Telegram/SourceFiles/boxes/select_future_owner_box.cpp b/Telegram/SourceFiles/boxes/select_future_owner_box.cpp index e040b20cf1..dcefc7d68c 100644 --- a/Telegram/SourceFiles/boxes/select_future_owner_box.cpp +++ b/Telegram/SourceFiles/boxes/select_future_owner_box.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "core/core_cloud_password.h" #include "data/data_channel.h" +#include "data/data_chat.h" #include "dialogs/ui/chat_search_empty.h" #include "boxes/peers/channel_ownership_transfer.h" #include "data/data_session.h" @@ -44,58 +45,25 @@ enum class ParticipantType { Members }; -class ParticipantsController : public PeerListController { +class FutureOwnerController : public PeerListController { public: - ParticipantsController( - not_null window, - not_null channel, - ParticipantType type); - - Main::Session &session() const override; - void prepare() override; void rowClicked(not_null row) override; - void loadMoreRows() override; void itemDeselectedHook(not_null peer) override; void setOnRowClicked(Fn callback); rpl::producer<> itemDeselected() const; private: - const not_null _window; - const not_null _channel; - const ParticipantType _type; - MTP::Sender _api; Fn _onRowClicked; rpl::event_stream<> _itemDeselected; - mtpRequestId _loadRequestId = 0; - int _offset = 0; - bool _allLoaded = false; }; -ParticipantsController::ParticipantsController( - not_null window, - not_null channel, - ParticipantType type) -: _window(window) -, _channel(channel) -, _type(type) -, _api(&channel->session().mtp()) { -} - -Main::Session &ParticipantsController::session() const { - return _channel->session(); -} - -void ParticipantsController::setOnRowClicked(Fn callback) { +void FutureOwnerController::setOnRowClicked(Fn callback) { _onRowClicked = callback; } -void ParticipantsController::prepare() { - loadMoreRows(); -} - -void ParticipantsController::rowClicked(not_null row) { +void FutureOwnerController::rowClicked(not_null row) { delegate()->peerListSetRowChecked( row, !delegate()->peerListIsRowChecked(row)); @@ -110,14 +78,51 @@ void ParticipantsController::rowClicked(not_null row) { } } -void ParticipantsController::itemDeselectedHook(not_null peer) { +void FutureOwnerController::itemDeselectedHook(not_null peer) { _itemDeselected.fire({}); } -rpl::producer<> ParticipantsController::itemDeselected() const { +rpl::producer<> FutureOwnerController::itemDeselected() const { return _itemDeselected.events(); } +class ParticipantsController : public FutureOwnerController { +public: + ParticipantsController( + not_null channel, + ParticipantType type); + + Main::Session &session() const override; + void prepare() override; + void loadMoreRows() override; + +private: + const not_null _channel; + const ParticipantType _type; + MTP::Sender _api; + + mtpRequestId _loadRequestId = 0; + int _offset = 0; + bool _allLoaded = false; + +}; + +ParticipantsController::ParticipantsController( + not_null channel, + ParticipantType type) +: _channel(channel) +, _type(type) +, _api(&channel->session().mtp()) { +} + +Main::Session &ParticipantsController::session() const { + return _channel->session(); +} + +void ParticipantsController::prepare() { + loadMoreRows(); +} + void ParticipantsController::loadMoreRows() { if (_loadRequestId || _allLoaded) { return; @@ -191,13 +196,71 @@ void ParticipantsController::loadMoreRows() { }).send(); } +class LegacyParticipantsController : public FutureOwnerController { +public: + LegacyParticipantsController( + not_null chat, + ParticipantType type); + + Main::Session &session() const override; + void prepare() override; + void loadMoreRows() override; + +private: + const not_null _chat; + const ParticipantType _type; + +}; + +LegacyParticipantsController::LegacyParticipantsController( + not_null chat, + ParticipantType type) +: _chat(chat) +, _type(type) { +} + +Main::Session &LegacyParticipantsController::session() const { + return _chat->session(); +} + +void LegacyParticipantsController::prepare() { + if (_chat->noParticipantInfo()) { + _chat->updateFullForced(); + } + const auto &source = (_type == ParticipantType::Admins) + ? _chat->admins + : _chat->participants; + for (const auto &user : source) { + if (user->isBot()) { + continue; + } + if (user->id == peerFromUser(_chat->creator)) { + continue; + } + if (_type == ParticipantType::Members + && _chat->admins.contains(user)) { + continue; + } + delegate()->peerListAppendRow( + std::make_unique(user)); + } + delegate()->peerListRefreshRows(); +} + +void LegacyParticipantsController::loadMoreRows() { +} + } // namespace void SelectFutureOwnerbox( not_null box, - not_null channel, + not_null peer, not_null user) { const auto content = box->verticalLayout(); + const auto channel = peer->asChannel(); + const auto chat = peer->asChat(); + const auto isGroup = peer->isMegagroup() || peer->isChat(); + const auto isLegacy = (chat != nullptr); Ui::AddSkip(content); Ui::AddSkip(content); content->add( @@ -205,7 +268,7 @@ void SelectFutureOwnerbox( content, rpl::single(std::vector>{ user->session().user(), - channel, + peer, }), user, UserpicsTransferType::ChannelFutureOwner), @@ -215,24 +278,36 @@ void SelectFutureOwnerbox( content->add( object_ptr( content, - channel->isMegagroup() + isGroup ? tr::lng_leave_next_owner_box_title_group() : tr::lng_leave_next_owner_box_title(), box->getDelegate()->style().title), st::boxRowPadding); Ui::AddSkip(content); Ui::AddSkip(content); - const auto adminsAreEqual = (channel->adminsCount() <= 1); + const auto adminsCount = [&] { + if (channel) { + return channel->adminsCount(); + } else if (chat) { + return int(chat->admins.size()) + 1; + } + return 0; + }(); + const auto adminsAreEqual = (adminsCount <= 1); content->add( object_ptr( content, - (adminsAreEqual - ? tr::lng_leave_next_owner_box_about - : tr::lng_leave_next_owner_box_about_admin)( + (isLegacy + ? (adminsAreEqual + ? tr::lng_leave_next_owner_box_about_legacy + : tr::lng_leave_next_owner_box_about_admin_legacy) + : (adminsAreEqual + ? tr::lng_leave_next_owner_box_about + : tr::lng_leave_next_owner_box_about_admin))( lt_user, Info::Profile::NameValue(user) | rpl::map(tr::marked), lt_chat, - Info::Profile::NameValue(channel) | rpl::map(tr::marked), + Info::Profile::NameValue(peer) | rpl::map(tr::marked), tr::rich), st::boxLabel), st::boxRowPadding); @@ -264,14 +339,14 @@ void SelectFutureOwnerbox( const auto leave = content->add( object_ptr( content, - channel->isMegagroup() + isGroup ? tr::lng_profile_leave_group() : tr::lng_profile_leave_channel(), st::attentionBoxButton), st::boxRowPadding, style::al_justify); leave->setClickedCallback([=, revoke = false] { - channel->session().api().deleteConversation(channel, revoke); + peer->session().api().deleteConversation(peer, revoke); box->closeBox(); }); select->setClickedCallback([=] { @@ -283,16 +358,31 @@ void SelectFutureOwnerbox( return; } - auto adminsOwned = std::make_unique( - sessionController, - channel, - ParticipantType::Admins); - - auto membersOwned = std::make_unique( - sessionController, - channel, - ParticipantType::Members); - + using Pair = std::pair< + std::unique_ptr, + std::unique_ptr>; + auto makeControllers = [&]() -> Pair { + if (channel) { + return { + std::make_unique( + channel, + ParticipantType::Admins), + std::make_unique( + channel, + ParticipantType::Members), + }; + } else { + return { + std::make_unique( + chat, + ParticipantType::Admins), + std::make_unique( + chat, + ParticipantType::Members), + }; + } + }; + auto [adminsOwned, membersOwned] = makeControllers(); const auto admins = adminsOwned.get(); const auto members = membersOwned.get(); @@ -331,14 +421,14 @@ void SelectFutureOwnerbox( 0, CreatePeerListSectionSubtitle( selectBox, - !channel->isMegagroup() + !isGroup ? tr::lng_select_next_owner_box_sub_admins() : tr::lng_select_next_owner_box_sub_admins_group())); const auto separatorMembers = selectBox->addSeparatorBefore( 1, CreatePeerListSectionSubtitle( selectBox, - !channel->isMegagroup() + !isGroup ? tr::lng_select_next_owner_box_sub_members() : tr::lng_select_next_owner_box_sub_members_group())); rpl::combine( @@ -407,13 +497,13 @@ void SelectFutureOwnerbox( if (const auto user = selected.front()->asUser()) { auto &lifetime = selectBox->lifetime(); lifetime.make_state( - channel, + peer, user, selectBox->uiShow(), [=](std::shared_ptr show) { const auto revoke = false; - channel->session().api().deleteConversation( - channel, + peer->session().api().deleteConversation( + peer, revoke); show->hideLayer(); })->start(); diff --git a/Telegram/SourceFiles/boxes/select_future_owner_box.h b/Telegram/SourceFiles/boxes/select_future_owner_box.h index 7f48bf64c1..a0e1aa716b 100644 --- a/Telegram/SourceFiles/boxes/select_future_owner_box.h +++ b/Telegram/SourceFiles/boxes/select_future_owner_box.h @@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -class ChannelData; class PeerData; class UserData; @@ -18,5 +17,5 @@ class Show; void SelectFutureOwnerbox( not_null box, - not_null channel, + not_null peer, not_null user); diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 7af24dd128..66fdf9261a 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -303,7 +303,8 @@ std::vector ParseText( return Type::Blockquote; }, [](const MTPDmessageEntityBankCard&) { return Type::BankCard; }, [](const MTPDmessageEntitySpoiler&) { return Type::Spoiler; }, - [](const MTPDmessageEntityCustomEmoji&) { return Type::CustomEmoji; }); + [](const MTPDmessageEntityCustomEmoji&) { return Type::CustomEmoji; }, + [](const MTPDmessageEntityFormattedDate&) { return Type::Unknown; }); part.text = mid(start, length); part.additional = entity.match( [](const MTPDmessageEntityPre &data) { diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 77968bd224..54b9b683b9 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -33,7 +33,7 @@ inputMediaUploadedPhoto#1e287d04 flags:# spoiler:flags.2?true file:InputFile sti inputMediaPhoto#b3ba0635 flags:# spoiler:flags.1?true id:InputPhoto ttl_seconds:flags.0?int = InputMedia; inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia; inputMediaContact#f8ab7dfb phone_number:string first_name:string last_name:string vcard:string = InputMedia; -inputMediaUploadedDocument#37c9330 flags:# nosound_video:flags.3?true force_file:flags.4?true spoiler:flags.5?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector stickers:flags.0?Vector video_cover:flags.6?InputPhoto video_timestamp:flags.7?int ttl_seconds:flags.1?int = InputMedia; +inputMediaUploadedDocument#37c9330 flags:# nosound_video:flags.3?true force_file:flags.4?true spoiler:flags.5?true live_photo:flags.8?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector stickers:flags.0?Vector video_cover:flags.6?InputPhoto video_timestamp:flags.7?int ttl_seconds:flags.1?int = InputMedia; inputMediaDocument#a8763ab5 flags:# spoiler:flags.2?true id:InputDocument video_cover:flags.3?InputPhoto video_timestamp:flags.4?int ttl_seconds:flags.0?int query:flags.1?string = InputMedia; inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string = InputMedia; inputMediaPhotoExternal#e5bbfe1a flags:# spoiler:flags.1?true url:string ttl_seconds:flags.0?int = InputMedia; @@ -126,7 +126,7 @@ messageMediaPhoto#695150d7 flags:# spoiler:flags.3?true photo:flags.0?Photo ttl_ messageMediaGeo#56e0d474 geo:GeoPoint = MessageMedia; messageMediaContact#70322949 phone_number:string first_name:string last_name:string vcard:string user_id:long = MessageMedia; messageMediaUnsupported#9f84f49e = MessageMedia; -messageMediaDocument#52d8ccd9 flags:# nopremium:flags.3?true spoiler:flags.4?true video:flags.6?true round:flags.7?true voice:flags.8?true document:flags.0?Document alt_documents:flags.5?Vector video_cover:flags.9?Photo video_timestamp:flags.10?int ttl_seconds:flags.2?int = MessageMedia; +messageMediaDocument#52d8ccd9 flags:# nopremium:flags.3?true spoiler:flags.4?true video:flags.6?true round:flags.7?true voice:flags.8?true live_photo:flags.11?true document:flags.0?Document alt_documents:flags.5?Vector video_cover:flags.9?Photo video_timestamp:flags.10?int ttl_seconds:flags.2?int = MessageMedia; messageMediaWebPage#ddf10c3b flags:# force_large_media:flags.0?true force_small_media:flags.1?true manual:flags.3?true safe:flags.4?true webpage:WebPage = MessageMedia; messageMediaVenue#2ec0533f geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MessageMedia; messageMediaGame#fdb19008 game:Game = MessageMedia; @@ -255,7 +255,7 @@ inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; -userFull#a02bc13e flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# sponsored_enabled:flags2.7?true can_view_revenue:flags2.9?true bot_can_manage_emoji_status:flags2.10?true display_gifts_button:flags2.16?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme:flags.15?ChatTheme private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro birthday:flags2.5?Birthday personal_channel_id:flags2.6?long personal_channel_message:flags2.6?int stargifts_count:flags2.8?int starref_program:flags2.11?StarRefProgram bot_verification:flags2.12?BotVerification send_paid_messages_stars:flags2.14?long disallowed_gifts:flags2.15?DisallowedGiftsSettings stars_rating:flags2.17?StarsRating stars_my_pending_rating:flags2.18?StarsRating stars_my_pending_rating_date:flags2.18?int main_tab:flags2.20?ProfileTab saved_music:flags2.21?Document note:flags2.22?TextWithEntities = UserFull; +userFull#a02bc13e flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# sponsored_enabled:flags2.7?true can_view_revenue:flags2.9?true bot_can_manage_emoji_status:flags2.10?true display_gifts_button:flags2.16?true noforwards_my_enabled:flags2.23?true noforwards_peer_enabled:flags2.24?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme:flags.15?ChatTheme private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro birthday:flags2.5?Birthday personal_channel_id:flags2.6?long personal_channel_message:flags2.6?int stargifts_count:flags2.8?int starref_program:flags2.11?StarRefProgram bot_verification:flags2.12?BotVerification send_paid_messages_stars:flags2.14?long disallowed_gifts:flags2.15?DisallowedGiftsSettings stars_rating:flags2.17?StarsRating stars_my_pending_rating:flags2.18?StarsRating stars_my_pending_rating_date:flags2.18?int main_tab:flags2.20?ProfileTab saved_music:flags2.21?Document note:flags2.22?TextWithEntities = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; @@ -457,6 +457,7 @@ updateStarGiftAuctionState#48e246c2 gift_id:long state:StarGiftAuctionState = Up updateStarGiftAuctionUserState#dc58f31e gift_id:long user_state:StarGiftAuctionUserState = Update; updateEmojiGameInfo#fb9c547a info:messages.EmojiGameInfo = Update; updateStarGiftCraftFail#ac072444 = Update; +updatePeerHistoryNoForwards#5736b39a flags:# my_enabled:flags.0?true peer_enabled:flags.1?true peer:Peer = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -727,6 +728,7 @@ messageEntityBankCard#761e6af4 offset:int length:int = MessageEntity; messageEntitySpoiler#32ca960f offset:int length:int = MessageEntity; messageEntityCustomEmoji#c8cf05f8 offset:int length:int document_id:long = MessageEntity; messageEntityBlockquote#f1ccaaac flags:# collapsed:flags.0?true offset:int length:int = MessageEntity; +messageEntityFormattedDate#904ac7c7 flags:# relative:flags.0?true short_time:flags.1?true long_time:flags.2?true short_date:flags.3?true long_date:flags.4?true offset:int length:int date:int = MessageEntity; inputChannelEmpty#ee8c1e86 = InputChannel; inputChannel#f35aec28 channel_id:long access_hash:long = InputChannel; @@ -2565,6 +2567,8 @@ messages.createForumTopic#2f98c3d5 flags:# title_missing:flags.4?true peer:Input messages.deleteTopicHistory#d2816f10 peer:InputPeer top_msg_id:int = messages.AffectedHistory; messages.getEmojiGameInfo#fb7e8ca7 = messages.EmojiGameInfo; messages.summarizeText#9d4104e2 flags:# peer:InputPeer id:int to_lang:flags.0?string = TextWithEntities; +messages.editChatCreator#f743b857 peer:InputPeer user_id:InputUser password:InputCheckPasswordSRP = Updates; +messages.getFutureChatCreatorAfterLeave#3b7d0ea6 peer:InputPeer = User; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; @@ -2641,7 +2645,6 @@ channels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Upd channels.getLeftChannels#8341ecc0 offset:int = messages.Chats; channels.getGroupsForDiscussion#f5dad378 = messages.Chats; channels.setDiscussionGroup#40582bb2 broadcast:InputChannel group:InputChannel = Bool; -channels.editCreator#8f38cd1f channel:InputChannel user_id:InputUser password:InputCheckPasswordSRP = Updates; channels.editLocation#58e63f6d channel:InputChannel geo_point:InputGeoPoint address:string = Bool; channels.toggleSlowMode#edd49ef0 channel:InputChannel seconds:int = Updates; channels.getInactiveChannels#11e831ee = messages.InactiveChats; @@ -2670,7 +2673,6 @@ channels.toggleAutotranslation#167fc0a1 channel:InputChannel enabled:Bool = Upda channels.getMessageAuthor#ece2a0e6 channel:InputChannel id:int = User; channels.checkSearchPostsFlood#22567115 flags:# query:flags.0?string = SearchPostsFlood; channels.setMainProfileTab#3583fcb1 channel:InputChannel tab:ProfileTab = Bool; -channels.getFutureCreatorAfterLeave#a00918af channel:InputChannel = User; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; @@ -2903,4 +2905,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -// LAYER 222 +// LAYER 223 diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index a6ad45c90b..83a197b615 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -3656,20 +3656,27 @@ Fn ClearHistoryHandler( Fn DeleteAndLeaveHandler( not_null controller, not_null peer) { - if (const auto channel = peer->asChannel(); - channel && channel->amCreator()) { + const auto isCreator = [&] { + if (const auto channel = peer->asChannel()) { + return channel->amCreator(); + } else if (const auto chat = peer->asChat()) { + return chat->amCreator(); + } + return false; + }(); + if (isCreator) { const auto requestId = std::make_shared(0); return [=] { if (controller->showFrozenError() || (*requestId > 0)) { return; } *requestId = peer->session().api().request( - MTPchannels_GetFutureCreatorAfterLeave( - channel->inputChannel() + MTPmessages_GetFutureChatCreatorAfterLeave( + peer->input() )).done([=](const MTPUser &result) { *requestId = 0; const auto user = peer->owner().processUser(result); - controller->show(Box(SelectFutureOwnerbox, channel, user)); + controller->show(Box(SelectFutureOwnerbox, peer, user)); }).fail([=](const MTP::Error &error) { *requestId = 0; controller->show(Box(DeleteChatBox, peer)); From 4e9c0ea3a5ec13fc34a8c72ba269040af63c7795 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 6 Feb 2026 14:18:30 +0400 Subject: [PATCH 081/179] Implement private chat noforward following. --- Telegram/Resources/langs/lang.strings | 3 + Telegram/SourceFiles/api/api_updates.cpp | 12 +++ Telegram/SourceFiles/apiwrap.cpp | 2 + Telegram/SourceFiles/data/data_peer.cpp | 4 +- Telegram/SourceFiles/data/data_user.cpp | 16 ++++ Telegram/SourceFiles/data/data_user.h | 4 + .../history/history_inner_widget.cpp | 91 ++++++++++++------- .../view/history_view_context_menu.cpp | 2 + .../history/view/history_view_list_widget.cpp | 10 +- .../history/view/history_view_list_widget.h | 1 + .../info/media/info_media_provider.cpp | 9 +- .../media/view/media_view_overlay_widget.cpp | 2 + 12 files changed, 120 insertions(+), 36 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index a14ec5350e..08819cd5b7 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -313,6 +313,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_error_noforwards_channel" = "Sorry, forwarding from this channel is disabled by admins."; "lng_error_nocopy_group" = "Sorry, copying from this group is disabled by admins."; "lng_error_nocopy_channel" = "Sorry, copying from this channel is disabled by admins."; +"lng_error_noforwards_user" = "Sorry, forwarding from this chat is restricted."; +"lng_error_nocopy_user" = "Sorry, copying from this chat is restricted."; "lng_error_nocopy_story" = "Sorry, the creator of this story disabled copying."; "lng_error_schedule_limit" = "Sorry, you can't schedule more than 100 messages."; "lng_sure_add_admin_invite" = "This user is not a member of this group. Add them to the group and promote them to admin?"; @@ -5001,6 +5003,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_noforwards_info_channel" = "Copying and forwarding is not allowed in this channel."; "lng_context_noforwards_info_group" = "Copying and forwarding is not allowed in this group."; "lng_context_noforwards_info_bot" = "Copying and forwarding is not allowed from this bot."; +"lng_context_noforwards_info_user" = "Copying and forwarding is not allowed in this chat."; "lng_context_spoiler_effect" = "Hide with Spoiler"; "lng_context_disable_spoiler" = "Remove Spoiler"; diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 2454724110..636b81b161 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -2685,6 +2685,18 @@ void Updates::feedUpdate(const MTPUpdate &update) { const auto &data = update.c_updateEmojiGameInfo(); _session->diceStickersPacks().apply(data); } break; + + case mtpc_updatePeerHistoryNoForwards: { + const auto &d = update.c_updatePeerHistoryNoForwards(); + const auto peerId = peerFromMTP(d.vpeer()); + if (const auto peer = session().data().peerLoaded(peerId)) { + if (const auto user = peer->asUser()) { + user->setNoForwardsFlags( + d.is_my_enabled(), + d.is_peer_enabled()); + } + } + } break; } } diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index c4c97630e6..228e726e25 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -524,6 +524,8 @@ void ApiWrap::sendMessageFail( } else if (show && error == u"CHAT_FORWARDS_RESTRICTED"_q) { show->showToast(peer->isBroadcast() ? tr::lng_error_noforwards_channel(tr::now) + : peer->isUser() + ? tr::lng_error_noforwards_user(tr::now) : tr::lng_error_noforwards_group(tr::now), kJoinErrorDuration); } else if (error == u"PREMIUM_ACCOUNT_REQUIRED"_q) { Settings::ShowPremium(&session(), "premium_stickers"); diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 2228505b24..8243d72cc0 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -1713,8 +1713,8 @@ void PeerData::processTopics(const MTPVector &topics) { } bool PeerData::allowsForwarding() const { - if (isUser()) { - return true; + if (const auto user = asUser()) { + return user->allowsForwarding(); } else if (const auto channel = asChannel()) { return channel->allowsForwarding(); } else if (const auto chat = asChat()) { diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 700c14d416..d415246ead 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -646,6 +646,18 @@ bool UserData::readDatesPrivate() const { return (flags() & UserDataFlag::ReadDatesPrivate); } +bool UserData::allowsForwarding() const { + return !(flags() & Flag::NoForwardsMyEnabled) + && !(flags() & Flag::NoForwardsPeerEnabled); +} + +void UserData::setNoForwardsFlags(bool myEnabled, bool peerEnabled) { + const auto mask = Flag::NoForwardsMyEnabled | Flag::NoForwardsPeerEnabled; + setFlags((flags() & ~mask) + | (myEnabled ? Flag::NoForwardsMyEnabled : Flag()) + | (peerEnabled ? Flag::NoForwardsPeerEnabled : Flag())); +} + int UserData::starsPerMessage() const { return _starsPerMessage; } @@ -1036,6 +1048,10 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { user->setNote(TextWithEntities()); } + user->setNoForwardsFlags( + update.is_noforwards_my_enabled(), + update.is_noforwards_peer_enabled()); + user->fullUpdated(); } diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index 4243548d93..4a8d21bcfd 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -135,6 +135,8 @@ enum class UserDataFlag : uint32 { StoriesCorrespondent = (1 << 26), Forum = (1 << 27), HasActiveVideoStream = (1 << 28), + NoForwardsMyEnabled = (1 << 29), + NoForwardsPeerEnabled = (1 << 30), }; inline constexpr bool is_flag_type(UserDataFlag) { return true; }; using UserDataFlags = base::flags; @@ -201,6 +203,8 @@ public: [[nodiscard]] bool messageMoneyRestrictionsKnown() const; [[nodiscard]] bool canSendIgnoreMoneyRestrictions() const; [[nodiscard]] bool readDatesPrivate() const; + [[nodiscard]] bool allowsForwarding() const; + void setNoForwardsFlags(bool myEnabled, bool peerEnabled); [[nodiscard]] bool isForum() const { return flags() & Flag::Forum; } diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 6a6c584ba4..5169bb7bce 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -504,40 +504,65 @@ Main::Session &HistoryInner::session() const { void HistoryInner::setupSharingDisallowed() { Expects(_peer != nullptr); - if (_peer->isUser()) { - _sharingDisallowed = false; - return; + if (const auto user = _peer->asUser()) { + _sharingDisallowed = rpl::combine( + Data::PeerFlagValue(user, UserDataFlag::NoForwardsMyEnabled), + Data::PeerFlagValue(user, UserDataFlag::NoForwardsPeerEnabled) + ) | rpl::map([](bool my, bool peer) { + return my || peer; + }); + } else { + const auto chat = _peer->asChat(); + const auto channel = _peer->asChannel(); + _sharingDisallowed = chat + ? Data::PeerFlagValue(chat, ChatDataFlag::NoForwards) + : Data::PeerFlagValue( + channel, + ChannelDataFlag::NoForwards + ) | rpl::type_erased; } - const auto chat = _peer->asChat(); - const auto channel = _peer->asChannel(); - _sharingDisallowed = chat - ? Data::PeerFlagValue(chat, ChatDataFlag::NoForwards) - : Data::PeerFlagValue( - channel, - ChannelDataFlag::NoForwards - ) | rpl::type_erased; - auto rights = chat - ? chat->adminRightsValue() - : channel->adminRightsValue(); - auto canDelete = std::move( - rights - ) | rpl::map([=] { - return chat - ? chat->canDeleteMessages() - : channel->canDeleteMessages(); - }); - rpl::combine( - _sharingDisallowed.value(), - std::move(canDelete) - ) | rpl::filter([=](bool disallowed, bool canDelete) { - return hasSelectRestriction() && !getSelectedItems().empty(); - }) | rpl::on_next([=] { - _widget->clearSelected(); - if (_mouseAction == MouseAction::PrepareSelect) { - mouseActionCancel(); + const auto clearIfRestricted = [=] { + if (hasSelectRestriction() && !getSelectedItems().empty()) { + _widget->clearSelected(); + if (_mouseAction == MouseAction::PrepareSelect) { + mouseActionCancel(); + } } - }, lifetime()); + }; + + if (const auto chat = _peer->asChat()) { + auto rights = chat->adminRightsValue(); + auto canDelete = std::move( + rights + ) | rpl::map([=] { + return chat->canDeleteMessages(); + }); + rpl::combine( + _sharingDisallowed.value(), + std::move(canDelete) + ) | rpl::on_next([=] { + clearIfRestricted(); + }, lifetime()); + } else if (const auto channel = _peer->asChannel()) { + auto rights = channel->adminRightsValue(); + auto canDelete = std::move( + rights + ) | rpl::map([=] { + return channel->canDeleteMessages(); + }); + rpl::combine( + _sharingDisallowed.value(), + std::move(canDelete) + ) | rpl::on_next([=] { + clearIfRestricted(); + }, lifetime()); + } else { + _sharingDisallowed.value( + ) | rpl::on_next([=] { + clearIfRestricted(); + }, lifetime()); + } } void HistoryInner::setupSwipeReplyAndBack() { @@ -3322,6 +3347,8 @@ bool HistoryInner::showCopyRestriction(HistoryItem *item) { } _controller->showToast(_peer->isBroadcast() ? tr::lng_error_nocopy_channel(tr::now) + : _peer->isUser() + ? tr::lng_error_nocopy_user(tr::now) : tr::lng_error_nocopy_group(tr::now)); return true; } @@ -3332,6 +3359,8 @@ bool HistoryInner::showCopyMediaRestriction(not_null item) { } _controller->showToast(_peer->isBroadcast() ? tr::lng_error_nocopy_channel(tr::now) + : _peer->isUser() + ? tr::lng_error_nocopy_user(tr::now) : tr::lng_error_nocopy_group(tr::now)); return true; } diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 6967d2b66c..4034082710 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -2061,6 +2061,8 @@ void AddSelectRestrictionAction( ? tr::lng_context_noforwards_info_channel : (peer->isUser() && peer->asUser()->isBot()) ? tr::lng_context_noforwards_info_bot + : peer->isUser() + ? tr::lng_context_noforwards_info_user : tr::lng_context_noforwards_info_channel)( tr::now, tr::rich), diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index eeb3667bd4..701a8c32a0 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -1617,6 +1617,8 @@ bool ListWidget::showCopyRestriction(HistoryItem *item) { } _delegate->listUiShow()->showToast((type == CopyRestrictionType::Channel) ? tr::lng_error_nocopy_channel(tr::now) + : (type == CopyRestrictionType::User) + ? tr::lng_error_nocopy_user(tr::now) : tr::lng_error_nocopy_group(tr::now)); return true; } @@ -1628,6 +1630,8 @@ bool ListWidget::showCopyMediaRestriction(not_null item) { } _delegate->listUiShow()->showToast((type == CopyRestrictionType::Channel) ? tr::lng_error_nocopy_channel(tr::now) + : (type == CopyRestrictionType::User) + ? tr::lng_error_nocopy_user(tr::now) : tr::lng_error_nocopy_group(tr::now)); return true; } @@ -4508,6 +4512,8 @@ CopyRestrictionType CopyRestrictionTypeFor( HistoryItem *item) { return (peer->allowsForwarding() && (!item || !item->forbidsForward())) ? CopyRestrictionType::None + : peer->isUser() + ? CopyRestrictionType::User : peer->isBroadcast() ? CopyRestrictionType::Channel : CopyRestrictionType::Group; @@ -4522,6 +4528,8 @@ CopyRestrictionType CopyMediaRestrictionTypeFor( } return !item->forbidsSaving() ? CopyRestrictionType::None + : peer->isUser() + ? CopyRestrictionType::User : peer->isBroadcast() ? CopyRestrictionType::Channel : CopyRestrictionType::Group; @@ -4538,7 +4546,7 @@ CopyRestrictionType SelectRestrictionTypeFor( ? CopyRestrictionType::None : CopyRestrictionTypeFor(peer); } - return CopyRestrictionType::None; + return CopyRestrictionTypeFor(peer); } } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index a0d0bec38f..cbb6a260ba 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -71,6 +71,7 @@ enum class CopyRestrictionType : char { None, Group, Channel, + User, }; struct SelectedItem { diff --git a/Telegram/SourceFiles/info/media/info_media_provider.cpp b/Telegram/SourceFiles/info/media/info_media_provider.cpp index 3b4680bad4..5420d9d6c5 100644 --- a/Telegram/SourceFiles/info/media/info_media_provider.cpp +++ b/Telegram/SourceFiles/info/media/info_media_provider.cpp @@ -91,8 +91,13 @@ bool Provider::hasSelectRestriction() { } rpl::producer Provider::hasSelectRestrictionChanges() { - if (_peer->isUser()) { - return rpl::never(); + if (const auto user = _peer->asUser()) { + return rpl::combine( + Data::PeerFlagValue(user, UserDataFlag::NoForwardsMyEnabled), + Data::PeerFlagValue(user, UserDataFlag::NoForwardsPeerEnabled) + ) | rpl::map([=] { + return hasSelectRestriction(); + }) | rpl::distinct_until_changed() | rpl::skip(1); } const auto chat = _peer->asChat(); const auto channel = _peer->asChannel(); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 9b7a202b12..d63161b487 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -1164,6 +1164,8 @@ bool OverlayWidget::showCopyMediaRestriction(bool skipPRemiumCheck) { } else if (_history) { uiShow()->showToast(_history->peer->isBroadcast() ? tr::lng_error_nocopy_channel(tr::now) + : _history->peer->isUser() + ? tr::lng_error_nocopy_user(tr::now) : tr::lng_error_nocopy_group(tr::now)); } return true; From 62d32a1bdf1d4d997c2e1b8f49a4948883e47f1d Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 10 Feb 2026 16:38:39 +0400 Subject: [PATCH 082/179] Update API scheme on layer 223. --- Telegram/SourceFiles/api/api_updates.cpp | 2 ++ .../data/business/data_shortcut_messages.cpp | 1 + .../data/components/scheduled_messages.cpp | 2 ++ Telegram/SourceFiles/data/data_session.cpp | 1 + .../admin_log/history_admin_log_item.cpp | 1 + Telegram/SourceFiles/mtproto/scheme/api.tl | 19 +++++++++++-------- .../settings/settings_privacy_controllers.cpp | 1 + 7 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 636b81b161..0f7db43b9b 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -1119,6 +1119,7 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { ? peerToMTP(_session->userPeerId()) : MTP_peerUser(d.vuser_id())), MTPint(), // from_boosts_applied + MTPstring(), // from_rank MTP_peerUser(d.vuser_id()), MTPPeer(), // saved_peer_id d.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(), @@ -1161,6 +1162,7 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { d.vid(), MTP_peerUser(d.vfrom_id()), MTPint(), // from_boosts_applied + MTPstring(), // from_rank MTP_peerChat(d.vchat_id()), MTPPeer(), // saved_peer_id d.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(), diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp index 259f38cb96..13c74ef7a1 100644 --- a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp @@ -67,6 +67,7 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); data.vid(), data.vfrom_id() ? *data.vfrom_id() : MTPPeer(), MTPint(), // from_boosts_applied + MTPstring(), // from_rank data.vpeer_id(), data.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(), data.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(), diff --git a/Telegram/SourceFiles/data/components/scheduled_messages.cpp b/Telegram/SourceFiles/data/components/scheduled_messages.cpp index ba5059df46..9fc3b15026 100644 --- a/Telegram/SourceFiles/data/components/scheduled_messages.cpp +++ b/Telegram/SourceFiles/data/components/scheduled_messages.cpp @@ -71,6 +71,7 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); data.vid(), data.vfrom_id() ? *data.vfrom_id() : MTPPeer(), MTPint(), // from_boosts_applied + MTPstring(), // from_rank data.vpeer_id(), data.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(), data.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(), @@ -251,6 +252,7 @@ void ScheduledMessages::sendNowSimpleMessage( update.vid(), peerToMTP(local->from()->id), MTPint(), // from_boosts_applied + MTPstring(), // from_rank peerToMTP(history->peer->id), MTPPeer(), // saved_peer_id MTPMessageFwdHeader(), diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 32737ebb07..755d14d1d4 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -5111,6 +5111,7 @@ void Session::insertCheckedServiceNotification( MTP_int(0), // Not used (would've been trimmed to 32 bits). peerToMTP(PeerData::kServiceNotificationsId), MTPint(), // from_boosts_applied + MTPstring(), // from_rank peerToMTP(PeerData::kServiceNotificationsId), MTPPeer(), // saved_peer_id MTPMessageFwdHeader(), diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index e8b30c43ae..25b17ea0d4 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -172,6 +172,7 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { data.vid(), data.vfrom_id() ? *data.vfrom_id() : MTPPeer(), MTPint(), // from_boosts_applied + MTPstring(), // from_rank data.vpeer_id(), MTPPeer(), // saved_peer_id data.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(), diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 54b9b683b9..b2dfeded0a 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -99,17 +99,17 @@ userStatusLastWeek#541a1d1a flags:# by_me:flags.0?true = UserStatus; userStatusLastMonth#65899777 flags:# by_me:flags.0?true = UserStatus; chatEmpty#29562865 id:long = Chat; -chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; +chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true custom_ranks_enabled:flags.19?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; chatForbidden#6592a1a7 id:long title:string = Chat; -channel#1c32b11c flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true signature_profiles:flags2.12?true autotranslation:flags2.15?true broadcast_messages_allowed:flags2.16?true monoforum:flags2.17?true forum_tabs:flags2.19?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector stories_max_id:flags2.4?RecentStory color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int subscription_until_date:flags2.11?int bot_verification_icon:flags2.13?long send_paid_messages_stars:flags2.14?long linked_monoforum_id:flags2.18?long = Chat; +channel#1c32b11c flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true signature_profiles:flags2.12?true autotranslation:flags2.15?true broadcast_messages_allowed:flags2.16?true monoforum:flags2.17?true forum_tabs:flags2.19?true custom_ranks_enabled:flags2.20?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector stories_max_id:flags2.4?RecentStory color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int subscription_until_date:flags2.11?int bot_verification_icon:flags2.13?long send_paid_messages_stars:flags2.14?long linked_monoforum_id:flags2.18?long = Chat; channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true monoforum:flags.10?true id:long access_hash:long title:string until_date:flags.16?int = Chat; chatFull#2633421b flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull; channelFull#e4e0b29d flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int send_paid_messages_stars:flags2.21?long main_tab:flags2.22?ProfileTab = ChatFull; -chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant; -chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant; -chatParticipantAdmin#a0933f5b user_id:long inviter_id:long date:int = ChatParticipant; +chatParticipant#38e79fde flags:# user_id:long inviter_id:long date:int rank:flags.0?string = ChatParticipant; +chatParticipantCreator#e1f867b8 flags:# user_id:long rank:flags.0?string = ChatParticipant; +chatParticipantAdmin#360d5d2 flags:# user_id:long inviter_id:long date:int rank:flags.0?string = ChatParticipant; chatParticipantsForbidden#8763d3e1 flags:# chat_id:long self_participant:flags.0?ChatParticipant = ChatParticipants; chatParticipants#3cbc93f8 chat_id:long participants:Vector version:int = ChatParticipants; @@ -118,7 +118,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#9cb490e9 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true paid_suggested_post_stars:flags2.8?true paid_suggested_post_ton:flags2.9?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long suggested_post:flags2.7?SuggestedPost schedule_repeat_period:flags2.10?int summary_from_language:flags2.11?string = Message; +message#3ae56482 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true paid_suggested_post_stars:flags2.8?true paid_suggested_post_ton:flags2.9?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int from_rank:flags2.12?string peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long suggested_post:flags2.7?SuggestedPost schedule_repeat_period:flags2.10?int summary_from_language:flags2.11?string = Message; messageService#7a800e0a flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true reactions_are_possible:flags.9?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer saved_peer_id:flags.28?Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; @@ -458,6 +458,7 @@ updateStarGiftAuctionUserState#dc58f31e gift_id:long user_state:StarGiftAuctionU updateEmojiGameInfo#fb9c547a info:messages.EmojiGameInfo = Update; updateStarGiftCraftFail#ac072444 = Update; updatePeerHistoryNoForwards#5736b39a flags:# my_enabled:flags.0?true peer_enabled:flags.1?true peer:Peer = Update; +updateChatParticipantRank#33d2633 chat_id:int user_id:int rank:string version:int = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -745,8 +746,8 @@ updates.channelDifference#2064674e flags:# final:flags.0?true pts:int timeout:fl channelMessagesFilterEmpty#94d42ee7 = ChannelMessagesFilter; channelMessagesFilter#cd77d957 flags:# exclude_new_messages:flags.1?true ranges:Vector = ChannelMessagesFilter; -channelParticipant#cb397619 flags:# user_id:long date:int subscription_until_date:flags.0?int = ChannelParticipant; -channelParticipantSelf#4f607bef flags:# via_request:flags.0?true user_id:long inviter_id:long date:int subscription_until_date:flags.1?int = ChannelParticipant; +channelParticipant#1bd54456 flags:# user_id:long date:int subscription_until_date:flags.0?int rank:flags.2?string = ChannelParticipant; +channelParticipantSelf#a9478a1a flags:# via_request:flags.0?true user_id:long inviter_id:long date:int subscription_until_date:flags.1?int rank:flags.2?string = ChannelParticipant; channelParticipantCreator#2fe601d3 flags:# user_id:long admin_rights:ChatAdminRights rank:flags.0?string = ChannelParticipant; channelParticipantAdmin#34c3bb53 flags:# can_edit:flags.0?true self:flags.1?true user_id:long inviter_id:flags.1?long promoted_by:long date:int admin_rights:ChatAdminRights rank:flags.2?string = ChannelParticipant; channelParticipantBanned#6df8014e flags:# left:flags.0?true peer:Peer kicked_by:long date:int banned_rights:ChatBannedRights = ChannelParticipant; @@ -2569,6 +2570,8 @@ messages.getEmojiGameInfo#fb7e8ca7 = messages.EmojiGameInfo; messages.summarizeText#9d4104e2 flags:# peer:InputPeer id:int to_lang:flags.0?string = TextWithEntities; messages.editChatCreator#f743b857 peer:InputPeer user_id:InputUser password:InputCheckPasswordSRP = Updates; messages.getFutureChatCreatorAfterLeave#3b7d0ea6 peer:InputPeer = User; +messages.toggleChatCustomRanks#341861e6 peer:InputPeer enabled:Bool = Updates; +messages.editChatParticipantRank#a00f32b0 peer:InputPeer participant:InputPeer rank:string = Updates; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp index a9adb9a4c0..1737f27d81 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp @@ -168,6 +168,7 @@ AdminLog::OwnedItem GenerateForwardedItem( MTP_int(0), // Not used (would've been trimmed to 32 bits). peerToMTP(history->peer->id), MTPint(), // from_boosts_applied + MTPstring(), // from_rank peerToMTP(history->peer->id), MTPPeer(), // saved_peer_id MTP_messageFwdHeader( From 7969795a01ec39a0f9606aa25fbbf752a212f3eb Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 10 Feb 2026 16:39:56 +0400 Subject: [PATCH 083/179] Show custom ranks, allow toggling on. --- Telegram/Resources/langs/lang.strings | 3 + .../SourceFiles/api/api_chat_participants.cpp | 6 ++ Telegram/SourceFiles/api/api_updates.cpp | 4 + .../boxes/peers/edit_participants_box.cpp | 91 +++++++++++-------- .../boxes/peers/edit_participants_box.h | 2 + .../boxes/peers/edit_peer_info_box.cpp | 44 +++++++++ .../boxes/peers/edit_peer_type_box.cpp | 35 +++++++ .../boxes/peers/edit_peer_type_box.h | 1 + Telegram/SourceFiles/data/data_channel.cpp | 4 + Telegram/SourceFiles/data/data_channel.h | 3 + Telegram/SourceFiles/data/data_chat.cpp | 38 +++++++- Telegram/SourceFiles/data/data_chat.h | 6 ++ Telegram/SourceFiles/data/data_session.cpp | 18 +++- Telegram/SourceFiles/data/data_session.h | 1 + Telegram/SourceFiles/history/history_item.cpp | 14 +++ Telegram/SourceFiles/history/history_item.h | 2 + .../history/history_item_components.h | 5 + .../history/view/history_view_message.cpp | 43 +++++++-- 18 files changed, 270 insertions(+), 50 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 08819cd5b7..e41e0b12eb 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -5981,6 +5981,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_rights_edit_admin_header" = "What can this admin do?"; "lng_rights_edit_admin_rank_name" = "Custom title"; "lng_rights_edit_admin_rank_about" = "A title that members will see instead of '{title}'."; +"lng_manage_peer_custom_ranks_title" = "Custom Member Titles"; +"lng_manage_peer_custom_ranks" = "Allow Custom Titles"; +"lng_manage_peer_custom_ranks_about" = "Allow members to set custom titles that will be shown next to their names in messages."; "lng_rights_about_add_admins_yes" = "This admin will be able to add new admins with equal or fewer rights."; "lng_rights_about_add_admins_no" = "This admin will not be able to add new admins."; "lng_rights_about_by" = "This admin promoted by {user} on {date}."; diff --git a/Telegram/SourceFiles/api/api_chat_participants.cpp b/Telegram/SourceFiles/api/api_chat_participants.cpp index 495a4cbfe2..a1d0a8cb94 100644 --- a/Telegram/SourceFiles/api/api_chat_participants.cpp +++ b/Telegram/SourceFiles/api/api_chat_participants.cpp @@ -112,6 +112,7 @@ void ApplyLastList( channel->mgInfo->lastAdmins.clear(); channel->mgInfo->lastRestricted.clear(); channel->mgInfo->lastParticipants.clear(); + channel->mgInfo->memberRanks.clear(); channel->mgInfo->lastParticipantsStatus = MegagroupInfo::LastParticipantsUpToDate | MegagroupInfo::LastParticipantsOnceReceived; @@ -148,6 +149,9 @@ void ApplyLastList( channel->mgInfo->botStatus = 2; } } + if (!p.rank().isEmpty() && !p.isCreatorOrAdmin()) { + channel->mgInfo->memberRanks[p.userId()] = p.rank(); + } } } // @@ -287,12 +291,14 @@ ChatParticipant::ChatParticipant( _type = Type::Member; _date = data.vdate().v; _by = peerToUser(peerFromUser(data.vinviter_id())); + _rank = qs(data.vrank().value_or_empty()); if (data.vsubscription_until_date()) { _subscriptionDate = data.vsubscription_until_date()->v; } }, [&](const MTPDchannelParticipant &data) { _type = Type::Member; _date = data.vdate().v; + _rank = qs(data.vrank().value_or_empty()); if (data.vsubscription_until_date()) { _subscriptionDate = data.vsubscription_until_date()->v; } diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 0f7db43b9b..1d41f6033c 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -1894,6 +1894,10 @@ void Updates::feedUpdate(const MTPUpdate &update) { session().data().applyUpdate(update.c_updateChatParticipantAdmin()); } break; + case mtpc_updateChatParticipantRank: { + session().data().applyUpdate(update.c_updateChatParticipantRank()); + } break; + case mtpc_updateChatDefaultBannedRights: { session().data().applyUpdate(update.c_updateChatDefaultBannedRights()); } break; diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index 66de189874..94f6952493 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -389,6 +389,12 @@ QString ParticipantsAdditionalData::adminRank( return (i != end(_adminRanks)) ? i->second : QString(); } +QString ParticipantsAdditionalData::memberRank( + not_null user) const { + const auto i = _memberRanks.find(user); + return (i != end(_memberRanks)) ? i->second : QString(); +} + TimeId ParticipantsAdditionalData::adminPromotedSince( not_null user) const { const auto i = _adminPromotedSince.find(user); @@ -626,43 +632,53 @@ PeerData *ParticipantsAdditionalData::applyParticipant( return nullptr; }; - switch (data.type()) { - case Api::ChatParticipant::Type::Creator: { - if (overrideRole != Role::Profile - && overrideRole != Role::Members - && overrideRole != Role::Admins) { - return logBad(); + const auto result = [&]() -> PeerData* { + switch (data.type()) { + case Api::ChatParticipant::Type::Creator: { + if (overrideRole != Role::Profile + && overrideRole != Role::Members + && overrideRole != Role::Admins) { + return logBad(); + } + return applyCreator(data); + } + case Api::ChatParticipant::Type::Admin: { + if (overrideRole != Role::Profile + && overrideRole != Role::Members + && overrideRole != Role::Admins) { + return logBad(); + } + return applyAdmin(data); + } + case Api::ChatParticipant::Type::Member: { + if (overrideRole != Role::Profile + && overrideRole != Role::Members) { + return logBad(); + } + return applyRegular(data.userId()); + } + case Api::ChatParticipant::Type::Restricted: + case Api::ChatParticipant::Type::Banned: + if (overrideRole != Role::Profile + && overrideRole != Role::Members + && overrideRole != Role::Restricted + && overrideRole != Role::Kicked) { + return logBad(); + } + return applyBanned(data); + case Api::ChatParticipant::Type::Left: + return logBad(); + }; + Unexpected("Api::ChatParticipant::type in applyParticipant."); + }(); + if (const auto user = result ? result->asUser() : nullptr) { + if (!data.rank().isEmpty() && !data.isCreatorOrAdmin()) { + _memberRanks[user] = data.rank(); + } else { + _memberRanks.remove(user); } - return applyCreator(data); } - case Api::ChatParticipant::Type::Admin: { - if (overrideRole != Role::Profile - && overrideRole != Role::Members - && overrideRole != Role::Admins) { - return logBad(); - } - return applyAdmin(data); - } - case Api::ChatParticipant::Type::Member: { - if (overrideRole != Role::Profile - && overrideRole != Role::Members) { - return logBad(); - } - return applyRegular(data.userId()); - } - case Api::ChatParticipant::Type::Restricted: - case Api::ChatParticipant::Type::Banned: - if (overrideRole != Role::Profile - && overrideRole != Role::Members - && overrideRole != Role::Restricted - && overrideRole != Role::Kicked) { - return logBad(); - } - return applyBanned(data); - case Api::ChatParticipant::Type::Left: - return logBad(); - }; - Unexpected("Api::ChatParticipant::type in applyParticipant."); + return result; } UserData *ParticipantsAdditionalData::applyCreator( @@ -2135,7 +2151,10 @@ auto ParticipantsBoxController::computeType( && user && user->isInaccessible(); if (!result.canRemove) { - result.adminRank = user ? _additional.adminRank(user) : QString(); + const auto aRank = user ? _additional.adminRank(user) : QString(); + result.adminRank = !aRank.isEmpty() + ? aRank + : (user ? _additional.memberRank(user) : QString()); } return result; } diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h index 6e14120302..91cc64375d 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h @@ -107,6 +107,7 @@ public: [[nodiscard]] std::optional adminRights( not_null user) const; [[nodiscard]] QString adminRank(not_null user) const; + [[nodiscard]] QString memberRank(not_null user) const; [[nodiscard]] std::optional restrictedRights( not_null participant) const; [[nodiscard]] bool isCreator(not_null user) const; @@ -149,6 +150,7 @@ private: // Data for channels. base::flat_map, ChatAdminRightsInfo> _adminRights; base::flat_map, QString> _adminRanks; + base::flat_map, QString> _memberRanks; base::flat_map, TimeId> _adminPromotedSince; base::flat_map, TimeId> _restrictedSince; base::flat_map, TimeId> _memberSince; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index f3c2c6fc43..f5613e4c1d 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -405,6 +405,7 @@ private: std::optional signatures; std::optional signatureProfiles; std::optional noForwards; + std::optional customRanks; std::optional joinToWrite; std::optional requestToJoin; std::optional discussionLink; @@ -465,6 +466,7 @@ private: [[nodiscard]] bool validateAutotranslate(Saving &to) const; [[nodiscard]] bool validateSignatures(Saving &to) const; [[nodiscard]] bool validateForwards(Saving &to) const; + [[nodiscard]] bool validateCustomRanks(Saving &to) const; [[nodiscard]] bool validateJoinToWrite(Saving &to) const; [[nodiscard]] bool validateRequestToJoin(Saving &to) const; @@ -480,6 +482,7 @@ private: void saveAutotranslate(); void saveSignatures(); void saveForwards(); + void saveCustomRanks(); void saveJoinToWrite(); void saveRequestToJoin(); void savePhoto(); @@ -999,6 +1002,11 @@ void Controller::fillPrivacyTypeButton() { ? _peer->asChannel()->usernames() : std::vector()), .noForwards = !_peer->allowsForwarding(), + .customRanks = (_peer->isChat() + ? _peer->asChat()->customRanksEnabled() + : _peer->isChannel() + ? _peer->asChannel()->customRanksEnabled() + : false), .joinToWrite = (_peer->isMegagroup() && _peer->asChannel()->joinToWrite()), .requestToJoin = (_peer->isMegagroup() @@ -2096,6 +2104,7 @@ std::optional Controller::validate() const { && validateAutotranslate(result) && validateSignatures(result) && validateForwards(result) + && validateCustomRanks(result) && validateJoinToWrite(result) && validateRequestToJoin(result)) { return result; @@ -2218,6 +2227,14 @@ bool Controller::validateForwards(Saving &to) const { return true; } +bool Controller::validateCustomRanks(Saving &to) const { + if (!_typeDataSavedValue) { + return true; + } + to.customRanks = _typeDataSavedValue->customRanks; + return true; +} + bool Controller::validateJoinToWrite(Saving &to) const { if (!_typeDataSavedValue) { return true; @@ -2253,6 +2270,7 @@ void Controller::save() { pushSaveStage([=] { saveAutotranslate(); }); pushSaveStage([=] { saveSignatures(); }); pushSaveStage([=] { saveForwards(); }); + pushSaveStage([=] { saveCustomRanks(); }); pushSaveStage([=] { saveJoinToWrite(); }); pushSaveStage([=] { saveRequestToJoin(); }); pushSaveStage([=] { savePhoto(); }); @@ -2761,6 +2779,32 @@ void Controller::saveForwards() { }).send(); } +void Controller::saveCustomRanks() { + const auto isEnabled = _peer->isChat() + ? _peer->asChat()->customRanksEnabled() + : _peer->isChannel() + ? _peer->asChannel()->customRanksEnabled() + : false; + if (!_savingData.customRanks + || *_savingData.customRanks == isEnabled) { + return continueSave(); + } + _api.request(MTPmessages_ToggleChatCustomRanks( + _peer->input(), + MTP_bool(*_savingData.customRanks) + )).done([=](const MTPUpdates &result) { + _peer->session().api().applyUpdates(result); + continueSave(); + }).fail([=](const MTP::Error &error) { + if (error.type() == u"CHAT_NOT_MODIFIED"_q) { + continueSave(); + } else { + _navigation->showToast(error.type()); + cancelSave(); + } + }).send(); +} + void Controller::saveJoinToWrite() { const auto joinToWrite = _peer->isMegagroup() && _peer->asChannel()->joinToWrite(); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp index 4c2a53fc11..2218ee64ac 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp @@ -83,6 +83,10 @@ public: [[nodiscard]] bool noForwards() const { return _controls.noForwards->toggled(); } + [[nodiscard]] bool customRanks() const { + return _controls.customRanks + && _controls.customRanks->toggled(); + } [[nodiscard]] bool joinToWrite() const { return _controls.joinToWrite && _controls.joinToWrite->toggled(); } @@ -112,6 +116,7 @@ private: Ui::SlideWrap *whoSendWrap = nullptr; Ui::SettingsButton *noForwards = nullptr; + Ui::SettingsButton *customRanks = nullptr; Ui::SettingsButton *joinToWrite = nullptr; Ui::SettingsButton *requestToJoin = nullptr; }; @@ -296,6 +301,35 @@ void Controller::createContent() { (_isGroup ? tr::lng_manage_peer_no_forwards_about : tr::lng_manage_peer_no_forwards_about_channel)()); + + const auto amCreator = (_peer->isChat() + && _peer->asChat()->amCreator()) + || (_peer->isChannel() + && _peer->asChannel()->amCreator()); + if (amCreator) { + Ui::AddSkip(_wrap.get()); + Ui::AddSubsectionTitle( + _wrap.get(), + tr::lng_manage_peer_custom_ranks_title()); + _controls.customRanks = _wrap->add( + EditPeerInfoBox::CreateButton( + _wrap.get(), + tr::lng_manage_peer_custom_ranks(), + rpl::single(QString()), + [] {}, + st::peerPermissionsButton, + {})); + _controls.customRanks->toggleOn( + rpl::single(_dataSavedValue->customRanks) + )->toggledValue( + ) | rpl::on_next([=](bool toggled) { + _dataSavedValue->customRanks = toggled; + }, _wrap->lifetime()); + Ui::AddSkip(_wrap.get()); + Ui::AddDividerText( + _wrap.get(), + tr::lng_manage_peer_custom_ranks_about()); + } } if (_linkOnly) { _controls.inviteLinkWrap->show(anim::type::instant); @@ -777,6 +811,7 @@ void EditPeerTypeBox::prepare() { ? controller->usernamesOrder() : std::vector()), .noForwards = controller->noForwards(), + .customRanks = controller->customRanks(), .joinToWrite = controller->joinToWrite(), .requestToJoin = controller->requestToJoin(), }); // We don't need username with private type. diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.h index 7b9aba6f9a..3238894481 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.h @@ -39,6 +39,7 @@ struct EditPeerTypeData { std::vector usernamesOrder; bool hasDiscussionLink = false; bool noForwards = false; + bool customRanks = false; bool joinToWrite = false; bool requestToJoin = false; }; diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 108a9eddc7..94049d9270 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -701,6 +701,10 @@ bool ChannelData::allowsForwarding() const { return !(flags() & Flag::NoForwards); } +bool ChannelData::customRanksEnabled() const { + return flags() & Flag::CustomRanksEnabled; +} + bool ChannelData::canViewMembers() const { return (flags() & Flag::CanViewParticipants) && (!(flags() & Flag::ParticipantsHidden) diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index ef5eaeea84..c805f29fa2 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -86,6 +86,7 @@ enum class ChannelDataFlag : uint64 { HasStarsPerMessage = (1ULL << 43), StarsPerMessageKnown = (1ULL << 44), HasActiveVideoStream = (1ULL << 45), + CustomRanksEnabled = (1ULL << 46), }; inline constexpr bool is_flag_type(ChannelDataFlag) { return true; }; using ChannelDataFlags = base::flags; @@ -143,6 +144,7 @@ public: // For admin badges, full admins list with ranks. base::flat_map admins; + base::flat_map memberRanks; UserData *creator = nullptr; // nullptr means unknown QString creatorRank; @@ -355,6 +357,7 @@ public: [[nodiscard]] bool autoTranslation() const { return flags() & Flag::AutoTranslation; } + [[nodiscard]] bool customRanksEnabled() const; [[nodiscard]] auto adminRights() const { return _adminRights.current(); diff --git a/Telegram/SourceFiles/data/data_chat.cpp b/Telegram/SourceFiles/data/data_chat.cpp index 4eea779332..669eac3d10 100644 --- a/Telegram/SourceFiles/data/data_chat.cpp +++ b/Telegram/SourceFiles/data/data_chat.cpp @@ -67,6 +67,10 @@ bool ChatData::allowsForwarding() const { return !(flags() & Flag::NoForwards); } +bool ChatData::customRanksEnabled() const { + return flags() & Flag::CustomRanksEnabled; +} + bool ChatData::canEditInformation() const { return amIn() && !amRestricted(ChatRestriction::ChangeInfo); } @@ -437,6 +441,25 @@ void ApplyChatUpdate( session->changes().peerUpdated(chat, UpdateFlag::Admins); } +void ApplyChatUpdate( + not_null chat, + const MTPDupdateChatParticipantRank &update) { + if (chat->applyUpdateVersion(update.vversion().v) + != ChatData::UpdateStatus::Good) { + return; + } + const auto rank = qs(update.vrank().v); + const auto userId = UserId(update.vuser_id().v); + if (rank.isEmpty()) { + chat->memberRanks.remove(userId); + } else { + chat->memberRanks[userId] = rank; + } + chat->session().changes().peerUpdated( + chat, + Data::PeerUpdate::Flag::Members); +} + void ApplyChatUpdate( not_null chat, const MTPDupdateChatDefaultBannedRights &update) { @@ -532,6 +555,7 @@ void ApplyChatUpdate( chat->participants.clear(); chat->invitedByMe.clear(); chat->admins.clear(); + chat->memberRanks.clear(); chat->setAdminRights(ChatAdminRights()); const auto selfUserId = session->userId(); for (const auto &participant : list) { @@ -558,13 +582,25 @@ void ApplyChatUpdate( participant.match([&](const MTPDchatParticipantCreator &data) { chat->creator = userId; + const auto rank = qs(data.vrank().value_or_empty()); + if (!rank.isEmpty()) { + chat->memberRanks[userId] = rank; + } }, [&](const MTPDchatParticipantAdmin &data) { chat->admins.emplace(user); if (user->isSelf()) { chat->setAdminRights( chat->defaultAdminRights(user).flags); } - }, [](const MTPDchatParticipant &) { + const auto rank = qs(data.vrank().value_or_empty()); + if (!rank.isEmpty()) { + chat->memberRanks[userId] = rank; + } + }, [&](const MTPDchatParticipant &data) { + const auto rank = qs(data.vrank().value_or_empty()); + if (!rank.isEmpty()) { + chat->memberRanks[userId] = rank; + } }); } if (chat->participants.empty()) { diff --git a/Telegram/SourceFiles/data/data_chat.h b/Telegram/SourceFiles/data/data_chat.h index 24b719c619..d4d5f5f15c 100644 --- a/Telegram/SourceFiles/data/data_chat.h +++ b/Telegram/SourceFiles/data/data_chat.h @@ -23,6 +23,7 @@ enum class ChatDataFlag { CallNotEmpty = (1 << 6), CanSetUsername = (1 << 7), NoForwards = (1 << 8), + CustomRanksEnabled = (1 << 9), }; inline constexpr bool is_flag_type(ChatDataFlag) { return true; }; using ChatDataFlags = base::flags; @@ -99,6 +100,7 @@ public: // Like in ChannelData. [[nodiscard]] bool allowsForwarding() const; + [[nodiscard]] bool customRanksEnabled() const; [[nodiscard]] bool canEditInformation() const; [[nodiscard]] bool canEditPermissions() const; [[nodiscard]] bool canEditUsername() const; @@ -178,6 +180,7 @@ public: base::flat_set> admins; std::deque> lastAuthors; base::flat_set> markupSenders; + base::flat_map memberRanks; int botStatus = 0; // -1 - no bots, 0 - unknown, 1 - one bot, that sees all history, 2 - other private: @@ -215,6 +218,9 @@ void ApplyChatUpdate( void ApplyChatUpdate( not_null chat, const MTPDupdateChatParticipantAdmin &update); +void ApplyChatUpdate( + not_null chat, + const MTPDupdateChatParticipantRank &update); void ApplyChatUpdate( not_null chat, const MTPDupdateChatDefaultBannedRights &update); diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 755d14d1d4..55622c912a 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -866,7 +866,8 @@ not_null Session::processChat(const MTPChat &data) { | Flag::Forbidden | Flag::CallActive | Flag::CallNotEmpty - | Flag::NoForwards; + | Flag::NoForwards + | Flag::CustomRanksEnabled; const auto flagsSet = (data.is_left() ? Flag::Left : Flag()) | (data.is_creator() ? Flag::Creator : Flag()) | (data.is_deactivated() ? Flag::Deactivated : Flag()) @@ -876,7 +877,8 @@ not_null Session::processChat(const MTPChat &data) { && chat->groupCall()->fullCount() > 0)) ? Flag::CallNotEmpty : Flag()) - | (data.is_noforwards() ? Flag::NoForwards : Flag()); + | (data.is_noforwards() ? Flag::NoForwards : Flag()) + | (data.is_custom_ranks_enabled() ? Flag::CustomRanksEnabled : Flag()); chat->setFlags((chat->flags() & ~flagsMask) | flagsSet); chat->count = data.vparticipants_count().v; @@ -997,7 +999,8 @@ not_null Session::processChat(const MTPChat &data) { | Flag::AutoTranslation | Flag::Monoforum | Flag::HasStarsPerMessage - | Flag::StarsPerMessageKnown; + | Flag::StarsPerMessageKnown + | Flag::CustomRanksEnabled; const auto hasStarsPerMessage = data.vsend_paid_messages_stars().has_value(); if (!hasStarsPerMessage) { @@ -1055,7 +1058,8 @@ not_null Session::processChat(const MTPChat &data) { | (channel->starsPerMessageKnown() ? Flag::StarsPerMessageKnown : Flag())) - : Flag::StarsPerMessageKnown); + : Flag::StarsPerMessageKnown) + | (data.is_custom_ranks_enabled() ? Flag::CustomRanksEnabled : Flag()); channel->setFlags((channel->flags() & ~flagsMask) | flagsSet); channel->setBotVerifyDetailsIcon( data.vbot_verification_icon().value_or_empty()); @@ -4407,6 +4411,12 @@ void Session::applyUpdate(const MTPDupdateChatParticipantAdmin &update) { } } +void Session::applyUpdate(const MTPDupdateChatParticipantRank &update) { + if (const auto chat = chatLoaded(update.vchat_id().v)) { + ApplyChatUpdate(chat, update); + } +} + void Session::applyUpdate(const MTPDupdateChatDefaultBannedRights &update) { if (const auto peer = peerLoaded(peerFromMTP(update.vpeer()))) { if (const auto chat = peer->asChat()) { diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index d4096e7845..b64fefd748 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -469,6 +469,7 @@ public: void applyUpdate(const MTPDupdateChatParticipantAdd &update); void applyUpdate(const MTPDupdateChatParticipantDelete &update); void applyUpdate(const MTPDupdateChatParticipantAdmin &update); + void applyUpdate(const MTPDupdateChatParticipantRank &update); void applyUpdate(const MTPDupdateChatDefaultBannedRights &update); void applyDialogs( diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 00b1a22a5b..0629a64e36 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -478,6 +478,13 @@ HistoryItem::HistoryItem( } } + if (const auto rank = data.vfrom_rank()) { + if (!rank->v.isEmpty()) { + AddComponents(HistoryMessageFromRank::Bit()); + Get()->rank = qs(*rank); + } + } + if (const auto until = data.vreport_delivery_until_date()) { if (base::unixtime::now() < TimeId(until->v)) { history->owner().histories().reportDelivery(this); @@ -3276,6 +3283,13 @@ QString HistoryItem::originalPostAuthor() const { return QString(); } +QString HistoryItem::fromRank() const { + if (const auto component = Get()) { + return component->rank; + } + return QString(); +} + MsgId HistoryItem::originalId() const { if (const auto forwarded = Get()) { return forwarded->originalId; diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index b483ac7a43..4e21760ed3 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -604,6 +604,8 @@ public: return _boostsApplied; } + [[nodiscard]] QString fromRank() const; + MsgId id; private: diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 4267e928fb..50dc9b204e 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -103,6 +103,11 @@ struct HistoryMessageSigned bool isAnonymousRank = false; }; +struct HistoryMessageFromRank +: RuntimeComponent { + QString rank; +}; + struct HistoryMessageEdited : RuntimeComponent { TimeId date = 0; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 314766a84e..2d7e3ec04b 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -39,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/components/sponsored_messages.h" #include "data/data_session.h" #include "data/data_user.h" +#include "data/data_chat.h" #include "data/data_channel.h" #include "data/data_forum_topic.h" #include "data/data_message_reactions.h" @@ -272,23 +273,47 @@ void Message::refreshRightBadge() { } const auto channel = item->history()->peer->asMegagroup(); const auto user = item->author()->asUser(); - if (!channel || !user) { + if (!channel) { + if (const auto chat = item->history()->peer->asChat()) { + if (user) { + const auto j = chat->memberRanks.find( + peerToUser(user->id)); + if (j != chat->memberRanks.end()) { + return j->second; + } + } + } + return QString(); + } + if (!user) { return QString(); } const auto info = channel->mgInfo.get(); - const auto i = info->admins.find(peerToUser(user->id)); + const auto userId = peerToUser(user->id); + const auto i = info->admins.find(userId); const auto custom = (i != info->admins.end()) ? i->second : (info->creator == user) ? info->creatorRank : QString(); - return !custom.isEmpty() - ? custom - : (info->creator == user) - ? tr::lng_owner_badge(tr::now) - : (i != info->admins.end()) - ? tr::lng_admin_badge(tr::now) - : QString(); + if (!custom.isEmpty()) { + return custom; + } + if (info->creator == user) { + return tr::lng_owner_badge(tr::now); + } + if (i != info->admins.end()) { + return tr::lng_admin_badge(tr::now); + } + const auto fromRank = item->fromRank(); + if (!fromRank.isEmpty()) { + return fromRank; + } + const auto j = info->memberRanks.find(userId); + if (j != info->memberRanks.end()) { + return j->second; + } + return QString(); }(); auto badge = TextWithEntities{ (text.isEmpty() From aa94491ad714132b71cfd56d1697065ddf437bf5 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 10 Feb 2026 17:38:11 +0400 Subject: [PATCH 084/179] Allow editing my rank / other ranks. --- Telegram/Resources/langs/lang.strings | 9 +- .../boxes/peers/edit_members_visible.cpp | 102 ++++++---- .../boxes/peers/edit_participants_box.cpp | 184 ++++++++++++++++++ .../boxes/peers/edit_participants_box.h | 12 ++ .../boxes/peers/edit_peer_info_box.cpp | 44 ----- .../boxes/peers/edit_peer_type_box.cpp | 34 ---- .../boxes/peers/edit_peer_type_box.h | 1 - Telegram/SourceFiles/data/data_changes.cpp | 11 ++ Telegram/SourceFiles/data/data_changes.h | 13 ++ Telegram/SourceFiles/data/data_channel.cpp | 11 ++ Telegram/SourceFiles/data/data_channel.h | 3 + .../SourceFiles/data/data_channel_admins.cpp | 33 ++++ .../SourceFiles/data/data_channel_admins.h | 15 ++ .../SourceFiles/history/history_widget.cpp | 1 + .../view/history_view_chat_section.cpp | 8 +- .../SourceFiles/window/window_peer_menu.cpp | 71 +++++++ .../SourceFiles/window/window_peer_menu.h | 1 + 17 files changed, 437 insertions(+), 116 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index e41e0b12eb..dedca84f8c 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -5981,9 +5981,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_rights_edit_admin_header" = "What can this admin do?"; "lng_rights_edit_admin_rank_name" = "Custom title"; "lng_rights_edit_admin_rank_about" = "A title that members will see instead of '{title}'."; -"lng_manage_peer_custom_ranks_title" = "Custom Member Titles"; -"lng_manage_peer_custom_ranks" = "Allow Custom Titles"; -"lng_manage_peer_custom_ranks_about" = "Allow members to set custom titles that will be shown next to their names in messages."; +"lng_manage_peer_custom_tags" = "Allow Members Tags"; +"lng_manage_peer_custom_tags_about" = "Turn this on to let members of the group add short tags next to their names."; +"lng_context_add_my_tag" = "Add Tag"; +"lng_context_edit_my_tag" = "Edit Tag"; +"lng_context_add_member_tag" = "Add Member Tag"; +"lng_context_edit_member_tag" = "Edit Member Tag"; "lng_rights_about_add_admins_yes" = "This admin will be able to add new admins with equal or fewer rights."; "lng_rights_about_add_admins_no" = "This admin will not be able to add new admins."; "lng_rights_about_by" = "This admin promoted by {user} on {date}."; diff --git a/Telegram/SourceFiles/boxes/peers/edit_members_visible.cpp b/Telegram/SourceFiles/boxes/peers/edit_members_visible.cpp index 0fd9fd007b..a2b5af312d 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_members_visible.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_members_visible.cpp @@ -34,44 +34,80 @@ namespace { [[nodiscard]] object_ptr CreateMembersVisibleButton( not_null megagroup) { - auto result = object_ptr((QWidget*)nullptr); - const auto container = result.data(); - const auto min = EnableHideMembersMin(megagroup); - if (!megagroup->canBanMembers() || megagroup->membersCount() < min) { + const auto showHideMembers = megagroup->canBanMembers() + && megagroup->membersCount() >= min; + const auto showCustomTags = megagroup->amCreator(); + if (!showHideMembers && !showCustomTags) { return { nullptr }; } - struct State { - rpl::event_stream toggled; - }; - Ui::AddSkip(container); - const auto state = container->lifetime().make_state(); - const auto button = container->add( - EditPeerInfoBox::CreateButton( - container, - tr::lng_profile_hide_participants(), - rpl::single(QString()), - [] {}, - st::manageGroupNoIconButton, - {} - ))->toggleOn(rpl::single( - (megagroup->flags() & ChannelDataFlag::ParticipantsHidden) != 0 - ) | rpl::then(state->toggled.events())); - Ui::AddSkip(container); - Ui::AddDividerText(container, tr::lng_profile_hide_participants_about()); + auto result = object_ptr((QWidget*)nullptr); + const auto container = result.data(); - button->toggledValue( - ) | rpl::on_next([=](bool toggled) { - megagroup->session().api().request( - MTPchannels_ToggleParticipantsHidden( - megagroup->inputChannel(), - MTP_bool(toggled) - ) - ).done([=](const MTPUpdates &result) { - megagroup->session().api().applyUpdates(result); - }).send(); - }, button->lifetime()); + if (showHideMembers) { + struct State { + rpl::event_stream toggled; + }; + Ui::AddSkip(container); + const auto state = container->lifetime().make_state(); + const auto button = container->add( + EditPeerInfoBox::CreateButton( + container, + tr::lng_profile_hide_participants(), + rpl::single(QString()), + [] {}, + st::manageGroupNoIconButton, + {} + ))->toggleOn(rpl::single( + (megagroup->flags() & ChannelDataFlag::ParticipantsHidden) != 0 + ) | rpl::then(state->toggled.events())); + Ui::AddSkip(container); + Ui::AddDividerText( + container, + tr::lng_profile_hide_participants_about()); + + button->toggledValue( + ) | rpl::on_next([=](bool toggled) { + megagroup->session().api().request( + MTPchannels_ToggleParticipantsHidden( + megagroup->inputChannel(), + MTP_bool(toggled) + ) + ).done([=](const MTPUpdates &result) { + megagroup->session().api().applyUpdates(result); + }).send(); + }, button->lifetime()); + } + + if (showCustomTags) { + Ui::AddSkip(container); + const auto tagsButton = container->add( + EditPeerInfoBox::CreateButton( + container, + tr::lng_manage_peer_custom_tags(), + rpl::single(QString()), + [] {}, + st::manageGroupNoIconButton, + {} + ))->toggleOn(rpl::single(megagroup->customRanksEnabled())); + Ui::AddSkip(container); + Ui::AddDividerText( + container, + tr::lng_manage_peer_custom_tags_about()); + + tagsButton->toggledValue( + ) | rpl::on_next([=](bool toggled) { + megagroup->session().api().request( + MTPmessages_ToggleChatCustomRanks( + megagroup->input(), + MTP_bool(toggled) + ) + ).done([=](const MTPUpdates &result) { + megagroup->session().api().applyUpdates(result); + }).send(); + }, tagsButton->lifetime()); + } return result; } diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index 94f6952493..28ad8156ea 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -30,12 +30,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "base/unixtime.h" #include "ui/effects/outline_segments.h" +#include "ui/layers/generic_box.h" +#include "ui/widgets/fields/input_field.h" #include "ui/widgets/menu/menu_multiline_action.h" #include "ui/widgets/popup_menu.h" #include "ui/text/text_utilities.h" #include "info/profile/info_profile_values.h" #include "window/window_session_controller.h" #include "history/history.h" +#include "styles/style_boxes.h" #include "styles/style_chat.h" #include "styles/style_menu_icons.h" @@ -168,6 +171,48 @@ void SaveChannelAdmin( }).send(); } +void SaveMemberRank( + std::shared_ptr show, + not_null peer, + not_null user, + const QString &rank, + Fn onDone, + Fn onFail) { + peer->session().api().request(MTPmessages_EditChatParticipantRank( + peer->input(), + user->input(), + MTP_string(rank) + )).done([=](const MTPUpdates &result) { + peer->session().api().applyUpdates(result); + if (const auto channel = peer->asChannel()) { + channel->applyEditMemberRank(user, rank); + } else if (const auto chat = peer->asChat()) { + if (rank.isEmpty()) { + chat->memberRanks.remove(peerToUser(user->id)); + } else { + chat->memberRanks[peerToUser(user->id)] = rank; + } + chat->session().changes().peerUpdated( + chat, + Data::PeerUpdate::Flag::Members); + } + peer->session().changes().chatMemberRankChanged( + peer, + user, + rank); + if (onDone) { + onDone(); + } + }).fail([=](const MTP::Error &error) { + if (show) { + show->showToast(error.type()); + } + if (onFail) { + onFail(); + } + }).send(); +} + void SaveChatParticipantKick( not_null chat, not_null user, @@ -616,6 +661,16 @@ void ParticipantsAdditionalData::applyBannedLocally( } } +void ParticipantsAdditionalData::applyMemberRankLocally( + not_null user, + const QString &rank) { + if (rank.isEmpty()) { + _memberRanks.remove(user); + } else { + _memberRanks[user] = rank; + } +} + PeerData *ParticipantsAdditionalData::applyParticipant( const Api::ChatParticipant &data) { return applyParticipant(data, _role); @@ -1316,6 +1371,16 @@ void ParticipantsBoxController::prepare() { recomputeTypeFor(user); refreshRows(); }, lifetime()); + + _peer->session().changes().chatMemberRankChanges( + ) | rpl::on_next([=](const Data::ChatMemberRankChange &update) { + if (update.peer != _peer) { + return; + } + _additional.applyMemberRankLocally(update.user, update.rank); + recomputeTypeFor(update.user); + refreshRows(); + }, lifetime()); } void ParticipantsBoxController::unload() { @@ -1754,6 +1819,57 @@ base::unique_qptr ParticipantsBoxController::rowContextMenu( ? &st::menuIconProfile : &st::menuIconInfo)); } + if (user) { + const auto isAdmin = _peer->isChat() + ? (_peer->asChat()->hasAdminRights() + || _peer->asChat()->amCreator()) + : (_peer->isChannel() + ? (_peer->asChannel()->hasAdminRights() + || _peer->asChannel()->amCreator()) + : false); + const auto isSelf = user->isSelf(); + const auto canEditSelf = isSelf + && (isAdmin + || (_peer->isChat() + ? _peer->asChat()->customRanksEnabled() + : (_peer->isChannel() + ? _peer->asChannel()->customRanksEnabled() + : false))); + const auto targetIsAdmin = _additional.adminRights(user).has_value() + || _additional.isCreator(user); + const auto canEditTarget = !isSelf + && isAdmin + && (!targetIsAdmin || _additional.canEditAdmin(user)); + if (canEditSelf || canEditTarget) { + const auto currentRank = _additional.memberRank(user); + const auto show = delegate()->peerListUiShow(); + const auto peer = _peer; + const auto actionText = canEditSelf + ? (currentRank.isEmpty() + ? tr::lng_context_add_my_tag(tr::now) + : tr::lng_context_edit_my_tag(tr::now)) + : (currentRank.isEmpty() + ? tr::lng_context_add_member_tag(tr::now) + : tr::lng_context_edit_member_tag(tr::now)); + result->addAction( + actionText, + crl::guard(this, [=] { + _editBox = show->show(Box( + EditCustomRankBox, + show, + peer, + user, + currentRank, + canEditSelf, + crl::guard(this, [=](const QString &rank) { + _additional.applyMemberRankLocally(user, rank); + recomputeTypeFor(user); + refreshRows(); + }))); + }), + &st::menuIconEdit); + } + } if (_role == Role::Kicked) { if (_peer->isMegagroup() && _additional.canRestrictParticipant(participant)) { @@ -2464,3 +2580,71 @@ void ParticipantsBoxSearchController::searchDone( delegate()->peerListSearchRefreshRows(); } + +void EditCustomRankBox( + not_null box, + std::shared_ptr show, + not_null peer, + not_null user, + const QString ¤tRank, + bool isSelf, + Fn onSaved) { + constexpr auto kRankLimit = 16; + struct State { + bool saving = false; + }; + const auto state = box->lifetime().make_state(); + + const auto hasRank = !currentRank.isEmpty(); + box->setTitle(isSelf + ? (hasRank + ? tr::lng_context_edit_my_tag() + : tr::lng_context_add_my_tag()) + : (hasRank + ? tr::lng_context_edit_member_tag() + : tr::lng_context_add_member_tag())); + + const auto field = box->addRow(object_ptr( + box, + st::customBadgeField, + tr::lng_rights_edit_admin_rank_name(), + TextUtilities::RemoveEmoji(currentRank))); + field->setMaxLength(kRankLimit); + field->setInstantReplaces(Ui::InstantReplaces::TextOnly()); + field->changes( + ) | rpl::on_next([=] { + const auto text = field->getLastText(); + const auto removed = TextUtilities::RemoveEmoji(text); + if (removed != text) { + field->setText(removed); + } + }, field->lifetime()); + + box->setFocusCallback([=] { field->setFocusFast(); }); + + const auto close = crl::guard(box, [=] { box->closeBox(); }); + const auto save = [=] { + if (state->saving) { + return; + } + state->saving = true; + const auto rank = TextUtilities::RemoveEmoji( + TextUtilities::SingleLine(field->getLastText().trimmed())); + SaveMemberRank( + show, + peer, + user, + rank, + [=] { + if (onSaved) { + onSaved(rank); + } + close(); + }, + [=] { state->saving = false; }); + }; + field->submits( + ) | rpl::on_next([=] { save(); }, field->lifetime()); + box->addButton(tr::lng_settings_save(), save); + box->addButton(tr::lng_cancel(), close); +} diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h index 91cc64375d..8bfa406f84 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h @@ -49,6 +49,15 @@ Fn onDone, Fn onFail); +void EditCustomRankBox( + not_null box, + std::shared_ptr show, + not_null peer, + not_null user, + const QString ¤tRank, + bool isSelf, + Fn onSaved); + void SubscribeToMigration( not_null peer, rpl::lifetime &lifetime, @@ -130,6 +139,9 @@ public: void applyBannedLocally( not_null participant, ChatRestrictionsInfo rights); + void applyMemberRankLocally( + not_null user, + const QString &rank); private: UserData *applyCreator(const Api::ChatParticipant &data); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index f5613e4c1d..f3c2c6fc43 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -405,7 +405,6 @@ private: std::optional signatures; std::optional signatureProfiles; std::optional noForwards; - std::optional customRanks; std::optional joinToWrite; std::optional requestToJoin; std::optional discussionLink; @@ -466,7 +465,6 @@ private: [[nodiscard]] bool validateAutotranslate(Saving &to) const; [[nodiscard]] bool validateSignatures(Saving &to) const; [[nodiscard]] bool validateForwards(Saving &to) const; - [[nodiscard]] bool validateCustomRanks(Saving &to) const; [[nodiscard]] bool validateJoinToWrite(Saving &to) const; [[nodiscard]] bool validateRequestToJoin(Saving &to) const; @@ -482,7 +480,6 @@ private: void saveAutotranslate(); void saveSignatures(); void saveForwards(); - void saveCustomRanks(); void saveJoinToWrite(); void saveRequestToJoin(); void savePhoto(); @@ -1002,11 +999,6 @@ void Controller::fillPrivacyTypeButton() { ? _peer->asChannel()->usernames() : std::vector()), .noForwards = !_peer->allowsForwarding(), - .customRanks = (_peer->isChat() - ? _peer->asChat()->customRanksEnabled() - : _peer->isChannel() - ? _peer->asChannel()->customRanksEnabled() - : false), .joinToWrite = (_peer->isMegagroup() && _peer->asChannel()->joinToWrite()), .requestToJoin = (_peer->isMegagroup() @@ -2104,7 +2096,6 @@ std::optional Controller::validate() const { && validateAutotranslate(result) && validateSignatures(result) && validateForwards(result) - && validateCustomRanks(result) && validateJoinToWrite(result) && validateRequestToJoin(result)) { return result; @@ -2227,14 +2218,6 @@ bool Controller::validateForwards(Saving &to) const { return true; } -bool Controller::validateCustomRanks(Saving &to) const { - if (!_typeDataSavedValue) { - return true; - } - to.customRanks = _typeDataSavedValue->customRanks; - return true; -} - bool Controller::validateJoinToWrite(Saving &to) const { if (!_typeDataSavedValue) { return true; @@ -2270,7 +2253,6 @@ void Controller::save() { pushSaveStage([=] { saveAutotranslate(); }); pushSaveStage([=] { saveSignatures(); }); pushSaveStage([=] { saveForwards(); }); - pushSaveStage([=] { saveCustomRanks(); }); pushSaveStage([=] { saveJoinToWrite(); }); pushSaveStage([=] { saveRequestToJoin(); }); pushSaveStage([=] { savePhoto(); }); @@ -2779,32 +2761,6 @@ void Controller::saveForwards() { }).send(); } -void Controller::saveCustomRanks() { - const auto isEnabled = _peer->isChat() - ? _peer->asChat()->customRanksEnabled() - : _peer->isChannel() - ? _peer->asChannel()->customRanksEnabled() - : false; - if (!_savingData.customRanks - || *_savingData.customRanks == isEnabled) { - return continueSave(); - } - _api.request(MTPmessages_ToggleChatCustomRanks( - _peer->input(), - MTP_bool(*_savingData.customRanks) - )).done([=](const MTPUpdates &result) { - _peer->session().api().applyUpdates(result); - continueSave(); - }).fail([=](const MTP::Error &error) { - if (error.type() == u"CHAT_NOT_MODIFIED"_q) { - continueSave(); - } else { - _navigation->showToast(error.type()); - cancelSave(); - } - }).send(); -} - void Controller::saveJoinToWrite() { const auto joinToWrite = _peer->isMegagroup() && _peer->asChannel()->joinToWrite(); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp index 2218ee64ac..0373cc6a16 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp @@ -83,10 +83,6 @@ public: [[nodiscard]] bool noForwards() const { return _controls.noForwards->toggled(); } - [[nodiscard]] bool customRanks() const { - return _controls.customRanks - && _controls.customRanks->toggled(); - } [[nodiscard]] bool joinToWrite() const { return _controls.joinToWrite && _controls.joinToWrite->toggled(); } @@ -116,7 +112,6 @@ private: Ui::SlideWrap *whoSendWrap = nullptr; Ui::SettingsButton *noForwards = nullptr; - Ui::SettingsButton *customRanks = nullptr; Ui::SettingsButton *joinToWrite = nullptr; Ui::SettingsButton *requestToJoin = nullptr; }; @@ -302,34 +297,6 @@ void Controller::createContent() { ? tr::lng_manage_peer_no_forwards_about : tr::lng_manage_peer_no_forwards_about_channel)()); - const auto amCreator = (_peer->isChat() - && _peer->asChat()->amCreator()) - || (_peer->isChannel() - && _peer->asChannel()->amCreator()); - if (amCreator) { - Ui::AddSkip(_wrap.get()); - Ui::AddSubsectionTitle( - _wrap.get(), - tr::lng_manage_peer_custom_ranks_title()); - _controls.customRanks = _wrap->add( - EditPeerInfoBox::CreateButton( - _wrap.get(), - tr::lng_manage_peer_custom_ranks(), - rpl::single(QString()), - [] {}, - st::peerPermissionsButton, - {})); - _controls.customRanks->toggleOn( - rpl::single(_dataSavedValue->customRanks) - )->toggledValue( - ) | rpl::on_next([=](bool toggled) { - _dataSavedValue->customRanks = toggled; - }, _wrap->lifetime()); - Ui::AddSkip(_wrap.get()); - Ui::AddDividerText( - _wrap.get(), - tr::lng_manage_peer_custom_ranks_about()); - } } if (_linkOnly) { _controls.inviteLinkWrap->show(anim::type::instant); @@ -811,7 +778,6 @@ void EditPeerTypeBox::prepare() { ? controller->usernamesOrder() : std::vector()), .noForwards = controller->noForwards(), - .customRanks = controller->customRanks(), .joinToWrite = controller->joinToWrite(), .requestToJoin = controller->requestToJoin(), }); // We don't need username with private type. diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.h index 3238894481..7b9aba6f9a 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.h @@ -39,7 +39,6 @@ struct EditPeerTypeData { std::vector usernamesOrder; bool hasDiscussionLink = false; bool noForwards = false; - bool customRanks = false; bool joinToWrite = false; bool requestToJoin = false; }; diff --git a/Telegram/SourceFiles/data/data_changes.cpp b/Telegram/SourceFiles/data/data_changes.cpp index 5356d48065..8b5d73c9a8 100644 --- a/Telegram/SourceFiles/data/data_changes.cpp +++ b/Telegram/SourceFiles/data/data_changes.cpp @@ -357,6 +357,17 @@ rpl::producer Changes::chatAdminChanges() const { return _chatAdminChanges.events(); } +void Changes::chatMemberRankChanged( + not_null peer, + not_null user, + QString rank) { + _chatMemberRankChanges.fire({ peer, user, std::move(rank) }); +} + +rpl::producer Changes::chatMemberRankChanges() const { + return _chatMemberRankChanges.events(); +} + void Changes::scheduleNotifications() { if (!_notify) { _notify = true; diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index 2471bf2e2c..f5c66416af 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -282,6 +282,12 @@ struct ChatAdminChange { QString rank; }; +struct ChatMemberRankChange { + not_null peer; + not_null user; + QString rank; +}; + class Changes final { public: explicit Changes(not_null session); @@ -401,6 +407,12 @@ public: QString rank); [[nodiscard]] rpl::producer chatAdminChanges() const; + void chatMemberRankChanged( + not_null peer, + not_null user, + QString rank); + [[nodiscard]] rpl::producer chatMemberRankChanges() const; + void sendNotifications(); private: @@ -454,6 +466,7 @@ private: Manager _entryChanges; Manager _storyChanges; rpl::event_stream _chatAdminChanges; + rpl::event_stream _chatMemberRankChanges; bool _notify = false; diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 94049d9270..f3df9e3ab2 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -510,6 +510,17 @@ void ChannelData::applyEditAdmin( session().changes().peerUpdated(this, UpdateFlag::Admins); } +void ChannelData::applyEditMemberRank( + not_null user, + const QString &rank) { + if (!mgInfo) { + return; + } + const auto userId = peerToUser(user->id); + Data::ChannelMemberRankChanges changes(this); + changes.feed(userId, rank); +} + void ChannelData::applyEditBanned( not_null participant, ChatRestrictionsInfo oldRights, diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index c805f29fa2..c49e904e16 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -308,6 +308,9 @@ public: ChatAdminRightsInfo oldRights, ChatAdminRightsInfo newRights, const QString &rank); + void applyEditMemberRank( + not_null user, + const QString &rank); void applyEditBanned( not_null participant, ChatRestrictionsInfo oldRights, diff --git a/Telegram/SourceFiles/data/data_channel_admins.cpp b/Telegram/SourceFiles/data/data_channel_admins.cpp index e7ac8ced18..6dd286573e 100644 --- a/Telegram/SourceFiles/data/data_channel_admins.cpp +++ b/Telegram/SourceFiles/data/data_channel_admins.cpp @@ -44,4 +44,37 @@ ChannelAdminChanges::~ChannelAdminChanges() { } } +ChannelMemberRankChanges::ChannelMemberRankChanges( + not_null channel) +: _channel(channel) +, _memberRanks(_channel->mgInfo->memberRanks) { +} + +void ChannelMemberRankChanges::feed( + UserId userId, + const QString &rank) { + if (rank.isEmpty()) { + if (_memberRanks.remove(userId)) { + _changes.emplace(userId); + } + } else { + const auto i = _memberRanks.find(userId); + if (i == end(_memberRanks) || i->second != rank) { + _memberRanks[userId] = rank; + _changes.emplace(userId); + } + } +} + +ChannelMemberRankChanges::~ChannelMemberRankChanges() { + if (_changes.size() > 1 + || (!_changes.empty() + && _changes.front() != _channel->session().userId())) { + if (const auto history + = _channel->owner().historyLoaded(_channel)) { + history->applyGroupAdminChanges(_changes); + } + } +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_channel_admins.h b/Telegram/SourceFiles/data/data_channel_admins.h index 7fa1b06fd9..b069e57a49 100644 --- a/Telegram/SourceFiles/data/data_channel_admins.h +++ b/Telegram/SourceFiles/data/data_channel_admins.h @@ -25,4 +25,19 @@ private: }; +class ChannelMemberRankChanges { +public: + ChannelMemberRankChanges(not_null channel); + + void feed(UserId userId, const QString &rank); + + ~ChannelMemberRankChanges(); + +private: + not_null _channel; + base::flat_map &_memberRanks; + base::flat_set _changes; + +}; + } // namespace Data diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 10f1946401..1a2b4368ac 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -8964,6 +8964,7 @@ void HistoryWidget::fillSenderUserpicMenu( Window::FillSenderUserpicMenu( controller(), peer, + inGroup ? _peer : nullptr, (inGroup && _canSendTexts) ? _field.data() : nullptr, inGroup ? _peer->owner().history(_peer) : Dialogs::Key(), Ui::Menu::CreateAddActionCallback(menu)); diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index fdbc38b1e1..10d809ff1c 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -3323,9 +3323,15 @@ base::unique_qptr ChatWidget::listFillSenderUserpicMenu( auto menu = base::make_unique_q( this, st::popupMenuWithIcons); + const auto senderPeer = _history->owner().peer(userpicPeerId); + const auto groupPeer = (_history->peer->isChat() + || _history->peer->isMegagroup()) + ? _history->peer.get() + : nullptr; Window::FillSenderUserpicMenu( controller(), - _history->owner().peer(userpicPeerId), + senderPeer, + groupPeer, _composeControls->fieldForMention(), searchInEntry, Ui::Menu::CreateAddActionCallback(menu.get())); diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 83a197b615..600360a928 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -109,6 +109,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "core/ui_integration.h" #include "export/export_manager.h" +#include "boxes/peers/edit_participants_box.h" #include "boxes/peers/edit_peer_info_box.h" #include "boxes/premium_preview_box.h" #include "styles/style_chat.h" @@ -181,6 +182,24 @@ namespace { constexpr auto kArchivedToastDuration = crl::time(5000); constexpr auto kMaxUnreadWithoutConfirmation = 1000; +[[nodiscard]] QString LookupMemberRank( + not_null peer, + not_null user) { + if (const auto chat = peer->asChat()) { + const auto i = chat->memberRanks.find(peerToUser(user->id)); + return (i != chat->memberRanks.end()) ? i->second : QString(); + } else if (const auto channel = peer->asChannel()) { + if (channel->mgInfo) { + const auto i = channel->mgInfo->memberRanks.find( + peerToUser(user->id)); + return (i != channel->mgInfo->memberRanks.end()) + ? i->second + : QString(); + } + } + return QString(); +} + base::options::toggle ViewProfileInChatsListContextMenu({ .id = kOptionViewProfileInChatsListContextMenu, .name = "Add \"View Profile\"", @@ -3761,6 +3780,7 @@ bool FillVideoChatMenu( void FillSenderUserpicMenu( not_null controller, not_null peer, + PeerData *groupPeer, Ui::InputField *fieldForMention, Dialogs::Key searchInEntry, const PeerMenuCallback &addAction) { @@ -3803,6 +3823,57 @@ void FillSenderUserpicMenu( controller->searchInChat(searchInEntry, peer); }, &st::menuIconSearch); } + + if (const auto user = peer->asUser()) { + if (groupPeer) { + const auto isAdmin = groupPeer->isChat() + ? (groupPeer->asChat()->hasAdminRights() + || groupPeer->asChat()->amCreator()) + : (groupPeer->isChannel() + ? (groupPeer->asChannel()->hasAdminRights() + || groupPeer->asChannel()->amCreator()) + : false); + const auto canEditTarget = [&] { + if (const auto chat = groupPeer->asChat()) { + if (peerToUser(user->id) == chat->creator) { + return chat->amCreator(); + } + if (chat->admins.contains(user)) { + return chat->amCreator(); + } + return true; + } else if (const auto channel = groupPeer->asChannel()) { + if (channel->mgInfo + && (channel->mgInfo->lastAdmins.contains(user) + || channel->mgInfo->creator == user)) { + return channel->canEditAdmin(user); + } + return true; + } + return false; + }(); + if (isAdmin && canEditTarget && !user->isSelf()) { + const auto currentRank = LookupMemberRank( + groupPeer, + user); + addAction( + (currentRank.isEmpty() + ? tr::lng_context_add_member_tag(tr::now) + : tr::lng_context_edit_member_tag(tr::now)), + [=] { + controller->show(Box( + EditCustomRankBox, + controller->uiShow(), + groupPeer, + user, + currentRank, + false, + nullptr)); + }, + &st::menuIconEdit); + } + } + } } void AddSenderUserpicModerateAction( diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index 4a08e99f49..48910a4824 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -81,6 +81,7 @@ bool FillVideoChatMenu( void FillSenderUserpicMenu( not_null controller, not_null peer, + PeerData *groupPeer, Ui::InputField *fieldForMention, Dialogs::Key searchInEntry, const PeerMenuCallback &addAction); From e85bb2085c6cc63808d7566d7bb731c6204e5842 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 12 Feb 2026 12:23:07 +0400 Subject: [PATCH 085/179] Update API scheme on layer 223. --- Telegram/Resources/langs/lang.strings | 14 ++++---- .../boxes/peers/edit_members_visible.cpp | 32 +------------------ .../boxes/peers/edit_participants_box.cpp | 16 ++-------- .../boxes/peers/edit_peer_permissions_box.cpp | 5 ++- Telegram/SourceFiles/boxes/url_auth_box.cpp | 6 ++-- Telegram/SourceFiles/data/data_channel.cpp | 4 --- Telegram/SourceFiles/data/data_channel.h | 3 -- Telegram/SourceFiles/data/data_chat.cpp | 4 --- Telegram/SourceFiles/data/data_chat.h | 2 -- .../data/data_chat_participant_status.cpp | 12 +++++-- .../data/data_chat_participant_status.h | 2 ++ Telegram/SourceFiles/data/data_peer.cpp | 11 +++++++ Telegram/SourceFiles/data/data_peer.h | 1 + Telegram/SourceFiles/data/data_session.cpp | 12 +++---- .../admin_log/history_admin_log_item.cpp | 2 ++ Telegram/SourceFiles/mtproto/scheme/api.tl | 11 +++---- .../SourceFiles/window/window_peer_menu.cpp | 9 +----- 17 files changed, 55 insertions(+), 91 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index dedca84f8c..3ecd42467f 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -5981,12 +5981,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_rights_edit_admin_header" = "What can this admin do?"; "lng_rights_edit_admin_rank_name" = "Custom title"; "lng_rights_edit_admin_rank_about" = "A title that members will see instead of '{title}'."; -"lng_manage_peer_custom_tags" = "Allow Members Tags"; -"lng_manage_peer_custom_tags_about" = "Turn this on to let members of the group add short tags next to their names."; -"lng_context_add_my_tag" = "Add Tag"; -"lng_context_edit_my_tag" = "Edit Tag"; -"lng_context_add_member_tag" = "Add Member Tag"; -"lng_context_edit_member_tag" = "Edit Member Tag"; +"lng_context_add_my_tag" = "Add tag"; +"lng_context_edit_my_tag" = "Edit tag"; +"lng_context_add_member_tag" = "Add member tag"; +"lng_context_edit_member_tag" = "Edit member tag"; "lng_rights_about_add_admins_yes" = "This admin will be able to add new admins with equal or fewer rights."; "lng_rights_about_add_admins_no" = "This admin will not be able to add new admins."; "lng_rights_about_by" = "This admin promoted by {user} on {date}."; @@ -6079,9 +6077,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_rights_group_pin" = "Pin messages"; "lng_rights_group_topics" = "Manage topics"; "lng_rights_group_add_topics" = "Create topics"; +"lng_rights_group_edit_rank" = "Edit own tags"; "lng_rights_group_manage_calls" = "Manage video chats"; "lng_rights_group_delete" = "Delete messages"; "lng_rights_group_anonymous" = "Remain anonymous"; +"lng_rights_group_manage_ranks" = "Edit member tags"; "lng_rights_add_admins" = "Add new admins"; "lng_rights_chat_send_text" = "Send messages"; "lng_rights_chat_send_media" = "Send media"; @@ -6392,11 +6392,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_admin_log_admin_invite_users" = "Add users"; "lng_admin_log_admin_invite_link" = "Invite users via link"; "lng_admin_log_admin_pin_messages" = "Pin messages"; +"lng_admin_log_banned_edit_rank" = "Edit own tags"; "lng_admin_log_admin_manage_topics" = "Manage topics"; "lng_admin_log_admin_create_topics" = "Create topics"; "lng_admin_log_admin_manage_calls" = "Manage video chats"; "lng_admin_log_admin_manage_calls_channel" = "Manage live streams"; "lng_admin_log_admin_manage_direct" = "Manage direct messages"; +"lng_admin_log_admin_manage_ranks" = "Edit member tags"; "lng_admin_log_admin_add_admins" = "Add new admins"; "lng_admin_log_subscription_extend" = "{name} renewed subscription until {date}"; diff --git a/Telegram/SourceFiles/boxes/peers/edit_members_visible.cpp b/Telegram/SourceFiles/boxes/peers/edit_members_visible.cpp index a2b5af312d..8d042eccfa 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_members_visible.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_members_visible.cpp @@ -37,8 +37,7 @@ namespace { const auto min = EnableHideMembersMin(megagroup); const auto showHideMembers = megagroup->canBanMembers() && megagroup->membersCount() >= min; - const auto showCustomTags = megagroup->amCreator(); - if (!showHideMembers && !showCustomTags) { + if (!showHideMembers) { return { nullptr }; } @@ -80,34 +79,5 @@ namespace { }, button->lifetime()); } - if (showCustomTags) { - Ui::AddSkip(container); - const auto tagsButton = container->add( - EditPeerInfoBox::CreateButton( - container, - tr::lng_manage_peer_custom_tags(), - rpl::single(QString()), - [] {}, - st::manageGroupNoIconButton, - {} - ))->toggleOn(rpl::single(megagroup->customRanksEnabled())); - Ui::AddSkip(container); - Ui::AddDividerText( - container, - tr::lng_manage_peer_custom_tags_about()); - - tagsButton->toggledValue( - ) | rpl::on_next([=](bool toggled) { - megagroup->session().api().request( - MTPmessages_ToggleChatCustomRanks( - megagroup->input(), - MTP_bool(toggled) - ) - ).done([=](const MTPUpdates &result) { - megagroup->session().api().applyUpdates(result); - }).send(); - }, tagsButton->lifetime()); - } - return result; } diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index 28ad8156ea..dc66c5453c 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -1820,25 +1820,13 @@ base::unique_qptr ParticipantsBoxController::rowContextMenu( : &st::menuIconInfo)); } if (user) { - const auto isAdmin = _peer->isChat() - ? (_peer->asChat()->hasAdminRights() - || _peer->asChat()->amCreator()) - : (_peer->isChannel() - ? (_peer->asChannel()->hasAdminRights() - || _peer->asChannel()->amCreator()) - : false); const auto isSelf = user->isSelf(); const auto canEditSelf = isSelf - && (isAdmin - || (_peer->isChat() - ? _peer->asChat()->customRanksEnabled() - : (_peer->isChannel() - ? _peer->asChannel()->customRanksEnabled() - : false))); + && !_peer->amRestricted(ChatRestriction::EditRank); const auto targetIsAdmin = _additional.adminRights(user).has_value() || _additional.isCreator(user); const auto canEditTarget = !isSelf - && isAdmin + && _peer->canManageRanks() && (!targetIsAdmin || _additional.canEditAdmin(user)); if (canEditSelf || canEditTarget) { const auto currentRank = _additional.memberRank(user); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp index df903d6ade..0d48843cd3 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp @@ -96,6 +96,7 @@ constexpr auto kDefaultChargeStars = 10; { Flag::AddParticipants, tr::lng_rights_chat_add_members(tr::now) }, { Flag::CreateTopics, tr::lng_rights_group_add_topics(tr::now) }, { Flag::PinMessages, tr::lng_rights_group_pin(tr::now) }, + { Flag::EditRank, tr::lng_rights_group_edit_rank(tr::now) }, { Flag::ChangeInfo, tr::lng_rights_group_info(tr::now) }, }; if (!options.isForum) { @@ -136,6 +137,7 @@ constexpr auto kDefaultChargeStars = 10; }; auto second = std::vector{ { Flag::ManageCall, tr::lng_rights_group_manage_calls(tr::now) }, + { Flag::ManageRanks, tr::lng_rights_group_manage_ranks(tr::now) }, { Flag::Anonymous, tr::lng_rights_group_anonymous(tr::now) }, { Flag::AddAdmins, tr::lng_rights_add_admins(tr::now) }, }; @@ -315,7 +317,8 @@ ChatRestrictions NegateRestrictions(ChatRestrictions value) { | Flag::SendMusic | Flag::SendVoiceMessages | Flag::SendFiles - | Flag::SendOther); + | Flag::SendOther + | Flag::EditRank); } auto Dependencies(ChatAdminRights) diff --git a/Telegram/SourceFiles/boxes/url_auth_box.cpp b/Telegram/SourceFiles/boxes/url_auth_box.cpp index c6e8085199..ec1fd8e6da 100644 --- a/Telegram/SourceFiles/boxes/url_auth_box.cpp +++ b/Telegram/SourceFiles/boxes/url_auth_box.cpp @@ -281,7 +281,8 @@ void RequestButton( inputPeer, MTP_int(itemId.msg), MTP_int(buttonId), - MTPstring() // #TODO auth url + MTPstring(), // #TODO auth url + MTPstring() )).done([=](const MTPUrlAuthResult &result) { const auto accepted = result.match( [](const MTPDurlAuthResultAccepted &data) { @@ -357,7 +358,8 @@ void RequestUrl( MTPInputPeer(), MTPint(), // msg_id MTPint(), // button_id - MTP_string(url) + MTP_string(url), + MTPstring() )).done([=](const MTPUrlAuthResult &result) { const auto accepted = result.match( [](const MTPDurlAuthResultAccepted &data) { diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index f3df9e3ab2..a79b0ca10b 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -712,10 +712,6 @@ bool ChannelData::allowsForwarding() const { return !(flags() & Flag::NoForwards); } -bool ChannelData::customRanksEnabled() const { - return flags() & Flag::CustomRanksEnabled; -} - bool ChannelData::canViewMembers() const { return (flags() & Flag::CanViewParticipants) && (!(flags() & Flag::ParticipantsHidden) diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index c49e904e16..259900ff60 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -86,7 +86,6 @@ enum class ChannelDataFlag : uint64 { HasStarsPerMessage = (1ULL << 43), StarsPerMessageKnown = (1ULL << 44), HasActiveVideoStream = (1ULL << 45), - CustomRanksEnabled = (1ULL << 46), }; inline constexpr bool is_flag_type(ChannelDataFlag) { return true; }; using ChannelDataFlags = base::flags; @@ -360,8 +359,6 @@ public: [[nodiscard]] bool autoTranslation() const { return flags() & Flag::AutoTranslation; } - [[nodiscard]] bool customRanksEnabled() const; - [[nodiscard]] auto adminRights() const { return _adminRights.current(); } diff --git a/Telegram/SourceFiles/data/data_chat.cpp b/Telegram/SourceFiles/data/data_chat.cpp index 669eac3d10..5105a1b75c 100644 --- a/Telegram/SourceFiles/data/data_chat.cpp +++ b/Telegram/SourceFiles/data/data_chat.cpp @@ -67,10 +67,6 @@ bool ChatData::allowsForwarding() const { return !(flags() & Flag::NoForwards); } -bool ChatData::customRanksEnabled() const { - return flags() & Flag::CustomRanksEnabled; -} - bool ChatData::canEditInformation() const { return amIn() && !amRestricted(ChatRestriction::ChangeInfo); } diff --git a/Telegram/SourceFiles/data/data_chat.h b/Telegram/SourceFiles/data/data_chat.h index d4d5f5f15c..459a29a260 100644 --- a/Telegram/SourceFiles/data/data_chat.h +++ b/Telegram/SourceFiles/data/data_chat.h @@ -23,7 +23,6 @@ enum class ChatDataFlag { CallNotEmpty = (1 << 6), CanSetUsername = (1 << 7), NoForwards = (1 << 8), - CustomRanksEnabled = (1 << 9), }; inline constexpr bool is_flag_type(ChatDataFlag) { return true; }; using ChatDataFlags = base::flags; @@ -100,7 +99,6 @@ public: // Like in ChannelData. [[nodiscard]] bool allowsForwarding() const; - [[nodiscard]] bool customRanksEnabled() const; [[nodiscard]] bool canEditInformation() const; [[nodiscard]] bool canEditPermissions() const; [[nodiscard]] bool canEditUsername() const; diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.cpp b/Telegram/SourceFiles/data/data_chat_participant_status.cpp index 6390f55947..d9b3975d56 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.cpp +++ b/Telegram/SourceFiles/data/data_chat_participant_status.cpp @@ -48,6 +48,9 @@ namespace { | (data.is_delete_stories() ? Flag::DeleteStories : Flag()) | (data.is_manage_direct_messages() ? Flag::ManageDirect + : Flag()) + | (data.is_manage_ranks() + ? Flag::ManageRanks : Flag()); }); } @@ -73,7 +76,8 @@ namespace { | (data.is_change_info() ? Flag::ChangeInfo : Flag()) | (data.is_invite_users() ? Flag::AddParticipants : Flag()) | (data.is_pin_messages() ? Flag::PinMessages : Flag()) - | (data.is_manage_topics() ? Flag::CreateTopics : Flag()); + | (data.is_manage_topics() ? Flag::CreateTopics : Flag()) + | (data.is_edit_rank() ? Flag::EditRank : Flag()); }); } @@ -112,6 +116,9 @@ MTPChatAdminRights AdminRightsToMTP(ChatAdminRightsInfo info) { | ((flags & R::DeleteStories) ? Flag::f_delete_stories : Flag()) | ((flags & R::ManageDirect) ? Flag::f_manage_direct_messages + : Flag()) + | ((flags & R::ManageRanks) + ? Flag::f_manage_ranks : Flag()))); } @@ -143,7 +150,8 @@ MTPChatBannedRights RestrictionsToMTP(ChatRestrictionsInfo info) { | ((flags & R::ChangeInfo) ? Flag::f_change_info : Flag()) | ((flags & R::AddParticipants) ? Flag::f_invite_users : Flag()) | ((flags & R::PinMessages) ? Flag::f_pin_messages : Flag()) - | ((flags & R::CreateTopics) ? Flag::f_manage_topics : Flag())), + | ((flags & R::CreateTopics) ? Flag::f_manage_topics : Flag()) + | ((flags & R::EditRank) ? Flag::f_edit_rank : Flag())), MTP_int(info.until)); } diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.h b/Telegram/SourceFiles/data/data_chat_participant_status.h index 073096b09b..4838876201 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.h +++ b/Telegram/SourceFiles/data/data_chat_participant_status.h @@ -37,6 +37,7 @@ enum class ChatAdminRight { EditStories = (1 << 15), DeleteStories = (1 << 16), ManageDirect = (1 << 17), + ManageRanks = (1 << 18), }; inline constexpr bool is_flag_type(ChatAdminRight) { return true; } using ChatAdminRights = base::flags; @@ -63,6 +64,7 @@ enum class ChatRestriction { AddParticipants = (1 << 15), PinMessages = (1 << 17), CreateTopics = (1 << 18), + EditRank = (1 << 26), }; inline constexpr bool is_flag_type(ChatRestriction) { return true; } using ChatRestrictions = base::flags; diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 8243d72cc0..d2a12f5695 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -1869,6 +1869,17 @@ bool PeerData::canManageGroupCall() const { Unexpected("Peer type in PeerData::canManageGroupCall."); } +bool PeerData::canManageRanks() const { + if (const auto chat = asChat()) { + return chat->amCreator() + || (chat->adminRights() & ChatAdminRight::ManageRanks); + } else if (const auto channel = asChannel()) { + return channel->amCreator() + || (channel->adminRights() & ChatAdminRight::ManageRanks); + } + return false; +} + bool PeerData::amMonoforumAdmin() const { if (const auto channel = asChannel()) { return channel->flags() & ChannelDataFlag::MonoforumAdmin; diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 92623e59cf..5f73007840 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -317,6 +317,7 @@ public: [[nodiscard]] rpl::producer slowmodeAppliedValue() const; [[nodiscard]] int slowmodeSecondsLeft() const; [[nodiscard]] bool canManageGroupCall() const; + [[nodiscard]] bool canManageRanks() const; [[nodiscard]] bool amMonoforumAdmin() const; [[nodiscard]] int starsPerMessage() const; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 55622c912a..b34e7770da 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -866,8 +866,7 @@ not_null Session::processChat(const MTPChat &data) { | Flag::Forbidden | Flag::CallActive | Flag::CallNotEmpty - | Flag::NoForwards - | Flag::CustomRanksEnabled; + | Flag::NoForwards; const auto flagsSet = (data.is_left() ? Flag::Left : Flag()) | (data.is_creator() ? Flag::Creator : Flag()) | (data.is_deactivated() ? Flag::Deactivated : Flag()) @@ -877,8 +876,7 @@ not_null Session::processChat(const MTPChat &data) { && chat->groupCall()->fullCount() > 0)) ? Flag::CallNotEmpty : Flag()) - | (data.is_noforwards() ? Flag::NoForwards : Flag()) - | (data.is_custom_ranks_enabled() ? Flag::CustomRanksEnabled : Flag()); + | (data.is_noforwards() ? Flag::NoForwards : Flag()); chat->setFlags((chat->flags() & ~flagsMask) | flagsSet); chat->count = data.vparticipants_count().v; @@ -999,8 +997,7 @@ not_null Session::processChat(const MTPChat &data) { | Flag::AutoTranslation | Flag::Monoforum | Flag::HasStarsPerMessage - | Flag::StarsPerMessageKnown - | Flag::CustomRanksEnabled; + | Flag::StarsPerMessageKnown; const auto hasStarsPerMessage = data.vsend_paid_messages_stars().has_value(); if (!hasStarsPerMessage) { @@ -1058,8 +1055,7 @@ not_null Session::processChat(const MTPChat &data) { | (channel->starsPerMessageKnown() ? Flag::StarsPerMessageKnown : Flag())) - : Flag::StarsPerMessageKnown) - | (data.is_custom_ranks_enabled() ? Flag::CustomRanksEnabled : Flag()); + : Flag::StarsPerMessageKnown); channel->setFlags((channel->flags() & ~flagsMask) | flagsSet); channel->setBotVerifyDetailsIcon( data.vbot_verification_icon().value_or_empty()); diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index 25b17ea0d4..510d5b5ebd 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -293,6 +293,7 @@ TextWithEntities GenerateAdminChangeText( { Flag::PinMessages, tr::lng_admin_log_admin_pin_messages }, { Flag::ManageCall, tr::lng_admin_log_admin_manage_calls }, { Flag::ManageDirect, tr::lng_admin_log_admin_manage_direct }, + { Flag::ManageRanks, tr::lng_admin_log_admin_manage_ranks }, { Flag::AddAdmins, tr::lng_admin_log_admin_add_admins }, { Flag::Anonymous, tr::lng_admin_log_admin_remain_anonymous }, }; @@ -339,6 +340,7 @@ QString GeneratePermissionsChangeText( { Flag::AddParticipants, tr::lng_admin_log_admin_invite_users }, { Flag::CreateTopics, tr::lng_admin_log_admin_create_topics }, { Flag::PinMessages, tr::lng_admin_log_admin_pin_messages }, + { Flag::EditRank, tr::lng_admin_log_banned_edit_rank }, }; return CollectChanges(phraseMap, prevRights.flags, newRights.flags); } diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index b2dfeded0a..60a71517f1 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -458,7 +458,7 @@ updateStarGiftAuctionUserState#dc58f31e gift_id:long user_state:StarGiftAuctionU updateEmojiGameInfo#fb9c547a info:messages.EmojiGameInfo = Update; updateStarGiftCraftFail#ac072444 = Update; updatePeerHistoryNoForwards#5736b39a flags:# my_enabled:flags.0?true peer_enabled:flags.1?true peer:Peer = Update; -updateChatParticipantRank#33d2633 chat_id:int user_id:int rank:string version:int = Update; +updateChatParticipantRank#bd8367b9 chat_id:long user_id:long rank:string version:int = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -1231,9 +1231,9 @@ chatOnlines#f041e250 onlines:int = ChatOnlines; statsURL#47a971e0 url:string = StatsURL; -chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true post_stories:flags.14?true edit_stories:flags.15?true delete_stories:flags.16?true manage_direct_messages:flags.17?true = ChatAdminRights; +chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true post_stories:flags.14?true edit_stories:flags.15?true delete_stories:flags.16?true manage_direct_messages:flags.17?true manage_ranks:flags.18?true = ChatAdminRights; -chatBannedRights#9f120418 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true send_polls:flags.8?true change_info:flags.10?true invite_users:flags.15?true pin_messages:flags.17?true manage_topics:flags.18?true send_photos:flags.19?true send_videos:flags.20?true send_roundvideos:flags.21?true send_audios:flags.22?true send_voices:flags.23?true send_docs:flags.24?true send_plain:flags.25?true until_date:int = ChatBannedRights; +chatBannedRights#9f120418 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true send_polls:flags.8?true change_info:flags.10?true invite_users:flags.15?true pin_messages:flags.17?true manage_topics:flags.18?true send_photos:flags.19?true send_videos:flags.20?true send_roundvideos:flags.21?true send_audios:flags.22?true send_voices:flags.23?true send_docs:flags.24?true send_plain:flags.25?true edit_rank:flags.26?true until_date:int = ChatBannedRights; inputWallPaper#e630b979 id:long access_hash:long = InputWallPaper; inputWallPaperSlug#72091c80 slug:string = InputWallPaper; @@ -1267,7 +1267,7 @@ folderPeer#e9baa668 peer:Peer folder_id:int = FolderPeer; messages.searchCounter#e844ebff flags:# inexact:flags.1?true filter:MessagesFilter count:int = messages.SearchCounter; -urlAuthResultRequest#32fabf1a flags:# request_write_access:flags.0?true request_phone_number:flags.1?true bot:User domain:string browser:flags.2?string platform:flags.2?string ip:flags.2?string region:flags.2?string = UrlAuthResult; +urlAuthResultRequest#c775e529 flags:# request_write_access:flags.0?true request_phone_number:flags.1?true bot:User domain:string browser:flags.2?string platform:flags.2?string ip:flags.2?string region:flags.2?string match_codes:flags.3?Vector = UrlAuthResult; urlAuthResultAccepted#623a8fa0 flags:# url:flags.0?string = UrlAuthResult; urlAuthResultDefault#a9d6db1f = UrlAuthResult; @@ -2437,7 +2437,7 @@ messages.getEmojiKeywordsLanguages#4e9963b2 lang_codes:Vector = Vector = Vector; messages.requestUrlAuth#198fb446 flags:# peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string = UrlAuthResult; -messages.acceptUrlAuth#b12c7125 flags:# write_allowed:flags.0?true share_phone_number:flags.3?true peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string = UrlAuthResult; +messages.acceptUrlAuth#67a3f0de flags:# write_allowed:flags.0?true share_phone_number:flags.3?true peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string match_code:flags.4?string = UrlAuthResult; messages.hidePeerSettingsBar#4facb138 peer:InputPeer = Bool; messages.getScheduledHistory#f516760b peer:InputPeer hash:long = messages.Messages; messages.getScheduledMessages#bdbb0464 peer:InputPeer id:Vector = messages.Messages; @@ -2570,7 +2570,6 @@ messages.getEmojiGameInfo#fb7e8ca7 = messages.EmojiGameInfo; messages.summarizeText#9d4104e2 flags:# peer:InputPeer id:int to_lang:flags.0?string = TextWithEntities; messages.editChatCreator#f743b857 peer:InputPeer user_id:InputUser password:InputCheckPasswordSRP = Updates; messages.getFutureChatCreatorAfterLeave#3b7d0ea6 peer:InputPeer = User; -messages.toggleChatCustomRanks#341861e6 peer:InputPeer enabled:Bool = Updates; messages.editChatParticipantRank#a00f32b0 peer:InputPeer participant:InputPeer rank:string = Updates; updates.getState#edd4882a = updates.State; diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 600360a928..73b1db002c 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -3826,13 +3826,6 @@ void FillSenderUserpicMenu( if (const auto user = peer->asUser()) { if (groupPeer) { - const auto isAdmin = groupPeer->isChat() - ? (groupPeer->asChat()->hasAdminRights() - || groupPeer->asChat()->amCreator()) - : (groupPeer->isChannel() - ? (groupPeer->asChannel()->hasAdminRights() - || groupPeer->asChannel()->amCreator()) - : false); const auto canEditTarget = [&] { if (const auto chat = groupPeer->asChat()) { if (peerToUser(user->id) == chat->creator) { @@ -3852,7 +3845,7 @@ void FillSenderUserpicMenu( } return false; }(); - if (isAdmin && canEditTarget && !user->isSelf()) { + if (groupPeer->canManageRanks() && canEditTarget && !user->isSelf()) { const auto currentRank = LookupMemberRank( groupPeer, user); From e509aa8072699e3a174b77312df1e7a726a1416c Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 12 Feb 2026 13:14:43 +0400 Subject: [PATCH 086/179] Add "All Messages" fake-topic to bot-forum forwards. --- Telegram/Resources/langs/lang.strings | 1 + .../boxes/peer_list_controllers.cpp | 69 +++++++++++++++++-- .../SourceFiles/boxes/peer_list_controllers.h | 28 ++++++-- Telegram/SourceFiles/boxes/share_box.cpp | 22 +++--- .../SourceFiles/window/window_peer_menu.cpp | 4 +- 5 files changed, 101 insertions(+), 23 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 3ecd42467f..985a57bb8b 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -458,6 +458,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_contacts_loading" = "Loading..."; "lng_contacts_not_found" = "No contacts found"; "lng_topics_not_found" = "No topics found."; +"lng_forum_all_messages" = "All Messages"; "lng_dlg_search_for_messages" = "Search for messages"; "lng_update_telegram" = "Update Telegram"; "lng_dlg_search_in" = "Search messages in"; diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index acca09e832..6979595d4e 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -54,6 +54,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_profile.h" #include "styles/style_dialogs.h" #include "styles/style_chat_helpers.h" +#include "styles/style_menu_icons.h" namespace { @@ -852,7 +853,7 @@ void ChooseRecipientBoxController::rowClicked(not_null row) { const auto peer = row->peer(); if (const auto forum = peer->forum()) { const auto weak = std::make_shared>(); - auto callback = [=](not_null topic) { + auto callback = [=](not_null thread) { const auto exists = guard.get(); if (!exists) { if (*weak) { @@ -861,15 +862,15 @@ void ChooseRecipientBoxController::rowClicked(not_null row) { return; } auto onstack = std::move(_callback); - onstack(topic); + onstack(thread); if (guard) { _callback = std::move(onstack); } else if (*weak) { (*weak)->closeBox(); } }; - const auto filter = [=](not_null topic) { - return guard && (!_filter || _filter(topic)); + const auto filter = [=](not_null thread) { + return guard && (!_filter || _filter(thread)); }; auto owned = Box( std::make_unique( @@ -1095,10 +1096,53 @@ auto ChooseTopicBoxController::Row::generateNameWords() const return _topic->chatListNameWords(); } +ChooseTopicBoxController::AllMessagesRow::AllMessagesRow() +: PeerListRow(PeerListRowId(0)) { + const auto name = tr::lng_forum_all_messages(tr::now); + const auto words = TextUtilities::PrepareSearchWords(name); + for (const auto &word : words) { + _nameWords.emplace(word); + _nameFirstLetters.emplace(word[0]); + } +} + +QString ChooseTopicBoxController::AllMessagesRow::generateName() { + return tr::lng_forum_all_messages(tr::now); +} + +QString ChooseTopicBoxController::AllMessagesRow::generateShortName() { + return tr::lng_forum_all_messages(tr::now); +} + +auto ChooseTopicBoxController::AllMessagesRow::generatePaintUserpicCallback( + bool forceRound) +-> PaintRoundImageCallback { + return []( + Painter &p, + int x, + int y, + int outerWidth, + int size) { + st::menuIconChats.paintInCenter( + p, + QRect(x, y - st::lineWidth, size, size)); + }; +} + +auto ChooseTopicBoxController::AllMessagesRow::generateNameFirstLetters() const +-> const base::flat_set & { + return _nameFirstLetters; +} + +auto ChooseTopicBoxController::AllMessagesRow::generateNameWords() const +-> const base::flat_set & { + return _nameWords; +} + ChooseTopicBoxController::ChooseTopicBoxController( not_null forum, - FnMut)> callback, - Fn)> filter) + FnMut)> callback, + Fn)> filter) : PeerListController(std::make_unique(forum)) , _forum(forum) , _callback(std::move(callback)) @@ -1127,7 +1171,11 @@ Main::Session &ChooseTopicBoxController::session() const { void ChooseTopicBoxController::rowClicked(not_null row) { const auto weak = base::make_weak(this); auto onstack = base::take(_callback); - onstack(static_cast(row.get())->topic()); + if (row->id() == PeerListRowId(0)) { + onstack(_forum->history()); + } else { + onstack(static_cast(row.get())->topic()); + } if (weak) { _callback = std::move(onstack); } @@ -1155,6 +1203,13 @@ void ChooseTopicBoxController::prepare() { void ChooseTopicBoxController::refreshRows(bool initial) { auto added = false; + if (_forum->bot() + && !delegate()->peerListFindRow(PeerListRowId(0)) + && (!_filter || _filter(_forum->history()))) { + delegate()->peerListAppendRow( + std::make_unique()); + added = true; + } for (const auto &row : _forum->topicsList()->indexed()->all()) { if (const auto topic = row->topic()) { const auto id = topic->rootId().bare; diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.h b/Telegram/SourceFiles/boxes/peer_list_controllers.h index fa02b17a58..73defcdb4c 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.h +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.h @@ -354,8 +354,8 @@ class ChooseTopicBoxController final public: ChooseTopicBoxController( not_null forum, - FnMut)> callback, - Fn)> filter = nullptr); + FnMut)> callback, + Fn)> filter = nullptr); Main::Session &session() const override; void rowClicked(not_null row) override; @@ -391,13 +391,33 @@ private: }; + class AllMessagesRow final : public PeerListRow { + public: + AllMessagesRow(); + + QString generateName() override; + QString generateShortName() override; + PaintRoundImageCallback generatePaintUserpicCallback( + bool forceRound) override; + + auto generateNameFirstLetters() const + -> const base::flat_set & override; + auto generateNameWords() const + -> const base::flat_set & override; + + private: + base::flat_set _nameFirstLetters; + base::flat_set _nameWords; + + }; + void refreshRows(bool initial = false); [[nodiscard]] std::unique_ptr createRow( not_null topic); const not_null _forum; - FnMut)> _callback; - Fn)> _filter; + FnMut)> _callback; + Fn)> _filter; }; diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index 2dc83c148f..9f4deabc9d 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -1379,24 +1379,26 @@ void ShareBox::Inner::changeCheckState(Chat *chat) { void ShareBox::Inner::chooseForumTopic(not_null forum) { const auto guard = base::make_weak(this); const auto weak = std::make_shared>(); - auto chosen = [=](not_null topic) { + auto chosen = [=](not_null thread) { if (const auto strong = *weak) { strong->closeBox(); } if (!guard) { return; } - const auto row = _chatsIndexed->getRow(topic->owningHistory()); + const auto row = _chatsIndexed->getRow(thread->owningHistory()); if (!row) { return; } const auto chat = getChat(row); - Assert(!chat->topic); - chat->topic = topic; - chat->topic->destroyed( - ) | rpl::on_next([=] { - changePeerCheckState(chat, false); - }, chat->topicLifetime); + if (const auto topic = thread->asTopic()) { + Assert(!chat->topic); + chat->topic = topic; + chat->topic->destroyed( + ) | rpl::on_next([=] { + changePeerCheckState(chat, false); + }, chat->topicLifetime); + } updateChatName(chat); changePeerCheckState(chat, true); }; @@ -1410,8 +1412,8 @@ void ShareBox::Inner::chooseForumTopic(not_null forum) { box->closeBox(); }, box->lifetime()); }; - auto filter = [=](not_null topic) { - return guard && _descriptor.filterCallback(topic); + auto filter = [=](not_null thread) { + return guard && _descriptor.filterCallback(thread); }; auto box = Box( std::make_unique( diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 73b1db002c..61cde97e4e 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -3276,9 +3276,9 @@ base::weak_qptr ShowDropMediaBox( callback = std::move(successCallback), weak, navigation - ](not_null topic) mutable { + ](not_null thread) mutable { const auto content = navigation->parentController()->content(); - if (!content->filesOrForwardDrop(topic, data.get())) { + if (!content->filesOrForwardDrop(thread, data.get())) { return; } else if (const auto strong = *weak) { strong->closeBox(); From 3dba01535b4c85d25e7cab5b2aa3ff6701b5b052 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 12 Feb 2026 14:18:36 +0400 Subject: [PATCH 087/179] Update API scheme on layer 223. --- Telegram/SourceFiles/mtproto/scheme/api.tl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 60a71517f1..8a0284a896 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -99,9 +99,9 @@ userStatusLastWeek#541a1d1a flags:# by_me:flags.0?true = UserStatus; userStatusLastMonth#65899777 flags:# by_me:flags.0?true = UserStatus; chatEmpty#29562865 id:long = Chat; -chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true custom_ranks_enabled:flags.19?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; +chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; chatForbidden#6592a1a7 id:long title:string = Chat; -channel#1c32b11c flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true signature_profiles:flags2.12?true autotranslation:flags2.15?true broadcast_messages_allowed:flags2.16?true monoforum:flags2.17?true forum_tabs:flags2.19?true custom_ranks_enabled:flags2.20?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector stories_max_id:flags2.4?RecentStory color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int subscription_until_date:flags2.11?int bot_verification_icon:flags2.13?long send_paid_messages_stars:flags2.14?long linked_monoforum_id:flags2.18?long = Chat; +channel#1c32b11c flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true signature_profiles:flags2.12?true autotranslation:flags2.15?true broadcast_messages_allowed:flags2.16?true monoforum:flags2.17?true forum_tabs:flags2.19?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector stories_max_id:flags2.4?RecentStory color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int subscription_until_date:flags2.11?int bot_verification_icon:flags2.13?long send_paid_messages_stars:flags2.14?long linked_monoforum_id:flags2.18?long = Chat; channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true monoforum:flags.10?true id:long access_hash:long title:string until_date:flags.16?int = Chat; chatFull#2633421b flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull; @@ -729,7 +729,7 @@ messageEntityBankCard#761e6af4 offset:int length:int = MessageEntity; messageEntitySpoiler#32ca960f offset:int length:int = MessageEntity; messageEntityCustomEmoji#c8cf05f8 offset:int length:int document_id:long = MessageEntity; messageEntityBlockquote#f1ccaaac flags:# collapsed:flags.0?true offset:int length:int = MessageEntity; -messageEntityFormattedDate#904ac7c7 flags:# relative:flags.0?true short_time:flags.1?true long_time:flags.2?true short_date:flags.3?true long_date:flags.4?true offset:int length:int date:int = MessageEntity; +messageEntityFormattedDate#904ac7c7 flags:# relative:flags.0?true short_time:flags.1?true long_time:flags.2?true short_date:flags.3?true long_date:flags.4?true day_of_week:flags.5?true offset:int length:int date:int = MessageEntity; inputChannelEmpty#ee8c1e86 = InputChannel; inputChannel#f35aec28 channel_id:long access_hash:long = InputChannel; @@ -1267,7 +1267,7 @@ folderPeer#e9baa668 peer:Peer folder_id:int = FolderPeer; messages.searchCounter#e844ebff flags:# inexact:flags.1?true filter:MessagesFilter count:int = messages.SearchCounter; -urlAuthResultRequest#c775e529 flags:# request_write_access:flags.0?true request_phone_number:flags.1?true bot:User domain:string browser:flags.2?string platform:flags.2?string ip:flags.2?string region:flags.2?string match_codes:flags.3?Vector = UrlAuthResult; +urlAuthResultRequest#f8f8eb1e flags:# request_write_access:flags.0?true request_phone_number:flags.1?true bot:User domain:string browser:flags.2?string platform:flags.2?string ip:flags.2?string region:flags.2?string match_codes:flags.3?Vector user_id_hint:flags.4?long = UrlAuthResult; urlAuthResultAccepted#623a8fa0 flags:# url:flags.0?string = UrlAuthResult; urlAuthResultDefault#a9d6db1f = UrlAuthResult; @@ -2571,6 +2571,7 @@ messages.summarizeText#9d4104e2 flags:# peer:InputPeer id:int to_lang:flags.0?st messages.editChatCreator#f743b857 peer:InputPeer user_id:InputUser password:InputCheckPasswordSRP = Updates; messages.getFutureChatCreatorAfterLeave#3b7d0ea6 peer:InputPeer = User; messages.editChatParticipantRank#a00f32b0 peer:InputPeer participant:InputPeer rank:string = Updates; +messages.declineUrlAuth#35436bbc url:string = Bool; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; From e9fced5c2cf4c41f9e5d0d63e2ed3009e7c0aa2d Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 13 Feb 2026 14:38:06 +0400 Subject: [PATCH 088/179] Nice design of admin/member tags. --- .../SourceFiles/api/api_chat_participants.cpp | 44 ++-- .../boxes/peers/add_participants_box.cpp | 2 +- .../boxes/peers/edit_participants_box.cpp | 81 +++--- .../boxes/peers/edit_participants_box.h | 6 +- .../chat_helpers/field_autocomplete.cpp | 2 +- Telegram/SourceFiles/data/data_channel.h | 4 +- .../SourceFiles/data/data_channel_admins.cpp | 19 +- .../SourceFiles/data/data_channel_admins.h | 2 +- .../history/view/history_view_message.cpp | 120 ++++++--- .../history/view/history_view_message.h | 4 +- Telegram/SourceFiles/info/info.style | 2 + .../info_profile_members_controllers.cpp | 232 ++++++++++++++++-- .../info_profile_members_controllers.h | 44 +++- Telegram/SourceFiles/ui/chat/chat.style | 1 + 14 files changed, 454 insertions(+), 109 deletions(-) diff --git a/Telegram/SourceFiles/api/api_chat_participants.cpp b/Telegram/SourceFiles/api/api_chat_participants.cpp index a1d0a8cb94..155b326716 100644 --- a/Telegram/SourceFiles/api/api_chat_participants.cpp +++ b/Telegram/SourceFiles/api/api_chat_participants.cpp @@ -56,35 +56,43 @@ void ApplyMegagroupAdmins(not_null channel, Members list) { i->tryApplyCreatorTo(channel); } else { channel->mgInfo->creator = nullptr; - channel->mgInfo->creatorRank = QString(); } - auto adding = base::flat_map(); + auto adding = base::flat_set(); + auto addingRanks = base::flat_map(); for (const auto &p : list) { if (p.isUser()) { - adding.emplace(p.userId(), p.rank()); + adding.emplace(p.userId()); + if (!p.rank().isEmpty()) { + addingRanks.emplace(p.userId(), p.rank()); + } } } if (channel->mgInfo->creator) { - adding.emplace( - peerToUser(channel->mgInfo->creator->id), - channel->mgInfo->creatorRank); + const auto creatorId = peerToUser(channel->mgInfo->creator->id); + adding.emplace(creatorId); + const auto r = channel->mgInfo->memberRanks.find(creatorId); + if (r != channel->mgInfo->memberRanks.end() + && !r->second.isEmpty()) { + addingRanks.emplace(creatorId, r->second); + } } auto removing = channel->mgInfo->admins; if (removing.empty() && adding.empty()) { - // Add some admin-placeholder so we don't DDOS - // server with admins list requests. LOG(("API Error: Got empty admins list from server.")); - adding.emplace(0, QString()); + adding.emplace(UserId(0)); } Data::ChannelAdminChanges changes(channel); - for (const auto &[addingId, rank] : adding) { - if (!removing.remove(addingId)) { - changes.add(addingId, rank); - } + for (const auto &addingId : adding) { + const auto r = addingRanks.find(addingId); + const auto rank = (r != end(addingRanks)) + ? r->second + : QString(); + removing.remove(addingId); + changes.add(addingId, rank); } - for (const auto &[removingId, rank] : removing) { + for (const auto &removingId : removing) { changes.remove(removingId); } } @@ -149,7 +157,7 @@ void ApplyLastList( channel->mgInfo->botStatus = 2; } } - if (!p.rank().isEmpty() && !p.isCreatorOrAdmin()) { + if (!p.rank().isEmpty()) { channel->mgInfo->memberRanks[p.userId()] = p.rank(); } } @@ -337,7 +345,11 @@ void ChatParticipant::tryApplyCreatorTo( if (isCreator() && isUser()) { if (const auto info = channel->mgInfo.get()) { info->creator = channel->owner().userLoaded(userId()); - info->creatorRank = rank(); + if (!rank().isEmpty()) { + info->memberRanks[userId()] = rank(); + } else { + info->memberRanks.remove(userId()); + } } } } diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp index 5355138ac8..018ea98b65 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp @@ -1500,7 +1500,7 @@ void AddSpecialBoxController::showAdmin( _peer, user, currentRights, - _additional.adminRank(user), + _additional.memberRank(user), _additional.adminPromotedSince(user), _additional.adminPromotedBy(user)); const auto show = delegate()->peerListUiShow(); diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index dc66c5453c..2a1df5285f 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/data_changes.h" #include "base/unixtime.h" +#include "ui/chat/chat_style.h" #include "ui/effects/outline_segments.h" #include "ui/layers/generic_box.h" #include "ui/widgets/fields/input_field.h" @@ -428,12 +429,6 @@ auto ParticipantsAdditionalData::adminRights( : std::nullopt; } -QString ParticipantsAdditionalData::adminRank( - not_null user) const { - const auto i = _adminRanks.find(user); - return (i != end(_adminRanks)) ? i->second : QString(); -} - QString ParticipantsAdditionalData::memberRank( not_null user) const { const auto i = _memberRanks.find(user); @@ -511,7 +506,6 @@ void ParticipantsAdditionalData::setExternal( _adminRights.erase(user); _adminCanEdit.erase(user); _adminPromotedBy.erase(user); - _adminRanks.erase(user); _admins.erase(user); } _restrictedRights.erase(participant); @@ -565,11 +559,10 @@ void ParticipantsAdditionalData::fillFromChannel( } if (information->creator) { _creator = information->creator; - _adminRanks[information->creator] = information->creatorRank; } for (const auto user : information->lastParticipants) { const auto admin = information->lastAdmins.find(user); - const auto rank = information->admins.find(peerToUser(user->id)); + const auto rank = information->memberRanks.find(peerToUser(user->id)); const auto restricted = information->lastRestricted.find(user); if (admin != information->lastAdmins.cend()) { _restrictedRights.erase(user); @@ -581,15 +574,14 @@ void ParticipantsAdditionalData::fillFromChannel( _adminCanEdit.erase(user); } _adminRights.emplace(user, admin->second.rights); - if (rank != end(information->admins) + if (rank != end(information->memberRanks) && !rank->second.isEmpty()) { - _adminRanks[user] = rank->second; + _memberRanks[user] = rank->second; } } else if (restricted != information->lastRestricted.cend()) { _adminRights.erase(user); _adminCanEdit.erase(user); _adminPromotedBy.erase(user); - _adminRanks.erase(user); _restrictedRights.emplace(user, restricted->second.rights); } } @@ -727,7 +719,7 @@ PeerData *ParticipantsAdditionalData::applyParticipant( Unexpected("Api::ChatParticipant::type in applyParticipant."); }(); if (const auto user = result ? result->asUser() : nullptr) { - if (!data.rank().isEmpty() && !data.isCreatorOrAdmin()) { + if (!data.rank().isEmpty()) { _memberRanks[user] = data.rank(); } else { _memberRanks.remove(user); @@ -746,11 +738,6 @@ UserData *ParticipantsAdditionalData::applyCreator( } else { _adminCanEdit.erase(user); } - if (!data.rank().isEmpty()) { - _adminRanks[user] = data.rank(); - } else { - _adminRanks.remove(user); - } return user; } return nullptr; @@ -777,11 +764,6 @@ UserData *ParticipantsAdditionalData::applyAdmin( } else { _adminCanEdit.erase(user); } - if (!data.rank().isEmpty()) { - _adminRanks[user] = data.rank(); - } else { - _adminRanks.remove(user); - } if (data.promotedSince()) { _adminPromotedSince[user] = data.promotedSince(); } else { @@ -815,7 +797,6 @@ UserData *ParticipantsAdditionalData::applyRegular(UserId userId) { _adminRights.erase(user); _adminCanEdit.erase(user); _adminPromotedBy.erase(user); - _adminRanks.erase(user); _restrictedRights.erase(user); _kicked.erase(user); _restrictedBy.erase(user); @@ -834,7 +815,6 @@ PeerData *ParticipantsAdditionalData::applyBanned( _adminRights.erase(user); _adminCanEdit.erase(user); _adminPromotedBy.erase(user); - _adminRanks.erase(user); } if (data.isKicked()) { _kicked.emplace(participant); @@ -949,6 +929,8 @@ ParticipantsBoxController::ParticipantsBoxController( : ParticipantsBoxController(CreateTag(), navigation, peer, role) { } +ParticipantsBoxController::~ParticipantsBoxController() = default; + ParticipantsBoxController::ParticipantsBoxController( CreateTag, Window::SessionNavigation *navigation, @@ -959,7 +941,9 @@ ParticipantsBoxController::ParticipantsBoxController( , _peer(peer) , _api(&_peer->session().mtp()) , _role(role) -, _additional(peer, _role) { +, _additional(peer, _role) +, _chatStyle( + std::make_unique(peer->session().colorIndicesValue())) { subscribeToMigration(); if (_role == Role::Profile) { setupListChangeViewers(); @@ -1381,6 +1365,10 @@ void ParticipantsBoxController::prepare() { recomputeTypeFor(update.user); refreshRows(); }, lifetime()); + + style::PaletteChanged() | rpl::on_next([=] { + _pillCircleCache.clear(); + }, lifetime()); } void ParticipantsBoxController::unload() { @@ -1741,6 +1729,26 @@ void ParticipantsBoxController::rowRightActionClicked( const auto participant = row->peer(); const auto user = participant->asUser(); if (_role == Role::Members || _role == Role::Profile) { + if (_role == Role::Profile && user) { + const auto memberRow = static_cast(row.get()); + if (memberRow->type().canAddTag) { + const auto show = delegate()->peerListUiShow(); + const auto peer = _peer; + _editBox = show->show(Box( + EditCustomRankBox, + show, + peer, + user, + QString(), + true, + crl::guard(this, [=](const QString &rank) { + _additional.applyMemberRankLocally(user, rank); + recomputeTypeFor(user); + refreshRows(); + }))); + return; + } + } kickParticipant(participant); } else if (_role == Role::Admins) { Assert(user != nullptr); @@ -1921,7 +1929,7 @@ void ParticipantsBoxController::showAdmin(not_null user) { _peer, user, currentRights, - _additional.adminRank(user), + _additional.memberRank(user), _additional.adminPromotedSince(user), _additional.adminPromotedBy(user)); if (_additional.canAddOrEditAdmin(user)) { @@ -2245,7 +2253,10 @@ std::unique_ptr ParticipantsBoxController::createRow( auto ParticipantsBoxController::computeType( not_null participant) const -> Type { const auto user = participant->asUser(); - auto result = Type(); + auto result = Type{ + .chatStyle = _chatStyle.get(), + .circleCache = &_pillCircleCache, + }; result.rights = (user && _additional.isCreator(user)) ? Rights::Creator : (user && _additional.adminRights(user).has_value()) @@ -2255,10 +2266,15 @@ auto ParticipantsBoxController::computeType( && user && user->isInaccessible(); if (!result.canRemove) { - const auto aRank = user ? _additional.adminRank(user) : QString(); - result.adminRank = !aRank.isEmpty() - ? aRank - : (user ? _additional.memberRank(user) : QString()); + result.rank = user ? _additional.memberRank(user) : QString(); + } + if (user + && user->isSelf() + && result.rank.isEmpty() + && !result.canRemove + && result.rights == Rights::Normal + && !_peer->amRestricted(ChatRestriction::EditRank)) { + result.canAddTag = true; } return result; } @@ -2271,6 +2287,7 @@ void ParticipantsBoxController::recomputeTypeFor( const auto row = delegate()->peerListFindRow(participant->id.value); if (row) { static_cast(row)->setType(computeType(participant)); + delegate()->peerListUpdateRow(row); } } diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h index 8bfa406f84..bbd74b1a90 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h @@ -115,7 +115,6 @@ public: not_null participant) const; [[nodiscard]] std::optional adminRights( not_null user) const; - [[nodiscard]] QString adminRank(not_null user) const; [[nodiscard]] QString memberRank(not_null user) const; [[nodiscard]] std::optional restrictedRights( not_null participant) const; @@ -161,7 +160,6 @@ private: // Data for channels. base::flat_map, ChatAdminRightsInfo> _adminRights; - base::flat_map, QString> _adminRanks; base::flat_map, QString> _memberRanks; base::flat_map, TimeId> _adminPromotedSince; base::flat_map, TimeId> _restrictedSince; @@ -192,6 +190,7 @@ public: not_null navigation, not_null peer, Role role); + ~ParticipantsBoxController(); Main::Session &session() const override; void prepare() override; @@ -322,6 +321,9 @@ private: std::unique_ptr _stories; + std::unique_ptr _chatStyle; + mutable base::flat_map _pillCircleCache; + }; // Members, banned and restricted users server side search. diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index b80e9c354a..905957f084 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -516,7 +516,7 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { _channel->session().api().chatParticipants().requestAdmins(_channel); } else { mrows.reserve(mrows.size() + _channel->mgInfo->admins.size()); - for (const auto &[userId, rank] : _channel->mgInfo->admins) { + for (const auto &userId : _channel->mgInfo->admins) { if (const auto user = _channel->owner().userLoaded(userId)) { if (user->isInaccessible()) continue; if (!listAllSuggestions && filterNotPassedByName(user)) continue; diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 259900ff60..38078150a6 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -141,12 +141,10 @@ public: base::flat_set> bots; rpl::event_stream unrestrictedByBoostsChanges; - // For admin badges, full admins list with ranks. - base::flat_map admins; + base::flat_set admins; base::flat_map memberRanks; UserData *creator = nullptr; // nullptr means unknown - QString creatorRank; int botStatus = 0; // -1 - no bots, 0 - unknown, 1 - one bot, that sees all history, 2 - other bool joinedMessageFound = false; bool adminsLoaded = false; diff --git a/Telegram/SourceFiles/data/data_channel_admins.cpp b/Telegram/SourceFiles/data/data_channel_admins.cpp index 6dd286573e..a14b548c52 100644 --- a/Telegram/SourceFiles/data/data_channel_admins.cpp +++ b/Telegram/SourceFiles/data/data_channel_admins.cpp @@ -20,16 +20,25 @@ ChannelAdminChanges::ChannelAdminChanges(not_null channel) } void ChannelAdminChanges::add(UserId userId, const QString &rank) { - const auto i = _admins.find(userId); - if (i == end(_admins) || i->second != rank) { - _admins[userId] = rank; + if (_admins.emplace(userId).second) { _changes.emplace(userId); } + auto &ranks = _channel->mgInfo->memberRanks; + if (!rank.isEmpty()) { + const auto i = ranks.find(userId); + if (i == end(ranks) || i->second != rank) { + ranks[userId] = rank; + _changes.emplace(userId); + } + } else { + if (ranks.remove(userId)) { + _changes.emplace(userId); + } + } } void ChannelAdminChanges::remove(UserId userId) { - if (_admins.contains(userId)) { - _admins.remove(userId); + if (_admins.remove(userId)) { _changes.emplace(userId); } } diff --git a/Telegram/SourceFiles/data/data_channel_admins.h b/Telegram/SourceFiles/data/data_channel_admins.h index b069e57a49..b37f67d6ff 100644 --- a/Telegram/SourceFiles/data/data_channel_admins.h +++ b/Telegram/SourceFiles/data/data_channel_admins.h @@ -20,7 +20,7 @@ public: private: not_null _channel; - base::flat_map &_admins; + base::flat_set &_admins; base::flat_set _changes; }; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 2d7e3ec04b..41266d6f97 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -258,16 +258,19 @@ void Message::initPaidInformation() { void Message::refreshRightBadge() { const auto item = data(); - const auto text = [&] { + const auto [text, isAdmin] = [&]() -> std::pair { if (item->isDiscussionPost()) { - return (delegate()->elementContext() == Context::Replies) - ? QString() - : tr::lng_channel_badge(tr::now); + return { + (delegate()->elementContext() == Context::Replies) + ? QString() + : tr::lng_channel_badge(tr::now), + false, + }; } else if (item->author()->isMegagroup()) { if (const auto msgsigned = item->Get()) { if (!msgsigned->viaBusinessBot) { Assert(msgsigned->isAnonymousRank); - return msgsigned->author; + return { msgsigned->author, false }; } } } @@ -279,47 +282,43 @@ void Message::refreshRightBadge() { const auto j = chat->memberRanks.find( peerToUser(user->id)); if (j != chat->memberRanks.end()) { - return j->second; + return { j->second, false }; } } } - return QString(); + return { QString(), false }; } if (!user) { - return QString(); + return { QString(), false }; } const auto info = channel->mgInfo.get(); const auto userId = peerToUser(user->id); - const auto i = info->admins.find(userId); - const auto custom = (i != info->admins.end()) - ? i->second - : (info->creator == user) - ? info->creatorRank + const auto isAdmin = info->admins.contains(userId); + const auto r = info->memberRanks.find(userId); + const auto custom = (r != info->memberRanks.end()) + ? r->second : QString(); if (!custom.isEmpty()) { - return custom; + return { custom, isAdmin || info->creator == user }; } if (info->creator == user) { - return tr::lng_owner_badge(tr::now); + return { tr::lng_owner_badge(tr::now), true }; } - if (i != info->admins.end()) { - return tr::lng_admin_badge(tr::now); + if (isAdmin) { + return { tr::lng_admin_badge(tr::now), true }; } const auto fromRank = item->fromRank(); if (!fromRank.isEmpty()) { - return fromRank; + return { fromRank, false }; } - const auto j = info->memberRanks.find(userId); - if (j != info->memberRanks.end()) { - return j->second; - } - return QString(); + return { QString(), false }; }(); auto badge = TextWithEntities{ (text.isEmpty() ? delegate()->elementAuthorRank(this) : TextUtilities::RemoveEmoji(TextUtilities::SingleLine(text))) }; + _rightBadgeIsAdmin = isAdmin ? 1 : 0; _rightBadgeHasBoosts = 0; if (const auto boosts = item->boostsApplied()) { _rightBadgeHasBoosts = 1; @@ -341,6 +340,20 @@ void Message::refreshRightBadge() { } } +int Message::rightBadgeWidth() const { + if (!_rightBadgeIsAdmin) { + return _rightBadge.maxWidth(); + } + const auto &padding = st::msgTagBadgePadding; + const auto textWidth = padding.left() + + _rightBadge.maxWidth() + + padding.right(); + const auto pillHeight = padding.top() + + st::msgFont->height + + padding.bottom(); + return std::max(textWidth, pillHeight); +} + void Message::applyGroupAdminChanges( const base::flat_set &changes) { if (!data()->out() @@ -713,9 +726,8 @@ QSize Message::performCountOptimalSize() { ? st::msgFont->width(FastReplyText()) : 0; if (!_rightBadge.isEmpty()) { - const auto badgeWidth = _rightBadge.maxWidth(); namew += st::msgPadding.right() - + std::max(badgeWidth, replyWidth); + + std::max(rightBadgeWidth(), replyWidth); } else if (replyWidth) { namew += st::msgPadding.right() + replyWidth; } @@ -1602,7 +1614,9 @@ void Message::paintFromName( if (!displayFromName()) { return; } - const auto badgeWidth = _rightBadge.isEmpty() ? 0 : _rightBadge.maxWidth(); + const auto badgeWidth = _rightBadge.isEmpty() + ? 0 + : rightBadgeWidth(); const auto replyWidth = [&] { if (isUnderCursor()) { if (displayFastForward()) { @@ -1710,6 +1724,56 @@ void Message::paintFromName( trect.left() + trect.width() - rightWidth, trect.top() + st::msgFont->ascent, hasFastForward() ? FastForwardText() : FastReplyText()); + } else if (_rightBadgeIsAdmin) { + const auto nameColor = FromNameFg( + context, + colorIndex(), + colorCollectible()); + auto bgColor = nameColor; + bgColor.setAlphaF(0.15); + const auto &padding = st::msgTagBadgePadding; + const auto textWidth = _rightBadge.maxWidth(); + const auto contentWidth = padding.left() + + textWidth + + padding.right(); + const auto pillHeight = padding.top() + + st::msgFont->height + + padding.bottom(); + const auto totalWidth = std::max(contentWidth, pillHeight); + const auto badgeLeft = trect.left() + + trect.width() + - totalWidth; + const auto badgeTop = trect.top() + + (st::msgNameFont->height - pillHeight) / 2; + const auto pillRect = QRect( + badgeLeft, + badgeTop, + totalWidth, + pillHeight); + p.setPen(Qt::NoPen); + p.setBrush(bgColor); + { + auto hq = PainterHighQualityEnabler(p); + p.drawRoundedRect( + pillRect, + pillHeight / 2., + pillHeight / 2.); + } + p.setPen(nameColor); + const auto boostPen = !_rightBadgeHasBoosts + ? QPen() + : QPen(nameColor); + auto colored = std::array{ + { { &boostPen, &boostPen } }, + }; + _rightBadge.draw(p, { + .position = QPoint( + badgeLeft + (totalWidth - textWidth) / 2, + badgeTop + padding.top()), + .availableWidth = textWidth, + .colors = colored, + .now = context.now, + }); } else { const auto shift = QPoint(trect.width() - rightWidth, 0); const auto pen = !_rightBadgeHasBoosts @@ -4195,8 +4259,8 @@ void Message::fromNameUpdated(int width) const { ? st::msgFont->width(FastReplyText()) : 0; if (!_rightBadge.isEmpty()) { - const auto badgeWidth = _rightBadge.maxWidth(); - width -= st::msgPadding.right() + std::max(badgeWidth, replyWidth); + width -= st::msgPadding.right() + + std::max(rightBadgeWidth(), replyWidth); } else if (replyWidth) { width -= st::msgPadding.right() + replyWidth; } diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index 0f1f11da16..b4e04c936d 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -323,6 +323,7 @@ private: void psaTooltipToggled(bool shown) const; void refreshRightBadge(); + [[nodiscard]] int rightBadgeWidth() const; void validateFromNameText(PeerData *from) const; void ensureFromNameStatusLink(not_null peer) const; @@ -338,10 +339,11 @@ private: mutable std::unique_ptr _selectionRoundCheckbox; Ui::Text::String _rightBadge; mutable int _fromNameVersion = 0; - uint32 _bubbleWidthLimit : 28 = 0; + uint32 _bubbleWidthLimit : 27 = 0; uint32 _invertMedia : 1 = 0; uint32 _hideReply : 1 = 0; uint32 _rightBadgeHasBoosts : 1 = 0; + uint32 _rightBadgeIsAdmin : 1 = 0; uint32 _postShowingAuthor : 1 = 0; BottomInfo _bottomInfo; diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index 49da24caf3..f2828617de 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -1458,3 +1458,5 @@ earnTonIconMargin: margins(0px, 2px, 0px, 0px); infoMusicButtonRipple: universalRippleAnimation; infoMusicButtonPadding: margins(16px, 6px, 13px, 6px); + +memberTagPillPadding: margins(5px, 2px, 5px, 2px); diff --git a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp index ff5e942166..908210632b 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp @@ -13,9 +13,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "ui/unread_badge.h" #include "lang/lang_keys.h" +#include "ui/chat/chat_style.h" +#include "ui/effects/ripple_animation.h" +#include "ui/painter.h" #include "styles/style_info.h" #include "styles/style_boxes.h" +#include "styles/style_chat.h" #include "styles/style_dialogs.h" +#include "styles/style_widgets.h" namespace Info { namespace Profile { @@ -28,17 +33,36 @@ MemberListRow::MemberListRow( setType(type); } +MemberListRow::~MemberListRow() = default; + void MemberListRow::setType(Type type) { _type = type; - PeerListRowWithLink::setActionLink(_type.canRemove - ? tr::lng_profile_delete_removed(tr::now) - : !_type.adminRank.isEmpty() - ? _type.adminRank - : (_type.rights == Rights::Creator) - ? tr::lng_owner_badge(tr::now) - : (_type.rights == Rights::Admin) - ? tr::lng_admin_badge(tr::now) - : QString()); + _actionRipple = nullptr; + if (_type.canRemove) { + _tagMode = TagMode::Remove; + _actionText = tr::lng_profile_delete_removed(tr::now); + } else if (_type.canAddTag) { + _tagMode = TagMode::AddTag; + _actionText = tr::lng_context_add_my_tag(tr::now); + } else if (!_type.rank.isEmpty()) { + _tagMode = (_type.rights == Rights::Admin + || _type.rights == Rights::Creator) + ? TagMode::AdminPill + : TagMode::NormalText; + _actionText = _type.rank; + } else if (_type.rights == Rights::Creator) { + _tagMode = TagMode::AdminPill; + _actionText = tr::lng_owner_badge(tr::now); + } else if (_type.rights == Rights::Admin) { + _tagMode = TagMode::AdminPill; + _actionText = tr::lng_admin_badge(tr::now); + } else { + _tagMode = TagMode::None; + _actionText = QString(); + } + _actionTextWidth = _actionText.isEmpty() + ? 0 + : st::normalFont->width(_actionText); } MemberListRow::Type MemberListRow::type() const { @@ -46,23 +70,44 @@ MemberListRow::Type MemberListRow::type() const { } bool MemberListRow::rightActionDisabled() const { + if (_tagMode == TagMode::AddTag || _tagMode == TagMode::Remove) { + return false; + } return !canRemove(); } +QSize MemberListRow::rightActionSize() const { + if (_actionTextWidth == 0) { + return QSize(); + } + switch (_tagMode) { + case TagMode::Remove: + case TagMode::AdminPill: + case TagMode::AddTag: { + const auto &p = st::memberTagPillPadding; + const auto h = p.top() + st::normalFont->height + p.bottom(); + const auto w = p.left() + _actionTextWidth + p.right(); + return QSize(std::max(w, h), h); + } + case TagMode::NormalText: + return QSize(_actionTextWidth, st::normalFont->height); + case TagMode::None: + return QSize(); + } + return QSize(); +} + QMargins MemberListRow::rightActionMargins() const { const auto skip = st::contactsCheckPosition.x(); - if (canRemove()) { - const auto &st = st::defaultPeerListItem; - return QMargins( - skip, - (st.height - st.nameStyle.font->height) / 2, - st.photoPosition.x() + skip, - 0); + const auto &st = st::defaultPeerListItem; + const auto size = rightActionSize(); + if (size.isEmpty()) { + return QMargins(); } return QMargins( skip, - st::defaultPeerListItem.namePosition.y(), - st::defaultPeerListItem.photoPosition.x() + skip, + (st.height - size.height()) / 2, + st.photoPosition.x() + skip, 0); } @@ -86,6 +131,157 @@ bool MemberListRow::canRemove() const { return _type.canRemove; } +int MemberListRow::pillHeight() const { + const auto &p = st::memberTagPillPadding; + return p.top() + st::normalFont->height + p.bottom(); +} + +const QImage &MemberListRow::ensurePillCircle(QRgb color) const { + auto &cache = *_type.circleCache; + const auto it = cache.find(color); + if (it != end(cache)) { + return it->second; + } + const auto h = pillHeight(); + const auto ratio = style::DevicePixelRatio(); + auto image = QImage( + QSize(h, h) * ratio, + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(ratio); + image.fill(Qt::transparent); + { + auto p = QPainter(&image); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(QColor::fromRgba(color)); + p.drawEllipse(0, 0, h, h); + } + return cache.emplace(color, std::move(image)).first->second; +} + +void MemberListRow::paintPill( + Painter &p, + int x, + int y, + int width, + QRgb bgColor) const { + const auto h = pillHeight(); + const auto &circle = ensurePillCircle(bgColor); + const auto ratio = style::DevicePixelRatio(); + const auto half = h / 2; + const auto otherHalf = h - half; + p.drawImage( + QRect(x, y, half, h), + circle, + QRect(0, 0, half * ratio, h * ratio)); + if (width > h) { + p.fillRect( + x + half, + y, + width - h, + h, + QColor::fromRgba(bgColor)); + } + p.drawImage( + QRect(x + width - otherHalf, y, otherHalf, h), + circle, + QRect(half * ratio, 0, otherHalf * ratio, h * ratio)); +} + +void MemberListRow::rightActionPaint( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) { + if (_actionTextWidth == 0) { + return; + } + const auto &pad = st::memberTagPillPadding; + switch (_tagMode) { + case TagMode::AdminPill: { + const auto nameColor = Ui::FromNameFg( + _type.chatStyle, + false, + user()->colorIndex(), + user()->colorCollectible()); + auto bgColor = nameColor; + bgColor.setAlphaF(0.15); + const auto h = pillHeight(); + const auto cw = pad.left() + _actionTextWidth + pad.right(); + const auto w = std::max(cw, h); + paintPill(p, x, y, w, bgColor.rgba()); + p.setFont(st::normalFont); + p.setPen(nameColor); + p.drawTextLeft( + x + (w - _actionTextWidth) / 2, + y + pad.top(), + outerWidth, + _actionText, + _actionTextWidth); + } break; + case TagMode::NormalText: { + p.setFont(st::normalFont); + p.setPen(st::windowSubTextFg); + p.drawTextLeft( + x, y, outerWidth, _actionText, _actionTextWidth); + } break; + case TagMode::Remove: + case TagMode::AddTag: { + const auto h = pillHeight(); + const auto cw = pad.left() + _actionTextWidth + pad.right(); + const auto w = std::max(cw, h); + if (actionSelected) { + paintPill(p, x, y, w, st::lightButtonBgOver->c.rgba()); + } + if (_actionRipple) { + const auto color = st::lightButtonBgRipple->c; + _actionRipple->paint(p, x, y, outerWidth, &color); + if (_actionRipple->empty()) { + _actionRipple.reset(); + } + } + p.setFont(st::normalFont); + p.setPen(actionSelected + ? st::lightButtonFgOver + : st::lightButtonFg); + p.drawTextLeft( + x + (w - _actionTextWidth) / 2, + y + pad.top(), + outerWidth, + _actionText, + _actionTextWidth); + } break; + case TagMode::None: + break; + } +} + +void MemberListRow::rightActionAddRipple( + QPoint point, + Fn updateCallback) { + if (_tagMode != TagMode::AddTag && _tagMode != TagMode::Remove) { + return; + } + if (!_actionRipple) { + const auto size = rightActionSize(); + const auto radius = size.height() / 2; + auto mask = Ui::RippleAnimation::RoundRectMask(size, radius); + _actionRipple = std::make_unique( + st::defaultLightButton.ripple, + std::move(mask), + std::move(updateCallback)); + } + _actionRipple->add(point); +} + +void MemberListRow::rightActionStopLastRipple() { + if (_actionRipple) { + _actionRipple->lastStop(); + } +} + std::unique_ptr CreateMembersController( not_null navigation, not_null peer) { diff --git a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h index c1c0b2f9cc..055b7051e7 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h +++ b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h @@ -8,8 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "boxes/peer_list_controllers.h" +#include "base/flat_map.h" #include "ui/unread_badge.h" +namespace Ui { +class ChatStyle; +} // namespace Ui + class ParticipantsBoxController; namespace Window { @@ -28,23 +33,60 @@ public: }; struct Type { Rights rights; + QString rank; + not_null chatStyle; + not_null*> circleCache; + bool canAddTag = false; bool canRemove = false; - QString adminRank; }; MemberListRow(not_null user, Type type); + ~MemberListRow(); void setType(Type type); [[nodiscard]] Type type() const; bool rightActionDisabled() const override; + QSize rightActionSize() const override; QMargins rightActionMargins() const override; + void rightActionPaint( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) override; + void rightActionAddRipple( + QPoint point, + Fn updateCallback) override; + void rightActionStopLastRipple() override; void refreshStatus() override; not_null user() const; private: + enum class TagMode { + None, + Remove, + AdminPill, + NormalText, + AddTag, + }; + [[nodiscard]] bool canRemove() const; + [[nodiscard]] int pillHeight() const; + [[nodiscard]] const QImage &ensurePillCircle(QRgb color) const; + void paintPill( + Painter &p, + int x, + int y, + int width, + QRgb bgColor) const; + Type _type; + TagMode _tagMode = TagMode::None; + QString _actionText; + int _actionTextWidth = 0; + std::unique_ptr _actionRipple; }; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 5c697f6bfd..abb09bf92d 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -23,6 +23,7 @@ msgMinWidth: 160px; msgPhotoSize: 33px; msgPhotoSkip: 40px; msgPadding: margins(11px, 8px, 11px, 8px); +msgTagBadgePadding: margins(5px, 1px, 5px, 1px); msgMargin: margins(16px, 6px, 56px, 2px); msgMarginTopAttached: 0px; msgShadow: 2px; From f4dec971786181bfd9a1f66690ffc9db5321b36c Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 13 Feb 2026 15:04:46 +0400 Subject: [PATCH 089/179] Support icons in native bot webview buttons. --- .../inline_bots/bot_attach_web_view.cpp | 4 ++ .../inline_bots/bot_attach_web_view.h | 1 + .../ui/chat/attach/attach_bot_webview.cpp | 70 ++++++++++++++----- .../ui/chat/attach/attach_bot_webview.h | 5 ++ 4 files changed, 62 insertions(+), 18 deletions(-) diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 5367321bfe..8fd3696360 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -1483,6 +1483,10 @@ Webview::ThemeParams WebViewInstance::botThemeParams() { return result; } +Ui::Text::MarkedContext WebViewInstance::botTextContext() { + return Core::TextContext({ .session = _session }); +} + auto WebViewInstance::botDownloads(bool forceCheck) -> const std::vector & { return _session->attachWebView().downloads().list(_bot, forceCheck); diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index 7487de27e1..422219c221 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -273,6 +273,7 @@ private: -> Fn; Webview::ThemeParams botThemeParams() override; + Ui::Text::MarkedContext botTextContext() override; auto botDownloads(bool forceCheck = false) -> const std::vector & override; void botDownloadsAction( diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp index be1a3e7401..fbf03dbdef 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -58,6 +58,7 @@ struct ButtonArgs { bool isActive = false; bool isVisible = false; bool isProgressVisible = false; + uint64 iconCustomEmojiId = 0; QString text; }; @@ -138,7 +139,10 @@ struct ButtonArgs { class Panel::Button final : public RippleButton { public: - Button(QWidget *parent, const style::RoundButton &st); + Button( + QWidget *parent, + const style::RoundButton &st, + Text::MarkedContext textContext); ~Button(); void updateBg(QColor bg); @@ -156,11 +160,12 @@ private: void toggleProgress(bool shown); void setupProgressGeometry(); + void updateText(uint64 iconCustomEmojiId, const QString &text); std::unique_ptr _progress; - rpl::variable _textFull; Ui::Text::String _text; + Text::MarkedContext _textContext; const style::RoundButton &_st; QColor _fg; style::owned_color _bg; @@ -192,17 +197,15 @@ struct Panel::WebviewWithLifetime { rpl::lifetime lifetime; }; -Panel::Button::Button(QWidget *parent, const style::RoundButton &st) +Panel::Button::Button( + QWidget *parent, + const style::RoundButton &st, + Text::MarkedContext textContext) : RippleButton(parent, st.ripple) +, _textContext(std::move(textContext)) , _st(st) , _bg(st::windowBgActive->c) , _roundRect(st::callRadius, st::windowBgActive) { - _textFull.value( - ) | rpl::on_next([=](const QString &text) { - _text.setText(st::semiboldTextStyle, text); - update(); - }, lifetime()); - resize( _st.padding.left() + _text.maxWidth() + _st.padding.right(), _st.padding.top() + _st.height + _st.padding.bottom()); @@ -210,6 +213,27 @@ Panel::Button::Button(QWidget *parent, const style::RoundButton &st) Panel::Button::~Button() = default; +void Panel::Button::updateText( + uint64 iconCustomEmojiId, + const QString &text) { + if (iconCustomEmojiId) { + auto result = Text::SingleCustomEmoji( + QString::number(iconCustomEmojiId)); + if (!text.isEmpty()) { + result.append(' ').append(text); + } + auto context = _textContext; + context.repaint = [=] { update(); }; + _text.setMarkedText( + st::semiboldTextStyle, + result, + kMarkupTextOptions, + context); + } else { + _text.setText(st::semiboldTextStyle, text); + } +} + void Panel::Button::updateBg(QColor bg) { _bg.update(bg); _roundRect.setColor(_bg.color()); @@ -240,7 +264,7 @@ void Panel::Button::updateFg(not_null paletteFg) { } void Panel::Button::updateArgs(ButtonArgs &&args) { - _textFull = std::move(args.text); + updateText(args.iconCustomEmojiId, args.text); setDisabled(!args.isActive); setPointerCursor(false); setCursor(args.isActive ? style::cur_pointer : Qt::ForbiddenCursor); @@ -337,13 +361,18 @@ void Panel::Button::paintEvent(QPaintEvent *e) { const auto height = rect().height(); const auto progress = st::paymentsLoading.size; - const auto skip = (height - progress.height()) / 2; - const auto padding = skip + progress.width() + skip; - - const auto space = width() - padding * 2; - const auto textWidth = std::min(space, _text.maxWidth()); + const auto minPad = (height - progress.height()) / 2; + const auto rightReserved = _progress + ? (minPad + progress.width() + minPad) + : minPad; + const auto maxTextEnd = width() - rightReserved; + const auto maxSpace = std::max(maxTextEnd - minPad, 0); + const auto textWidth = std::min(_text.maxWidth(), maxSpace); + const auto centered = (width() - textWidth) / 2; + const auto textLeft = std::max( + std::min(centered, maxTextEnd - textWidth), + minPad); const auto textTop = _st.padding.top() + _st.textTop; - const auto textLeft = padding + (space - textWidth) / 2; p.setPen(_fg); _text.drawLeftElided(p, textLeft, textTop, textWidth, width()); } @@ -1637,7 +1666,10 @@ void Panel::processButtonMessage( }); const auto text = args["text"].toString().trimmed(); - const auto visible = args["is_visible"].toBool() && !text.isEmpty(); + const auto iconCustomEmojiId + = args["icon_custom_emoji_id"].toString().toULongLong(); + const auto visible = args["is_visible"].toBool() + && (!text.isEmpty() || iconCustomEmojiId); if (!button) { if (visible) { createButton(button); @@ -1663,6 +1695,7 @@ void Panel::processButtonMessage( .isActive = args["is_active"].toBool(), .isVisible = visible, .isProgressVisible = args["is_progress_visible"].toBool(), + .iconCustomEmojiId = iconCustomEmojiId, .text = args["text"].toString(), }); if (button.get() == _secondaryButton.get()) { @@ -1820,7 +1853,8 @@ void Panel::createButton(std::unique_ptr