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.