diff --git a/Telegram/Resources/animations/cloud_filters.tgs b/Telegram/Resources/animations/cloud_filters.tgs
new file mode 100644
index 000000000..3e71d25b4
Binary files /dev/null and b/Telegram/Resources/animations/cloud_filters.tgs differ
diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc
index 06a600c00..01ab9a5f7 100644
--- a/Telegram/Resources/qrc/telegram/animations.qrc
+++ b/Telegram/Resources/qrc/telegram/animations.qrc
@@ -3,6 +3,7 @@
../../animations/change_number.tgs
../../animations/blocked_peers_empty.tgs
../../animations/filters.tgs
+ ../../animations/cloud_filters.tgs
../../animations/local_passcode_enter.tgs
../../animations/cloud_password/intro.tgs
../../animations/cloud_password/password_input.tgs
diff --git a/Telegram/SourceFiles/api/api_chat_filters.cpp b/Telegram/SourceFiles/api/api_chat_filters.cpp
index 7908ddc40..1e1476b11 100644
--- a/Telegram/SourceFiles/api/api_chat_filters.cpp
+++ b/Telegram/SourceFiles/api/api_chat_filters.cpp
@@ -191,35 +191,39 @@ void InitFilterLinkHeader(
}
void ImportInvite(
- base::weak_ptr weak,
const QString &slug,
+ FilterId filterId,
const base::flat_set> &peers,
Fn done,
- Fn fail) {
+ Fn fail) {
Expects(!peers.empty());
const auto peer = peers.front();
const auto api = &peer->session().api();
const auto callback = [=](const MTPUpdates &result) {
api->applyUpdates(result);
+ if (slug.isEmpty()) {
+ peer->owner().chatsFilters().moreChatsHide(filterId, true);
+ }
done();
};
const auto error = [=](const MTP::Error &error) {
- if (const auto strong = weak.get()) {
- Ui::ShowMultilineToast({
- .parentOverride = Window::Show(strong).toastParent(),
- .text = { error.type() },
- });
- }
- fail();
+ fail(error.type());
};
auto inputs = peers | ranges::views::transform([](auto peer) {
return MTPInputPeer(peer->input);
}) | ranges::to();
- api->request(MTPchatlists_JoinChatlistInvite(
- MTP_string(slug),
- MTP_vector(std::move(inputs))
- )).done(callback).fail(error).send();
+ if (!slug.isEmpty()) {
+ api->request(MTPchatlists_JoinChatlistInvite(
+ MTP_string(slug),
+ MTP_vector(std::move(inputs))
+ )).done(callback).fail(error).send();
+ } else {
+ api->request(MTPchatlists_JoinChatlistUpdates(
+ MTP_inputChatlistDialogFilter(MTP_int(filterId)),
+ MTP_vector(std::move(inputs))
+ )).done(callback).fail(error).send();
+ }
}
ToggleChatsController::ToggleChatsController(
@@ -462,10 +466,17 @@ void ProcessFilterInvite(
// #TODO filters
} else if (!state->importing) {
state->importing = true;
- ImportInvite(weak, slug, peers, crl::guard(box, [=] {
+ ImportInvite(slug, filterId, peers, crl::guard(box, [=] {
ShowImportToast(weak, title, type, peers.size());
box->closeBox();
- }), crl::guard(box, [=] {
+ }), crl::guard(box, [=](QString text) {
+ if (const auto strong = weak.get()) {
+ Ui::ShowMultilineToast({
+ .parentOverride = Window::Show(
+ strong).toastParent(),
+ .text = { text },
+ });
+ }
state->importing = false;
}));
}
@@ -604,6 +615,17 @@ void CheckFilterInvite(
});
}
+void ProcessFilterUpdate(
+ base::weak_ptr weak,
+ FilterId filterId,
+ std::vector> missing) {
+ if (const auto strong = missing.empty() ? weak.get() : nullptr) {
+ strong->session().data().chatsFilters().moreChatsHide(filterId);
+ return;
+ }
+ ProcessFilterInvite(weak, QString(), filterId, std::move(missing), {});
+}
+
void ProcessFilterRemove(
base::weak_ptr weak,
const QString &title,
diff --git a/Telegram/SourceFiles/api/api_chat_filters.h b/Telegram/SourceFiles/api/api_chat_filters.h
index 7691d899b..c9d7e6732 100644
--- a/Telegram/SourceFiles/api/api_chat_filters.h
+++ b/Telegram/SourceFiles/api/api_chat_filters.h
@@ -25,6 +25,11 @@ void CheckFilterInvite(
not_null controller,
const QString &slug);
+void ProcessFilterUpdate(
+ base::weak_ptr weak,
+ FilterId filterId,
+ std::vector> missing);
+
void ProcessFilterRemove(
base::weak_ptr weak,
const QString &title,
diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp
index c750c47f8..15ee8c07f 100644
--- a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp
+++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp
@@ -102,13 +102,19 @@ struct Errors {
Unexpected("Peer type in ErrorForSharing.");
}
-void ShowEmptyLinkError(not_null window) {
+void ShowSaveError(
+ not_null window,
+ QString error) {
Ui::ShowMultilineToast({
.parentOverride = Window::Show(window).toastParent(),
- .text = { tr::lng_filters_empty(tr::now) },
+ .text = { error },
});
}
+void ShowEmptyLinkError(not_null window) {
+ ShowSaveError(window, tr::lng_filters_empty(tr::now));
+}
+
void ChatFilterLinkBox(
not_null box,
not_null session,
@@ -545,7 +551,7 @@ void LinkController::addHeader(not_null container) {
auto icon = CreateLottieIcon(
verticalLayout,
{
- .name = u"filters"_q,
+ .name = u"cloud_filters"_q,
.sizeOverride = {
st::settingsFilterIconSize,
st::settingsFilterIconSize,
@@ -1084,7 +1090,8 @@ void ExportFilterLink(
void EditLinkChats(
const Data::ChatFilterLink &link,
- base::flat_set> peers) {
+ base::flat_set> peers,
+ Fn done) {
Expects(!peers.empty());
Expects(link.id != 0);
Expects(!link.url.isEmpty());
@@ -1104,9 +1111,9 @@ void EditLinkChats(
)).done([=](const MTPExportedChatlistInvite &result) {
const auto &data = result.data();
const auto link = session->data().chatsFilters().add(id, result);
- //done(link);
+ done(QString());
}).fail([=](const MTP::Error &error) {
- //done({ .id = id });
+ done(error.type());
}).send();
}
@@ -1122,6 +1129,7 @@ object_ptr ShowLinkBox(
? rpl::single(link.title)
: tr::lng_filters_link_title());
+ const auto saving = std::make_shared(false);
raw->hasChangesValue(
) | rpl::start_with_next([=](bool has) {
box->setCloseByOutsideClick(!has);
@@ -1129,11 +1137,23 @@ object_ptr ShowLinkBox(
box->clearButtons();
if (has) {
box->addButton(tr::lng_settings_save(), [=] {
+ if (*saving) {
+ return;
+ }
const auto chosen = raw->selected();
if (chosen.empty()) {
ShowEmptyLinkError(window);
} else {
- EditLinkChats(link, chosen);
+ *saving = true;
+ EditLinkChats(link, chosen, crl::guard(box, [=](
+ QString error) {
+ *saving = false;
+ if (error.isEmpty()) {
+ box->closeBox();
+ } else {
+ ShowSaveError(window, error);
+ }
+ }));
}
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp
index e70665205..9d815ca18 100644
--- a/Telegram/SourceFiles/data/data_chat_filters.cpp
+++ b/Telegram/SourceFiles/data/data_chat_filters.cpp
@@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/history_unread_things.h"
#include "ui/ui_utility.h"
+#include "ui/chat/more_chats_bar.h"
#include "main/main_session.h"
#include "main/main_account.h"
#include "main/main_app_config.h"
@@ -31,6 +32,12 @@ constexpr auto kRefreshSuggestedTimeout = 7200 * crl::time(1000);
constexpr auto kLoadExceptionsAfter = 100;
constexpr auto kLoadExceptionsPerRequest = 100;
+[[nodiscard]] crl::time RequestUpdatesEach(not_null owner) {
+ const auto appConfig = &owner->session().account().appConfig();
+ return appConfig->get(u"chatlist_update_period"_q, 3600)
+ * crl::time(1000);
+}
+
} // namespace
ChatFilter::ChatFilter(
@@ -287,7 +294,9 @@ bool ChatFilter::contains(not_null history) const {
|| _always.contains(history);
}
-ChatFilters::ChatFilters(not_null owner) : _owner(owner) {
+ChatFilters::ChatFilters(not_null owner)
+: _owner(owner)
+, _moreChatsTimer([=] { checkLoadMoreChatsLists(); }) {
_list.emplace_back();
crl::on_main(&owner->session(), [=] { load(); });
}
@@ -855,4 +864,119 @@ rpl::producer<> ChatFilters::suggestedUpdated() const {
return _suggestedUpdated.events();
}
+rpl::producer ChatFilters::moreChatsContent(
+ FilterId id) {
+ if (!id) {
+ return rpl::single(Ui::MoreChatsBarContent{ .count = 0 });
+ }
+ return [=](auto consumer) {
+ auto result = rpl::lifetime();
+
+ auto &entry = _moreChatsData[id];
+ auto watching = entry.watching.lock();
+ if (!watching) {
+ watching = std::make_shared(true);
+ entry.watching = watching;
+ }
+ result.add([watching] {});
+
+ _moreChatsUpdated.events_starting_with_copy(
+ id
+ ) | rpl::start_with_next([=] {
+ consumer.put_next(Ui::MoreChatsBarContent{
+ .count = int(moreChats(id).size()),
+ });
+ }, result);
+ loadMoreChatsList(id);
+
+ return result;
+ };
+}
+
+const std::vector> &ChatFilters::moreChats(
+ FilterId id) const {
+ static const auto kEmpty = std::vector>();
+ if (!id) {
+ return kEmpty;
+ }
+ const auto i = _moreChatsData.find(id);
+ return (i != end(_moreChatsData)) ? i->second.missing : kEmpty;
+}
+
+void ChatFilters::moreChatsHide(FilterId id, bool localOnly) {
+ if (!localOnly) {
+ const auto api = &_owner->session().api();
+ api->request(MTPchatlists_HideChatlistUpdates(
+ MTP_inputChatlistDialogFilter(MTP_int(id))
+ )).send();
+ }
+
+ const auto i = _moreChatsData.find(id);
+ if (i != end(_moreChatsData)) {
+ if (const auto requestId = base::take(i->second.requestId)) {
+ _owner->session().api().request(requestId).cancel();
+ }
+ i->second.missing = {};
+ i->second.lastUpdate = crl::now();
+ _moreChatsUpdated.fire_copy(id);
+ }
+}
+
+void ChatFilters::loadMoreChatsList(FilterId id) {
+ Expects(id != 0);
+
+ const auto i = ranges::find(_list, id, &ChatFilter::id);
+ if (i == end(_list) || !i->chatlist()) {
+ return;
+ }
+
+ auto &entry = _moreChatsData[id];
+ const auto now = crl::now();
+ if (!entry.watching.lock() || entry.requestId) {
+ return;
+ }
+ const auto last = entry.lastUpdate;
+ const auto next = last ? (last + RequestUpdatesEach(_owner)) : 0;
+ if (next > now) {
+ if (!_moreChatsTimer.isActive()) {
+ _moreChatsTimer.callOnce(next - now);
+ }
+ return;
+ }
+ auto &api = _owner->session().api();
+ entry.requestId = api.request(MTPchatlists_GetChatlistUpdates(
+ MTP_inputChatlistDialogFilter(MTP_int(id))
+ )).done([=](const MTPchatlists_ChatlistUpdates &result) {
+ const auto &data = result.data();
+ _owner->processUsers(data.vusers());
+ _owner->processChats(data.vchats());
+ auto list = ranges::views::all(
+ data.vmissing_peers().v
+ ) | ranges::views::transform([&](const MTPPeer &peer) {
+ return _owner->peer(peerFromMTP(peer));
+ }) | ranges::to_vector;
+
+ auto &entry = _moreChatsData[id];
+ entry.requestId = 0;
+ entry.lastUpdate = crl::now();
+ if (!_moreChatsTimer.isActive()) {
+ _moreChatsTimer.callOnce(RequestUpdatesEach(_owner));
+ }
+ if (entry.missing != list) {
+ entry.missing = std::move(list);
+ _moreChatsUpdated.fire_copy(id);
+ }
+ }).fail([=] {
+ auto &entry = _moreChatsData[id];
+ entry.requestId = 0;
+ entry.lastUpdate = crl::now();
+ }).send();
+}
+
+void ChatFilters::checkLoadMoreChatsLists() {
+ for (const auto &[id, entry] : _moreChatsData) {
+ loadMoreChatsList(id);
+ }
+}
+
} // namespace Data
diff --git a/Telegram/SourceFiles/data/data_chat_filters.h b/Telegram/SourceFiles/data/data_chat_filters.h
index f07ec5a1b..024240061 100644
--- a/Telegram/SourceFiles/data/data_chat_filters.h
+++ b/Telegram/SourceFiles/data/data_chat_filters.h
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "base/flags.h"
+#include "base/timer.h"
class History;
@@ -16,6 +17,10 @@ class MainList;
class Key;
} // namespace Dialogs
+namespace Ui {
+struct MoreChatsBarContent;
+} // namespace Ui
+
namespace Data {
class Session;
@@ -159,7 +164,20 @@ public:
FilterId id) const;
void reloadChatlistLinks(FilterId id);
+ [[nodiscard]] rpl::producer moreChatsContent(
+ FilterId id);
+ [[nodiscard]] const std::vector> &moreChats(
+ FilterId id) const;
+ void moreChatsHide(FilterId id, bool localOnly = false);
+
private:
+ struct MoreChatsData {
+ std::vector> missing;
+ crl::time lastUpdate = 0;
+ mtpRequestId requestId = 0;
+ std::weak_ptr watching;
+ };
+
void load(bool force);
void received(const QVector &list);
bool applyOrder(const QVector &order);
@@ -167,6 +185,9 @@ private:
void applyInsert(ChatFilter filter, int position);
void applyRemove(int position);
+ void checkLoadMoreChatsLists();
+ void loadMoreChatsList(FilterId id);
+
const not_null _owner;
std::vector _list;
@@ -190,6 +211,10 @@ private:
rpl::event_stream _chatlistLinksUpdated;
mtpRequestId _linksRequestId = 0;
+ base::flat_map _moreChatsData;
+ rpl::event_stream _moreChatsUpdated;
+ base::Timer _moreChatsTimer;
+
};
} // namespace Data
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 91e5ac701..9f4522e47 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/radial_animation.h"
#include "ui/chat/requests_bar.h"
#include "ui/chat/group_call_bar.h"
+#include "ui/chat/more_chats_bar.h"
#include "ui/controls/download_bar.h"
#include "ui/controls/jump_down_button.h"
#include "ui/painter.h"
@@ -34,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_domain.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
+#include "api/api_chat_filters.h"
#include "apiwrap.h"
#include "base/event_filter.h"
#include "core/application.h"
@@ -440,6 +442,7 @@ Widget::Widget(
_searchForNarrowFilters->setRippleColorOverride(color);
}, lifetime());
+ setupMoreChatsBar();
setupDownloadBar();
}
}
@@ -542,6 +545,49 @@ void Widget::setupScrollUpButton() {
updateScrollUpVisibility();
}
+void Widget::setupMoreChatsBar() {
+ if (_layout == Layout::Child) {
+ return;
+ }
+ controller()->activeChatsFilter(
+ ) | rpl::start_with_next([=](FilterId id) {
+ if (!id) {
+ _moreChatsBar = nullptr;
+ updateControlsGeometry();
+ return;
+ }
+ const auto filters = &session().data().chatsFilters();
+ _moreChatsBar = std::make_unique(
+ this,
+ filters->moreChatsContent(id));
+
+ _moreChatsBar->barClicks(
+ ) | rpl::start_with_next([=] {
+ if (const auto missing = filters->moreChats(id)
+ ; !missing.empty()) {
+ Api::ProcessFilterUpdate(controller(), id, missing);
+ }
+ }, _moreChatsBar->lifetime());
+
+ _moreChatsBar->closeClicks(
+ ) | rpl::start_with_next([=] {
+ Api::ProcessFilterUpdate(controller(), id, {});
+ }, _moreChatsBar->lifetime());
+
+ if (_showAnimation) {
+ _moreChatsBar->hide();
+ } else {
+ _moreChatsBar->show();
+ _moreChatsBar->finishAnimating();
+ }
+
+ _moreChatsBar->heightValue(
+ ) | rpl::start_with_next([=] {
+ updateControlsGeometry();
+ }, _moreChatsBar->lifetime());
+ }, lifetime());
+}
+
void Widget::setupDownloadBar() {
if (_layout == Layout::Child) {
return;
@@ -735,6 +781,9 @@ void Widget::updateControlsVisibility(bool fast) {
_updateTelegram->show();
}
_searchControls->setVisible(!_openedFolder && !_openedForum);
+ if (_moreChatsBar) {
+ _moreChatsBar->show();
+ }
if (_openedFolder || _openedForum) {
_subsectionTopBar->show();
if (_forumTopShadow) {
@@ -1165,6 +1214,9 @@ void Widget::startSlideAnimation(
if (_subsectionTopBar) {
_subsectionTopBar->hide();
}
+ if (_moreChatsBar) {
+ _moreChatsBar->hide();
+ }
if (_forumTopShadow) {
_forumTopShadow->hide();
}
@@ -2417,7 +2469,13 @@ void Widget::updateControlsGeometry() {
barw,
st::lineWidth);
}
- const auto forumGroupCallTop = filterAreaTop + filterAreaHeight;
+ const auto moreChatsBarTop = filterAreaTop + filterAreaHeight;
+ if (_moreChatsBar) {
+ _moreChatsBar->move(0, moreChatsBarTop);
+ _moreChatsBar->resizeToWidth(barw);
+ }
+ const auto forumGroupCallTop = moreChatsBarTop
+ + (_moreChatsBar ? _moreChatsBar->height() : 0);
if (_forumGroupCallBar) {
_forumGroupCallBar->move(0, forumGroupCallTop);
_forumGroupCallBar->resizeToWidth(barw);
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h
index b33dce8fd..009f6ca3a 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h
@@ -44,6 +44,7 @@ class PlainShadow;
class DownloadBar;
class GroupCallBar;
class RequestsBar;
+class MoreChatsBar;
class JumpDownButton;
template
class FadeWrapScaled;
@@ -158,6 +159,7 @@ private:
void setupSupportMode();
void setupConnectingWidget();
void setupMainMenuToggle();
+ void setupMoreChatsBar();
void setupDownloadBar();
void setupShortcuts();
[[nodiscard]] bool searchForPeersRequired(const QString &query) const;
@@ -234,6 +236,8 @@ private:
object_ptr _cancelSearch;
object_ptr _lockUnlock;
+ std::unique_ptr _moreChatsBar;
+
std::unique_ptr _forumTopShadow;
std::unique_ptr _forumGroupCallBar;
std::unique_ptr _forumRequestsBar;
diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style
index b62b92ebf..a60ee8383 100644
--- a/Telegram/SourceFiles/ui/chat/chat.style
+++ b/Telegram/SourceFiles/ui/chat/chat.style
@@ -1281,3 +1281,21 @@ historySendDisabled: FlatLabel(defaultFlatLabel) {
historySendDisabledIcon: icon {{ "emoji/premium_lock", placeholderFgActive }};
historySendDisabledIconSkip: 20px;
historySendDisabledPosition: point(0px, 0px);
+
+moreChatsBarHeight: 48px;
+moreChatsBarTextPosition: point(12px, 4px);
+moreChatsBarStatusPosition: point(12px, 24px);
+moreChatsBarClose: IconButton(defaultIconButton) {
+ width: 48px;
+ height: 48px;
+
+ icon: boxTitleCloseIcon;
+ iconOver: boxTitleCloseIconOver;
+ iconPosition: point(12px, -1px);
+
+ rippleAreaPosition: point(0px, 4px);
+ rippleAreaSize: 40px;
+ ripple: RippleAnimation(defaultRippleAnimation) {
+ color: windowBgOver;
+ }
+}
diff --git a/Telegram/SourceFiles/ui/chat/more_chats_bar.cpp b/Telegram/SourceFiles/ui/chat/more_chats_bar.cpp
new file mode 100644
index 000000000..e645801a2
--- /dev/null
+++ b/Telegram/SourceFiles/ui/chat/more_chats_bar.cpp
@@ -0,0 +1,223 @@
+/*
+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/chat/more_chats_bar.h"
+
+#include "ui/widgets/buttons.h"
+#include "ui/widgets/shadow.h"
+#include "ui/text/text_options.h"
+#include "ui/painter.h"
+#include "lang/lang_keys.h"
+#include "styles/style_chat.h"
+#include "styles/style_window.h" // st::columnMinimalWidthLeft
+
+namespace Ui {
+
+MoreChatsBar::MoreChatsBar(
+ not_null parent,
+ rpl::producer content)
+: _wrap(parent, object_ptr(parent))
+, _inner(_wrap.entity())
+, _shadow(std::make_unique(_wrap.parentWidget()))
+, _close(_inner.get(), st::moreChatsBarClose) {
+ _wrap.hide(anim::type::instant);
+ _shadow->hide();
+
+ _wrap.entity()->paintRequest(
+ ) | rpl::start_with_next([=](QRect clip) {
+ QPainter(_wrap.entity()).fillRect(clip, st::historyPinnedBg);
+ }, lifetime());
+ _wrap.setAttribute(Qt::WA_OpaquePaintEvent);
+
+ auto copy = std::move(
+ content
+ ) | rpl::start_spawning(_wrap.lifetime());
+
+ rpl::duplicate(
+ copy
+ ) | rpl::start_with_next([=](MoreChatsBarContent &&content) {
+ _content = content;
+ if (_content.count > 0) {
+ _text.setText(
+ st::defaultMessageBar.title,
+ tr::lng_filters_bar_you_can(
+ tr::now,
+ lt_count,
+ _content.count),
+ Ui::NameTextOptions());
+ _status.setText(
+ st::defaultMessageBar.text,
+ tr::lng_filters_bar_view(
+ tr::now,
+ lt_count,
+ _content.count),
+ Ui::NameTextOptions());
+ }
+ _inner->update();
+ }, lifetime());
+
+ std::move(
+ copy
+ ) | rpl::map([=](const MoreChatsBarContent &content) {
+ return !content.count;
+ }) | rpl::start_with_next_done([=](bool hidden) {
+ _shouldBeShown = !hidden;
+ if (!_forceHidden) {
+ _wrap.toggle(_shouldBeShown, anim::type::normal);
+ }
+ }, [=] {
+ _forceHidden = true;
+ _wrap.toggle(false, anim::type::normal);
+ }, lifetime());
+
+ setupInner();
+}
+
+MoreChatsBar::~MoreChatsBar() = default;
+
+void MoreChatsBar::setupInner() {
+ _inner->resize(0, st::moreChatsBarHeight);
+ _inner->paintRequest(
+ ) | rpl::start_with_next([=](QRect rect) {
+ auto p = Painter(_inner);
+ paint(p);
+ }, _inner->lifetime());
+
+ // Clicks.
+ _inner->setCursor(style::cur_pointer);
+ _inner->events(
+ ) | rpl::filter([=](not_null event) {
+ return (event->type() == QEvent::MouseButtonPress);
+ }) | rpl::map([=] {
+ return _inner->events(
+ ) | rpl::filter([=](not_null event) {
+ return (event->type() == QEvent::MouseButtonRelease);
+ }) | rpl::take(1) | rpl::filter([=](not_null event) {
+ return _inner->rect().contains(
+ static_cast(event.get())->pos());
+ });
+ }) | rpl::flatten_latest(
+ ) | rpl::to_empty | rpl::start_to_stream(_barClicks, _inner->lifetime());
+
+ _wrap.geometryValue(
+ ) | rpl::start_with_next([=](QRect rect) {
+ updateShadowGeometry(rect);
+ updateControlsGeometry(rect);
+ }, _inner->lifetime());
+}
+
+void MoreChatsBar::paint(Painter &p) {
+ p.fillRect(_inner->rect(), st::historyComposeAreaBg);
+
+ const auto width = std::max(
+ _inner->width(),
+ st::columnMinimalWidthLeft);
+ const auto available = width
+ - st::moreChatsBarTextPosition.x()
+ - st::moreChatsBarClose.width;
+
+ p.setPen(st::defaultMessageBar.titleFg);
+ _text.drawElided(
+ p,
+ st::moreChatsBarTextPosition.x(),
+ st::moreChatsBarTextPosition.y(),
+ available);
+
+ p.setPen(st::defaultMessageBar.textFg);
+ _status.drawElided(
+ p,
+ st::moreChatsBarStatusPosition.x(),
+ st::moreChatsBarStatusPosition.y(),
+ available);
+}
+
+void MoreChatsBar::updateControlsGeometry(QRect wrapGeometry) {
+ const auto hidden = _wrap.isHidden() || !wrapGeometry.height();
+ if (_shadow->isHidden() != hidden) {
+ _shadow->setVisible(!hidden);
+ }
+ const auto width = std::max(
+ wrapGeometry.width(),
+ st::columnMinimalWidthLeft);
+ _close->move(width - _close->width(), 0);
+}
+
+void MoreChatsBar::setShadowGeometryPostprocess(Fn postprocess) {
+ _shadowGeometryPostprocess = std::move(postprocess);
+ updateShadowGeometry(_wrap.geometry());
+}
+
+void MoreChatsBar::updateShadowGeometry(QRect wrapGeometry) {
+ const auto regular = QRect(
+ wrapGeometry.x(),
+ wrapGeometry.y() + wrapGeometry.height(),
+ wrapGeometry.width(),
+ st::lineWidth);
+ _shadow->setGeometry(_shadowGeometryPostprocess
+ ? _shadowGeometryPostprocess(regular)
+ : regular);
+}
+
+void MoreChatsBar::show() {
+ if (!_forceHidden) {
+ return;
+ }
+ _forceHidden = false;
+ if (_shouldBeShown) {
+ _wrap.show(anim::type::instant);
+ _shadow->show();
+ }
+}
+
+void MoreChatsBar::hide() {
+ if (_forceHidden) {
+ return;
+ }
+ _forceHidden = true;
+ _wrap.hide(anim::type::instant);
+ _shadow->hide();
+}
+
+void MoreChatsBar::raise() {
+ _wrap.raise();
+ _shadow->raise();
+}
+
+void MoreChatsBar::finishAnimating() {
+ _wrap.finishAnimating();
+}
+
+void MoreChatsBar::move(int x, int y) {
+ _wrap.move(x, y);
+}
+
+void MoreChatsBar::resizeToWidth(int width) {
+ _wrap.entity()->resizeToWidth(width);
+ _inner->resizeToWidth(width);
+}
+
+int MoreChatsBar::height() const {
+ return !_forceHidden
+ ? _wrap.height()
+ : _shouldBeShown
+ ? st::moreChatsBarHeight
+ : 0;
+}
+
+rpl::producer MoreChatsBar::heightValue() const {
+ return _wrap.heightValue();
+}
+
+rpl::producer<> MoreChatsBar::barClicks() const {
+ return _barClicks.events();
+}
+
+rpl::producer<> MoreChatsBar::closeClicks() const {
+ return _close->clicks() | rpl::to_empty;
+}
+
+} // namespace Ui
diff --git a/Telegram/SourceFiles/ui/chat/more_chats_bar.h b/Telegram/SourceFiles/ui/chat/more_chats_bar.h
new file mode 100644
index 000000000..5536349c8
--- /dev/null
+++ b/Telegram/SourceFiles/ui/chat/more_chats_bar.h
@@ -0,0 +1,73 @@
+/*
+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/wrap/slide_wrap.h"
+#include "ui/effects/animations.h"
+#include "ui/text/text.h"
+#include "base/object_ptr.h"
+#include "base/timer.h"
+
+class Painter;
+
+namespace Ui {
+
+class PlainShadow;
+class IconButton;
+
+struct MoreChatsBarContent {
+ int count = 0;
+};
+
+class MoreChatsBar final {
+public:
+ MoreChatsBar(
+ not_null parent,
+ rpl::producer content);
+ ~MoreChatsBar();
+
+ void show();
+ void hide();
+ void raise();
+ void finishAnimating();
+
+ void setShadowGeometryPostprocess(Fn postprocess);
+
+ void move(int x, int y);
+ void resizeToWidth(int width);
+ [[nodiscard]] int height() const;
+ [[nodiscard]] rpl::producer heightValue() const;
+ [[nodiscard]] rpl::producer<> barClicks() const;
+ [[nodiscard]] rpl::producer<> closeClicks() const;
+
+ [[nodiscard]] rpl::lifetime &lifetime() {
+ return _wrap.lifetime();
+ }
+
+private:
+ void updateShadowGeometry(QRect wrapGeometry);
+ void updateControlsGeometry(QRect wrapGeometry);
+ void setupInner();
+ void paint(Painter &p);
+
+ SlideWrap<> _wrap;
+ not_null _inner;
+ std::unique_ptr _shadow;
+ object_ptr _close;
+ rpl::event_stream<> _barClicks;
+ Fn _shadowGeometryPostprocess;
+ bool _shouldBeShown = false;
+ bool _forceHidden = false;
+
+ MoreChatsBarContent _content;
+ Ui::Text::String _text;
+ Ui::Text::String _status;
+
+};
+
+} // namespace Ui
diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake
index cae79a558..404f72080 100644
--- a/Telegram/cmake/td_ui.cmake
+++ b/Telegram/cmake/td_ui.cmake
@@ -222,6 +222,8 @@ PRIVATE
ui/chat/message_bar.h
ui/chat/message_bubble.cpp
ui/chat/message_bubble.h
+ ui/chat/more_chats_bar.cpp
+ ui/chat/more_chats_bar.h
ui/chat/pinned_bar.cpp
ui/chat/pinned_bar.h
ui/chat/requests_bar.cpp