diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index bbabb2939893a1..5d39fba913f904 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -116,6 +116,8 @@ PRIVATE api/api_bot.h api/api_chat_filters.cpp api/api_chat_filters.h + api/api_chat_filters_remove_manager.cpp + api/api_chat_filters_remove_manager.h api/api_chat_invite.cpp api/api_chat_invite.h api/api_chat_links.cpp @@ -642,6 +644,8 @@ PRIVATE data/data_thread.h data/data_types.cpp data/data_types.h + data/data_unread_value.cpp + data/data_unread_value.h data/data_user.cpp data/data_user.h data/data_user_photos.cpp @@ -1549,6 +1553,8 @@ PRIVATE ui/widgets/expandable_peer_list.h ui/widgets/label_with_custom_emoji.cpp ui/widgets/label_with_custom_emoji.h + ui/widgets/chat_filters_tabs_strip.cpp + ui/widgets/chat_filters_tabs_strip.h ui/countryinput.cpp ui/countryinput.h ui/dynamic_thumbnails.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index b1c061e1b2bf90..896ba977b3cb40 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -5139,6 +5139,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_filters_toast_add" = "{chat} added to {folder} folder"; "lng_filters_toast_remove" = "{chat} removed from {folder} folder"; "lng_filters_shareable_status" = "shareable folder"; +"lng_filters_view_subtitle" = "Tabs view"; +"lng_filters_vertical" = "Tabs on the left"; +"lng_filters_horizontal" = "Tabs at the top"; "lng_filters_delete_sure" = "Are you sure you want to delete this folder? This will also deactivate all the invite links created to share this folder."; "lng_filters_link" = "Share Folder"; diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index a96cc4ed2d6129..f93f27343276a6 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="5.7.3.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index a52fa98bcf3a92..5140957e390299 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 5,7,2,0 - PRODUCTVERSION 5,7,2,0 + FILEVERSION 5,7,3,0 + PRODUCTVERSION 5,7,3,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "" VALUE "FileDescription", "Telegram Desktop" - VALUE "FileVersion", "5.7.2.0" + VALUE "FileVersion", "5.7.3.0" VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "5.7.2.0" + VALUE "ProductVersion", "5.7.3.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 1c7272371a4a0a..1c6c33c11c7f6b 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // VS_VERSION_INFO VERSIONINFO - FILEVERSION 5,7,2,0 - PRODUCTVERSION 5,7,2,0 + FILEVERSION 5,7,3,0 + PRODUCTVERSION 5,7,3,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -53,10 +53,10 @@ BEGIN BEGIN VALUE "CompanyName", "" VALUE "FileDescription", "Telegram Desktop Updater" - VALUE "FileVersion", "5.7.2.0" + VALUE "FileVersion", "5.7.3.0" VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "5.7.2.0" + VALUE "ProductVersion", "5.7.3.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_chat_filters.cpp b/Telegram/SourceFiles/api/api_chat_filters.cpp index ee6b97b5849273..2c8410b8029470 100644 --- a/Telegram/SourceFiles/api/api_chat_filters.cpp +++ b/Telegram/SourceFiles/api/api_chat_filters.cpp @@ -12,6 +12,7 @@ For license and copyright information please follow this link: #include "boxes/premium_limits_box.h" #include "boxes/filters/edit_filter_links.h" // FilterChatStatusText #include "core/application.h" +#include "core/core_settings.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_chat_filters.h" @@ -152,6 +153,7 @@ void InitFilterLinkHeader( .badge = (type == Ui::FilterLinkHeaderType::AddingChats ? std::move(count) : rpl::single(0)), + .horizontalFilters = Core::App().settings().chatFiltersHorizontal(), }); const auto widget = header.widget; widget->resizeToWidth(st::boxWideWidth); diff --git a/Telegram/SourceFiles/api/api_chat_filters_remove_manager.cpp b/Telegram/SourceFiles/api/api_chat_filters_remove_manager.cpp new file mode 100644 index 00000000000000..fe96accd61dbb1 --- /dev/null +++ b/Telegram/SourceFiles/api/api_chat_filters_remove_manager.cpp @@ -0,0 +1,128 @@ +/* +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 "api/api_chat_filters_remove_manager.h" + +#include "api/api_chat_filters.h" +#include "apiwrap.h" +#include "data/data_chat_filters.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/boxes/confirm_box.h" +#include "ui/ui_utility.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" +#include "styles/style_layers.h" + +namespace Api { +namespace { + +void RemoveChatFilter( + not_null session, + FilterId filterId, + std::vector> leave) { + const auto api = &session->api(); + session->data().chatsFilters().apply(MTP_updateDialogFilter( + MTP_flags(MTPDupdateDialogFilter::Flag(0)), + MTP_int(filterId), + MTPDialogFilter())); + if (leave.empty()) { + api->request(MTPmessages_UpdateDialogFilter( + MTP_flags(MTPmessages_UpdateDialogFilter::Flag(0)), + MTP_int(filterId), + MTPDialogFilter() + )).send(); + } else { + api->request(MTPchatlists_LeaveChatlist( + MTP_inputChatlistDialogFilter(MTP_int(filterId)), + MTP_vector(ranges::views::all( + leave + ) | ranges::views::transform([](not_null peer) { + return MTPInputPeer(peer->input); + }) | ranges::to>()) + )).done([=](const MTPUpdates &result) { + api->applyUpdates(result); + }).send(); + } +} + +} // namespace + +RemoveComplexChatFilter::RemoveComplexChatFilter() = default; + +void RemoveComplexChatFilter::request( + QPointer widget, + base::weak_ptr weak, + FilterId id) { + const auto session = &weak->session(); + const auto &list = session->data().chatsFilters().list(); + const auto i = ranges::find(list, id, &Data::ChatFilter::id); + const auto filter = (i != end(list)) ? *i : Data::ChatFilter(); + const auto has = filter.hasMyLinks(); + const auto confirm = [=](Fn action, bool onlyWhenHas = false) { + if (!has && onlyWhenHas) { + action(); + return; + } + weak->window().show(Ui::MakeConfirmBox({ + .text = (has + ? tr::lng_filters_delete_sure() + : tr::lng_filters_remove_sure()), + .confirmed = [=](Fn &&close) { close(); action(); }, + .confirmText = (has + ? tr::lng_box_delete() + : tr::lng_filters_remove_yes()), + .confirmStyle = &st::attentionBoxButton, + })); + }; + const auto simple = [=] { + confirm([=] { RemoveChatFilter(session, id, {}); }); + }; + const auto suggestRemoving = Api::ExtractSuggestRemoving(filter); + if (suggestRemoving.empty()) { + simple(); + return; + } else if (_removingRequestId) { + if (_removingId == id) { + return; + } + session->api().request(_removingRequestId).cancel(); + } + _removingId = id; + _removingRequestId = session->api().request( + MTPchatlists_GetLeaveChatlistSuggestions( + MTP_inputChatlistDialogFilter( + MTP_int(id))) + ).done(crl::guard(widget, [=, this](const MTPVector &result) { + _removingRequestId = 0; + const auto suggestRemovePeers = ranges::views::all( + result.v + ) | ranges::views::transform([=](const MTPPeer &peer) { + return session->data().peer(peerFromMTP(peer)); + }) | ranges::to_vector; + const auto chosen = crl::guard(widget, [=]( + std::vector> peers) { + RemoveChatFilter(session, id, std::move(peers)); + }); + confirm(crl::guard(widget, [=] { + Api::ProcessFilterRemove( + weak, + filter.title(), + filter.iconEmoji(), + suggestRemoving, + suggestRemovePeers, + chosen); + }), true); + })).fail(crl::guard(widget, [=, this] { + _removingRequestId = 0; + simple(); + })).send(); +} + +} // namespace Api diff --git a/Telegram/SourceFiles/api/api_chat_filters_remove_manager.h b/Telegram/SourceFiles/api/api_chat_filters_remove_manager.h new file mode 100644 index 00000000000000..ce92b2df3c6a1a --- /dev/null +++ b/Telegram/SourceFiles/api/api_chat_filters_remove_manager.h @@ -0,0 +1,35 @@ +/* +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 + +namespace Window { +class SessionController; +} // namespace Window + +namespace Ui { +class RpWidget; +} // namespace Ui + +namespace Api { + +class RemoveComplexChatFilter final { +public: + RemoveComplexChatFilter(); + + void request( + QPointer widget, + base::weak_ptr weak, + FilterId id); + +private: + FilterId _removingId = 0; + mtpRequestId _removingRequestId = 0; + +}; + +} // namespace Api diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index b47d4b2035b3fc..a94b34cc0f6d9d 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -206,7 +206,9 @@ void PeerListBox::keyPressEvent(QKeyEvent *e) { content()->selectSkipPage(height(), 1); } else if (e->key() == Qt::Key_PageUp) { content()->selectSkipPage(height(), -1); - } else if (e->key() == Qt::Key_Escape && _select && !_select->entity()->getQuery().isEmpty()) { + } else if (e->key() == Qt::Key_Escape + && _select + && !_select->entity()->getQuery().isEmpty()) { _select->entity()->clearQuery(); } else { BoxContent::keyPressEvent(e); @@ -215,7 +217,16 @@ void PeerListBox::keyPressEvent(QKeyEvent *e) { void PeerListBox::searchQueryChanged(const QString &query) { scrollToY(0); - content()->searchQueryChanged(query); + const auto isEmpty = content()->searchQueryChanged(query); + if (_specialTabsMode.enabled) { + _specialTabsMode.searchIsActive = !isEmpty; + if (_specialTabsMode.searchIsActive) { + _specialTabsMode.topSkip = _addedTopScrollSkip; + setAddedTopScrollSkip(0); + } else { + setAddedTopScrollSkip(_specialTabsMode.topSkip); + } + } } void PeerListBox::resizeEvent(QResizeEvent *e) { @@ -543,6 +554,19 @@ auto PeerListBox::collectSelectedRows() return result; } +rpl::producer PeerListBox::multiSelectHeightValue() const { + return _select ? _select->heightValue() : rpl::single(0); +} + +void PeerListBox::setSpecialTabMode(bool value) { + content()->setIgnoreHiddenRowsOnSearch(value); + if (value) { + _specialTabsMode.enabled = true; + } else { + _specialTabsMode = {}; + } +} + PeerListRow::PeerListRow(not_null peer) : PeerListRow(peer, peer->id.value) { } @@ -1385,10 +1409,12 @@ int PeerListContent::labelHeight() const { void PeerListContent::refreshRows() { if (!_hiddenRows.empty()) { - _filterResults.clear(); - for (const auto &row : _rows) { - if (!row->hidden()) { - _filterResults.push_back(row.get()); + if (!_ignoreHiddenRowsOnSearch || _normalizedSearchQuery.isEmpty()) { + _filterResults.clear(); + for (const auto &row : _rows) { + if (!row->hidden()) { + _filterResults.push_back(row.get()); + } } } } @@ -2050,13 +2076,16 @@ void PeerListContent::checkScrollForPreload() { } } -void PeerListContent::searchQueryChanged(QString query) { +PeerListContent::IsEmpty PeerListContent::searchQueryChanged(QString query) { const auto searchWordsList = TextUtilities::PrepareSearchWords(query); const auto normalizedQuery = searchWordsList.join(' '); + if (_ignoreHiddenRowsOnSearch && !normalizedQuery.isEmpty()) { + _filterResults.clear(); + } if (_normalizedSearchQuery != normalizedQuery) { setSearchQuery(query, normalizedQuery); if (_controller->searchInLocal() && !searchWordsList.isEmpty()) { - Assert(_hiddenRows.empty()); + Assert(_hiddenRows.empty() || _ignoreHiddenRowsOnSearch); auto minimalList = (const std::vector>*)nullptr; for (const auto &searchWord : searchWordsList) { @@ -2104,6 +2133,7 @@ void PeerListContent::searchQueryChanged(QString query) { } refreshRows(); } + return _normalizedSearchQuery.isEmpty(); } std::unique_ptr PeerListContent::saveState() const { @@ -2192,6 +2222,10 @@ void PeerListContent::dragLeft() { clearSelection(); } +void PeerListContent::setIgnoreHiddenRowsOnSearch(bool value) { + _ignoreHiddenRowsOnSearch = value; +} + void PeerListContent::visibleTopBottomUpdated( int visibleTop, int visibleBottom) { diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index ce1cc122008d38..bd71470a7c3d45 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -652,12 +652,15 @@ class PeerListContent : public Ui::RpWidget { [[nodiscard]] bool hasPressed() const; void clearSelection(); - void searchQueryChanged(QString query); + using IsEmpty = bool; + IsEmpty searchQueryChanged(QString query); bool submitted(); PeerListRowId updateFromParentDrag(QPoint globalPosition); void dragLeft(); + void setIgnoreHiddenRowsOnSearch(bool value); + // Interface for the controller. void appendRow(std::unique_ptr row); void appendSearchRow(std::unique_ptr row); @@ -879,6 +882,7 @@ class PeerListContent : public Ui::RpWidget { int _aboveHeight = 0; int _belowHeight = 0; bool _hideEmpty = false; + bool _ignoreHiddenRowsOnSearch = false; object_ptr _aboveWidget = { nullptr }; object_ptr _aboveSearchWidget = { nullptr }; object_ptr _belowWidget = { nullptr }; @@ -1102,6 +1106,9 @@ class PeerListBox [[nodiscard]] std::vector collectSelectedIds(); [[nodiscard]] std::vector> collectSelectedRows(); + [[nodiscard]] rpl::producer multiSelectHeightValue() const; + + void setSpecialTabMode(bool value); void peerListSetTitle(rpl::producer title) override { setTitle(std::move(title)); @@ -1168,4 +1175,11 @@ class PeerListBox bool _scrollBottomFixed = false; int _addedTopScrollSkip = 0; + struct SpecialTabsMode final { + bool enabled = false; + bool searchIsActive = false; + int topSkip = 0; + }; + SpecialTabsMode _specialTabsMode; + }; diff --git a/Telegram/SourceFiles/boxes/report_messages_box.cpp b/Telegram/SourceFiles/boxes/report_messages_box.cpp index e09825926cfa89..25e548f1b26af1 100644 --- a/Telegram/SourceFiles/boxes/report_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/report_messages_box.cpp @@ -79,7 +79,6 @@ void ShowReportMessageBox( auto performRequest = [=]( const auto &repeatRequest, Data::ReportInput reportInput) -> void { - constexpr auto kToastDuration = crl::time(4000); report(reportInput, [=](const Api::ReportResult &result) { if (!result.error.isEmpty()) { if (result.error == u"MESSAGE_ID_REQUIRED"_q) { @@ -199,6 +198,7 @@ void ShowReportMessageBox( } })); } else if (result.successful) { + constexpr auto kToastDuration = crl::time(4000); show->showToast( tr::lng_report_thanks(tr::now), kToastDuration); diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index ad25d436e494c1..8d8c7dc6ca84cc 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -17,6 +17,7 @@ For license and copyright information please follow this link: #include "storage/storage_account.h" #include "ui/boxes/confirm_box.h" #include "apiwrap.h" +#include "ui/widgets/chat_filters_tabs_strip.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/multi_select.h" #include "ui/widgets/scroll_area.h" @@ -42,6 +43,7 @@ For license and copyright information please follow this link: #include "chat_helpers/share_message_phrase_factory.h" #include "data/business/data_shortcut_messages.h" #include "data/data_channel.h" +#include "data/data_chat_filters.h" #include "data/data_game.h" #include "data/data_histories.h" #include "data/data_user.h" @@ -84,11 +86,14 @@ class ShareBox::Inner final : public Ui::RpWidget { void activateSkipColumn(int direction); void activateSkipPage(int pageHeight, int direction); void updateFilter(QString filter = QString()); + [[nodiscard]] bool isFilterEmpty() const; void selectActive(); rpl::producer scrollToRequests() const; rpl::producer<> searchRequests() const; + void applyChatFilter(FilterId id); + protected: void visibleTopBottomUpdated( int visibleTop, @@ -169,7 +174,9 @@ class ShareBox::Inner final : public Ui::RpWidget { int _upon = -1; int _visibleTop = 0; - std::unique_ptr _chatsIndexed; + std::unique_ptr _defaultChatsIndexed; + std::unique_ptr _customChatsIndexed; + not_null _chatsIndexed; QString _filter; std::vector> _filtered; @@ -285,6 +292,10 @@ void ShareBox::prepare() { _select->setQueryChangedCallback([=](const QString &query) { applyFilterUpdate(query); + if (_chatsFilters) { + updateScrollSkips(); + scrollToY(0); + } }); _select->setItemRemovedCallback([=](uint64 itemId) { if (const auto peer = _descriptor.session->data().peerLoaded(PeerId(itemId))) { @@ -340,10 +351,32 @@ void ShareBox::prepare() { { .suggestCustomEmoji = true }); _select->raise(); + + { + const auto chatsFilters = AddChatFiltersTabsStrip( + this, + _descriptor.session, + [this](FilterId id) { + _inner->applyChatFilter(id); + scrollToY(0); + }); + chatsFilters->lower(); + chatsFilters->heightValue() | rpl::start_with_next([this](int h) { + updateScrollSkips(); + scrollToY(0); + }, lifetime()); + _select->heightValue() | rpl::start_with_next([=](int h) { + chatsFilters->moveToLeft(0, h); + }, chatsFilters->lifetime()); + _chatsFilters = chatsFilters; + } } int ShareBox::getTopScrollSkip() const { - return _select->isHidden() ? 0 : _select->height(); + return (_select->isHidden() ? 0 : _select->height()) + + ((_chatsFilters && _inner && _inner->isFilterEmpty()) + ? _chatsFilters->height() + : 0); } int ShareBox::getBottomScrollSkip() const { @@ -691,9 +724,10 @@ ShareBox::Inner::Inner( , _descriptor(descriptor) , _show(std::move(show)) , _st(_descriptor.st ? *_descriptor.st : st::shareBoxList) -, _chatsIndexed( +, _defaultChatsIndexed( std::make_unique( - Dialogs::SortMode::Add)) { + Dialogs::SortMode::Add)) +, _chatsIndexed(_defaultChatsIndexed.get()) { _rowsTop = st::shareRowsTop; _rowHeight = st::shareRowHeight; setAttribute(Qt::WA_OpaquePaintEvent); @@ -711,7 +745,7 @@ ShareBox::Inner::Inner( const auto self = _descriptor.session->user(); const auto selfHistory = self->owner().history(self); if (_descriptor.filterCallback(selfHistory)) { - _chatsIndexed->addToEnd(selfHistory); + _defaultChatsIndexed->addToEnd(selfHistory); } const auto addList = [&](not_null list) { for (const auto &row : list->all()) { @@ -719,7 +753,7 @@ ShareBox::Inner::Inner( if (!history->peer->isSelf() && (history->asForum() || _descriptor.filterCallback(history))) { - _chatsIndexed->addToEnd(history); + _defaultChatsIndexed->addToEnd(history); } } } @@ -742,7 +776,7 @@ ShareBox::Inner::Inner( _descriptor.session->changes().realtimeNameUpdates( ) | rpl::start_with_next([=](const Data::NameUpdate &update) { - _chatsIndexed->peerNameChanged( + _defaultChatsIndexed->peerNameChanged( update.peer, update.oldFirstLetters); }, lifetime()); @@ -1351,6 +1385,10 @@ void ShareBox::Inner::updateFilter(QString filter) { } } +bool ShareBox::Inner::isFilterEmpty() const { + return _filter.isEmpty(); +} + rpl::producer ShareBox::Inner::scrollToRequests() const { return _scrollToRequests.events(); } @@ -1359,6 +1397,29 @@ rpl::producer<> ShareBox::Inner::searchRequests() const { return _searchRequests.events(); } +void ShareBox::Inner::applyChatFilter(FilterId id) { + if (!id) { + _chatsIndexed = _defaultChatsIndexed.get(); + } else { + _customChatsIndexed = std::make_unique( + Dialogs::SortMode::Add); + _chatsIndexed = _customChatsIndexed.get(); + + const auto addList = [&](not_null list) { + for (const auto &row : list->all()) { + if (const auto history = row->history()) { + if (_descriptor.filterCallback(history)) { + _customChatsIndexed->addToEnd(history); + } + } + } + }; + const auto &data = _descriptor.session->data(); + addList(data.chatsFilters().chatsList(id)->indexed()); + } + update(); +} + void ShareBox::Inner::peopleReceived( const QString &query, const QVector &my, diff --git a/Telegram/SourceFiles/boxes/share_box.h b/Telegram/SourceFiles/boxes/share_box.h index 7754d9cad802ba..990a084cd0b785 100644 --- a/Telegram/SourceFiles/boxes/share_box.h +++ b/Telegram/SourceFiles/boxes/share_box.h @@ -179,6 +179,8 @@ class ShareBox final : public Ui::BoxContent { bool _peopleFull = false; mtpRequestId _peopleRequest = 0; + RpWidget *_chatsFilters = nullptr; + using PeopleCache = QMap; PeopleCache _peopleCache; diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp index 7d399a59647d28..da083896f36fc8 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp @@ -33,6 +33,10 @@ base::options::toggle TabbedPanelShowOnClick({ const char kOptionTabbedPanelShowOnClick[] = "tabbed-panel-show-on-click"; +bool ShowPanelOnClick() { + return TabbedPanelShowOnClick.value(); +} + TabbedPanel::TabbedPanel( QWidget *parent, not_null controller, diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_panel.h b/Telegram/SourceFiles/chat_helpers/tabbed_panel.h index fcedb5efc6a53c..97d66e2c711307 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_panel.h +++ b/Telegram/SourceFiles/chat_helpers/tabbed_panel.h @@ -25,6 +25,7 @@ namespace ChatHelpers { class TabbedSelector; extern const char kOptionTabbedPanelShowOnClick[]; +[[nodiscard]] bool ShowPanelOnClick(); struct TabbedPanelDescriptor { Window::SessionController *regularWindow = nullptr; diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index 72d93474141337..b3361e561fcfe1 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -423,7 +423,7 @@ QByteArray Settings::serialize() const { << qint32(!_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2) << _tonsiteStorageToken << qint32(_includeMutedCounterFolders ? 1 : 0) - << qint32(0) // Old IV zoom + << qint32(_chatFiltersHorizontal.current() ? 1 : 0) << qint32(_skipToastsInFocus ? 1 : 0) << qint32(_recordVideoMessages ? 1 : 0) << SerializeVideoQuality(_videoQuality) @@ -568,6 +568,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { qint32 skipToastsInFocus = _skipToastsInFocus ? 1 : 0; qint32 recordVideoMessages = _recordVideoMessages ? 1 : 0; quint32 videoQuality = SerializeVideoQuality(_videoQuality); + quint32 chatFiltersHorizontal = _chatFiltersHorizontal.current() ? 1 : 0; stream >> themesAccentColors; if (!stream.atEnd()) { @@ -891,8 +892,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { stream >> includeMutedCounterFolders; } if (!stream.atEnd()) { - qint32 oldIvZoom = 0; - stream >> oldIvZoom; + stream >> chatFiltersHorizontal; } if (!stream.atEnd()) { stream >> skipToastsInFocus; @@ -1135,6 +1135,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { _skipToastsInFocus = (skipToastsInFocus == 1); _recordVideoMessages = (recordVideoMessages == 1); _videoQuality = DeserializeVideoQuality(videoQuality); + _chatFiltersHorizontal = (chatFiltersHorizontal == 1); } QString Settings::getSoundPath(const QString &key) const { @@ -1528,6 +1529,7 @@ void Settings::resetOnLastLogout() { _ivZoom = 100; _recordVideoMessages = false; _videoQuality = {}; + _chatFiltersHorizontal = false; _recentEmojiPreload.clear(); _recentEmoji.clear(); @@ -1703,4 +1705,16 @@ void Settings::setVideoQuality(Media::VideoQuality value) { _videoQuality = value; } +bool Settings::chatFiltersHorizontal() const { + return _chatFiltersHorizontal.current(); +} + +rpl::producer Settings::chatFiltersHorizontalChanges() const { + return _chatFiltersHorizontal.changes(); +} + +void Settings::setChatFiltersHorizontal(bool value) { + _chatFiltersHorizontal = value; +} + } // namespace Core diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index f98026aee0b55b..3f376cccb6130e 100644 --- a/Telegram/SourceFiles/core/core_settings.h +++ b/Telegram/SourceFiles/core/core_settings.h @@ -935,6 +935,10 @@ class Settings final { [[nodiscard]] rpl::producer ivZoomValue() const; void setIvZoom(int value); + [[nodiscard]] bool chatFiltersHorizontal() const; + [[nodiscard]] rpl::producer chatFiltersHorizontalChanges() const; + void setChatFiltersHorizontal(bool value); + [[nodiscard]] Media::VideoQuality videoQuality() const; void setVideoQuality(Media::VideoQuality quality); @@ -1078,6 +1082,7 @@ class Settings final { QByteArray _tonsiteStorageToken; rpl::variable _ivZoom = 100; Media::VideoQuality _videoQuality; + rpl::variable _chatFiltersHorizontal = false; bool _tabbedReplacedWithInfo = false; // per-window rpl::event_stream _tabbedReplacedWithInfoValue; // per-window diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 11fb39bd6cdce1..2915c4ffde3c1f 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -24,7 +24,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs; constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs; constexpr auto AppName = "Telegram Desktop"_cs; constexpr auto AppFile = "Telegram"_cs; -constexpr auto AppVersion = 5007002; -constexpr auto AppVersionStr = "5.7.2"; -constexpr auto AppBetaVersion = false; +constexpr auto AppVersion = 5007003; +constexpr auto AppVersionStr = "5.7.3"; +constexpr auto AppBetaVersion = true; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/data/components/sponsored_messages.cpp b/Telegram/SourceFiles/data/components/sponsored_messages.cpp index d2746ad9a594f7..61e36d1c5a4e0a 100644 --- a/Telegram/SourceFiles/data/components/sponsored_messages.cpp +++ b/Telegram/SourceFiles/data/components/sponsored_messages.cpp @@ -74,6 +74,9 @@ void SponsoredMessages::clearOldRequests() { SponsoredMessages::AppendResult SponsoredMessages::append( not_null history) { + if (isTopBarFor(history)) { + return SponsoredMessages::AppendResult::None; + } const auto it = _data.find(history); if (it == end(_data)) { return SponsoredMessages::AppendResult::None; diff --git a/Telegram/SourceFiles/data/data_unread_value.cpp b/Telegram/SourceFiles/data/data_unread_value.cpp new file mode 100644 index 00000000000000..c374dc0f6558dc --- /dev/null +++ b/Telegram/SourceFiles/data/data_unread_value.cpp @@ -0,0 +1,68 @@ +/* +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_unread_value.h" + +#include "core/application.h" +#include "core/core_settings.h" +#include "data/data_chat_filters.h" +#include "data/data_folder.h" +#include "data/data_session.h" +#include "main/main_session.h" +#include "window/notifications_manager.h" + +namespace Data { +namespace { + +rpl::producer MainListUnreadState( + not_null list) { + return rpl::single(rpl::empty) | rpl::then( + list->unreadStateChanges() | rpl::to_empty + ) | rpl::map([=] { + return list->unreadState(); + }); +} + +} // namespace + +[[nodiscard]] Dialogs::UnreadState MainListMapUnreadState( + not_null session, + const Dialogs::UnreadState &state) { + const auto folderId = Data::Folder::kId; + if (const auto folder = session->data().folderLoaded(folderId)) { + return state - folder->chatsList()->unreadState(); + } + return state; +} + +rpl::producer UnreadStateValue( + not_null session, + FilterId filterId) { + if (filterId > 0) { + const auto filters = &session->data().chatsFilters(); + return MainListUnreadState(filters->chatsList(filterId)); + } + return MainListUnreadState( + session->data().chatsList() + ) | rpl::map([=](const Dialogs::UnreadState &state) { + return MainListMapUnreadState(session, state); + }); +} + +rpl::producer IncludeMutedCounterFoldersValue() { + using namespace Window::Notifications; + return rpl::single(rpl::empty_value()) | rpl::then( + Core::App().notifications().settingsChanged( + ) | rpl::filter( + rpl::mappers::_1 == ChangeType::IncludeMuted + ) | rpl::to_empty + ) | rpl::map([] { + return Core::App().settings().includeMutedCounterFolders(); + }); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_unread_value.h b/Telegram/SourceFiles/data/data_unread_value.h new file mode 100644 index 00000000000000..1acdbcc983a15b --- /dev/null +++ b/Telegram/SourceFiles/data/data_unread_value.h @@ -0,0 +1,30 @@ +/* +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 + +namespace Dialogs { +struct UnreadState; +} // namespace Dialogs + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +[[nodiscard]] Dialogs::UnreadState MainListMapUnreadState( + not_null session, + const Dialogs::UnreadState &state); + +[[nodiscard]] rpl::producer UnreadStateValue( + not_null session, + FilterId filterId); + +[[nodiscard]] rpl::producer IncludeMutedCounterFoldersValue(); + +} // namespace Data diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index c11cf3c063bad1..ad40b84d87996c 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -11,13 +11,11 @@ For license and copyright information please follow this link: #include "dialogs/ui/chat_search_empty.h" #include "dialogs/ui/chat_search_in.h" #include "dialogs/ui/dialogs_layout.h" -#include "dialogs/ui/dialogs_stories_content.h" #include "dialogs/ui/dialogs_video_userpic.h" #include "dialogs/dialogs_indexed_list.h" #include "dialogs/dialogs_widget.h" #include "dialogs/dialogs_search_from_controllers.h" #include "dialogs/dialogs_search_tags.h" -#include "history/view/history_view_chat_preview.h" #include "history/view/history_view_context_menu.h" #include "history/history.h" #include "history/history_item.h" @@ -45,7 +43,6 @@ For license and copyright information please follow this link: #include "data/data_peer_values.h" #include "data/data_histories.h" #include "data/data_chat_filters.h" -#include "data/data_cloud_file.h" #include "data/data_changes.h" #include "data/data_message_reactions.h" #include "data/data_saved_messages.h" @@ -69,7 +66,6 @@ For license and copyright information please follow this link: #include "ui/effects/loading_element.h" #include "ui/widgets/multi_select.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" -#include "ui/empty_userpic.h" #include "ui/unread_badge.h" #include "boxes/filters/edit_filter_box.h" #include "boxes/peers/edit_forum_topic_box.h" diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 7b9b351dccf112..d7c9267400ebdf 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -16,7 +16,6 @@ For license and copyright information please follow this link: #include "dialogs/dialogs_inner_widget.h" #include "dialogs/dialogs_search_from_controllers.h" #include "dialogs/dialogs_key.h" -#include "dialogs/dialogs_entry.h" #include "history/history.h" #include "history/history_item.h" #include "history/view/history_view_top_bar_widget.h" @@ -26,6 +25,7 @@ For license and copyright information please follow this link: #include "boxes/peers/edit_peer_requests_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/chat_filters_tabs_strip.h" #include "ui/widgets/elastic_scroll.h" #include "ui/widgets/fields/input_field.h" #include "ui/wrap/fade_wrap.h" @@ -46,14 +46,10 @@ For license and copyright information please follow this link: #include "main/main_session_settings.h" #include "api/api_chat_filters.h" #include "apiwrap.h" -#include "base/event_filter.h" #include "core/application.h" #include "core/ui_integration.h" #include "core/update_checker.h" #include "core/shortcuts.h" -#include "boxes/peer_list_box.h" -#include "boxes/peers/edit_participants_box.h" -#include "window/window_adaptive.h" #include "window/window_controller.h" #include "window/window_session_controller.h" #include "window/window_slide_animation.h" @@ -655,6 +651,11 @@ Widget::Widget( setupMoreChatsBar(); setupDownloadBar(); } + + if (session().settings().dialogsFiltersEnabled() + && Core::App().settings().chatFiltersHorizontal()) { + toggleFiltersMenu(true); + } } void Widget::chosenRow(const ChosenRow &row) { @@ -1242,6 +1243,9 @@ void Widget::updateControlsVisibility(bool fast) { if (_moreChatsBar) { _moreChatsBar->show(); } + if (_chatFilters) { + _chatFilters->show(); + } if (_openedFolder || _openedForum) { _subsectionTopBar->show(); if (_forumTopShadow) { @@ -1311,6 +1315,41 @@ void Widget::updateHasFocus(not_null focused) { } } +void Widget::toggleFiltersMenu(bool enabled) { + if (!enabled == !_chatFilters) { + return; + } else if (enabled) { + _chatFilters = base::make_unique_q(this); + const auto raw = _chatFilters.get(); + const auto inner = Ui::AddChatFiltersTabsStrip( + _chatFilters.get(), + &session(), + [this](FilterId id) { + if (controller()->activeChatsFilterCurrent() != id) { + controller()->setActiveChatsFilter(id); + } + }, + true); + raw->show(); + raw->stackUnder(_scroll); + raw->resizeToWidth(width()); + const auto shadow = Ui::CreateChild(raw); + shadow->show(); + inner->sizeValue() | rpl::start_with_next([=, this](const QSize &s) { + raw->resize(s); + shadow->setGeometry( + 0, + s.height() - shadow->height(), + s.width(), + shadow->height()); + updateControlsGeometry(); + }, _chatFilters->lifetime()); + updateControlsGeometry(); + } else { + _chatFilters = nullptr; + } +} + bool Widget::cancelSearchByMouseBack() { return _searchHasFocus && !_searchSuggestionsLocked @@ -1860,6 +1899,12 @@ void Widget::scrollToDefault(bool verytop) { this, QPoint(), QRect(0, top, wideGeometry.width(), skip)); + if (_chatFilters) { + Ui::RenderWidget( + p, + _chatFilters, + QPoint(0, skip - _chatFilters->height())); + } Ui::RenderWidget(p, _scroll, QPoint(0, skip)); } if (scrollGeometry != wideGeometry) { @@ -1875,6 +1920,9 @@ void Widget::startWidthAnimation() { } _widthAnimationCache = grabNonNarrowScrollFrame(); _scroll->hide(); + if (_chatFilters) { + _chatFilters->hide(); + } updateStoriesVisibility(); } @@ -1882,6 +1930,9 @@ void Widget::stopWidthAnimation() { _widthAnimationCache = QPixmap(); if (!_showAnimation) { _scroll->setVisible(!_suggestions); + if (_chatFilters) { + _chatFilters->setVisible(!_suggestions); + } } updateStoriesVisibility(); update(); @@ -1976,6 +2027,9 @@ void Widget::startSlideAnimation( if (_moreChatsBar) { _moreChatsBar->hide(); } + if (_chatFilters) { + _chatFilters->hide(); + } if (_forumTopShadow) { _forumTopShadow->hide(); } @@ -3100,6 +3154,9 @@ bool Widget::applySearchState(SearchState state) { const auto tagsChanged = (_searchState.tags != state.tags); const auto queryChanged = (_searchState.query != state.query); const auto tabChanged = (_searchState.tab != state.tab); + const auto queryEmptyChanged = queryChanged + ? (_searchState.query.isEmpty() != state.query.isEmpty()) + : false; if (forum) { if (_openedForum == forum) { @@ -3135,6 +3192,10 @@ bool Widget::applySearchState(SearchState state) { ? peer->owner().history(migrateFrom).get() : nullptr; _searchState = state; + if (_chatFilters && queryEmptyChanged) { + _chatFilters->setVisible(_searchState.query.isEmpty()); + updateControlsGeometry(); + } _searchWithPostsPreview = computeSearchWithPostsPreview(); if (queryChanged) { updateLockUnlockVisibility(anim::type::normal); @@ -3522,6 +3583,9 @@ void Widget::updateControlsGeometry() { if (_forumRequestsBar) { _forumRequestsBar->resizeToWidth(barw); } + if (_chatFilters) { + _chatFilters->resizeToWidth(barw); + } _updateScrollGeometryCached = [=] { const auto moreChatsBarTop = expandedStoriesTop + ((!_stories || _stories->isHidden()) ? 0 : _aboveScrollAdded); @@ -3543,8 +3607,15 @@ void Widget::updateControlsGeometry() { if (_forumReportBar) { _forumReportBar->bar().move(0, forumReportTop); } - const auto scrollTop = forumReportTop + const auto chatFiltersTop = forumReportTop + (_forumReportBar ? _forumReportBar->bar().height() : 0); + if (_chatFilters) { + _chatFilters->move(0, chatFiltersTop); + } + const auto scrollTop = chatFiltersTop + + ((_chatFilters && _searchState.query.isEmpty()) + ? (_chatFilters->height() * (1. - narrowRatio)) + : 0); const auto scrollHeight = height() - scrollTop - bottomSkip; const auto wasScrollHeight = _scroll->height(); _scroll->setGeometry(0, scrollTop, scrollWidth, scrollHeight); diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index ff751c714270e7..b163cef854606d 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -129,6 +129,7 @@ class Widget final : public Window::AbstractSectionWidget { [[nodiscard]] RowDescriptor resolveChatNext(RowDescriptor from = {}) const; [[nodiscard]] RowDescriptor resolveChatPrevious(RowDescriptor from = {}) const; void updateHasFocus(not_null focused); + void toggleFiltersMenu(bool value); // Float player interface. bool floatPlayerHandleWheelEvent(QEvent *e) override; @@ -317,6 +318,8 @@ class Widget final : public Window::AbstractSectionWidget { std::unique_ptr _forumRequestsBar; std::unique_ptr _forumReportBar; + base::unique_qptr _chatFilters; + object_ptr _scroll; QPointer _inner; std::unique_ptr _suggestions; 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 05bcd35e4057a8..459daa9f2ea14e 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -667,7 +667,8 @@ bool InnerWidget::elementUnderCursor( return (Element::Hovered() == view); } -HistoryView::SelectionModeResult InnerWidget::elementInSelectionMode() { +HistoryView::SelectionModeResult InnerWidget::elementInSelectionMode( + const HistoryView::Element *) { return {}; } 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 bdf9c049156cd7..209cc0384ebe83 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h @@ -94,7 +94,8 @@ class InnerWidget final HistoryView::Context elementContext() override; bool elementUnderCursor( not_null view) override; - HistoryView::SelectionModeResult elementInSelectionMode() override; + HistoryView::SelectionModeResult elementInSelectionMode( + const HistoryView::Element *view) override; bool elementIntersectsRange( not_null view, int from, diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index d96ad033c54cc7..b2fe8b9983f2ee 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -163,7 +163,11 @@ class HistoryMainElementDelegate final not_null view) override { return (Element::Moused() == view); } - HistoryView::SelectionModeResult elementInSelectionMode() override { + HistoryView::SelectionModeResult elementInSelectionMode( + const Element *view) override { + if (view && view->data()->isSponsored()) { + return HistoryView::SelectionModeResult(); + } return _widget ? _widget->inSelectionMode() : HistoryView::SelectionModeResult(); diff --git a/Telegram/SourceFiles/history/history_view_swipe.cpp b/Telegram/SourceFiles/history/history_view_swipe.cpp index 214310fead1105..cbe9d65a1481e3 100644 --- a/Telegram/SourceFiles/history/history_view_swipe.cpp +++ b/Telegram/SourceFiles/history/history_view_swipe.cpp @@ -198,7 +198,8 @@ void SetupSwipeHandler( const auto &touches = t->touchPoints(); const auto released = [&](int index) { return (touches.size() > index) - && (touches.at(index).state() & Qt::TouchPointReleased); + && (int(touches.at(index).state()) + & int(Qt::TouchPointReleased)); }; const auto cancel = released(0) || released(1) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index ccf88004cfa26d..131e98c5f0a5c9 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -426,8 +426,16 @@ HistoryWidget::HistoryWidget( initTabbedSelector(); _attachToggle->setClickedCallback([=] { + const auto toggle = _attachBotsMenu && _attachBotsMenu->isHidden(); base::call_delayed(st::historyAttach.ripple.hideDuration, this, [=] { - chooseAttach(); + if (_attachBotsMenu && toggle) { + _attachBotsMenu->showAnimated(); + } else { + chooseAttach(); + if (_attachBotsMenu) { + _attachBotsMenu->hideAnimated(); + } + } }); }); @@ -2605,6 +2613,8 @@ void HistoryWidget::showHistory( } })); checkState(); + } else { + requestSponsoredMessageBar(); } } else { _chooseForReport = nullptr; @@ -2642,7 +2652,7 @@ void HistoryWidget::setHistory(History *history) { if (was && !now) { _attachToggle->removeEventFilter(_attachBotsMenu.get()); _attachBotsMenu->hideFast(); - } else if (now && !was) { + } else if (now && !was && !ChatHelpers::ShowPanelOnClick()) { _attachToggle->installEventFilter(_attachBotsMenu.get()); } @@ -2730,7 +2740,9 @@ void HistoryWidget::refreshAttachBotsMenu() { } _attachBotsMenu->setOrigin( Ui::PanelAnimation::Origin::BottomLeft); - _attachToggle->installEventFilter(_attachBotsMenu.get()); + if (!ChatHelpers::ShowPanelOnClick()) { + _attachToggle->installEventFilter(_attachBotsMenu.get()); + } _attachBotsMenu->heightValue( ) | rpl::start_with_next([=] { moveFieldControls(); @@ -4552,7 +4564,6 @@ void HistoryWidget::showFinished() { _showAnimation = nullptr; doneShow(); synteticScrollToY(_scroll->scrollTop()); - requestSponsoredMessageBar(); } void HistoryWidget::doneShow() { diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 95d5e7a29da317..3b6dde0c8b79ac 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -111,7 +111,8 @@ bool DefaultElementDelegate::elementUnderCursor( return false; } -SelectionModeResult DefaultElementDelegate::elementInSelectionMode() { +SelectionModeResult DefaultElementDelegate::elementInSelectionMode( + const Element *view) { return {}; } diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index be41e07dcb41b9..e8a5a1c5334001 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -80,7 +80,8 @@ class ElementDelegate { public: virtual Context elementContext() = 0; virtual bool elementUnderCursor(not_null view) = 0; - virtual SelectionModeResult elementInSelectionMode() = 0; + virtual SelectionModeResult elementInSelectionMode( + const Element *view) = 0; virtual bool elementIntersectsRange( not_null view, int from, @@ -136,7 +137,7 @@ class ElementDelegate { class DefaultElementDelegate : public ElementDelegate { public: bool elementUnderCursor(not_null view) override; - SelectionModeResult elementInSelectionMode() override; + SelectionModeResult elementInSelectionMode(const Element *view) override; bool elementIntersectsRange( not_null view, int from, diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index ca1e1632798435..68a28f1f87b483 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -1782,7 +1782,11 @@ bool ListWidget::elementUnderCursor( return (_overElement == view); } -SelectionModeResult ListWidget::elementInSelectionMode() { +SelectionModeResult ListWidget::elementInSelectionMode( + const HistoryView::Element *view) { + if (view && !_delegate->listIsItemGoodForSelection(view->data())) { + return {}; + } return inSelectionMode(); } diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index 8fa1b1f529aad9..056d6ce95ab71a 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -391,7 +391,7 @@ class ListWidget final // ElementDelegate interface. Context elementContext() override; bool elementUnderCursor(not_null view) override; - SelectionModeResult elementInSelectionMode() override; + SelectionModeResult elementInSelectionMode(const Element *view) override; bool elementIntersectsRange( not_null view, int from, diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 3a84ee92710065..0dae7fbf063d0f 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -1100,7 +1100,7 @@ void Message::draw(Painter &p, const PaintContext &context) const { if (hasGesture) { p.translate(context.gestureHorizontal.translation, 0); } - const auto selectionModeResult = delegate()->elementInSelectionMode(); + const auto selectionModeResult = delegate()->elementInSelectionMode(this); const auto selectionTranslation = (selectionModeResult.progress > 0) ? (selectionModeResult.progress * AdditionalSpaceForSelectionCheckbox(this, g)) @@ -3868,7 +3868,7 @@ bool Message::displayFastReply() const { return hasFastReply() && data()->isRegular() && canSendAnything() - && !delegate()->elementInSelectionMode().inSelectionMode; + && !delegate()->elementInSelectionMode(this).inSelectionMode; } bool Message::displayRightActionComments() const { @@ -4032,7 +4032,7 @@ void Message::drawRightAction( ClickHandlerPtr Message::rightActionLink( std::optional pressPoint) const { - if (delegate()->elementInSelectionMode().progress > 0) { + if (delegate()->elementInSelectionMode(this).progress > 0) { return nullptr; } ensureRightAction(); diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 107e4bf7fb3901..081d01f44e9e2c 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -898,7 +898,7 @@ void RepliesWidget::setupSwipeReply() { } }, [=, show = controller()->uiShow()](int cursorTop) { auto result = HistoryView::SwipeHandlerFinishData(); - if (_inner->elementInSelectionMode().inSelectionMode) { + if (_inner->elementInSelectionMode(nullptr).inSelectionMode) { return result; } const auto view = _inner->lookupItemByY(cursorTop); diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp index 46241702b9a8b9..4f54fc7d37e0bf 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -663,8 +663,8 @@ QSize WebPage::countCurrentSize(int newWidth) { const auto stickerSet = stickerSetData(); const auto factcheck = factcheckData(); const auto sponsored = sponsoredData(); - const auto specialRightPix = ((sponsored && !sponsored->hasMedia) - || stickerSet); + const auto specialRightPix = (stickerSet + || (sponsored && !sponsored->hasMedia && _data->photo)); const auto lineHeight = UnitedLineHeight(); const auto factcheckMetrics = factcheck ? computeFactcheckMetrics(_description.countHeight(innerWidth)) @@ -678,7 +678,7 @@ QSize WebPage::countCurrentSize(int newWidth) { } const auto linesMax = factcheck ? (factcheckMetrics.lines + 1) - : (specialRightPix || isLogEntryOriginal()) + : (sponsored || isLogEntryOriginal()) ? kMaxOriginalEntryLines : 5; const auto siteNameHeight = _siteNameLines ? lineHeight : 0; @@ -711,7 +711,9 @@ QSize WebPage::countCurrentSize(int newWidth) { newHeight += _titleLines * lineHeight; } - const auto descriptionHeight = _description.countHeight(wleft); + const auto descriptionHeight = _description.countHeight(sponsored + ? innerWidth + : wleft); const auto restLines = (linesMax - _siteNameLines - _titleLines); if (descriptionHeight < restLines * descriptionLineHeight) { // We have height for all the lines. @@ -1510,7 +1512,7 @@ void WebPage::clickHandlerPressedChanged( } return; } - if (p == _openl) { + if ((p == _openl) || (sponsoredData() && sponsoredData()->link == p)) { if (pressed) { if (!_ripple) { const auto full = Rect(currentSize()); diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index be215bc5079173..c3290ea240f192 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -16,6 +16,7 @@ For license and copyright information please follow this link: #include "base/timer_rpl.h" #include "boxes/peer_list_controllers.h" #include "boxes/share_box.h" +#include "chat_helpers/tabbed_panel.h" #include "core/application.h" #include "core/click_handler_types.h" #include "core/local_url_handlers.h" @@ -2039,9 +2040,6 @@ std::unique_ptr MakeAttachBotsMenu( not_null peer, Fn actionFactory, Fn attach) { - if (!Data::CanSend(peer, ChatRestriction::SendInline)) { - return nullptr; - } auto result = std::make_unique( parent, st::dropdownMenuWithIcons); @@ -2096,8 +2094,10 @@ std::unique_ptr MakeAttachBotsMenu( ChooseAndSendLocation(controller, config, actionFactory()); }, &st::menuIconAddress); } + const auto addBots = Data::CanSend(peer, ChatRestriction::SendInline); for (const auto &bot : bots->attachBots()) { - if (!bot.inAttachMenu + if (!addBots + || !bot.inAttachMenu || !PeerMatchesTypes(peer, bot.user, bot.types)) { continue; } @@ -2128,7 +2128,11 @@ std::unique_ptr MakeAttachBotsMenu( }, action->lifetime()); raw->addAction(std::move(action)); } - if (raw->actions().size() <= minimal) { + const auto actions = raw->actions().size(); + const auto onclick = ChatHelpers::ShowPanelOnClick(); + if (!actions) { + return nullptr; + } else if (actions <= minimal && !onclick) { return nullptr; } return result; diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 5fc463f0f1262c..5cd766f03f936c 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -1098,6 +1098,12 @@ void MainWidget::dialogsCancelled() { _history->activate(); } +void MainWidget::toggleFiltersMenu(bool value) const { + if (_dialogs) { + _dialogs->toggleFiltersMenu(value); + } +} + void MainWidget::setChatBackground( const Data::WallPaper &background, QImage &&image) { diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index 32e5fff5d71f7c..5d6379896262ec 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -213,6 +213,7 @@ class MainWidget final void showNonPremiumLimitToast(bool download); void dialogsCancelled(); + void toggleFiltersMenu(bool value) const; private: void paintEvent(QPaintEvent *e) override; diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index 6f50c871fafc98..849a081209c0a1 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -1855,44 +1855,48 @@ void AddWithdrawalWidget( }, label->lifetime()); const auto lockedColor = anim::with_alpha(stButton.textFg->c, .5); - const auto lockedLabelTop = Ui::CreateChild( - button, - tr::lng_bot_earn_balance_button_locked(), - st::botEarnLockedButtonLabel); - lockedLabelTop->setTextColorOverride(lockedColor); - lockedLabelTop->setAttribute(Qt::WA_TransparentForMouseEvents); - const auto lockedLabelBottom = Ui::CreateChild( - button, - QString(), - st::botEarnLockedButtonLabel); - lockedLabelBottom->setTextColorOverride(lockedColor); - lockedLabelBottom->setAttribute(Qt::WA_TransparentForMouseEvents); + const auto lockedLabel = Ui::CreateChild(button); + lockedLabel->setAttribute(Qt::WA_TransparentForMouseEvents); + struct LockedState final { + Ui::Text::String text; + bool locked = false; + bool dateIsNull = false; + rpl::lifetime dateUpdateLifetime; + }; + const auto state = lockedLabel->lifetime().make_state(); rpl::combine( rpl::duplicate(lockedValue), - button->sizeValue(), - lockedLabelTop->sizeValue(), - lockedLabelBottom->sizeValue() - ) | rpl::start_with_next([=]( - bool locked, - const QSize &b, - const QSize &top, - const QSize &bottom) { - const auto factor = locked ? 1 : -10; - const auto sumHeight = top.height() + bottom.height(); - lockedLabelTop->moveToLeft( - (b.width() - top.width()) / 2, - factor * (b.height() - sumHeight) / 2); - lockedLabelBottom->moveToLeft( - (b.width() - bottom.width()) / 2, - factor * ((b.height() - sumHeight) / 2 + top.height())); - }, lockedLabelTop->lifetime()); - - const auto dateUpdateLifetime - = lockedLabelBottom->lifetime().make_state(); + button->sizeValue() + ) | rpl::start_with_next([=](bool locked, const QSize &s) { + state->locked = locked; + lockedLabel->resize(s); + }, lockedLabel->lifetime()); + lockedLabel->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(lockedLabel); + p.setPen(state->locked ? QPen(lockedColor) : stButton.textFg->p); + if (state->dateIsNull) { + p.setFont(st::channelEarnSemiboldLabel.style.font); + p.drawText( + lockedLabel->rect(), + style::al_center, + tr::lng_bot_earn_balance_button_locked(tr::now)); + return; + } + state->text.draw(p, { + .position = QPoint( + 0, + (lockedLabel->height() - state->text.minHeight()) / 2), + .outerWidth = lockedLabel->width(), + .availableWidth = lockedLabel->width(), + .align = style::al_center, + }); + }, lockedLabel->lifetime()); + std::move( dateValue ) | rpl::start_with_next([=](const QDateTime &dt) { - dateUpdateLifetime->destroy(); + state->dateUpdateLifetime.destroy(); + state->dateIsNull = dt.isNull(); if (dt.isNull()) { return; } @@ -1901,7 +1905,7 @@ void AddWithdrawalWidget( const auto context = Core::MarkedTextContext{ .session = session, - .customEmojiRepaint = [=] { lockedLabelBottom->update(); }, + .customEmojiRepaint = [=] { lockedLabel->update(); }, }; const auto emoji = Ui::Text::SingleCustomEmoji( session->data().customEmojiManager().registerInternalEmoji( @@ -1929,11 +1933,18 @@ void AddWithdrawalWidget( : (u"%1:%2"_q) .arg(minutes, 2, 10, kZero) .arg(seconds, 2, 10, kZero); - lockedLabelBottom->setMarkedText( - base::duplicate(emoji).append(formatted), + state->text.setMarkedText( + st::botEarnLockedButtonLabel.style, + TextWithEntities() + .append(tr::lng_bot_earn_balance_button_locked(tr::now)) + .append('\n') + .append(emoji) + .append(formatted), + kMarkupTextOptions, context); - }, *dateUpdateLifetime); - }, lockedLabelBottom->lifetime()); + lockedLabel->update(); + }, state->dateUpdateLifetime); + }, lockedLabel->lifetime()); Api::HandleWithdrawalButton( Api::RewardReceiver{ diff --git a/Telegram/SourceFiles/settings/settings_folders.cpp b/Telegram/SourceFiles/settings/settings_folders.cpp index 7d9c3707ae63c9..f2ba21dd2461d6 100644 --- a/Telegram/SourceFiles/settings/settings_folders.cpp +++ b/Telegram/SourceFiles/settings/settings_folders.cpp @@ -30,6 +30,7 @@ For license and copyright information please follow this link: #include "ui/text/text_utilities.h" #include "ui/widgets/box_content_divider.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/labels.h" #include "ui/wrap/slide_wrap.h" @@ -843,6 +844,34 @@ void SetupTopContent( } +void SetupView(not_null content) { + Ui::AddDivider(content); + Ui::AddSkip(content); + Ui::AddSubsectionTitle(content, tr::lng_filters_view_subtitle()); + + const auto group = std::make_shared>( + Core::App().settings().chatFiltersHorizontal()); + const auto addSend = [&](bool value, const QString &text) { + content->add( + object_ptr>( + content, + group, + value, + text, + st::settingsSendType), + st::settingsSendTypePadding); + }; + addSend(false, tr::lng_filters_vertical(tr::now)); + addSend(true, tr::lng_filters_horizontal(tr::now)); + + group->setChangedCallback([=](bool value) { + Core::App().settings().setChatFiltersHorizontal(value); + Core::App().saveSettingsDelayed(); + }); + Ui::AddSkip(content); + Ui::AddSkip(content); +} + } // namespace Folders::Folders( @@ -871,6 +900,8 @@ void Folders::setupContent(not_null controller) { _save = SetupFoldersContent(controller, content); + SetupView(content); + Ui::ResizeFitChild(this, content); } diff --git a/Telegram/SourceFiles/storage/file_upload.cpp b/Telegram/SourceFiles/storage/file_upload.cpp index eef376b749fa00..765bcb72efcb19 100644 --- a/Telegram/SourceFiles/storage/file_upload.cpp +++ b/Telegram/SourceFiles/storage/file_upload.cpp @@ -229,6 +229,8 @@ void Uploader::processDocumentProgress(FullMsgId itemId) { const auto document = media ? media->document() : nullptr; const auto sendAction = (document && document->isVoiceMessage()) ? Api::SendProgressType::UploadVoice + : (document && document->isVideoMessage()) + ? Api::SendProgressType::UploadRound : Api::SendProgressType::UploadFile; const auto progress = (document && document->uploading()) ? ((document->uploadingData->offset * 100) @@ -250,6 +252,8 @@ void Uploader::processDocumentFailed(FullMsgId itemId) { const auto document = media ? media->document() : nullptr; const auto sendAction = (document && document->isVoiceMessage()) ? Api::SendProgressType::UploadVoice + : (document && document->isVideoMessage()) + ? Api::SendProgressType::UploadRound : Api::SendProgressType::UploadFile; sendProgressUpdate(item, sendAction, -1); } diff --git a/Telegram/SourceFiles/ui/controls/filter_link_header.cpp b/Telegram/SourceFiles/ui/controls/filter_link_header.cpp index 0d69c0efdfd81c..8228d33324179f 100644 --- a/Telegram/SourceFiles/ui/controls/filter_link_header.cpp +++ b/Telegram/SourceFiles/ui/controls/filter_link_header.cpp @@ -8,11 +8,15 @@ For license and copyright information please follow this link: #include "ui/controls/filter_link_header.h" #include "lang/lang_keys.h" +#include "ui/image/image_prepare.h" #include "ui/painter.h" #include "ui/rp_widget.h" -#include "ui/image/image_prepare.h" +#include "ui/ui_utility.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/chat_filters_tabs_slider.h" #include "ui/widgets/labels.h" +#include "styles/style_chat_helpers.h" // defaultEmojiSuggestions +#include "styles/style_dialogs.h" // dialogsSearchTabs #include "styles/style_filter_icons.h" #include "styles/style_layers.h" #include "styles/style_settings.h" @@ -68,6 +72,7 @@ class Widget final : public RpWidget { QString _folderTitle; not_null _folderIcon; + bool _horizontalFilters = false; int _maxHeight = 0; @@ -75,6 +80,46 @@ class Widget final : public RpWidget { }; +[[nodiscard]] QImage GeneratePreview( + not_null parent, + const QString &title, + int badge) { + using Tabs = Ui::ChatsFiltersTabs; + auto owned = parent->lifetime().make_state>( + base::make_unique_q(parent, st::dialogsSearchTabs)); + const auto raw = owned->get(); + raw->setSections({ + tr::lng_filters_name_people(tr::now), + title, + tr::lng_filters_name_unread(tr::now), + }); + raw->fitWidthToSections(); + raw->setActiveSectionFast(1); + raw->stopAnimation(); + + auto result = QImage( + raw->size() * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(style::DevicePixelRatio()); + result.fill(st::windowBg->c); + { + auto p = QPainter(&result); + Ui::RenderWidget(p, raw, QPoint(), raw->rect()); + + const auto &r = st::defaultEmojiSuggestions.fadeRight; + const auto &l = st::defaultEmojiSuggestions.fadeLeft; + const auto padding = st::filterLinkSubsectionTitlePadding.top(); + const auto w = raw->width(); + const auto h = raw->height(); + r.fill(p, QRect(w - r.width() - padding, 0, r.width(), h)); + l.fill(p, QRect(padding, 0, l.width(), h)); + p.fillRect(0, 0, padding, h, st::windowBg); + p.fillRect(w - padding, 0, padding, raw->height(), st::windowBg); + } + owned->reset(); + return result; +} + [[nodiscard]] QImage GeneratePreview( const QString &title, not_null icon, @@ -199,17 +244,18 @@ Widget::Widget( , _titleFont(st::boxTitle.style.font) , _titlePadding(st::filterLinkTitlePadding) , _folderTitle(descriptor.folderTitle) -, _folderIcon(descriptor.folderIcon) { +, _folderIcon(descriptor.folderIcon) +, _horizontalFilters(descriptor.horizontalFilters) { setMinimumHeight(st::boxTitleHeight); refreshTitleText(); setTitlePosition(st::boxTitlePosition.x(), st::boxTitlePosition.y()); style::PaletteChanged( - ) | rpl::start_with_next([=] { + ) | rpl::start_with_next([this] { _preview = QImage(); }, lifetime()); - _badge.changes() | rpl::start_with_next([=] { + _badge.changes() | rpl::start_with_next([this] { _preview = QImage(); update(); }, lifetime()); @@ -242,7 +288,9 @@ void Widget::resizeEvent(QResizeEvent *e) { _about->resizeToWidth(availableWidth); const auto minHeight = minimumHeight(); - const auto maxHeight = st::filterLinkAboutTop + const auto maxHeight = (_horizontalFilters + ? (st::filterLinkAboutTop * 0.8) + : st::filterLinkAboutTop) + _about->height() + st::filterLinkAboutBottom; if (maxHeight <= minHeight) { @@ -283,12 +331,22 @@ void Widget::resizeEvent(QResizeEvent *e) { QRectF Widget::previewRect( float64 topProgress, float64 sizeProgress) const { - const auto size = st::filterLinkPreview * sizeProgress; - return QRectF( - (width() - size) / 2., - st::filterLinkPreviewTop * topProgress, - size, - size); + if (_horizontalFilters) { + const auto size = (_preview.size() / style::DevicePixelRatio()) + * sizeProgress; + return QRectF( + (width() - size.width()) / 2., + st::filterLinkPreviewTop * 1.5 * topProgress, + size.width(), + size.height()); + } else { + const auto size = st::filterLinkPreview * sizeProgress; + return QRectF( + (width() - size) / 2., + st::filterLinkPreviewTop * topProgress, + size, + size); + } }; void Widget::paintEvent(QPaintEvent *e) { @@ -298,10 +356,13 @@ void Widget::paintEvent(QPaintEvent *e) { if (_progress.top) { auto hq = PainterHighQualityEnabler(p); if (_preview.isNull()) { - _preview = GeneratePreview( - _folderTitle, - _folderIcon, - _badge.current()); + const auto badge = _badge.current(); + if (_horizontalFilters) { + _preview = GeneratePreview(this, _folderTitle, badge); + Widget::resizeEvent(nullptr); + } else { + _preview = GeneratePreview(_folderTitle, _folderIcon, badge); + } } p.drawImage(_previewRect, _preview); } diff --git a/Telegram/SourceFiles/ui/controls/filter_link_header.h b/Telegram/SourceFiles/ui/controls/filter_link_header.h index 8769bf0699e31d..c55e2084658002 100644 --- a/Telegram/SourceFiles/ui/controls/filter_link_header.h +++ b/Telegram/SourceFiles/ui/controls/filter_link_header.h @@ -29,6 +29,7 @@ struct FilterLinkHeaderDescriptor { base::required folderTitle; not_null folderIcon; rpl::producer badge; + bool horizontalFilters = false; }; struct FilterLinkHeader { diff --git a/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp b/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp index f01df2165be3fd..4ba0e7768bd129 100644 --- a/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp +++ b/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp @@ -107,9 +107,15 @@ class RoundVideoRecorder::Private final { std::array lastDts = { 0 }; }; +#if DA_FFMPEG_CONST_WRITE_CALLBACK + static int Write(void *opaque, const uint8_t *_buf, int buf_size) { + uint8_t *buf = const_cast(_buf); +#else static int Write(void *opaque, uint8_t *buf, int buf_size) { +#endif return static_cast(opaque)->write(buf, buf_size); } + static int64_t Seek(void *opaque, int64_t offset, int whence) { return static_cast(opaque)->seek(offset, whence); } @@ -388,7 +394,6 @@ bool RoundVideoRecorder::Private::initAudio() { _audioCodec->sample_rate = kAudioFrequency; #if DA_FFMPEG_NEW_CHANNEL_LAYOUT _audioCodec->ch_layout = AV_CHANNEL_LAYOUT_MONO; - _audioCodec->channels = _audioCodec->ch_layout.nb_channels; #else _audioCodec->channel_layout = AV_CH_LAYOUT_MONO; _audioCodec->channels = _audioChannels; diff --git a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp index a2f8f7229e0968..78e84f5cd984d1 100644 --- a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp @@ -13,6 +13,7 @@ For license and copyright information please follow this link: #include "ui/effects/ripple_animation.h" #include "ui/chat/group_call_userpics.h" #include "ui/text/text_custom_emoji.h" +#include "ui/emoji_config.h" #include "ui/painter.h" #include "ui/ui_utility.h" #include "lang/lang_keys.h" @@ -20,6 +21,8 @@ For license and copyright information please follow this link: #include "styles/style_chat_helpers.h" #include "styles/style_menu_icons.h" +#include + namespace Lang { namespace { diff --git a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h index f0b2545f552fcb..60110d6546dc4e 100644 --- a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h @@ -8,8 +8,8 @@ For license and copyright information please follow this link: #pragma once #include "base/unique_qptr.h" -#include "ui/text/text_block.h" #include "ui/widgets/menu/menu_item_base.h" +#include "ui/text/text_custom_emoji.h" namespace Ui { diff --git a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.cpp b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.cpp new file mode 100644 index 00000000000000..0bf4704b3d8662 --- /dev/null +++ b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.cpp @@ -0,0 +1,418 @@ +/* +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/chat_filters_tabs_slider.h" + +#include "ui/effects/ripple_animation.h" +#include "ui/widgets/side_bar_button.h" +#include "styles/style_widgets.h" + +#include + +namespace Ui { + +ChatsFiltersTabs::ChatsFiltersTabs( + not_null parent, + const style::SettingsSlider &st) +: Ui::SettingsSlider(parent, st) +, _st(st) +, _unreadSt([&] { + auto st = Ui::UnreadBadgeStyle(); + st.align = style::al_left; + return st; +}()) +, _unreadMaxString(u"99+"_q) +, _unreadSkip(st::lineWidth * 5) { + Expects(_st.barSnapToLabel && _st.strictSkip); + if (_st.barRadius > 0) { + _bar.emplace(_st.barRadius, _st.barFg); + _barActive.emplace(_st.barRadius, _st.barFgActive); + } + { + const auto one = Ui::CountUnreadBadgeSize(u"9"_q, _unreadSt, 1); + _cachedBadgeWidths = { + one.width(), + Ui::CountUnreadBadgeSize(u"99"_q, _unreadSt, 2).width(), + Ui::CountUnreadBadgeSize(u"999"_q, _unreadSt, 2).width(), + }; + _cachedBadgeHeight = one.height(); + } + Ui::DiscreteSlider::setSelectOnPress(false); +} + +int ChatsFiltersTabs::centerOfSection(int section) const { + const auto widths = countSectionsWidths(0); + auto result = 0; + if (section >= 0 && section < widths.size()) { + for (auto i = 0; i < section; i++) { + result += widths[i]; + } + result += widths[section] / 2; + } + return result; +} + +void ChatsFiltersTabs::fitWidthToSections() { + const auto widths = countSectionsWidths(0); + resizeToWidth(ranges::accumulate(widths, .0)); + _lockedFromX = calculateLockedFromX(); + + { + _sections.clear(); + enumerateSections([&](Section §ion) { + _sections.push_back({ not_null{ §ion }, 0, false }); + return true; + }); + } +} + +void ChatsFiltersTabs::setUnreadCount(int index, int unreadCount) { + const auto it = _unreadCounts.find(index); + if (it == _unreadCounts.end()) { + if (unreadCount) { + _unreadCounts.emplace(index, Unread{ + .cache = cacheUnreadCount(unreadCount), + .count = unreadCount, + }); + } + } else { + if (unreadCount) { + it->second.count = unreadCount; + it->second.cache = cacheUnreadCount(unreadCount); + } else { + _unreadCounts.erase(it); + } + } + if (unreadCount) { + const auto widthIndex = (unreadCount < 10) + ? 0 + : (unreadCount < 100) + ? 1 + : 2; + setAdditionalContentWidthToSection( + index, + _cachedBadgeWidths[widthIndex] + _unreadSkip); + } else { + setAdditionalContentWidthToSection(index, 0); + } +} + +int ChatsFiltersTabs::calculateLockedFromX() const { + if (!_lockedFrom) { + return std::numeric_limits::max(); + } + auto left = 0; + auto index = 0; + enumerateSections([&](const Section §ion) { + const auto currentRight = section.left + section.width; + if (index == _lockedFrom) { + return false; + } + left = currentRight; + index++; + return true; + }); + return left ? left : std::numeric_limits::max(); +} + +void ChatsFiltersTabs::setLockedFrom(int index) { + _lockedFrom = index; + _lockedFromX = calculateLockedFromX(); + if (!index) { + _paletteLifetime.destroy(); + return; + } + _paletteLifetime = style::PaletteChanged( + ) | rpl::start_with_next([this] { + _lockCache.emplace(Ui::SideBarLockIcon(_st.labelFg)); + }); +} + +QImage ChatsFiltersTabs::cacheUnreadCount(int count) const { + const auto widthIndex = (count < 10) ? 0 : (count < 100) ? 1 : 2; + auto image = QImage( + QSize(_cachedBadgeWidths[widthIndex], _cachedBadgeHeight) + * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(style::DevicePixelRatio()); + image.fill(Qt::transparent); + const auto string = (count > 99) + ? _unreadMaxString + : QString::number(count); + { + auto p = QPainter(&image); + Ui::PaintUnreadBadge(p, string, 0, 0, _unreadSt, 0); + } + return image; +} + +void ChatsFiltersTabs::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + const auto clip = e->rect(); + const auto range = getCurrentActiveRange(); + const auto activeIndex = activeSection(); + + auto index = 0; + auto raisedIndex = -1; + auto activeHorizontalShift = 0; + const auto drawSection = [&](Section §ion) { + // const auto activeWidth = _st.barSnapToLabel + // ? section.contentWidth + // : section.width; + + const auto horizontalShift = _sections[index].horizontalShift; + const auto shiftedLeft = section.left + horizontalShift; + if (_sections[index].raise) { + raisedIndex = index; + } + if (index == activeIndex) { + activeHorizontalShift = horizontalShift; + } + + // const auto activeLeft = shiftedLeft + // + (section.width - activeWidth) / 2; + // const auto active = 1. + // - std::clamp( + // std::abs(range.left - activeLeft) / float64(range.width), + // 0., + // 1.); + const auto active = (index == activeIndex) ? 1. : 0.; + if (section.ripple) { + const auto color = anim::color( + _st.rippleBg, + _st.rippleBgActive, + active); + section.ripple->paint(p, shiftedLeft, 0, width(), &color); + if (section.ripple->empty()) { + section.ripple.reset(); + } + } + const auto labelLeft = shiftedLeft + + (section.width - section.contentWidth) / 2; + const auto rect = myrtlrect( + labelLeft, + _st.labelTop, + section.contentWidth, + _st.labelStyle.font->height); + if (rect.intersects(clip)) { + const auto locked = (_lockedFrom && (index >= _lockedFrom)); + if (locked) { + constexpr auto kPremiumLockedOpacity = 0.6; + p.setOpacity(kPremiumLockedOpacity); + } + p.setPen(anim::pen(_st.labelFg, _st.labelFgActive, active)); + section.label.draw(p, { + .position = QPoint(labelLeft, _st.labelTop), + .outerWidth = width(), + .availableWidth = section.label.maxWidth(), + }); + { + const auto it = _unreadCounts.find(index); + if (it != _unreadCounts.end()) { + p.drawImage( + labelLeft + + _unreadSkip + + section.label.maxWidth(), + _st.labelTop, + it->second.cache); + } + } + if (locked) { + if (!_lockCache) { + _lockCache.emplace(Ui::SideBarLockIcon(_st.labelFg)); + } + const auto size = _lockCache->size() + / style::DevicePixelRatio(); + p.drawImage( + labelLeft + (section.label.maxWidth() - size.width()) / 2, + height() - size.height() - st::lineWidth, + *_lockCache); + p.setOpacity(1.0); + } + } + index++; + return true; + }; + enumerateSections(drawSection); + if (raisedIndex >= 0) { + index = raisedIndex; + drawSection(*_sections[raisedIndex].section); + } + if (_st.barSnapToLabel) { + const auto drawRect = [&](QRect rect, bool active) { + const auto &bar = active ? _barActive : _bar; + if (bar) { + bar->paint(p, rect); + } else { + p.fillRect(rect, active ? _st.barFgActive : _st.barFg); + } + }; + const auto add = _st.barStroke / 2; + const auto from = std::max(range.left - add, 0); + const auto till = std::min(range.left + range.width + add, width()); + if (from < till) { + drawRect( + myrtlrect( + from, + _st.barTop, + till - from, + _st.barStroke).translated(activeHorizontalShift, 0), + true); + } + } +} + +void ChatsFiltersTabs::mousePressEvent(QMouseEvent *e) { + const auto mouseButton = e->button(); + if (mouseButton == Qt::MouseButton::LeftButton) { + _lockedPressed = (e->pos().x() >= _lockedFromX); + if (_lockedPressed) { + Ui::RpWidget::mousePressEvent(e); + } else { + Ui::SettingsSlider::mousePressEvent(e); + } + } else { + Ui::RpWidget::mousePressEvent(e); + } +} + +void ChatsFiltersTabs::mouseMoveEvent(QMouseEvent *e) { + if (_reordering) { + Ui::RpWidget::mouseMoveEvent(e); + } else { + Ui::SettingsSlider::mouseMoveEvent(e); + } +} + +void ChatsFiltersTabs::mouseReleaseEvent(QMouseEvent *e) { + const auto mouseButton = e->button(); + if (mouseButton == Qt::MouseButton::LeftButton) { + if (base::take(_lockedPressed)) { + _lockedPressed = false; + _lockedClicked.fire({}); + } else { + if (_reordering) { + for (const auto §ion : _sections) { + if (section.section->ripple) { + section.section->ripple->lastStop(); + } + } + } else { + Ui::SettingsSlider::mouseReleaseEvent(e); + } + } + } else { + Ui::RpWidget::mouseReleaseEvent(e); + } +} + +void ChatsFiltersTabs::contextMenuEvent(QContextMenuEvent *e) { + const auto pos = e->pos(); + if (pos.x() >= _lockedFromX) { + return; + } + auto left = 0; + auto index = 0; + enumerateSections([&](const Section §ion) { + const auto currentRight = section.left + section.width; + if (pos.x() > left && pos.x() < currentRight) { + return false; + } + left = currentRight; + index++; + return true; + }); + _contextMenuRequested.fire_copy(index); +} + +rpl::producer ChatsFiltersTabs::contextMenuRequested() const { + return _contextMenuRequested.events(); +} + +rpl::producer<> ChatsFiltersTabs::lockedClicked() const { + return _lockedClicked.events(); +} + +int ChatsFiltersTabs::count() const { + return _sections.size(); +} + +void ChatsFiltersTabs::setHorizontalShift(int index, int shift) { + Expects(index >= 0 && index < _sections.size()); + + auto §ion = _sections[index]; + if (const auto delta = shift - section.horizontalShift) { + section.horizontalShift = shift; + update(); + } +} + +void ChatsFiltersTabs::setRaised(int index) { + _sections[index].raise = true; + update(); +} + +void ChatsFiltersTabs::reorderSections(int oldIndex, int newIndex) { + Expects(oldIndex >= 0 && oldIndex < _sections.size()); + Expects(newIndex >= 0 && newIndex < _sections.size()); + // Expects(!_inResize); + auto lefts = std::vector(); + enumerateSections([&](Section §ion) { + lefts.emplace_back(section.left); + return true; + }); + const auto wasActive = activeSection(); + + { + auto unreadCounts = base::flat_map(); + for (auto &[index, unread] : _unreadCounts) { + unreadCounts.emplace( + base::reorder_index(index, oldIndex, newIndex), + std::move(unread)); + } + _unreadCounts = std::move(unreadCounts); + } + + base::reorder(sectionsRef(), oldIndex, newIndex); + Ui::DiscreteSlider::setActiveSectionFast( + base::reorder_index(wasActive, oldIndex, newIndex)); + Ui::DiscreteSlider::stopAnimation(); + + { + _sections.clear(); + auto left = 0; + enumerateSections([&](Section §ion) { + _sections.push_back({ not_null{ §ion }, 0, false }); + section.left = left; + left += section.width; + return true; + }); + } + update(); +} + +not_null ChatsFiltersTabs::widgetAt( + int index) const { + Expects(index >= 0 && index < count()); + + return _sections[index].section; +} + +void ChatsFiltersTabs::setReordering(int value) { + _reordering = value; +} + +int ChatsFiltersTabs::reordering() const { + return _reordering; +} + +void ChatsFiltersTabs::stopAnimation() { + Ui::DiscreteSlider::stopAnimation(); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.h b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.h new file mode 100644 index 00000000000000..652ee27597b4a3 --- /dev/null +++ b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.h @@ -0,0 +1,95 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/unread_badge_paint.h" +#include "ui/widgets/discrete_sliders.h" + +namespace style { +struct SettingsSlider; +} // namespace style + +namespace Ui { + +class RpWidget; +class SettingsSlider; + +class ChatsFiltersTabsReorder; + +class ChatsFiltersTabs final : public Ui::SettingsSlider { +public: + ChatsFiltersTabs( + not_null parent, + const style::SettingsSlider &st); + + [[nodiscard]] int centerOfSection(int section) const; + void fitWidthToSections(); + void setUnreadCount(int index, int unreadCount); + void setLockedFrom(int index); + + [[nodiscard]] rpl::producer contextMenuRequested() const; + [[nodiscard]] rpl::producer<> lockedClicked() const; + + void setHorizontalShift(int index, int shift); + void setRaised(int index); + [[nodiscard]] int count() const; + void reorderSections(int oldIndex, int newIndex); + [[nodiscard]] not_null widgetAt(int i) const; + void setReordering(int value); + [[nodiscard]] int reordering() const; + + void stopAnimation(); + +protected: + struct ShiftedSection { + not_null section; + int horizontalShift = 0; + bool raise = false; + }; + friend class ChatsFiltersTabsReorder; + + void paintEvent(QPaintEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void contextMenuEvent(QContextMenuEvent *e) override; + + std::vector _sections; + +private: + [[nodiscard]] QImage cacheUnreadCount(int count) const; + [[nodiscard]] int calculateLockedFromX() const; + + using Index = int; + struct Unread final { + QImage cache; + int count = 0; + }; + base::flat_map _unreadCounts; + const style::SettingsSlider &_st; + const UnreadBadgeStyle _unreadSt; + const QString _unreadMaxString; + const int _unreadSkip; + std::vector _cachedBadgeWidths; + int _cachedBadgeHeight = 0; + int _lockedFrom = 0; + int _lockedFromX = 0; + bool _lockedPressed = false; + std::optional _bar; + std::optional _barActive; + std::optional _lockCache; + + int _reordering = 0; + + rpl::lifetime _paletteLifetime; + rpl::event_stream _contextMenuRequested; + rpl::event_stream<> _lockedClicked; + +}; + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider_reorder.cpp b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider_reorder.cpp new file mode 100644 index 00000000000000..8695aac3228862 --- /dev/null +++ b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider_reorder.cpp @@ -0,0 +1,373 @@ +/* +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/chat_filters_tabs_slider_reorder.h" + +#include "ui/widgets/scroll_area.h" +#include "styles/style_basic.h" + +#include +#include +#include + +namespace Ui { +namespace { + +constexpr auto kScrollFactor = 0.05; + +} // namespace + +ChatsFiltersTabsReorder::ChatsFiltersTabsReorder( + not_null layout, + not_null scroll) +: _layout(layout) +, _scroll(scroll) +, _scrollAnimation([this] { updateScrollCallback(); }) { +} + +ChatsFiltersTabsReorder::ChatsFiltersTabsReorder( + not_null layout) +: _layout(layout) { +} + +void ChatsFiltersTabsReorder::cancel() { + if (_currentWidget) { + cancelCurrent(indexOf(_currentWidget)); + } + _lifetime.destroy(); + for (auto i = 0, count = _layout->count(); i != count; ++i) { + _layout->setHorizontalShift(i, 0); + } + _entries.clear(); +} + +void ChatsFiltersTabsReorder::start() { + const auto count = _layout->count(); + if (count < 2) { + return; + } + _layout->events() + | rpl::start_with_next_done([this](not_null e) { + switch (e->type()) { + case QEvent::MouseMove: + mouseMove(static_cast(e.get())->globalPos()); + break; + case QEvent::MouseButtonPress: { + const auto m = static_cast(e.get()); + mousePress(m->button(), m->pos(), m->globalPos()); + break; + } + case QEvent::MouseButtonRelease: + mouseRelease(static_cast(e.get())->button()); + break; + } + }, [this] { + cancel(); + }, _lifetime); + + for (auto i = 0; i != count; ++i) { + const auto widget = _layout->widgetAt(i); + _entries.push_back({ widget }); + } +} + +void ChatsFiltersTabsReorder::addPinnedInterval(int from, int length) { + _pinnedIntervals.push_back({ from, length }); +} + +void ChatsFiltersTabsReorder::clearPinnedIntervals() { + _pinnedIntervals.clear(); +} + +bool ChatsFiltersTabsReorder::Interval::isIn(int index) const { + return (index >= from) && (index < (from + length)); +} + +bool ChatsFiltersTabsReorder::isIndexPinned(int index) const { + return ranges::any_of(_pinnedIntervals, [&](const Interval &i) { + return i.isIn(index); + }); +} + +void ChatsFiltersTabsReorder::checkForStart(QPoint position) { + const auto shift = position.x() - _currentStart; + const auto delta = QApplication::startDragDistance(); + if (std::abs(shift) <= delta) { + return; + } + _currentState = State::Started; + _currentStart += (shift > 0) ? delta : -delta; + + const auto index = indexOf(_currentWidget); + _layout->setRaised(index); + _currentDesiredIndex = index; + _updates.fire({ _currentWidget, index, index, _currentState }); + + updateOrder(index, position); +} + +void ChatsFiltersTabsReorder::updateOrder(int index, QPoint position) { + if (isIndexPinned(index)) { + return; + } + const auto shift = position.x() - _currentStart; + auto ¤t = _entries[index]; + current.shiftAnimation.stop(); + current.shift = current.finalShift = shift; + _layout->setHorizontalShift(index, shift); + + checkForScrollAnimation(); + + const auto count = _entries.size(); + const auto currentWidth = current.widget->width; + const auto currentMiddle = current.widget->left + + shift + + currentWidth / 2; + _currentDesiredIndex = index; + if (shift > 0) { + for (auto next = index + 1; next != count; ++next) { + if (isIndexPinned(next)) { + return; + } + const auto &e = _entries[next]; + if (currentMiddle < e.widget->left + e.widget->width / 2) { + moveToShift(next, 0); + } else { + _currentDesiredIndex = next; + moveToShift(next, -currentWidth); + } + } + for (auto prev = index - 1; prev >= 0; --prev) { + moveToShift(prev, 0); + } + } else { + for (auto next = index + 1; next != count; ++next) { + moveToShift(next, 0); + } + for (auto prev = index - 1; prev >= 0; --prev) { + if (isIndexPinned(prev)) { + return; + } + const auto &e = _entries[prev]; + if (currentMiddle >= e.widget->left + e.widget->width / 2) { + moveToShift(prev, 0); + } else { + _currentDesiredIndex = prev; + moveToShift(prev, currentWidth); + } + } + } +} + +void ChatsFiltersTabsReorder::mousePress( + Qt::MouseButton button, + QPoint position, + QPoint globalPosition) { + if (button != Qt::LeftButton) { + return; + } + auto widget = (ChatsFiltersTabs::ShiftedSection*)(nullptr); + for (auto i = 0; i != _layout->_sections.size(); ++i) { + auto §ion = _layout->_sections[i]; + if ((position.x() >= section.section->left) + && (position.x() < (section.section->left + section.section->width))) { + widget = §ion; + break; + } + } + cancelCurrent(); + _currentWidget = widget->section; + _currentShiftedWidget = widget; + _currentStart = globalPosition.x(); +} + +void ChatsFiltersTabsReorder::mouseMove(QPoint position) { + if (!_currentWidget) { + // if (_currentWidget != widget) { + return; + } else if (_currentState != State::Started) { + checkForStart(position); + } else { + updateOrder(indexOf(_currentWidget), position); + } +} + +void ChatsFiltersTabsReorder::mouseRelease(Qt::MouseButton button) { + if (button != Qt::LeftButton) { + return; + } + finishReordering(); +} + +void ChatsFiltersTabsReorder::cancelCurrent() { + if (_currentWidget) { + cancelCurrent(indexOf(_currentWidget)); + } +} + +void ChatsFiltersTabsReorder::cancelCurrent(int index) { + Expects(_currentWidget != nullptr); + + if (_currentState == State::Started) { + _currentState = State::Cancelled; + _updates.fire({ _currentWidget, index, index, _currentState }); + } + _currentWidget = nullptr; + _currentShiftedWidget = nullptr; + for (auto i = 0, count = int(_entries.size()); i != count; ++i) { + moveToShift(i, 0); + } +} + +void ChatsFiltersTabsReorder::finishReordering() { + if (_scroll) { + _scrollAnimation.stop(); + } + finishCurrent(); +} + +void ChatsFiltersTabsReorder::finishCurrent() { + if (!_currentWidget) { + return; + } + const auto index = indexOf(_currentWidget); + if (_currentDesiredIndex == index || _currentState != State::Started) { + cancelCurrent(index); + return; + } + const auto result = _currentDesiredIndex; + const auto widget = _currentWidget; + _currentState = State::Cancelled; + _currentWidget = nullptr; + _currentShiftedWidget = nullptr; + + auto ¤t = _entries[index]; + const auto width = current.widget->width; + if (index < result) { + auto sum = 0; + for (auto i = index; i != result; ++i) { + auto &entry = _entries[i + 1]; + const auto widget = entry.widget; + entry.deltaShift += width; + updateShift(widget, i + 1); + sum += widget->width; + } + current.finalShift -= sum; + } else if (index > result) { + auto sum = 0; + for (auto i = result; i != index; ++i) { + auto &entry = _entries[i]; + const auto widget = entry.widget; + entry.deltaShift -= width; + updateShift(widget, i); + sum += widget->width; + } + current.finalShift += sum; + } + if (!(current.finalShift + current.deltaShift)) { + current.shift = 0; + _layout->setHorizontalShift(index, 0); + } + base::reorder(_entries, index, result); + _layout->reorderSections(index, _currentDesiredIndex); + for (auto i = 0; i != _layout->sectionsRef().size(); ++i) { + _entries[i].widget = &_layout->sectionsRef()[i]; + moveToShift(i, 0); + } + + _updates.fire({ widget, index, result, State::Applied }); +} + +void ChatsFiltersTabsReorder::moveToShift(int index, int shift) { + auto &entry = _entries[index]; + if (entry.finalShift + entry.deltaShift == shift) { + return; + } + const auto widget = entry.widget; + entry.shiftAnimation.start( + [=, this] { updateShift(widget, index); }, + entry.finalShift, + shift - entry.deltaShift, + st::slideWrapDuration); + entry.finalShift = shift - entry.deltaShift; +} + +void ChatsFiltersTabsReorder::updateShift( + not_null widget, + int indexHint) { + Expects(indexHint >= 0 && indexHint < _entries.size()); + + const auto index = (_entries[indexHint].widget == widget) + ? indexHint + : indexOf(widget); + auto &entry = _entries[index]; + entry.shift = base::SafeRound( + entry.shiftAnimation.value(entry.finalShift) + ) + entry.deltaShift; + if (entry.deltaShift && !entry.shiftAnimation.animating()) { + entry.finalShift += entry.deltaShift; + entry.deltaShift = 0; + } + _layout->setHorizontalShift(index, entry.shift); +} + +int ChatsFiltersTabsReorder::indexOf(not_null widget) const { + const auto i = ranges::find(_entries, widget, &Entry::widget); + Assert(i != end(_entries)); + return i - begin(_entries); +} + +auto ChatsFiltersTabsReorder::updates() const -> rpl::producer { + return _updates.events(); +} + +void ChatsFiltersTabsReorder::updateScrollCallback() { + if (!_scroll) { + return; + } + const auto delta = deltaFromEdge(); + const auto oldLeft = _scroll->scrollLeft(); + _scroll->horizontalScrollBar()->setValue(oldLeft + delta); + const auto newLeft = _scroll->scrollLeft(); + + _currentStart += oldLeft - newLeft; + if (newLeft == 0 || newLeft == _scroll->scrollLeftMax()) { + _scrollAnimation.stop(); + } +} + +void ChatsFiltersTabsReorder::checkForScrollAnimation() { + if (!_scroll || !deltaFromEdge() || _scrollAnimation.animating()) { + return; + } + _scrollAnimation.start(); +} + +int ChatsFiltersTabsReorder::deltaFromEdge() { + Expects(_currentWidget != nullptr); + Expects(_currentShiftedWidget != nullptr); + Expects(_scroll); + + const auto globalPosition = _layout->mapToGlobal( + QPoint( + _currentWidget->left + _currentShiftedWidget->horizontalShift, + 0)); + const auto localLeft = _scroll->mapFromGlobal(globalPosition).x(); + const auto localRight = localLeft + + _currentWidget->width + - _scroll->width(); + + const auto isLeftEdge = (localLeft < 0); + const auto isRightEdge = (localRight > 0); + if (!isLeftEdge && !isRightEdge) { + _scrollAnimation.stop(); + return 0; + } + return int((isRightEdge ? localRight : localLeft) * kScrollFactor); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider_reorder.h b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider_reorder.h new file mode 100644 index 00000000000000..4a167d3665b32f --- /dev/null +++ b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider_reorder.h @@ -0,0 +1,98 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/effects/animations.h" +#include "ui/widgets/chat_filters_tabs_slider.h" + +namespace Ui { + +class ScrollArea; + +class ChatsFiltersTabsReorder final { +public: + using Section = ChatsFiltersTabs::Section; + enum class State : uchar { + Started, + Applied, + Cancelled, + }; + + struct Single { + not_null widget; + int oldPosition = 0; + int newPosition = 0; + State state = State::Started; + }; + + ChatsFiltersTabsReorder( + not_null layout, + not_null scroll); + ChatsFiltersTabsReorder(not_null layout); + + void start(); + void cancel(); + void finishReordering(); + void addPinnedInterval(int from, int length); + void clearPinnedIntervals(); + [[nodiscard]] rpl::producer updates() const; + +private: + struct Entry { + not_null widget; + Ui::Animations::Simple shiftAnimation; + int shift = 0; + int finalShift = 0; + int deltaShift = 0; + }; + struct Interval { + [[nodiscard]] bool isIn(int index) const; + + int from = 0; + int length = 0; + }; + + void mousePress(Qt::MouseButton button, QPoint position, QPoint global); + void mouseMove(QPoint position); + void mouseRelease(Qt::MouseButton button); + + void checkForStart(QPoint position); + void updateOrder(int index, QPoint position); + void cancelCurrent(); + void finishCurrent(); + void cancelCurrent(int index); + + [[nodiscard]] int indexOf(not_null widget) const; + void moveToShift(int index, int shift); + void updateShift(not_null widget, int indexHint); + + void updateScrollCallback(); + void checkForScrollAnimation(); + [[nodiscard]] int deltaFromEdge(); + + [[nodiscard]] bool isIndexPinned(int index) const; + + const not_null _layout; + Ui::ScrollArea *_scroll = nullptr; + + Ui::Animations::Basic _scrollAnimation; + + std::vector _pinnedIntervals; + + Section *_currentWidget = nullptr; + ChatsFiltersTabs::ShiftedSection *_currentShiftedWidget = nullptr; + int _currentStart = 0; + int _currentDesiredIndex = 0; + State _currentState = State::Cancelled; + std::vector _entries; + rpl::event_stream _updates; + rpl::lifetime _lifetime; + +}; + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.cpp b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.cpp new file mode 100644 index 00000000000000..56be5ee5d3a97c --- /dev/null +++ b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.cpp @@ -0,0 +1,433 @@ +/* +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/chat_filters_tabs_strip.h" + +#include "api/api_chat_filters_remove_manager.h" +#include "boxes/filters/edit_filter_box.h" +#include "boxes/premium_limits_box.h" +#include "core/application.h" +#include "data/data_chat_filters.h" +#include "data/data_premium_limits.h" +#include "data/data_session.h" +#include "data/data_unread_value.h" +#include "data/data_user.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/settings_folders.h" +#include "ui/ui_utility.h" +#include "ui/widgets/chat_filters_tabs_slider_reorder.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/wrap/slide_wrap.h" +#include "window/window_controller.h" +#include "window/window_peer_menu.h" +#include "window/window_session_controller.h" +#include "styles/style_dialogs.h" // dialogsSearchTabs +#include "styles/style_media_player.h" // mediaPlayerMenuCheck +#include "styles/style_menu_icons.h" + +#include + +namespace Ui { +namespace { + +struct State final { + Ui::Animations::Simple animation; + std::optional lastFilterId = std::nullopt; + rpl::lifetime unreadLifetime; + base::unique_qptr menu; + + Api::RemoveComplexChatFilter removeApi; + bool waitingSuggested = false; + + std::unique_ptr reorder; + bool ignoreRefresh = false; +}; + +void ShowMenu( + not_null parent, + not_null controller, + not_null state, + int index) { + const auto session = &controller->session(); + + auto id = FilterId(0); + { + const auto &list = session->data().chatsFilters().list(); + if (index < 0 || index >= list.size()) { + return; + } + id = list[index].id(); + } + state->menu = base::make_unique_q( + parent, + st::popupMenuWithIcons); + const auto addAction = Ui::Menu::CreateAddActionCallback( + state->menu.get()); + + if (id) { + addAction( + tr::lng_filters_context_edit(tr::now), + [=] { EditExistingFilter(controller, id); }, + &st::menuIconEdit); + + Window::MenuAddMarkAsReadChatListAction( + controller, + [=] { return session->data().chatsFilters().chatsList(id); }, + addAction); + + auto showRemoveBox = [=] { + state->removeApi.request(Ui::MakeWeak(parent), controller, id); + }; + addAction( + tr::lng_filters_context_remove(tr::now), + std::move(showRemoveBox), + &st::menuIconDelete); + } else { + auto customUnreadState = [=] { + return Data::MainListMapUnreadState( + session, + session->data().chatsList()->unreadState()); + }; + Window::MenuAddMarkAsReadChatListAction( + controller, + [=] { return session->data().chatsList(); }, + addAction, + std::move(customUnreadState)); + + auto openFiltersSettings = [=] { + const auto filters = &session->data().chatsFilters(); + if (filters->suggestedLoaded()) { + controller->showSettings(Settings::Folders::Id()); + } else if (!state->waitingSuggested) { + state->waitingSuggested = true; + filters->requestSuggested(); + filters->suggestedUpdated( + ) | rpl::take(1) | rpl::start_with_next([=] { + controller->showSettings(Settings::Folders::Id()); + }, parent->lifetime()); + } + }; + addAction( + tr::lng_filters_setup_menu(tr::now), + std::move(openFiltersSettings), + &st::menuIconEdit); + } + if (state->menu->empty()) { + state->menu = nullptr; + return; + } + state->menu->popup(QCursor::pos()); +} + +void ShowFiltersListMenu( + not_null parent, + not_null session, + not_null state, + int active, + Fn changeActive) { + const auto &list = session->data().chatsFilters().list(); + + state->menu = base::make_unique_q( + parent, + st::popupMenuWithIcons); + + const auto reorderAll = session->user()->isPremium(); + const auto maxLimit = (reorderAll ? 1 : 0) + + Data::PremiumLimits(session).dialogFiltersCurrent(); + const auto premiumFrom = (reorderAll ? 0 : 1) + maxLimit; + + for (auto i = 0; i < list.size(); ++i) { + const auto &filter = list[i]; + auto text = filter.title().isEmpty() + ? tr::lng_filters_all(tr::now) + : filter.title(); + + const auto action = state->menu->addAction(std::move(text), [=] { + if (i != active) { + changeActive(i); + } + }, (i == active) ? &st::mediaPlayerMenuCheck : nullptr); + action->setEnabled(i < premiumFrom); + } + session->data().chatsFilters().changed() | rpl::start_with_next([=] { + state->menu->hideMenu(); + }, state->menu->lifetime()); + + if (state->menu->empty()) { + state->menu = nullptr; + return; + } + state->menu->popup(QCursor::pos()); +} + +} // namespace + +not_null AddChatFiltersTabsStrip( + not_null parent, + not_null session, + Fn choose, + bool trackActiveFilterAndUnreadAndReorder) { + const auto window = Core::App().findWindow(parent); + const auto controller = window ? window->sessionController() : nullptr; + + const auto &scrollSt = st::defaultScrollArea; + const auto wrap = Ui::CreateChild>( + parent, + object_ptr(parent)); + if (!controller) { + return wrap; + } + const auto container = wrap->entity(); + const auto scroll = Ui::CreateChild(container, scrollSt); + const auto sliderPadding = st::dialogsSearchTabsPadding; + const auto slider = scroll->setOwnedWidget( + object_ptr>( + parent, + object_ptr(parent, st::dialogsSearchTabs), + QMargins(sliderPadding, 0, sliderPadding, 0)))->entity(); + const auto state = wrap->lifetime().make_state(); + if (trackActiveFilterAndUnreadAndReorder) { + using Reorder = Ui::ChatsFiltersTabsReorder; + state->reorder = std::make_unique(slider, scroll); + const auto applyReorder = [=]( + int oldPosition, + int newPosition) { + if (newPosition == oldPosition) { + return; + } + + const auto filters = &session->data().chatsFilters(); + const auto &list = filters->list(); + if (!session->user()->isPremium()) { + if (list[0].id() != FilterId()) { + filters->moveAllToFront(); + } + } + Assert(oldPosition >= 0 && oldPosition < list.size()); + Assert(newPosition >= 0 && newPosition < list.size()); + + auto order = ranges::views::all( + list + ) | ranges::views::transform( + &Data::ChatFilter::id + ) | ranges::to_vector; + base::reorder(order, oldPosition, newPosition); + + state->ignoreRefresh = true; + filters->saveOrder(order); + state->ignoreRefresh = false; + }; + + state->reorder->updates( + ) | rpl::start_with_next([=](const Reorder::Single &data) { + if (data.state == Reorder::State::Started) { + slider->setReordering(slider->reordering() + 1); + } else { + Ui::PostponeCall(slider, [=] { + slider->setReordering(slider->reordering() - 1); + }); + if (data.state == Reorder::State::Applied) { + applyReorder(data.oldPosition, data.newPosition); + } + } + }, slider->lifetime()); + } + wrap->toggle(false, anim::type::instant); + scroll->setCustomWheelProcess([=](not_null e) { + const auto pixelDelta = e->pixelDelta(); + const auto angleDelta = e->angleDelta(); + if (std::abs(pixelDelta.x()) + std::abs(angleDelta.x())) { + return false; + } + const auto bar = scroll->horizontalScrollBar(); + const auto y = pixelDelta.y() ? pixelDelta.y() : angleDelta.y(); + bar->setValue(bar->value() - y); + return true; + }); + + const auto scrollToIndex = [=](int index, anim::type type) { + const auto to = index + ? (slider->centerOfSection(index) - scroll->width() / 2) + : 0; + const auto bar = scroll->horizontalScrollBar(); + state->animation.stop(); + if (type == anim::type::instant) { + bar->setValue(to); + } else { + state->animation.start( + [=](float64 v) { bar->setValue(v); }, + bar->value(), + std::min(to, bar->maximum()), + st::defaultTabsSlider.duration); + } + }; + + const auto applyFilter = [=](const Data::ChatFilter &filter) { + if (slider->reordering()) { + return; + } + choose(filter.id()); + }; + + const auto filterByIndex = [=](int index) -> const Data::ChatFilter& { + const auto &list = session->data().chatsFilters().list(); + Assert(index >= 0 && index < list.size()); + return list[index]; + }; + + const auto rebuild = [=] { + const auto &list = session->data().chatsFilters().list(); + if ((list.size() <= 1) || state->ignoreRefresh) { + return; + } + auto sections = ranges::views::all( + list + ) | ranges::views::transform([](const Data::ChatFilter &filter) { + return filter.title().isEmpty() + ? tr::lng_filters_all(tr::now) + : filter.title(); + }) | ranges::to_vector; + slider->setSections(std::move(sections)); + slider->fitWidthToSections(); + { + const auto reorderAll = session->user()->isPremium(); + const auto maxLimit = (reorderAll ? 1 : 0) + + Data::PremiumLimits(session).dialogFiltersCurrent(); + const auto premiumFrom = (reorderAll ? 0 : 1) + maxLimit; + slider->setLockedFrom((premiumFrom >= list.size()) + ? 0 + : premiumFrom); + slider->lockedClicked() | rpl::start_with_next([=] { + controller->show(Box(FiltersLimitBox, session, std::nullopt)); + }, slider->lifetime()); + if (state->reorder) { + state->reorder->cancel(); + if (!reorderAll) { + state->reorder->addPinnedInterval(0, 1); + } + state->reorder->addPinnedInterval( + premiumFrom, + std::max(1, int(list.size()) - maxLimit)); + } + } + if (trackActiveFilterAndUnreadAndReorder) { + auto includeMuted = Data::IncludeMutedCounterFoldersValue(); + state->unreadLifetime.destroy(); + for (auto i = 0; i < list.size(); i++) { + rpl::combine( + Data::UnreadStateValue(session, list[i].id()), + rpl::duplicate(includeMuted) + ) | rpl::start_with_next([=]( + const Dialogs::UnreadState &state, + bool includeMuted) { + const auto muted = (state.chatsMuted + state.marksMuted); + const auto count = (state.chats + state.marks) + - (includeMuted ? 0 : muted); + slider->setUnreadCount(i, count); + slider->fitWidthToSections(); + }, state->unreadLifetime); + } + } + [&] { + const auto lookingId = state->lastFilterId.value_or(list[0].id()); + for (auto i = 0; i < list.size(); i++) { + const auto &filter = list[i]; + if (filter.id() == lookingId) { + const auto wasLast = !!state->lastFilterId; + state->lastFilterId = filter.id(); + slider->setActiveSectionFast(i); + scrollToIndex( + i, + wasLast ? anim::type::normal : anim::type::instant); + applyFilter(filter); + return; + } + } + if (list.size()) { + const auto index = 0; + const auto &filter = filterByIndex(index); + state->lastFilterId = filter.id(); + slider->setActiveSectionFast(index); + scrollToIndex(index, anim::type::instant); + applyFilter(filter); + } + }(); + if (trackActiveFilterAndUnreadAndReorder) { + controller->activeChatsFilter( + ) | rpl::start_with_next([=](FilterId id) { + const auto &list = session->data().chatsFilters().list(); + for (auto i = 0; i < list.size(); ++i) { + if (list[i].id() == id) { + slider->setActiveSection(i); + scrollToIndex(i, anim::type::normal); + break; + } + } + state->reorder->finishReordering(); + }, slider->lifetime()); + } + slider->sectionActivated() | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](int index) { + if (slider->reordering()) { + return; + } + const auto &filter = filterByIndex(index); + state->lastFilterId = filter.id(); + scrollToIndex(index, anim::type::normal); + applyFilter(filter); + }, wrap->lifetime()); + slider->contextMenuRequested() | rpl::start_with_next([=](int index) { + if (trackActiveFilterAndUnreadAndReorder) { + ShowMenu(wrap, controller, state, index); + } else { + ShowFiltersListMenu( + wrap, + session, + state, + slider->activeSection(), + [=](int i) { slider->setActiveSection(i); }); + } + }, slider->lifetime()); + wrap->toggle((list.size() > 1), anim::type::instant); + + if (state->reorder) { + state->reorder->start(); + } + }; + session->data().chatsFilters().changed( + ) | rpl::start_with_next(rebuild, wrap->lifetime()); + rebuild(); + + session->data().chatsFilters().isChatlistChanged( + ) | rpl::start_with_next([=](FilterId id) { + if (!id || !state->lastFilterId || (id != state->lastFilterId)) { + return; + } + for (const auto &filter : session->data().chatsFilters().list()) { + if (filter.id() == id) { + applyFilter(filter); + return; + } + } + }, wrap->lifetime()); + + rpl::combine( + parent->widthValue() | rpl::filter(rpl::mappers::_1 > 0), + slider->heightValue() | rpl::filter(rpl::mappers::_1 > 0) + ) | rpl::start_with_next([=](int w, int h) { + scroll->resize(w, h + scrollSt.deltax * 4); + container->resize(w, h); + wrap->resize(w, h); + }, wrap->lifetime()); + + return wrap; +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.h b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.h new file mode 100644 index 00000000000000..641065ff8897ef --- /dev/null +++ b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.h @@ -0,0 +1,26 @@ +/* +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 + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class RpWidget; +} // namespace Ui + +namespace Ui { + +not_null AddChatFiltersTabsStrip( + not_null parent, + not_null session, + Fn choose, + bool trackActiveFilterAndUnreadAndReorder = false); + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp b/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp index 53dc4e3dd01882..f686cefd05568a 100644 --- a/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp +++ b/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp @@ -8,7 +8,7 @@ For license and copyright information please follow this link: #include "ui/widgets/discrete_sliders.h" #include "ui/effects/ripple_animation.h" -#include "ui/painter.h" +#include "styles/style_widgets.h" namespace Ui { @@ -57,10 +57,21 @@ void DiscreteSlider::finishAnimating() { } } +void DiscreteSlider::setAdditionalContentWidthToSection(int index, int w) { + if (index >= 0 && index < _sections.size()) { + auto §ion = _sections[index]; + section.contentWidth = section.label.maxWidth() + w; + } +} + void DiscreteSlider::setSelectOnPress(bool selectOnPress) { _selectOnPress = selectOnPress; } +std::vector &DiscreteSlider::sectionsRef() { + return _sections; +} + void DiscreteSlider::addSection(const QString &label) { _sections.push_back(Section(label, getLabelStyle())); resizeToWidth(width()); @@ -112,7 +123,7 @@ DiscreteSlider::Range DiscreteSlider::getFinalActiveRange() const { return { 0, 0 }; } const auto width = _snapToLabel - ? std::min(raw->width, raw->label.maxWidth()) + ? std::min(raw->width, raw->contentWidth) : raw->width; return { raw->left + ((raw->width - width) / 2), width }; } @@ -125,8 +136,7 @@ DiscreteSlider::Range DiscreteSlider::getCurrentActiveRange() const { }; } -template -void DiscreteSlider::enumerateSections(Lambda callback) { +void DiscreteSlider::enumerateSections(Fn callback) { for (auto §ion : _sections) { if (!callback(section)) { return; @@ -134,9 +144,9 @@ void DiscreteSlider::enumerateSections(Lambda callback) { } } -template -void DiscreteSlider::enumerateSections(Lambda callback) const { - for (auto §ion : _sections) { +void DiscreteSlider::enumerateSections( + Fn callback) const { + for (const auto §ion : _sections) { if (!callback(section)) { return; } @@ -144,7 +154,7 @@ void DiscreteSlider::enumerateSections(Lambda callback) const { } void DiscreteSlider::mousePressEvent(QMouseEvent *e) { - auto index = getIndexFromPosition(e->pos()); + const auto index = getIndexFromPosition(e->pos()); if (_selectOnPress) { setSelectedSection(index); } @@ -153,17 +163,21 @@ void DiscreteSlider::mousePressEvent(QMouseEvent *e) { } void DiscreteSlider::mouseMoveEvent(QMouseEvent *e) { - if (_pressed < 0) return; + if (_pressed < 0) { + return; + } if (_selectOnPress) { setSelectedSection(getIndexFromPosition(e->pos())); } } void DiscreteSlider::mouseReleaseEvent(QMouseEvent *e) { - auto pressed = std::exchange(_pressed, -1); - if (pressed < 0) return; + const auto pressed = std::exchange(_pressed, -1); + if (pressed < 0) { + return; + } - auto index = getIndexFromPosition(e->pos()); + const auto index = getIndexFromPosition(e->pos()); if (pressed < _sections.size()) { if (_sections[pressed].ripple) { _sections[pressed].ripple->lastStop(); @@ -175,14 +189,16 @@ void DiscreteSlider::mouseReleaseEvent(QMouseEvent *e) { } void DiscreteSlider::setSelectedSection(int index) { - if (index < 0 || index >= _sections.size()) return; + if (index < 0 || index >= _sections.size()) { + return; + } if (_selected != index) { const auto from = getFinalActiveRange(); _selected = index; const auto to = getFinalActiveRange(); const auto duration = getAnimationDuration(); - const auto updater = [=] { update(); }; + const auto updater = [this] { update(); }; _a_left.start(updater, from.left, to.left, duration); _a_width.start(updater, from.width, to.width, duration); _callbackAfterMs = crl::now() + duration; @@ -190,8 +206,8 @@ void DiscreteSlider::setSelectedSection(int index) { } int DiscreteSlider::getIndexFromPosition(QPoint pos) { - int count = _sections.size(); - for (int i = 0; i != count; ++i) { + const auto count = _sections.size(); + for (auto i = 0; i != count; ++i) { if (_sections[i].left + _sections[i].width > pos.x()) { return i; } @@ -202,7 +218,8 @@ int DiscreteSlider::getIndexFromPosition(QPoint pos) { DiscreteSlider::Section::Section( const QString &label, const style::TextStyle &st) -: label(st, label) { +: label(st, label) +, contentWidth(Section::label.maxWidth()) { } DiscreteSlider::Section::Section( @@ -210,6 +227,7 @@ DiscreteSlider::Section::Section( const style::TextStyle &st, const std::any &context) { this->label.setMarkedText(st, label, kMarkupTextOptions, context); + contentWidth = Section::label.maxWidth(); } SettingsSlider::SettingsSlider( @@ -237,10 +255,12 @@ int SettingsSlider::getAnimationDuration() const { } void SettingsSlider::resizeSections(int newWidth) { - auto count = getSectionsCount(); - if (!count) return; + const auto count = getSectionsCount(); + if (!count) { + return; + } - auto sectionWidths = countSectionsWidths(newWidth); + const auto sectionWidths = countSectionsWidths(newWidth); auto skip = 0; auto x = 0.; @@ -258,18 +278,17 @@ void SettingsSlider::resizeSections(int newWidth) { stopAnimation(); } -std::vector SettingsSlider::countSectionsWidths( - int newWidth) const { - auto count = getSectionsCount(); - auto sectionsWidth = newWidth - (count - 1) * _st.barSkip; - auto sectionWidth = sectionsWidth / float64(count); +std::vector SettingsSlider::countSectionsWidths(int newWidth) const { + const auto count = getSectionsCount(); + const auto sectionsWidth = newWidth - (count - 1) * _st.barSkip; + const auto sectionWidth = sectionsWidth / float64(count); auto result = std::vector(count, sectionWidth); auto labelsWidth = 0; auto commonWidth = true; enumerateSections([&](const Section §ion) { - labelsWidth += section.label.maxWidth(); - if (section.label.maxWidth() >= sectionWidth) { + labelsWidth += section.contentWidth; + if (section.contentWidth >= sectionWidth) { commonWidth = false; } return true; @@ -283,7 +302,7 @@ std::vector SettingsSlider::countSectionsWidths( enumerateSections([&](const Section §ion) { Expects(currentWidth != result.end()); - *currentWidth = padding + section.label.maxWidth() + padding; + *currentWidth = padding + section.contentWidth + padding; ++currentWidth; return true; }); @@ -297,7 +316,9 @@ int SettingsSlider::resizeGetHeight(int newWidth) { } void SettingsSlider::startRipple(int sectionIndex) { - if (!_st.ripple.showDuration) return; + if (!_st.ripple.showDuration) { + return; + } auto index = 0; enumerateSections([this, &index, sectionIndex](Section §ion) { if (index++ == sectionIndex) { @@ -319,13 +340,13 @@ void SettingsSlider::startRipple(int sectionIndex) { QImage SettingsSlider::prepareRippleMask( int sectionIndex, const Section §ion) { - auto size = QSize(section.width, height() - _st.rippleBottomSkip); + const auto size = QSize(section.width, height() - _st.rippleBottomSkip); if (!_rippleTopRoundRadius || (sectionIndex > 0 && sectionIndex + 1 < getSectionsCount())) { return RippleAnimation::RectMask(size); } return RippleAnimation::MaskByDrawer(size, false, [&](QPainter &p) { - auto plusRadius = _rippleTopRoundRadius + 1; + const auto plusRadius = _rippleTopRoundRadius + 1; p.drawRoundedRect( 0, 0, @@ -347,10 +368,10 @@ QImage SettingsSlider::prepareRippleMask( } void SettingsSlider::paintEvent(QPaintEvent *e) { - Painter p(this); + auto p = QPainter(this); - auto clip = e->rect(); - auto range = getCurrentActiveRange(); + const auto clip = e->rect(); + const auto range = DiscreteSlider::getCurrentActiveRange(); const auto drawRect = [&](QRect rect, bool active = false) { const auto &bar = active ? _barActive : _bar; @@ -362,32 +383,39 @@ void SettingsSlider::paintEvent(QPaintEvent *e) { }; enumerateSections([&](Section §ion) { const auto activeWidth = _st.barSnapToLabel - ? section.label.maxWidth() + ? section.contentWidth : section.width; const auto activeLeft = section.left + (section.width - activeWidth) / 2; - auto active = 1. + const auto active = 1. - std::clamp( - qAbs(range.left - activeLeft) / float64(range.width), + std::abs(range.left - activeLeft) / float64(range.width), 0., 1.); if (section.ripple) { - auto color = anim::color(_st.rippleBg, _st.rippleBgActive, active); + const auto color = anim::color( + _st.rippleBg, + _st.rippleBgActive, + active); section.ripple->paint(p, section.left, 0, width(), &color); if (section.ripple->empty()) { section.ripple.reset(); } } if (!_st.barSnapToLabel) { - auto from = activeLeft, tofill = activeWidth; + auto from = activeLeft; + auto tofill = activeWidth; if (range.left > from) { - auto fill = qMin(tofill, range.left - from); + const auto fill = std::min(tofill, range.left - from); drawRect(myrtlrect(from, _st.barTop, fill, _st.barStroke)); from += fill; tofill -= fill; } if (range.left + activeWidth > from) { - if (auto fill = qMin(tofill, range.left + activeWidth - from)) { + const auto fill = std::min( + tofill, + range.left + activeWidth - from); + if (fill) { drawRect( myrtlrect(from, _st.barTop, fill, _st.barStroke), true); @@ -399,15 +427,20 @@ void SettingsSlider::paintEvent(QPaintEvent *e) { drawRect(myrtlrect(from, _st.barTop, tofill, _st.barStroke)); } } - const auto labelLeft = section.left + (section.width - section.label.maxWidth()) / 2; - if (myrtlrect(labelLeft, _st.labelTop, section.label.maxWidth(), _st.labelStyle.font->height).intersects(clip)) { + const auto labelLeft = section.left + + (section.width - section.contentWidth) / 2; + const auto rect = myrtlrect( + labelLeft, + _st.labelTop, + section.contentWidth, + _st.labelStyle.font->height); + if (rect.intersects(clip)) { p.setPen(anim::pen(_st.labelFg, _st.labelFgActive, active)); - section.label.drawLeft( - p, - labelLeft, - _st.labelTop, - section.label.maxWidth(), - width()); + section.label.draw(p, { + .position = QPoint(labelLeft, _st.labelTop), + .outerWidth = width(), + .availableWidth = section.label.maxWidth(), + }); } return true; }); @@ -416,7 +449,9 @@ void SettingsSlider::paintEvent(QPaintEvent *e) { const auto from = std::max(range.left - add, 0); const auto till = std::min(range.left + range.width + add, width()); if (from < till) { - drawRect(myrtlrect(from, _st.barTop, till - from, _st.barStroke), true); + drawRect( + myrtlrect(from, _st.barTop, till - from, _st.barStroke), + true); } } } diff --git a/Telegram/SourceFiles/ui/widgets/discrete_sliders.h b/Telegram/SourceFiles/ui/widgets/discrete_sliders.h index 0cb930bf118619..c89b6cfefef4d5 100644 --- a/Telegram/SourceFiles/ui/widgets/discrete_sliders.h +++ b/Telegram/SourceFiles/ui/widgets/discrete_sliders.h @@ -10,7 +10,15 @@ For license and copyright information please follow this link: #include "ui/rp_widget.h" #include "ui/round_rect.h" #include "ui/effects/animations.h" -#include "styles/style_widgets.h" + +namespace style { +struct TextStyle; +struct SettingsSlider; +} // namespace style + +namespace st { +extern const style::SettingsSlider &defaultSettingsSlider; +} // namespace st namespace Ui { @@ -36,6 +44,8 @@ class DiscreteSlider : public RpWidget { void setActiveSectionFast(int index); void finishAnimating(); + void setAdditionalContentWidthToSection(int index, int width); + [[nodiscard]] rpl::producer sectionActivated() const { return _sectionActivated.events(); } @@ -55,10 +65,11 @@ class DiscreteSlider : public RpWidget { const style::TextStyle &st, const std::any &context); - int left = 0; - int width = 0; Ui::Text::String label; std::unique_ptr ripple; + int left = 0; + int width = 0; + int contentWidth = 0; }; struct Range { int left = 0; @@ -72,11 +83,8 @@ class DiscreteSlider : public RpWidget { return _sections.size(); } - template - void enumerateSections(Lambda callback); - - template - void enumerateSections(Lambda callback) const; + void enumerateSections(Fn callback); + void enumerateSections(Fn callback) const; virtual void startRipple(int sectionIndex) { } @@ -89,6 +97,8 @@ class DiscreteSlider : public RpWidget { void setSelectOnPress(bool selectOnPress); + std::vector
§ionsRef(); + private: void activateCallback(); virtual const style::TextStyle &getLabelStyle() const = 0; @@ -116,7 +126,9 @@ class DiscreteSlider : public RpWidget { class SettingsSlider : public DiscreteSlider { public: - SettingsSlider(QWidget *parent, const style::SettingsSlider &st = st::defaultSettingsSlider); + SettingsSlider( + QWidget *parent, + const style::SettingsSlider &st = st::defaultSettingsSlider); void setRippleTopRoundRadius(int radius); @@ -127,13 +139,14 @@ class SettingsSlider : public DiscreteSlider { void startRipple(int sectionIndex) override; + std::vector countSectionsWidths(int newWidth) const; + private: const style::TextStyle &getLabelStyle() const override; int getAnimationDuration() const override; QImage prepareRippleMask(int sectionIndex, const Section §ion); void resizeSections(int newWidth); - std::vector countSectionsWidths(int newWidth) const; const style::SettingsSlider &_st; std::optional _bar; diff --git a/Telegram/SourceFiles/window/window_controller.cpp b/Telegram/SourceFiles/window/window_controller.cpp index ec9fa548e9610b..da6efc8d12e14a 100644 --- a/Telegram/SourceFiles/window/window_controller.cpp +++ b/Telegram/SourceFiles/window/window_controller.cpp @@ -229,7 +229,8 @@ void Controller::setupSideBar() { sideBarChanged(); }, _sessionController->lifetime()); - if (_sessionController->session().settings().dialogsFiltersEnabled()) { + if (_sessionController->session().settings().dialogsFiltersEnabled() + && !Core::App().settings().chatFiltersHorizontal()) { _sessionController->toggleFiltersMenu(true); } else { sideBarChanged(); diff --git a/Telegram/SourceFiles/window/window_filters_menu.cpp b/Telegram/SourceFiles/window/window_filters_menu.cpp index 8d83f1bfa13276..c9e985f0247db3 100644 --- a/Telegram/SourceFiles/window/window_filters_menu.cpp +++ b/Telegram/SourceFiles/window/window_filters_menu.cpp @@ -16,15 +16,12 @@ For license and copyright information please follow this link: #include "window/window_main_menu.h" #include "window/window_peer_menu.h" #include "main/main_session.h" -#include "core/application.h" -#include "core/core_settings.h" -#include "window/notifications_manager.h" #include "data/data_session.h" #include "data/data_chat_filters.h" -#include "data/data_folder.h" #include "data/data_user.h" #include "data/data_peer_values.h" #include "data/data_premium_limits.h" +#include "data/data_unread_value.h" #include "lang/lang_keys.h" #include "ui/filter_icons.h" #include "ui/wrap/vertical_layout.h" @@ -44,42 +41,6 @@ For license and copyright information please follow this link: #include "styles/style_menu_icons.h" namespace Window { -namespace { - -[[nodiscard]] Dialogs::UnreadState MainListMapUnreadState( - not_null session, - const Dialogs::UnreadState &state) { - const auto folderId = Data::Folder::kId; - if (const auto folder = session->data().folderLoaded(folderId)) { - return state - folder->chatsList()->unreadState(); - } - return state; -} - -[[nodiscard]] rpl::producer MainListUnreadState( - not_null list) { - return rpl::single(rpl::empty) | rpl::then( - list->unreadStateChanges() | rpl::to_empty - ) | rpl::map([=] { - return list->unreadState(); - }); -} - -[[nodiscard]] rpl::producer UnreadStateValue( - not_null session, - FilterId filterId) { - if (filterId > 0) { - const auto filters = &session->data().chatsFilters(); - return MainListUnreadState(filters->chatsList(filterId)); - } - return MainListUnreadState( - session->data().chatsList() - ) | rpl::map([=](const Dialogs::UnreadState &state) { - return MainListMapUnreadState(session, state); - }); -} - -} // namespace FiltersMenu::FiltersMenu( not_null parent, @@ -91,14 +52,7 @@ FiltersMenu::FiltersMenu( , _scroll(&_outer) , _container( _scroll.setOwnedWidget( - object_ptr(&_scroll))) -, _includeMuted(Core::App().settings().includeMutedCounterFolders()) { - Core::App().notifications().settingsChanged( - ) | rpl::filter( - rpl::mappers::_1 == Window::Notifications::ChangeType::IncludeMuted - ) | rpl::start_with_next([=] { - _includeMuted = Core::App().settings().includeMutedCounterFolders(); - }, _outer.lifetime()); + object_ptr(&_scroll))) { _drag.timer.setCallback([=] { if (_drag.filterId >= 0) { @@ -269,7 +223,7 @@ void FiltersMenu::setupList() { _reorder = std::make_unique(_list, &_scroll); _reorder->updates( - ) | rpl::start_with_next([=](Ui::VerticalLayoutReorder::Single data) { + ) | rpl::start_with_next([=](Ui::VerticalLayoutReorder::Single data) { using State = Ui::VerticalLayoutReorder::State; if (data.state == State::Started) { ++_reordering; @@ -322,8 +276,8 @@ base::unique_qptr FiltersMenu::prepareButton( raw->setIconOverride(icons.normal, icons.active); if (id >= 0) { rpl::combine( - UnreadStateValue(&_session->session(), id), - _includeMuted.value() + Data::UnreadStateValue(&_session->session(), id), + Data::IncludeMutedCounterFoldersValue() ) | rpl::start_with_next([=]( const Dialogs::UnreadState &state, bool includeMuted) { @@ -425,7 +379,7 @@ void FiltersMenu::showMenu(QPoint position, FilterId id) { if (id) { addAction( tr::lng_filters_context_edit(tr::now), - [=] { showEditBox(id); }, + [=] { EditExistingFilter(_session, id); }, &st::menuIconEdit); auto filteredChats = [=] { @@ -438,12 +392,12 @@ void FiltersMenu::showMenu(QPoint position, FilterId id) { addAction( tr::lng_filters_context_remove(tr::now), - [=] { showRemoveBox(id); }, + [=] { _removeApi.request(Ui::MakeWeak(&_outer), _session, id); }, &st::menuIconDelete); } else { auto customUnreadState = [=] { const auto session = &_session->session(); - return MainListMapUnreadState( + return Data::MainListMapUnreadState( session, session->data().chatsList()->unreadState()); }; @@ -465,105 +419,6 @@ void FiltersMenu::showMenu(QPoint position, FilterId id) { _popupMenu->popup(position); } -void FiltersMenu::showEditBox(FilterId id) { - EditExistingFilter(_session, id); -} - -void FiltersMenu::showRemoveBox(FilterId id) { - const auto session = &_session->session(); - const auto &list = session->data().chatsFilters().list(); - const auto i = ranges::find(list, id, &Data::ChatFilter::id); - const auto filter = (i != end(list)) ? *i : Data::ChatFilter(); - const auto has = filter.hasMyLinks(); - const auto confirm = [=](Fn action, bool onlyWhenHas = false) { - if (!has && onlyWhenHas) { - action(); - return; - } - _session->window().show(Ui::MakeConfirmBox({ - .text = (has - ? tr::lng_filters_delete_sure() - : tr::lng_filters_remove_sure()), - .confirmed = [=](Fn &&close) { close(); action(); }, - .confirmText = (has - ? tr::lng_box_delete() - : tr::lng_filters_remove_yes()), - .confirmStyle = &st::attentionBoxButton, - })); - }; - const auto simple = [=] { - confirm([=] { remove(id); }); - }; - const auto suggestRemoving = Api::ExtractSuggestRemoving(filter); - if (suggestRemoving.empty()) { - simple(); - return; - } else if (_removingRequestId) { - if (_removingId == id) { - return; - } - session->api().request(_removingRequestId).cancel(); - } - _removingId = id; - _removingRequestId = session->api().request( - MTPchatlists_GetLeaveChatlistSuggestions( - MTP_inputChatlistDialogFilter( - MTP_int(id))) - ).done(crl::guard(&_outer, [=](const MTPVector &result) { - _removingRequestId = 0; - const auto suggestRemovePeers = ranges::views::all( - result.v - ) | ranges::views::transform([=](const MTPPeer &peer) { - return session->data().peer(peerFromMTP(peer)); - }) | ranges::to_vector; - const auto chosen = crl::guard(&_outer, [=]( - std::vector> peers) { - remove(id, std::move(peers)); - }); - confirm(crl::guard(&_outer, [=] { - Api::ProcessFilterRemove( - _session, - filter.title(), - filter.iconEmoji(), - suggestRemoving, - suggestRemovePeers, - chosen); - }), true); - })).fail(crl::guard(&_outer, [=] { - _removingRequestId = 0; - simple(); - })).send(); -} - -void FiltersMenu::remove( - FilterId id, - std::vector> leave) { - const auto session = &_session->session(); - const auto api = &session->api(); - session->data().chatsFilters().apply(MTP_updateDialogFilter( - MTP_flags(MTPDupdateDialogFilter::Flag(0)), - MTP_int(id), - MTPDialogFilter())); - if (leave.empty()) { - api->request(MTPmessages_UpdateDialogFilter( - MTP_flags(MTPmessages_UpdateDialogFilter::Flag(0)), - MTP_int(id), - MTPDialogFilter() - )).send(); - } else { - api->request(MTPchatlists_LeaveChatlist( - MTP_inputChatlistDialogFilter(MTP_int(id)), - MTP_vector(ranges::views::all( - leave - ) | ranges::views::transform([](not_null peer) { - return MTPInputPeer(peer->input); - }) | ranges::to>()) - )).done([=](const MTPUpdates &result) { - api->applyUpdates(result); - }).send(); - } -} - void FiltersMenu::applyReorder( not_null widget, int oldPosition, diff --git a/Telegram/SourceFiles/window/window_filters_menu.h b/Telegram/SourceFiles/window/window_filters_menu.h index 54e3189c1e9975..805ae18c5b011c 100644 --- a/Telegram/SourceFiles/window/window_filters_menu.h +++ b/Telegram/SourceFiles/window/window_filters_menu.h @@ -7,6 +7,7 @@ For license and copyright information please follow this link: */ #pragma once +#include "api/api_chat_filters_remove_manager.h" #include "base/timer.h" #include "ui/effects/animations.h" #include "ui/widgets/side_bar_button.h" @@ -48,9 +49,6 @@ class FiltersMenu final { bool toBeginning = false); void setupMainMenuIcon(); void showMenu(QPoint position, FilterId id); - void showEditBox(FilterId id); - void showRemoveBox(FilterId id); - void remove(FilterId id, std::vector> leave = {}); void scrollToButton(not_null widget); void openFiltersSettings(); @@ -70,6 +68,8 @@ class FiltersMenu final { bool _ignoreRefresh = false; bool _waitingSuggested = false; + Api::RemoveComplexChatFilter _removeApi; + FilterId _removingId = 0; mtpRequestId _removingRequestId = 0; diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 70ff0bb6dbb68f..877af2d217fefe 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -7,7 +7,6 @@ For license and copyright information please follow this link: */ #include "window/window_peer_menu.h" -#include "api/api_report.h" #include "menu/menu_check_item.h" #include "boxes/share_box.h" #include "boxes/star_gift_box.h" @@ -38,10 +37,10 @@ For license and copyright information please follow this link: #include "boxes/peers/edit_contact_box.h" #include "calls/calls_instance.h" #include "inline_bots/bot_attach_web_view.h" // InlineBots::PeerType. -#include "ui/boxes/report_box_graphics.h" #include "ui/toast/toast.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" +#include "ui/widgets/chat_filters_tabs_strip.h" #include "ui/widgets/labels.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/popup_menu.h" @@ -2095,7 +2094,77 @@ QPointer ShowForwardMessagesBox( const auto state = [&] { auto controller = std::make_unique(session); const auto controllerRaw = controller.get(); - auto box = Box(std::move(controller), nullptr); + auto init = [=](not_null box) { + box->setSpecialTabMode(true); + auto applyFilter = [=](FilterId id) { + box->scrollToY(0); + auto &filters = session->data().chatsFilters(); + const auto &list = filters.list(); + if (list.size() <= 1) { + return; + } + const auto pinned = filters.chatsList(id)->pinned()->order(); + box->peerListSortRows([&]( + const PeerListRow &r1, + const PeerListRow &r2) { + const auto it1 = ranges::find_if(pinned, [&]( + const Dialogs::Key &k) { + return k.peer() == r1.peer(); + }); + const auto it2 = ranges::find_if(pinned, [&]( + const Dialogs::Key &k) { + return k.peer() == r2.peer(); + }); + if (it1 == pinned.end() && it2 != pinned.end()) { + return false; + } else if (it2 == pinned.end() && it1 != pinned.end()) { + return true; + } else if (it1 != pinned.end() && it2 != pinned.end()) { + return it1 < it2; + } + const auto history1 = session->data().history(r1.peer()); + const auto history2 = session->data().history(r2.peer()); + const auto date1 = history1->lastMessage() + ? history1->lastMessage()->date() + : TimeId(0); + const auto date2 = history2->lastMessage() + ? history2->lastMessage()->date() + : TimeId(0); + return date1 > date2; + }); + const auto filter = ranges::find( + list, + id, + &Data::ChatFilter::id); + if (filter == list.end()) { + return; + } + box->peerListPartitionRows([&](const PeerListRow &row) { + const auto rowPtr = const_cast(&row); + if (!filter->id()) { + box->peerListSetRowHidden(rowPtr, false); + } else { + const auto result = filter->contains( + session->data().history(row.peer())); + box->peerListSetRowHidden(rowPtr, !result); + } + return false; + }); + box->peerListRefreshRows(); + }; + const auto chatsFilters = Ui::AddChatFiltersTabsStrip( + box, + session, + std::move(applyFilter)); + chatsFilters->lower(); + chatsFilters->heightValue() | rpl::start_with_next([box](int h) { + box->setAddedTopScrollSkip(h); + }, box->lifetime()); + box->multiSelectHeightValue() | rpl::start_with_next([=](int h) { + chatsFilters->moveToLeft(0, h); + }, chatsFilters->lifetime()); + }; + auto box = Box(std::move(controller), std::move(init)); const auto boxRaw = box.data(); boxRaw->setForwardOptions({ .sendersCount = sendersCount, diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 06ce1e5281e438..5c54a79d2e07ac 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -1314,11 +1314,21 @@ SessionController::SessionController( closeFolder(); }, lifetime()); - session->data().chatsFilters().changed( + rpl::merge( + Core::App().settings().chatFiltersHorizontalChanges() | rpl::to_empty, + session->data().chatsFilters().changed() ) | rpl::start_with_next([=] { checkOpenedFilter(); - crl::on_main(this, [=] { - refreshFiltersMenu(); + crl::on_main(this, [this] { + if (SessionNavigation::session().data().chatsFilters().has()) { + const auto isHorizontal + = Core::App().settings().chatFiltersHorizontal(); + content()->toggleFiltersMenu(isHorizontal); + toggleFiltersMenu(!isHorizontal); + } else { + content()->toggleFiltersMenu(false); + toggleFiltersMenu(false); + } }); }, lifetime()); @@ -1549,10 +1559,6 @@ void SessionController::toggleFiltersMenu(bool enabled) { _filtersMenuChanged.fire({}); } -void SessionController::refreshFiltersMenu() { - toggleFiltersMenu(session().data().chatsFilters().has()); -} - rpl::producer<> SessionController::filtersMenuChanged() const { return _filtersMenuChanged.events(); } diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index c925aacc79da4b..8cb9a590fac083 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -630,7 +630,6 @@ class SessionController : public SessionNavigation { void init(); void setupShortcuts(); - void refreshFiltersMenu(); void checkOpenedFilter(); void suggestArchiveAndMute(); void activateFirstChatsFilter(); diff --git a/Telegram/build/deploy.sh b/Telegram/build/deploy.sh index e3217ca40973ee..90618ea558edc8 100755 --- a/Telegram/build/deploy.sh +++ b/Telegram/build/deploy.sh @@ -70,9 +70,9 @@ else DeployMac="1" DeployWin="1" DeployWin64="1" - DeployWinArm="0" + DeployWinArm="1" DeployLinux="1" - echo "Deploying four versions of $AppVersionStrFull: for Windows 32 bit, Windows 64 bit, macOS and Linux 64 bit.." + echo "Deploying five versions of $AppVersionStrFull: for Windows 32 bit / 64 bit / on ARM, macOS and Linux 64 bit.." fi if [ "$BuildTarget" == "mac" ]; then BackupPath="$HOME/Projects/backup/tdesktop" @@ -102,7 +102,7 @@ Win64RemoteFolder="tx64" WinArmDeployPath="$BackupPath/$AppVersionStrMajor/$AppVersionStrFull/tarm64" WinArmUpdateFile="tarm64upd$AppVersion" WinArmSetupFile="tsetup-arm64.$AppVersionStrFull.exe" -WinArmPortablefile="tportable-arm64.$AppVersionStrFull.zip" +WinArmPortableFile="tportable-arm64.$AppVersionStrFull.zip" WinArmRemoteFolder="tarm64" LinuxDeployPath="$BackupPath/$AppVersionStrMajor/$AppVersionStrFull/tlinux" LinuxUpdateFile="tlinuxupd$AppVersion" @@ -138,7 +138,7 @@ if [ "$AlphaVersion" != "0" ]; then Win64UpdateFile="${Win64UpdateFile}_${AlphaSignature}" Win64PortableFile="talpha${AlphaVersion}_${AlphaSignature}.zip" WinArmUpdateFile="${WinArmUpdateFile}_${AlphaSignature}" - WinArmPortablefile="talpha${AlphaVersion}_${AlphaSignature}.zip" + WinArmPortableFile="talpha${AlphaVersion}_${AlphaSignature}.zip" LinuxUpdateFile="${LinuxUpdateFile}_${AlphaSignature}" LinuxSetupFile="talpha${AlphaVersion}_${AlphaSignature}.tar.xz" fi diff --git a/Telegram/build/setup.iss b/Telegram/build/setup.iss index b0dd43f6571179..d2c75fa1b42c0d 100644 --- a/Telegram/build/setup.iss +++ b/Telegram/build/setup.iss @@ -42,8 +42,8 @@ SignTool=sha256 #define ArchModulesFolder "arm64" AppVerName={#MyAppName} {#MyAppVersion} arm64 #elif MyBuildTarget == "win64" - ArchitecturesAllowed="x64 arm64" - ArchitecturesInstallIn64BitMode="x64 arm64" + ArchitecturesAllowed="x64compatible" + ArchitecturesInstallIn64BitMode="x64compatible" OutputBaseFilename=tsetup-x64.{#MyAppVersionFull} #define ArchModulesFolder "x64" AppVerName={#MyAppName} {#MyAppVersion} 64bit diff --git a/Telegram/build/version b/Telegram/build/version index e586f2905d609c..d480f65bc0ebd4 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,7 +1,7 @@ -AppVersion 5007002 +AppVersion 5007003 AppVersionStrMajor 5.7 -AppVersionStrSmall 5.7.2 -AppVersionStr 5.7.2 -BetaChannel 0 +AppVersionStrSmall 5.7.3 +AppVersionStr 5.7.3 +BetaChannel 1 AlphaVersion 0 -AppVersionOriginal 5.7.2 +AppVersionOriginal 5.7.3.beta diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 3da81f9d2f1f5f..c7af0a7484db89 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -421,6 +421,10 @@ PRIVATE ui/widgets/fields/time_part_input_with_placeholder.cpp ui/widgets/fields/time_part_input_with_placeholder.h + ui/widgets/chat_filters_tabs_slider.cpp + ui/widgets/chat_filters_tabs_slider.h + ui/widgets/chat_filters_tabs_slider_reorder.cpp + ui/widgets/chat_filters_tabs_slider_reorder.h ui/widgets/color_editor.cpp ui/widgets/color_editor.h ui/widgets/continuous_sliders.cpp diff --git a/Telegram/lib_base b/Telegram/lib_base index 21d1ac8bfcca03..2b622fd0b223ed 160000 --- a/Telegram/lib_base +++ b/Telegram/lib_base @@ -1 +1 @@ -Subproject commit 21d1ac8bfcca03f67d7f6df75e265cd5597dc101 +Subproject commit 2b622fd0b223ed6266a32dc07382975769cc031c diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 6059e056b2a410..5c4b9657fab20c 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 6059e056b2a410688c69fb9a2d68037e6f7ca8ed +Subproject commit 5c4b9657fab20cb1cf3f487f3ffb8975fe18c28e diff --git a/changelog.txt b/changelog.txt index 5bc1594c24c2a1..b6c45eb5645699 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,8 @@ +5.7.3 beta (13.11.24) + +- Option to show folders above the chats list. +- Folders on top of recipients list box. + 5.7.2 (05.11.24) - Fix recompressed video playback cutoff.