tdesktop/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp

1571 lines
43 KiB
C++

/*
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 "dialogs/ui/dialogs_suggestions.h"
#include "api/api_chat_participants.h"
#include "apiwrap.h"
#include "base/unixtime.h"
#include "boxes/peer_list_box.h"
#include "data/components/recent_peers.h"
#include "data/components/top_peers.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_folder.h"
#include "data/data_peer_values.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/history.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "main/main_session.h"
#include "settings/settings_common.h"
#include "ui/boxes/confirm_box.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/discrete_sliders.h"
#include "ui/widgets/elastic_scroll.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/shadow.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/delayed_activation.h"
#include "ui/dynamic_thumbnails.h"
#include "ui/painter.h"
#include "ui/unread_badge_paint.h"
#include "window/window_session_controller.h"
#include "window/window_peer_menu.h"
#include "styles/style_chat.h"
#include "styles/style_dialogs.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include "styles/style_window.h"
namespace Dialogs {
namespace {
constexpr auto kCollapsedChannelsCount = 5;
constexpr auto kProbablyMaxChannels = 1000;
constexpr auto kProbablyMaxRecommendations = 100;
class RecentRow final : public PeerListRow {
public:
explicit RecentRow(not_null<PeerData*> peer);
bool refreshBadge();
QSize rightActionSize() const override;
QMargins rightActionMargins() const override;
void rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) override;
bool rightActionDisabled() const override;
const style::PeerListItem &computeSt(
const style::PeerListItem &st) const override;
private:
const not_null<History*> _history;
QString _badgeString;
QSize _badgeSize;
uint32 _counter : 30 = 0;
uint32 _unread : 1 = 0;
uint32 _muted : 1 = 0;
};
class RecentsController final
: public PeerListController
, public base::has_weak_ptr {
public:
RecentsController(
not_null<Window::SessionController*> window,
RecentPeersList list);
[[nodiscard]] rpl::producer<int> count() const {
return _count.value();
}
[[nodiscard]] rpl::producer<not_null<PeerData*>> chosen() const {
return _chosen.events();
}
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
Main::Session &session() const override;
QString savedMessagesChatStatus() const override;
private:
void setupDivider();
void subscribeToEvents();
[[nodiscard]] Fn<void()> removeAllCallback();
const not_null<Window::SessionController*> _window;
RecentPeersList _recent;
rpl::variable<int> _count;
rpl::event_stream<not_null<PeerData*>> _chosen;
rpl::lifetime _lifetime;
};
class ChannelRow final : public PeerListRow {
public:
using PeerListRow::PeerListRow;
void setActive(bool active);
const style::PeerListItem &computeSt(
const style::PeerListItem &st) const override;
private:
bool _active = false;
};
class MyChannelsController final
: public PeerListController
, public base::has_weak_ptr {
public:
explicit MyChannelsController(
not_null<Window::SessionController*> window);
[[nodiscard]] rpl::producer<int> count() const {
return _count.value();
}
[[nodiscard]] rpl::producer<not_null<PeerData*>> chosen() const {
return _chosen.events();
}
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
Main::Session &session() const override;
private:
void setupDivider();
void appendRow(not_null<ChannelData*> channel);
void fill(bool force = false);
const not_null<Window::SessionController*> _window;
std::vector<not_null<History*>> _channels;
rpl::variable<Ui::RpWidget*> _toggleExpanded = nullptr;
rpl::variable<int> _count = 0;
rpl::variable<bool> _expanded = false;
rpl::event_stream<not_null<PeerData*>> _chosen;
rpl::lifetime _lifetime;
};
class RecommendationsController final
: public PeerListController
, public base::has_weak_ptr {
public:
explicit RecommendationsController(
not_null<Window::SessionController*> window);
[[nodiscard]] rpl::producer<int> count() const {
return _count.value();
}
[[nodiscard]] rpl::producer<not_null<PeerData*>> chosen() const {
return _chosen.events();
}
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
Main::Session &session() const override;
void load();
private:
void fill();
void setupDivider();
void appendRow(not_null<ChannelData*> channel);
const not_null<Window::SessionController*> _window;
rpl::variable<int> _count;
History *_activeHistory = nullptr;
bool _requested = false;
rpl::event_stream<not_null<PeerData*>> _chosen;
rpl::lifetime _lifetime;
};
struct EntryMenuDescriptor {
not_null<Window::SessionController*> controller;
not_null<PeerData*> peer;
QString removeOneText;
Fn<void()> removeOne;
QString removeAllText;
QString removeAllConfirm;
Fn<void()> removeAll;
};
[[nodiscard]] Fn<void()> RemoveAllConfirm(
not_null<Window::SessionController*> controller,
QString removeAllConfirm,
Fn<void()> removeAll) {
return [=] {
controller->show(Ui::MakeConfirmBox({
.text = removeAllConfirm,
.confirmed = [=](Fn<void()> close) { removeAll(); close(); }
}));
};
}
void FillEntryMenu(
const Ui::Menu::MenuCallback &add,
EntryMenuDescriptor &&descriptor) {
const auto peer = descriptor.peer;
const auto controller = descriptor.controller;
const auto group = peer->isMegagroup();
const auto channel = peer->isChannel();
add(tr::lng_context_new_window(tr::now), [=] {
Ui::PreventDelayedActivation();
controller->showInNewWindow(peer);
}, &st::menuIconNewWindow);
Window::AddSeparatorAndShiftUp(add);
const auto showHistoryText = group
? tr::lng_context_open_group(tr::now)
: channel
? tr::lng_context_open_channel(tr::now)
: tr::lng_profile_send_message(tr::now);
add(showHistoryText, [=] {
controller->showPeerHistory(peer);
}, channel ? &st::menuIconChannel : &st::menuIconChatBubble);
const auto viewProfileText = group
? tr::lng_context_view_group(tr::now)
: channel
? tr::lng_context_view_channel(tr::now)
: tr::lng_context_view_profile(tr::now);
add(viewProfileText, [=] {
controller->showPeerInfo(peer);
}, channel ? &st::menuIconInfo : &st::menuIconProfile);
add({ .separatorSt = &st::expandedMenuSeparator });
add({
.text = descriptor.removeOneText,
.handler = descriptor.removeOne,
.icon = &st::menuIconDeleteAttention,
.isAttention = true,
});
add({
.text = descriptor.removeAllText,
.handler = RemoveAllConfirm(
descriptor.controller,
descriptor.removeAllConfirm,
descriptor.removeAll),
.icon = &st::menuIconCancelAttention,
.isAttention = true,
});
}
RecentRow::RecentRow(not_null<PeerData*> peer)
: PeerListRow(peer)
, _history(peer->owner().history(peer)) {
if (peer->isSelf() || peer->isRepliesChat()) {
setCustomStatus(u" "_q);
} else if (const auto chat = peer->asChat()) {
if (chat->count > 0) {
setCustomStatus(
tr::lng_chat_status_members(tr::now, lt_count, chat->count));
}
} else if (const auto channel = peer->asChannel()) {
if (channel->membersCountKnown()) {
setCustomStatus((channel->isBroadcast()
? tr::lng_chat_status_subscribers
: tr::lng_chat_status_members)(
tr::now,
lt_count,
channel->membersCount()));
}
}
refreshBadge();
}
bool RecentRow::refreshBadge() {
if (_history->peer->isSelf()) {
return false;
}
auto result = false;
const auto muted = _history->muted() ? 1 : 0;
if (_muted != muted) {
_muted = muted;
if (_counter || _unread) {
result = true;
}
}
const auto badges = _history->chatListBadgesState();
const auto unread = badges.unread ? 1 : 0;
if (_counter != badges.unreadCounter || _unread != unread) {
_counter = badges.unreadCounter;
_unread = unread;
result = true;
_badgeString = !_counter
? (_unread ? u" "_q : QString())
: (_counter < 1000)
? QString::number(_counter)
: (QString::number(_counter / 1000) + 'K');
if (_badgeString.isEmpty()) {
_badgeSize = QSize();
} else {
auto st = Ui::UnreadBadgeStyle();
const auto unreadRectHeight = st.size;
const auto unreadWidth = st.font->width(_badgeString);
_badgeSize = QSize(
std::max(unreadWidth + 2 * st.padding, unreadRectHeight),
unreadRectHeight);
}
}
return result;
}
QSize RecentRow::rightActionSize() const {
return _badgeSize;
}
QMargins RecentRow::rightActionMargins() const {
if (_badgeSize.isEmpty()) {
return {};
}
const auto x = st::recentPeersItem.photoPosition.x();
const auto y = (st::recentPeersItem.height - _badgeSize.height()) / 2;
return QMargins(x, y, x, y);
}
void RecentRow::rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) {
if (!_counter && !_unread) {
return;
} else if (_badgeString.isEmpty()) {
_badgeString = !_counter
? u" "_q
: (_counter < 1000)
? QString::number(_counter)
: (QString::number(_counter / 1000) + 'K');
}
auto st = Ui::UnreadBadgeStyle();
st.selected = selected;
st.muted = _muted;
const auto &counter = _badgeString;
PaintUnreadBadge(p, counter, x + _badgeSize.width(), y, st);
}
bool RecentRow::rightActionDisabled() const {
return true;
}
const style::PeerListItem &RecentRow::computeSt(
const style::PeerListItem &st) const {
return (peer()->isSelf() || peer()->isRepliesChat())
? st::recentPeersSpecialName
: st;
}
void ChannelRow::setActive(bool active) {
_active = active;
}
const style::PeerListItem &ChannelRow::computeSt(
const style::PeerListItem &st) const {
return _active ? st::recentPeersItemActive : st::recentPeersItem;
}
RecentsController::RecentsController(
not_null<Window::SessionController*> window,
RecentPeersList list)
: _window(window)
, _recent(std::move(list)) {
}
void RecentsController::prepare() {
setupDivider();
for (const auto &peer : _recent.list) {
delegate()->peerListAppendRow(std::make_unique<RecentRow>(peer));
}
delegate()->peerListRefreshRows();
_count = _recent.list.size();
subscribeToEvents();
}
void RecentsController::rowClicked(not_null<PeerListRow*> row) {
_chosen.fire(row->peer());
}
Fn<void()> RecentsController::removeAllCallback() {
const auto weak = base::make_weak(this);
const auto session = &_window->session();
return crl::guard(session, [=] {
if (weak) {
_count = 0;
while (delegate()->peerListFullRowsCount() > 0) {
delegate()->peerListRemoveRow(delegate()->peerListRowAt(0));
}
delegate()->peerListRefreshRows();
}
session->recentPeers().clear();
});
}
base::unique_qptr<Ui::PopupMenu> RecentsController::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
auto result = base::make_unique_q<Ui::PopupMenu>(
parent,
st::popupMenuWithIcons);
const auto peer = row->peer();
const auto weak = base::make_weak(this);
const auto session = &_window->session();
const auto removeOne = crl::guard(session, [=] {
if (weak) {
const auto rowId = peer->id.value;
if (const auto row = delegate()->peerListFindRow(rowId)) {
_count = std::max(0, _count.current() - 1);
delegate()->peerListRemoveRow(row);
delegate()->peerListRefreshRows();
}
}
session->recentPeers().remove(peer);
});
FillEntryMenu(Ui::Menu::CreateAddActionCallback(result), {
.controller = _window,
.peer = peer,
.removeOneText = tr::lng_recent_remove(tr::now),
.removeOne = removeOne,
.removeAllText = tr::lng_recent_clear_all(tr::now),
.removeAllConfirm = tr::lng_recent_clear_sure(tr::now),
.removeAll = removeAllCallback(),
});
return result;
}
Main::Session &RecentsController::session() const {
return _window->session();
}
QString RecentsController::savedMessagesChatStatus() const {
return tr::lng_saved_forward_here(tr::now);
}
void RecentsController::setupDivider() {
auto result = object_ptr<Ui::FixedHeightWidget>(
(QWidget*)nullptr,
st::searchedBarHeight);
const auto raw = result.data();
const auto label = Ui::CreateChild<Ui::FlatLabel>(
raw,
tr::lng_recent_title(),
st::searchedBarLabel);
const auto clear = Ui::CreateChild<Ui::LinkButton>(
raw,
tr::lng_recent_clear(tr::now),
st::searchedBarLink);
clear->setClickedCallback(RemoveAllConfirm(
_window,
tr::lng_recent_clear_sure(tr::now),
removeAllCallback()));
rpl::combine(
raw->sizeValue(),
clear->widthValue()
) | rpl::start_with_next([=](QSize size, int width) {
const auto x = st::searchedBarPosition.x();
const auto y = st::searchedBarPosition.y();
clear->moveToRight(0, 0, size.width());
label->resizeToWidth(size.width() - x - width);
label->moveToLeft(x, y, size.width());
}, raw->lifetime());
raw->paintRequest() | rpl::start_with_next([=](QRect clip) {
QPainter(raw).fillRect(clip, st::searchedBarBg);
}, raw->lifetime());
delegate()->peerListSetAboveWidget(std::move(result));
}
void RecentsController::subscribeToEvents() {
using Flag = Data::PeerUpdate::Flag;
session().changes().peerUpdates(
Flag::Notifications
| Flag::OnlineStatus
) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
const auto peer = update.peer;
if (peer->isSelf()) {
return;
}
auto refreshed = false;
const auto row = delegate()->peerListFindRow(update.peer->id.value);
if (!row) {
return;
} else if (update.flags & Flag::Notifications) {
refreshed = static_cast<RecentRow*>(row)->refreshBadge();
}
if (!peer->isRepliesChat() && (update.flags & Flag::OnlineStatus)) {
row->clearCustomStatus();
refreshed = true;
}
if (refreshed) {
delegate()->peerListUpdateRow(row);
}
}, _lifetime);
session().data().unreadBadgeChanges(
) | rpl::start_with_next([=] {
for (auto i = 0; i != _count.current(); ++i) {
const auto row = delegate()->peerListRowAt(i);
if (static_cast<RecentRow*>(row.get())->refreshBadge()) {
delegate()->peerListUpdateRow(row);
}
}
}, _lifetime);
}
MyChannelsController::MyChannelsController(
not_null<Window::SessionController*> window)
: _window(window) {
}
void MyChannelsController::prepare() {
setupDivider();
session().changes().peerUpdates(
Data::PeerUpdate::Flag::ChannelAmIn
) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
const auto channel = update.peer->asBroadcast();
if (!channel || channel->amIn()) {
return;
}
const auto history = channel->owner().history(channel);
const auto i = ranges::remove(_channels, history);
if (i == end(_channels)) {
return;
}
_channels.erase(i, end(_channels));
const auto row = delegate()->peerListFindRow(channel->id.value);
if (row) {
delegate()->peerListRemoveRow(row);
}
_count = int(_channels.size());
fill(true);
}, _lifetime);
_channels.reserve(kProbablyMaxChannels);
const auto owner = &session().data();
const auto add = [&](not_null<Dialogs::MainList*> list) {
for (const auto &row : list->indexed()->all()) {
if (const auto history = row->history()) {
if (const auto channel = history->peer->asBroadcast()) {
_channels.push_back(history);
}
}
}
};
add(owner->chatsList());
if (const auto folder = owner->folderLoaded(Data::Folder::kId)) {
add(owner->chatsList(folder));
}
ranges::sort(_channels, ranges::greater(), &History::chatListTimeId);
_count = int(_channels.size());
_expanded.value() | rpl::start_with_next([=] {
fill();
}, _lifetime);
auto loading = owner->chatsListChanges(
) | rpl::take_while([=](Data::Folder *folder) {
return !owner->chatsListLoaded(folder);
});
rpl::merge(
std::move(loading),
owner->chatsListLoadedEvents()
) | rpl::start_with_next([=](Data::Folder *folder) {
const auto list = owner->chatsList(folder);
for (const auto &row : list->indexed()->all()) {
if (const auto history = row->history()) {
if (const auto channel = history->peer->asBroadcast()) {
if (ranges::contains(_channels, not_null(history))) {
_channels.push_back(history);
}
}
}
}
const auto was = _count.current();
const auto now = int(_channels.size());
if (was != now) {
_count = now;
fill();
}
}, _lifetime);
}
void MyChannelsController::fill(bool force) {
const auto count = _count.current();
const auto limit = _expanded.current()
? count
: std::min(count, kCollapsedChannelsCount);
const auto already = delegate()->peerListFullRowsCount();
const auto delta = limit - already;
if (!delta && !force) {
return;
} else if (delta > 0) {
for (auto i = already; i != limit; ++i) {
appendRow(_channels[i]->peer->asBroadcast());
}
} else if (delta < 0) {
for (auto i = already; i != limit;) {
delegate()->peerListRemoveRow(delegate()->peerListRowAt(--i));
}
}
delegate()->peerListRefreshRows();
}
void MyChannelsController::appendRow(not_null<ChannelData*> channel) {
auto row = std::make_unique<PeerListRow>(channel);
if (channel->membersCountKnown()) {
row->setCustomStatus((channel->isBroadcast()
? tr::lng_chat_status_subscribers
: tr::lng_chat_status_members)(
tr::now,
lt_count,
channel->membersCount()));
}
delegate()->peerListAppendRow(std::move(row));
}
void MyChannelsController::rowClicked(not_null<PeerListRow*> row) {
_chosen.fire(row->peer());
}
base::unique_qptr<Ui::PopupMenu> MyChannelsController::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
auto result = base::make_unique_q<Ui::PopupMenu>(
parent,
st::popupMenuWithIcons);
const auto peer = row->peer();
const auto addAction = Ui::Menu::CreateAddActionCallback(result);
Window::FillDialogsEntryMenu(
_window,
Dialogs::EntryState{
.key = peer->owner().history(peer),
.section = Dialogs::EntryState::Section::ContextMenu,
},
addAction);
return result;
}
Main::Session &MyChannelsController::session() const {
return _window->session();
}
void MyChannelsController::setupDivider() {
auto result = object_ptr<Ui::FixedHeightWidget>(
(QWidget*)nullptr,
st::searchedBarHeight);
const auto raw = result.data();
const auto label = Ui::CreateChild<Ui::FlatLabel>(
raw,
tr::lng_channels_your_title(),
st::searchedBarLabel);
_count.value(
) | rpl::map(
rpl::mappers::_1 > kCollapsedChannelsCount
) | rpl::distinct_until_changed() | rpl::start_with_next([=](bool more) {
_expanded = false;
if (!more) {
const auto toggle = _toggleExpanded.current();
_toggleExpanded = nullptr;
delete toggle;
return;
} else if (_toggleExpanded.current()) {
return;
}
const auto toggle = Ui::CreateChild<Ui::LinkButton>(
raw,
tr::lng_channels_your_more(tr::now),
st::searchedBarLink);
toggle->show();
toggle->setClickedCallback([=] {
const auto expand = !_expanded.current();
toggle->setText(expand
? tr::lng_channels_your_less(tr::now)
: tr::lng_channels_your_more(tr::now));
_expanded = expand;
});
rpl::combine(
raw->sizeValue(),
toggle->widthValue()
) | rpl::start_with_next([=](QSize size, int width) {
const auto x = st::searchedBarPosition.x();
const auto y = st::searchedBarPosition.y();
toggle->moveToRight(0, 0, size.width());
label->resizeToWidth(size.width() - x - width);
label->moveToLeft(x, y, size.width());
}, toggle->lifetime());
_toggleExpanded = toggle;
}, raw->lifetime());
rpl::combine(
raw->sizeValue(),
_toggleExpanded.value()
) | rpl::filter(
rpl::mappers::_2 == nullptr
) | rpl::start_with_next([=](QSize size, const auto) {
const auto x = st::searchedBarPosition.x();
const auto y = st::searchedBarPosition.y();
label->resizeToWidth(size.width() - x * 2);
label->moveToLeft(x, y, size.width());
}, raw->lifetime());
raw->paintRequest() | rpl::start_with_next([=](QRect clip) {
QPainter(raw).fillRect(clip, st::searchedBarBg);
}, raw->lifetime());
delegate()->peerListSetAboveWidget(std::move(result));
}
RecommendationsController::RecommendationsController(
not_null<Window::SessionController*> window)
: _window(window) {
}
void RecommendationsController::prepare() {
setupDivider();
fill();
}
void RecommendationsController::load() {
if (_requested || _count.current()) {
return;
}
_requested = true;
const auto participants = &session().api().chatParticipants();
participants->loadRecommendations();
participants->recommendationsLoaded(
) | rpl::take(1) | rpl::start_with_next([=] {
fill();
}, _lifetime);
}
void RecommendationsController::fill() {
const auto participants = &session().api().chatParticipants();
const auto &list = participants->recommendations().list;
if (list.empty()) {
return;
}
for (const auto &peer : list) {
if (const auto channel = peer->asBroadcast()) {
appendRow(channel);
}
}
delegate()->peerListRefreshRows();
_count = delegate()->peerListFullRowsCount();
_window->activeChatValue() | rpl::start_with_next([=](const Key &key) {
const auto history = key.history();
if (_activeHistory == history) {
return;
} else if (_activeHistory) {
const auto id = _activeHistory->peer->id.value;
if (const auto row = delegate()->peerListFindRow(id)) {
static_cast<ChannelRow*>(row)->setActive(false);
delegate()->peerListUpdateRow(row);
}
}
_activeHistory = history;
if (_activeHistory) {
const auto id = _activeHistory->peer->id.value;
if (const auto row = delegate()->peerListFindRow(id)) {
static_cast<ChannelRow*>(row)->setActive(true);
delegate()->peerListUpdateRow(row);
}
}
}, _lifetime);
}
void RecommendationsController::appendRow(not_null<ChannelData*> channel) {
auto row = std::make_unique<ChannelRow>(channel);
if (channel->membersCountKnown()) {
row->setCustomStatus((channel->isBroadcast()
? tr::lng_chat_status_subscribers
: tr::lng_chat_status_members)(
tr::now,
lt_count,
channel->membersCount()));
}
delegate()->peerListAppendRow(std::move(row));
}
void RecommendationsController::rowClicked(not_null<PeerListRow*> row) {
_chosen.fire(row->peer());
}
base::unique_qptr<Ui::PopupMenu> RecommendationsController::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
return nullptr;
}
Main::Session &RecommendationsController::session() const {
return _window->session();
}
void RecommendationsController::setupDivider() {
auto result = object_ptr<Ui::FixedHeightWidget>(
(QWidget*)nullptr,
st::searchedBarHeight);
const auto raw = result.data();
const auto label = Ui::CreateChild<Ui::FlatLabel>(
raw,
tr::lng_channels_recommended(),
st::searchedBarLabel);
raw->sizeValue(
) | rpl::start_with_next([=](QSize size) {
const auto x = st::searchedBarPosition.x();
const auto y = st::searchedBarPosition.y();
label->resizeToWidth(size.width() - x * 2);
label->moveToLeft(x, y, size.width());
}, raw->lifetime());
raw->paintRequest() | rpl::start_with_next([=](QRect clip) {
QPainter(raw).fillRect(clip, st::searchedBarBg);
}, raw->lifetime());
delegate()->peerListSetAboveWidget(std::move(result));
}
} // namespace
Suggestions::Suggestions(
not_null<QWidget*> parent,
not_null<Window::SessionController*> controller,
rpl::producer<TopPeersList> topPeers,
RecentPeersList recentPeers)
: RpWidget(parent)
, _controller(controller)
, _tabs(std::make_unique<Ui::SettingsSlider>(this, st::dialogsSearchTabs))
, _chatsScroll(std::make_unique<Ui::ElasticScroll>(this))
, _chatsContent(
_chatsScroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(this)))
, _topPeersWrap(
_chatsContent->add(object_ptr<Ui::SlideWrap<TopPeersStrip>>(
this,
object_ptr<TopPeersStrip>(this, std::move(topPeers)))))
, _topPeers(_topPeersWrap->entity())
, _recentPeers(_chatsContent->add(setupRecentPeers(std::move(recentPeers))))
, _emptyRecent(_chatsContent->add(setupEmptyRecent()))
, _channelsScroll(std::make_unique<Ui::ElasticScroll>(this))
, _channelsContent(
_channelsScroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(this)))
, _myChannels(_channelsContent->add(setupMyChannels()))
, _recommendations(_channelsContent->add(setupRecommendations()))
, _emptyChannels(_channelsContent->add(setupEmptyChannels())) {
setupTabs();
setupChats();
setupChannels();
}
Suggestions::~Suggestions() = default;
void Suggestions::setupTabs() {
const auto shadow = Ui::CreateChild<Ui::PlainShadow>(this);
shadow->lower();
_tabs->sizeValue() | rpl::start_with_next([=](QSize size) {
const auto line = st::lineWidth;
shadow->setGeometry(0, size.height() - line, width(), line);
}, shadow->lifetime());
shadow->showOn(_tabs->shownValue());
_tabs->setSections({
tr::lng_recent_chats(tr::now),
tr::lng_recent_channels(tr::now),
});
_tabs->sectionActivated(
) | rpl::start_with_next([=](int section) {
switchTab(section ? Tab::Channels : Tab::Chats);
}, _tabs->lifetime());
}
void Suggestions::setupChats() {
_recentCount.value() | rpl::start_with_next([=](int count) {
_recentPeers->toggle(count > 0, anim::type::instant);
_emptyRecent->toggle(count == 0, anim::type::instant);
}, _recentPeers->lifetime());
_topPeers->emptyValue() | rpl::start_with_next([=](bool empty) {
_topPeersWrap->toggle(!empty, anim::type::instant);
}, _topPeers->lifetime());
_topPeers->clicks() | rpl::start_with_next([=](uint64 peerIdRaw) {
const auto peerId = PeerId(peerIdRaw);
_topPeerChosen.fire(_controller->session().data().peer(peerId));
}, _topPeers->lifetime());
_topPeers->showMenuRequests(
) | rpl::start_with_next([=](const ShowTopPeerMenuRequest &request) {
const auto weak = Ui::MakeWeak(this);
const auto owner = &_controller->session().data();
const auto peer = owner->peer(PeerId(request.id));
const auto removeOne = [=] {
peer->session().topPeers().remove(peer);
if (weak) {
_topPeers->removeLocally(peer->id.value);
}
};
const auto session = &_controller->session();
const auto removeAll = crl::guard(session, [=] {
session->topPeers().toggleDisabled(true);
if (weak) {
_topPeers->removeLocally();
}
});
FillEntryMenu(request.callback, {
.controller = _controller,
.peer = peer,
.removeOneText = tr::lng_recent_remove(tr::now),
.removeOne = removeOne,
.removeAllText = tr::lng_recent_hide_top(
tr::now).replace('&', u"&&"_q),
.removeAllConfirm = tr::lng_recent_hide_sure(tr::now),
.removeAll = removeAll,
});
}, _topPeers->lifetime());
_chatsScroll->setVisible(_tab.current() == Tab::Chats);
}
void Suggestions::setupChannels() {
_myChannelsCount.value() | rpl::start_with_next([=](int count) {
_myChannels->toggle(count > 0, anim::type::instant);
}, _myChannels->lifetime());
_recommendationsCount.value() | rpl::start_with_next([=](int count) {
_recommendations->toggle(count > 0, anim::type::instant);
}, _recommendations->lifetime());
_emptyChannels->toggleOn(
rpl::combine(
_myChannelsCount.value(),
_recommendationsCount.value(),
rpl::mappers::_1 + rpl::mappers::_2 == 0),
anim::type::instant);
_channelsScroll->setVisible(_tab.current() == Tab::Channels);
}
void Suggestions::selectJump(Qt::Key direction, int pageSize) {
const auto recentHasSelection = [=] {
return _recentSelectJump(Qt::Key(), 0) == JumpResult::Applied;
};
if (pageSize) {
if (direction == Qt::Key_Down || direction == Qt::Key_Up) {
_topPeers->deselectByKeyboard();
if (!recentHasSelection()) {
if (direction == Qt::Key_Down) {
_recentSelectJump(direction, 0);
} else {
return;
}
}
if (_recentSelectJump(direction, pageSize) == JumpResult::AppliedAndOut) {
if (direction == Qt::Key_Up) {
_chatsScroll->scrollTo(0);
}
}
}
} else if (direction == Qt::Key_Up) {
if (_recentSelectJump(direction, pageSize)
== JumpResult::AppliedAndOut) {
_topPeers->selectByKeyboard(Qt::Key());
_chatsScroll->scrollTo(0);
} else {
_topPeers->deselectByKeyboard();
}
} else if (direction == Qt::Key_Down) {
if (_topPeers->selectedByKeyboard()) {
if (_recentCount.current() > 0) {
_topPeers->deselectByKeyboard();
_recentSelectJump(direction, pageSize);
}
} else if (!_topPeersWrap->toggled() || recentHasSelection()) {
_recentSelectJump(direction, pageSize);
} else {
_topPeers->selectByKeyboard(Qt::Key());
_chatsScroll->scrollTo(0);
}
} else if (direction == Qt::Key_Left || direction == Qt::Key_Right) {
if (!recentHasSelection()) {
_topPeers->selectByKeyboard(direction);
_chatsScroll->scrollTo(0);
}
}
}
void Suggestions::chooseRow() {
if (!_topPeers->chooseRow()) {
_recentPeersChoose();
}
}
void Suggestions::show(anim::type animated, Fn<void()> finish) {
RpWidget::show();
_hidden = false;
if (animated == anim::type::instant) {
finishShow();
} else {
startShownAnimation(true, std::move(finish));
}
}
void Suggestions::hide(anim::type animated, Fn<void()> finish) {
_hidden = true;
if (isHidden()) {
return;
} else if (animated == anim::type::instant) {
RpWidget::hide();
} else {
startShownAnimation(false, std::move(finish));
}
}
void Suggestions::switchTab(Tab tab) {
if (_tab.current() == tab) {
return;
}
_tab = tab;
_persist = false;
if (_tabs->isHidden()) {
return;
}
startSlideAnimation();
}
void Suggestions::startSlideAnimation() {
if (!_slideAnimation.animating()) {
_slideLeft = Ui::GrabWidget(_chatsScroll.get());
_slideRight = Ui::GrabWidget(_channelsScroll.get());
_chatsScroll->hide();
_channelsScroll->hide();
}
const auto channels = (_tab.current() == Tab::Channels);
const auto from = channels ? 0. : 1.;
const auto to = channels ? 1. : 0.;
_slideAnimation.start([=] {
update();
if (!_slideAnimation.animating() && !_shownAnimation.animating()) {
finishShow();
}
}, from, to, st::slideDuration, anim::sineInOut);
}
void Suggestions::startShownAnimation(bool shown, Fn<void()> finish) {
const auto from = shown ? 0. : 1.;
const auto to = shown ? 1. : 0.;
_shownAnimation.start([=] {
update();
if (!_shownAnimation.animating() && finish) {
finish();
if (shown) {
finishShow();
}
}
}, from, to, st::slideDuration, anim::easeOutQuint);
if (_cache.isNull()) {
const auto now = width();
if (now < st::columnMinimalWidthLeft) {
resize(st::columnMinimalWidthLeft, height());
}
_cache = Ui::GrabWidget(this);
if (now < st::columnMinimalWidthLeft) {
resize(now, height());
}
}
_tabs->hide();
_chatsScroll->hide();
_channelsScroll->hide();
_slideAnimation.stop();
}
void Suggestions::finishShow() {
_slideAnimation.stop();
_slideLeft = _slideRight = QPixmap();
_shownAnimation.stop();
_cache = QPixmap();
_tabs->show();
const auto channels = (_tab.current() == Tab::Channels);
_chatsScroll->setVisible(!channels);
_channelsScroll->setVisible(channels);
}
float64 Suggestions::shownOpacity() const {
return _shownAnimation.value(_hidden ? 0. : 1.);
}
void Suggestions::paintEvent(QPaintEvent *e) {
const auto opacity = shownOpacity();
auto color = st::windowBg->c;
color.setAlphaF(color.alphaF() * opacity);
auto p = QPainter(this);
p.fillRect(e->rect(), color);
if (!_cache.isNull()) {
const auto slide = st::topPeers.height + st::searchedBarHeight;
p.setOpacity(opacity);
p.drawPixmap(0, (opacity - 1.) * slide, _cache);
} else if (!_slideLeft.isNull()) {
const auto slide = st::topPeers.height + st::searchedBarHeight;
const auto right = (_tab.current() == Tab::Channels);
const auto progress = _slideAnimation.value(right ? 1. : 0.);
const auto shift = st::topPeers.height + st::searchedBarHeight;
p.setOpacity(1. - progress);
p.drawPixmap(
anim::interpolate(0, -slide, progress),
_chatsScroll->y(),
_slideLeft);
p.setOpacity(progress);
p.drawPixmap(
anim::interpolate(slide, 0, progress),
_channelsScroll->y(),
_slideRight);
}
}
void Suggestions::resizeEvent(QResizeEvent *e) {
const auto w = std::max(width(), st::columnMinimalWidthLeft);
_tabs->resizeToWidth(w);
const auto tabs = _tabs->height();
_chatsScroll->setGeometry(0, tabs, w, height() - tabs);
_chatsContent->resizeToWidth(w);
_channelsScroll->setGeometry(0, tabs, w, height() - tabs);
_channelsContent->resizeToWidth(w);
}
object_ptr<Ui::SlideWrap<>> Suggestions::setupRecentPeers(
RecentPeersList recentPeers) {
auto &lifetime = _chatsContent->lifetime();
const auto delegate = lifetime.make_state<
PeerListContentDelegateSimple
>();
const auto controller = lifetime.make_state<RecentsController>(
_controller,
std::move(recentPeers));
controller->setStyleOverrides(&st::recentPeersList);
_recentCount = controller->count();
controller->chosen(
) | rpl::start_with_next([=](not_null<PeerData*> peer) {
_controller->session().recentPeers().bump(peer);
_recentPeerChosen.fire_copy(peer);
}, lifetime);
auto content = object_ptr<PeerListContent>(_chatsContent, controller);
const auto raw = content.data();
_recentPeersChoose = [=] {
return raw->submitted();
};
_recentSelectJump = [raw](Qt::Key direction, int pageSize) {
const auto had = raw->hasSelection();
if (direction == Qt::Key()) {
return had ? JumpResult::Applied : JumpResult::NotApplied;
} else if (direction == Qt::Key_Up && !had) {
return JumpResult::NotApplied;
} else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {
const auto delta = (direction == Qt::Key_Down) ? 1 : -1;
if (pageSize > 0) {
raw->selectSkipPage(pageSize, delta);
} else {
raw->selectSkip(delta);
}
return raw->hasSelection()
? JumpResult::Applied
: had
? JumpResult::AppliedAndOut
: JumpResult::NotApplied;
}
return JumpResult::NotApplied;
};
raw->scrollToRequests(
) | rpl::start_with_next([this](Ui::ScrollToRequest request) {
const auto add = _topPeersWrap->toggled() ? _topPeers->height() : 0;
_chatsScroll->scrollToY(request.ymin + add, request.ymax + add);
}, lifetime);
delegate->setContent(raw);
controller->setDelegate(delegate);
return object_ptr<Ui::SlideWrap<>>(this, std::move(content));
}
object_ptr<Ui::SlideWrap<>> Suggestions::setupEmptyRecent() {
return setupEmpty(_chatsContent, "search", tr::lng_recent_none());
}
object_ptr<Ui::SlideWrap<>> Suggestions::setupEmptyChannels() {
return setupEmpty(
_channelsContent,
"noresults",
tr::lng_channels_none_about());
}
object_ptr<Ui::SlideWrap<>> Suggestions::setupEmpty(
not_null<QWidget*> parent,
const QString &animation,
rpl::producer<QString> text) {
auto content = object_ptr<Ui::RpWidget>(parent);
const auto raw = content.data();
const auto label = Ui::CreateChild<Ui::FlatLabel>(
raw,
std::move(text),
st::defaultPeerListAbout);
const auto size = st::recentPeersEmptySize;
const auto [widget, animate] = Settings::CreateLottieIcon(
raw,
{
.name = animation,
.sizeOverride = { size, size },
},
st::recentPeersEmptyMargin);
const auto icon = widget.data();
_chatsScroll->heightValue() | rpl::start_with_next([=](int height) {
raw->resize(raw->width(), height - st::topPeers.height);
}, raw->lifetime());
raw->sizeValue() | rpl::start_with_next([=](QSize size) {
const auto x = (size.width() - icon->width()) / 2;
const auto y = (size.height() - icon->height()) / 3;
icon->move(x, y);
label->move(
(size.width() - label->width()) / 2,
y + icon->height() + st::recentPeersEmptySkip);
}, raw->lifetime());
auto result = object_ptr<Ui::SlideWrap<>>(
parent,
std::move(content));
result->toggle(false, anim::type::instant);
result->toggledValue() | rpl::filter([=](bool shown) {
return shown && _controller->session().data().chatsListLoaded();
}) | rpl::start_with_next([=] {
animate(anim::repeat::once);
}, raw->lifetime());
return result;
}
object_ptr<Ui::SlideWrap<>> Suggestions::setupMyChannels() {
auto &lifetime = _channelsContent->lifetime();
const auto delegate = lifetime.make_state<
PeerListContentDelegateSimple
>();
const auto controller = lifetime.make_state<MyChannelsController>(
_controller);
controller->setStyleOverrides(&st::recentPeersList);
_myChannelsCount = controller->count();
controller->chosen(
) | rpl::start_with_next([=](not_null<PeerData*> peer) {
_persist = false;
_myChannelChosen.fire_copy(peer);
}, lifetime);
auto content = object_ptr<PeerListContent>(_channelsContent, controller);
const auto raw = content.data();
_myChannelsChoose = [=] {
return raw->submitted();
};
_myChannelsSelectJump = [raw](Qt::Key direction, int pageSize) {
const auto had = raw->hasSelection();
if (direction == Qt::Key()) {
return had ? JumpResult::Applied : JumpResult::NotApplied;
} else if (direction == Qt::Key_Up && !had) {
return JumpResult::NotApplied;
} else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {
const auto delta = (direction == Qt::Key_Down) ? 1 : -1;
if (pageSize > 0) {
raw->selectSkipPage(pageSize, delta);
} else {
raw->selectSkip(delta);
}
return raw->hasSelection()
? JumpResult::Applied
: had
? JumpResult::AppliedAndOut
: JumpResult::NotApplied;
}
return JumpResult::NotApplied;
};
raw->scrollToRequests(
) | rpl::start_with_next([this](Ui::ScrollToRequest request) {
_channelsScroll->scrollToY(request.ymin, request.ymax);
}, lifetime);
delegate->setContent(raw);
controller->setDelegate(delegate);
return object_ptr<Ui::SlideWrap<>>(this, std::move(content));
}
object_ptr<Ui::SlideWrap<>> Suggestions::setupRecommendations() {
auto &lifetime = _channelsContent->lifetime();
const auto delegate = lifetime.make_state<
PeerListContentDelegateSimple
>();
const auto controller = lifetime.make_state<RecommendationsController>(
_controller);
controller->setStyleOverrides(&st::recentPeersList);
_recommendationsCount = controller->count();
_tab.value() | rpl::filter(
rpl::mappers::_1 == Tab::Channels
) | rpl::start_with_next([=] {
controller->load();
}, lifetime);
controller->chosen(
) | rpl::start_with_next([=](not_null<PeerData*> peer) {
_persist = true;
_recommendationChosen.fire_copy(peer);
}, lifetime);
auto content = object_ptr<PeerListContent>(_channelsContent, controller);
const auto raw = content.data();
_recommendationsChoose = [=] {
return raw->submitted();
};
_recommendationsSelectJump = [raw](Qt::Key direction, int pageSize) {
const auto had = raw->hasSelection();
if (direction == Qt::Key()) {
return had ? JumpResult::Applied : JumpResult::NotApplied;
} else if (direction == Qt::Key_Up && !had) {
return JumpResult::NotApplied;
} else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {
const auto delta = (direction == Qt::Key_Down) ? 1 : -1;
if (pageSize > 0) {
raw->selectSkipPage(pageSize, delta);
} else {
raw->selectSkip(delta);
}
return raw->hasSelection()
? JumpResult::Applied
: had
? JumpResult::AppliedAndOut
: JumpResult::NotApplied;
}
return JumpResult::NotApplied;
};
raw->scrollToRequests(
) | rpl::start_with_next([this](Ui::ScrollToRequest request) {
const auto add = _myChannels->toggled() ? _myChannels->height() : 0;
_channelsScroll->scrollToY(request.ymin + add, request.ymax + add);
}, lifetime);
delegate->setContent(raw);
controller->setDelegate(delegate);
return object_ptr<Ui::SlideWrap<>>(this, std::move(content));
}
bool Suggestions::persist() const {
return _persist;
}
void Suggestions::clearPersistance() {
_persist = false;
}
rpl::producer<TopPeersList> TopPeersContent(
not_null<Main::Session*> session) {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
struct Entry {
not_null<History*> history;
int index = 0;
};
struct State {
TopPeersList data;
base::flat_map<not_null<PeerData*>, Entry> indices;
base::has_weak_ptr guard;
bool scheduled = true;
};
auto state = lifetime.make_state<State>();
const auto top = session->topPeers().list();
auto &entries = state->data.entries;
auto &indices = state->indices;
entries.reserve(top.size());
indices.reserve(top.size());
const auto now = base::unixtime::now();
for (const auto &peer : top) {
const auto user = peer->asUser();
if (user->isInaccessible()) {
continue;
}
const auto self = user && user->isSelf();
const auto history = peer->owner().history(peer);
const auto badges = history->chatListBadgesState();
entries.push_back({
.id = peer->id.value,
.name = (self
? tr::lng_saved_messages(tr::now)
: peer->shortName()),
.userpic = (self
? Ui::MakeSavedMessagesThumbnail()
: Ui::MakeUserpicThumbnail(peer)),
.badge = uint32(badges.unreadCounter),
.unread = badges.unread,
.muted = !self && history->muted(),
.online = user && !self && Data::IsUserOnline(user, now),
});
if (entries.back().online) {
user->owner().watchForOffline(user, now);
}
indices.emplace(peer, Entry{
.history = peer->owner().history(peer),
.index = int(entries.size()) - 1,
});
}
const auto push = [=] {
if (!state->scheduled) {
return;
}
state->scheduled = false;
consumer.put_next_copy(state->data);
};
const auto schedule = [=] {
if (state->scheduled) {
return;
}
state->scheduled = true;
crl::on_main(&state->guard, push);
};
using Flag = Data::PeerUpdate::Flag;
session->changes().peerUpdates(
Flag::Name
| Flag::Photo
| Flag::Notifications
| Flag::OnlineStatus
) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
const auto peer = update.peer;
if (peer->isSelf()) {
return;
}
const auto i = state->indices.find(peer);
if (i == end(state->indices)) {
return;
}
auto changed = false;
auto &entry = state->data.entries[i->second.index];
const auto flags = update.flags;
if (flags & Flag::Name) {
const auto now = peer->shortName();
if (entry.name != now) {
entry.name = now;
changed = true;
}
}
if (flags & Flag::Photo) {
entry.userpic = Ui::MakeUserpicThumbnail(peer);
changed = true;
}
if (flags & Flag::Notifications) {
const auto now = i->second.history->muted();
if (entry.muted != now) {
entry.muted = now;
changed = true;
}
}
if (flags & Flag::OnlineStatus) {
if (const auto user = peer->asUser()) {
const auto now = base::unixtime::now();
const auto value = Data::IsUserOnline(user, now);
if (entry.online != value) {
entry.online = value;
changed = true;
if (value) {
user->owner().watchForOffline(user, now);
}
}
}
}
if (changed) {
schedule();
}
}, lifetime);
session->data().unreadBadgeChanges(
) | rpl::start_with_next([=] {
auto changed = false;
auto &entries = state->data.entries;
for (const auto &[peer, data] : state->indices) {
const auto badges = data.history->chatListBadgesState();
auto &entry = entries[data.index];
if (entry.badge != badges.unreadCounter
|| entry.unread != badges.unread) {
entry.badge = badges.unreadCounter;
entry.unread = badges.unread;
changed = true;
}
}
if (changed) {
schedule();
}
}, lifetime);
push();
return lifetime;
};
}
RecentPeersList RecentPeersContent(not_null<Main::Session*> session) {
return RecentPeersList{ session->recentPeers().list() };
}
} // namespace Dialogs