tdesktop/Telegram/SourceFiles/window/window_peer_menu.cpp

852 lines
23 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 "window/window_peer_menu.h"
#include "lang/lang_keys.h"
#include "boxes/confirm_box.h"
#include "boxes/mute_settings_box.h"
#include "boxes/add_contact_box.h"
#include "boxes/report_box.h"
#include "boxes/create_poll_box.h"
#include "boxes/peers/add_participants_box.h"
#include "ui/toast/toast.h"
#include "auth_session.h"
#include "apiwrap.h"
#include "mainwidget.h"
#include "mainwindow.h"
#include "observer_peer.h"
#include "history/history.h"
#include "window/window_controller.h"
#include "support/support_helper.h"
#include "info/info_memento.h"
#include "info/info_controller.h"
//#include "info/feed/info_feed_channels_controllers.h" // #feed
#include "info/profile/info_profile_values.h"
#include "data/data_session.h"
#include "data/data_folder.h"
#include "data/data_poll.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_drafts.h"
#include "data/data_user.h"
#include "dialogs/dialogs_key.h"
#include "boxes/peers/edit_peer_info_box.h"
#include "styles/style_boxes.h"
#include "styles/style_window.h" // st::windowMinWidth
namespace Window {
namespace {
constexpr auto kArchivedToastDuration = crl::time(3000);
class Filler {
public:
Filler(
not_null<Controller*> controller,
not_null<PeerData*> peer,
const PeerMenuCallback &addAction,
PeerMenuSource source);
void fill();
private:
bool showInfo();
bool showToggleArchived();
bool showTogglePin();
void addTogglePin();
void addInfo();
//void addSearch();
void addToggleUnreadMark();
void addToggleArchive();
void addUserActions(not_null<UserData*> user);
void addBlockUser(not_null<UserData*> user);
void addChatActions(not_null<ChatData*> chat);
void addChannelActions(not_null<ChannelData*> channel);
not_null<Controller*> _controller;
not_null<PeerData*> _peer;
const PeerMenuCallback &_addAction;
PeerMenuSource _source;
};
class FolderFiller {
public:
FolderFiller(
not_null<Controller*> controller,
not_null<Data::Folder*> folder,
const PeerMenuCallback &addAction,
PeerMenuSource source);
void fill();
private:
void addToggleCollapse();
//bool showInfo();
//void addTogglePin();
//void addInfo();
//void addSearch();
//void addNotifications();
//void addUngroup();
not_null<Controller*> _controller;
not_null<Data::Folder*> _folder;
const PeerMenuCallback &_addAction;
PeerMenuSource _source;
};
History *FindWastedPin(Data::Folder *folder) {
const auto &order = Auth().data().pinnedChatsOrder(folder);
for (const auto &pinned : order) {
if (const auto history = pinned.history()) {
if (history->peer->isChat()
&& history->peer->asChat()->isDeactivated()
&& !history->inChatList()) {
return history;
}
}
}
return nullptr;
}
void AddChatMembers(not_null<ChatData*> chat) {
AddParticipantsBoxController::Start(chat);
}
bool PinnedLimitReached(Dialogs::Key key) {
Expects(key.entry()->folderKnown());
const auto folder = key.entry()->folder();
const auto pinnedCount = Auth().data().pinnedChatsCount(folder);
const auto pinnedMax = Auth().data().pinnedChatsLimit(folder);
if (pinnedCount < pinnedMax) {
return false;
}
// Some old chat, that was converted, maybe is still pinned.
if (const auto wasted = FindWastedPin(folder)) {
Auth().data().setChatPinned(wasted, false);
Auth().data().setChatPinned(key, true);
Auth().api().savePinnedOrder(folder);
} else {
auto errorText = lng_error_pinned_max(
lt_count,
pinnedMax);
Ui::show(Box<InformBox>(errorText));
}
return true;
}
void TogglePinnedDialog(Dialogs::Key key) {
if (!key.entry()->folderKnown()) {
return;
}
const auto isPinned = !key.entry()->isPinnedDialog();
if (isPinned && PinnedLimitReached(key)) {
return;
}
Auth().data().setChatPinned(key, isPinned);
const auto flags = isPinned
? MTPmessages_ToggleDialogPin::Flag::f_pinned
: MTPmessages_ToggleDialogPin::Flag(0);
if (const auto history = key.history()) {
history->session().api().request(MTPmessages_ToggleDialogPin(
MTP_flags(flags),
MTP_inputDialogPeer(key.history()->peer->input)
)).done([=](const MTPBool &result) {
Auth().data().notifyPinnedDialogsOrderUpdated();
}).send();
} else if (const auto folder = key.folder()) {
folder->session().api().request(MTPmessages_ToggleDialogPin(
MTP_flags(flags),
MTP_inputDialogPeerFolder(MTP_int(folder->id()))
)).send();
}
if (isPinned) {
if (const auto main = App::main()) {
main->dialogsToUp();
}
}
}
Filler::Filler(
not_null<Controller*> controller,
not_null<PeerData*> peer,
const PeerMenuCallback &addAction,
PeerMenuSource source)
: _controller(controller)
, _peer(peer)
, _addAction(addAction)
, _source(source) {
}
bool Filler::showInfo() {
if (_source == PeerMenuSource::Profile
|| _source == PeerMenuSource::ChatsList
|| _peer->isSelf()) {
return false;
} else if (_controller->activeChatCurrent().peer() != _peer) {
return true;
} else if (!Adaptive::ThreeColumn()) {
return true;
} else if (
!Auth().settings().thirdSectionInfoEnabled() &&
!Auth().settings().tabbedReplacedWithInfo()) {
return true;
}
return false;
}
bool Filler::showToggleArchived() {
if (_source != PeerMenuSource::ChatsList) {
return false;
}
const auto history = _peer->owner().historyLoaded(_peer);
if (history && history->useProxyPromotion()) {
return false;
} else if (!_peer->isNotificationsUser() && !_peer->isSelf()) {
return true;
}
return history && (history->folder() != nullptr);
}
bool Filler::showTogglePin() {
if (_source != PeerMenuSource::ChatsList) {
return false;
}
const auto history = _peer->owner().historyLoaded(_peer);
return history && !history->fixedOnTopIndex();
}
void Filler::addTogglePin() {
auto peer = _peer;
auto isPinned = false;
if (auto history = peer->owner().historyLoaded(peer)) {
isPinned = history->isPinnedDialog();
}
auto pinText = [](bool isPinned) {
return lang(isPinned
? lng_context_unpin_from_top
: lng_context_pin_to_top);
};
auto pinToggle = [=] {
TogglePinnedDialog(peer->owner().history(peer));
};
auto pinAction = _addAction(pinText(isPinned), pinToggle);
const auto lifetime = Ui::CreateChild<rpl::lifetime>(pinAction);
Notify::PeerUpdateViewer(
peer,
Notify::PeerUpdate::Flag::ChatPinnedChanged
) | rpl::start_with_next([peer, pinAction, pinText] {
auto isPinned = peer->owner().history(peer)->isPinnedDialog();
pinAction->setText(pinText(isPinned));
}, *lifetime);
}
void Filler::addInfo() {
auto controller = _controller;
auto peer = _peer;
auto infoKey = (peer->isChat() || peer->isMegagroup())
? lng_context_view_group
: (peer->isUser()
? lng_context_view_profile
: lng_context_view_channel);
_addAction(lang(infoKey), [=] {
controller->showPeerInfo(peer);
});
}
//void Filler::addSearch() {
// _addAction(lang(lng_profile_search_messages), [peer = _peer] {
// App::main()->searchInChat(peer->owner().history(peer));
// });
//}
void Filler::addToggleUnreadMark() {
const auto peer = _peer;
const auto isUnread = [](not_null<PeerData*> peer) {
if (const auto history = peer->owner().historyLoaded(peer)) {
return (history->chatListUnreadCount() > 0)
|| (history->chatListUnreadMark());
}
return false;
};
const auto label = [=](not_null<PeerData*> peer) {
return lang(isUnread(peer)
? lng_context_mark_read
: lng_context_mark_unread);
};
auto action = _addAction(label(peer), [=] {
const auto markAsRead = isUnread(peer);
const auto handle = [&](not_null<History*> history) {
if (markAsRead) {
Auth().api().readServerHistory(history);
} else {
Auth().api().changeDialogUnreadMark(history, !markAsRead);
}
};
const auto history = peer->owner().history(peer);
handle(history);
if (markAsRead) {
if (const auto migrated = history->migrateSibling()) {
handle(migrated);
}
}
});
const auto lifetime = Ui::CreateChild<rpl::lifetime>(action);
Notify::PeerUpdateViewer(
_peer,
Notify::PeerUpdate::Flag::UnreadViewChanged
) | rpl::start_with_next([=] {
action->setText(label(peer));
}, *lifetime);
}
void Filler::addToggleArchive() {
const auto peer = _peer;
const auto archived = [&] {
const auto history = peer->owner().historyLoaded(peer);
return history && history->folder();
}();
const auto toggle = [=] {
ToggleHistoryArchived(
peer->owner().history(peer),
!archived);
};
_addAction(
lang(archived ? lng_archived_remove : lng_archived_add),
toggle);
}
void Filler::addBlockUser(not_null<UserData*> user) {
auto blockText = [](not_null<UserData*> user) {
return lang(user->isBlocked()
? ((user->isBot() && !user->isSupport())
? lng_profile_restart_bot
: lng_profile_unblock_user)
: ((user->isBot() && !user->isSupport())
? lng_profile_block_bot
: lng_profile_block_user));
};
auto blockAction = _addAction(blockText(user), [=] {
if (user->isBlocked()) {
Auth().api().unblockUser(user);
} else {
Auth().api().blockUser(user);
}
});
const auto lifetime = Ui::CreateChild<rpl::lifetime>(blockAction);
Notify::PeerUpdateViewer(
_peer,
Notify::PeerUpdate::Flag::UserIsBlocked
) | rpl::start_with_next([=] {
blockAction->setText(blockText(user));
}, *lifetime);
if (user->blockStatus() == UserData::BlockStatus::Unknown) {
Auth().api().requestFullPeer(user);
}
}
void Filler::addUserActions(not_null<UserData*> user) {
if (_source != PeerMenuSource::ChatsList) {
if (Auth().supportMode()) {
_addAction("Edit support info", [=] {
Auth().supportHelper().editInfo(user);
});
}
if (user->isContact()) {
if (!user->isSelf()) {
_addAction(
lang(lng_info_share_contact),
[user] { PeerMenuShareContactBox(user); });
_addAction(
lang(lng_info_edit_contact),
[user] { Ui::show(Box<AddContactBox>(user)); });
_addAction(
lang(lng_info_delete_contact),
[user] { PeerMenuDeleteContact(user); });
}
} else if (user->canShareThisContact()) {
if (!user->isSelf()) {
_addAction(
lang(lng_info_add_as_contact),
[user] { PeerMenuAddContact(user); });
}
_addAction(
lang(lng_info_share_contact),
[user] { PeerMenuShareContactBox(user); });
} else if (user->botInfo && !user->botInfo->cantJoinGroups) {
_addAction(
lang(lng_profile_invite_to_group),
[user] { AddBotToGroupBoxController::Start(user); });
}
if (user->canExportChatHistory()) {
_addAction(
lang(lng_profile_export_chat),
[=] { PeerMenuExportChat(user); });
}
}
_addAction(
lang(lng_profile_delete_conversation),
DeleteAndLeaveHandler(user));
_addAction(
lang(lng_profile_clear_history),
ClearHistoryHandler(user));
if (!user->isInaccessible()
&& user != Auth().user()
&& _source != PeerMenuSource::ChatsList) {
addBlockUser(user);
}
}
void Filler::addChatActions(not_null<ChatData*> chat) {
if (_source != PeerMenuSource::ChatsList) {
if (EditPeerInfoBox::Available(chat)) {
const auto text = lang(lng_manage_group_title);
_addAction(text, [=] {
App::wnd()->controller()->showEditPeerBox(chat);
});
}
if (chat->canAddMembers()) {
_addAction(
lang(lng_profile_add_participant),
[chat] { AddChatMembers(chat); });
}
if (chat->canSendPolls()) {
_addAction(
lang(lng_polls_create),
[=] { PeerMenuCreatePoll(chat); });
}
if (chat->canExportChatHistory()) {
_addAction(
lang(lng_profile_export_chat),
[=] { PeerMenuExportChat(chat); });
}
}
_addAction(
lang(lng_profile_clear_and_exit),
DeleteAndLeaveHandler(_peer));
_addAction(
lang(lng_profile_clear_history),
ClearHistoryHandler(_peer));
}
void Filler::addChannelActions(not_null<ChannelData*> channel) {
auto isGroup = channel->isMegagroup();
//if (!isGroup) { // #feed
// const auto feed = channel->feed();
// const auto grouped = (feed != nullptr);
// if (!grouped || feed->channels().size() > 1) {
// _addAction( // #feed
// lang(grouped ? lng_feed_ungroup : lng_feed_group),
// [=] { ToggleChannelGrouping(channel, !grouped); });
// }
//}
if (_source != PeerMenuSource::ChatsList) {
if (EditPeerInfoBox::Available(channel)) {
const auto text = lang(isGroup
? lng_manage_group_title
: lng_manage_channel_title);
_addAction(text, [channel] {
App::wnd()->controller()->showEditPeerBox(channel);
});
}
if (channel->canAddMembers()) {
_addAction(
lang(lng_channel_add_members),
[channel] { PeerMenuAddChannelMembers(channel); });
}
if (channel->canSendPolls()) {
_addAction(
lang(lng_polls_create),
[=] { PeerMenuCreatePoll(channel); });
}
if (channel->canExportChatHistory()) {
_addAction(
lang(isGroup
? lng_profile_export_chat
: lng_profile_export_channel),
[=] { PeerMenuExportChat(channel); });
}
}
if (channel->amIn()) {
if (isGroup && !channel->isPublic()) {
_addAction(
lang(lng_profile_clear_history),
ClearHistoryHandler(channel));
}
auto text = lang(isGroup
? lng_profile_leave_group
: lng_profile_leave_channel);
_addAction(text, DeleteAndLeaveHandler(channel));
} else {
auto text = lang(isGroup
? lng_profile_join_group
: lng_profile_join_channel);
_addAction(
text,
[channel] { Auth().api().joinChannel(channel); });
}
if (_source != PeerMenuSource::ChatsList) {
auto needReport = !channel->amCreator()
&& (!isGroup || channel->isPublic());
if (needReport) {
_addAction(lang(lng_profile_report), [channel] {
Ui::show(Box<ReportBox>(channel));
});
}
}
}
void Filler::fill() {
if (showToggleArchived()) {
addToggleArchive();
}
if (showTogglePin()) {
addTogglePin();
}
if (showInfo()) {
addInfo();
}
if (_source != PeerMenuSource::Profile && !_peer->isSelf()) {
PeerMenuAddMuteAction(_peer, _addAction);
}
if (_source == PeerMenuSource::ChatsList) {
//addSearch();
addToggleUnreadMark();
}
if (const auto user = _peer->asUser()) {
addUserActions(user);
} else if (const auto chat = _peer->asChat()) {
addChatActions(chat);
} else if (const auto channel = _peer->asChannel()) {
addChannelActions(channel);
}
}
FolderFiller::FolderFiller(
not_null<Controller*> controller,
not_null<Data::Folder*> folder,
const PeerMenuCallback &addAction,
PeerMenuSource source)
: _controller(controller)
, _folder(folder)
, _addAction(addAction)
, _source(source) {
}
void FolderFiller::fill() {
if (_source == PeerMenuSource::ChatsList) {
addToggleCollapse();
}
}
void FolderFiller::addToggleCollapse() {
if (_folder->id() != Data::Folder::kId) {
return;
}
const auto controller = _controller;
const auto hidden = controller->session().settings().archiveCollapsed();
const auto text = lang(hidden
? lng_context_archive_expand
: lng_context_archive_collapse);
_addAction(text, [=] {
controller->session().settings().setArchiveCollapsed(!hidden);
controller->session().saveSettingsDelayed();
});
}
//
//void FolderFiller::addInfo() {
// auto controller = _controller;
// auto feed = _feed;
// _addAction(lang(lng_context_view_feed_info), [=] {
// controller->showSection(Info::Memento(
// feed,
// Info::Section(Info::Section::Type::Profile)));
// });
//}
//
//void FolderFiller::addNotifications() {
// const auto feed = _feed;
// _addAction(lang(lng_feed_notifications), [=] {
// Info::FeedProfile::NotificationsController::Start(feed);
// });
//}
//
//void FolderFiller::addSearch() {
// const auto feed = _feed;
// _addAction(lang(lng_profile_search_messages), [=] {
// App::main()->searchInChat(feed);
// });
//}
//
//void FolderFiller::addUngroup() {
// const auto feed = _feed;
// //_addAction(lang(lng_feed_ungroup_all), [=] { // #feed
// // PeerMenuUngroupFeed(feed);
// //});
//}
} // namespace
void PeerMenuExportChat(not_null<PeerData*> peer) {
Auth().data().startExport(peer);
}
void PeerMenuDeleteContact(not_null<UserData*> user) {
const auto text = lng_sure_delete_contact(
lt_contact,
App::peerName(user));
const auto deleteSure = [=] {
Ui::hideLayer();
user->session().api().request(MTPcontacts_DeleteContact(
user->inputUser
)).done([=](const MTPcontacts_Link &result) {
result.match([&](const MTPDcontacts_link &data) {
user->owner().processUser(data.vuser);
App::feedUserLink(
MTP_int(peerToUser(user->id)),
data.vmy_link,
data.vforeign_link);
});
}).send();
};
Ui::show(Box<ConfirmBox>(
text,
lang(lng_box_delete),
deleteSure));
}
void PeerMenuAddContact(not_null<UserData*> user) {
Ui::show(Box<AddContactBox>(
user->firstName,
user->lastName,
Auth().data().findContactPhone(user)));
}
void PeerMenuShareContactBox(not_null<UserData*> user) {
const auto weak = std::make_shared<QPointer<PeerListBox>>();
auto callback = [=](not_null<PeerData*> peer) {
if (!peer->canWrite()) {
Ui::show(Box<InformBox>(
lang(lng_forward_share_cant)),
LayerOption::KeepOther);
return;
} else if (peer->isSelf()) {
auto options = ApiWrap::SendOptions(peer->owner().history(peer));
Auth().api().shareContact(user, options);
Ui::Toast::Show(lang(lng_share_done));
if (auto strong = *weak) {
strong->closeBox();
}
return;
}
auto recipient = peer->isUser()
? peer->name
: '\xAB' + peer->name + '\xBB';
Ui::show(Box<ConfirmBox>(
lng_forward_share_contact(lt_recipient, recipient),
lang(lng_forward_send),
[peer, user] {
const auto history = peer->owner().history(peer);
Ui::showPeerHistory(history, ShowAtTheEndMsgId);
auto options = ApiWrap::SendOptions(history);
Auth().api().shareContact(user, options);
}), LayerOption::KeepOther);
};
*weak = Ui::show(Box<PeerListBox>(
std::make_unique<ChooseRecipientBoxController>(std::move(callback)),
[](not_null<PeerListBox*> box) {
box->addButton(langFactory(lng_cancel), [box] {
box->closeBox();
});
}));
}
void PeerMenuCreatePoll(not_null<PeerData*> peer) {
const auto box = Ui::show(Box<CreatePollBox>());
const auto lock = box->lifetime().make_state<bool>(false);
box->submitRequests(
) | rpl::start_with_next([=](const PollData &result) {
if (std::exchange(*lock, true)) {
return;
}
auto options = ApiWrap::SendOptions(peer->owner().history(peer));
if (const auto id = App::main()->currentReplyToIdFor(options.history)) {
options.replyTo = id;
}
if (const auto localDraft = options.history->localDraft()) {
options.clearDraft = localDraft->textWithTags.text.isEmpty();
}
Auth().api().createPoll(result, options, crl::guard(box, [=] {
box->closeBox();
}), crl::guard(box, [=](const RPCError &error) {
*lock = false;
box->submitFailed(lang(lng_attach_failed));
}));
}, box->lifetime());
}
QPointer<Ui::RpWidget> ShowForwardMessagesBox(
MessageIdsList &&items,
FnMut<void()> &&successCallback) {
const auto weak = std::make_shared<QPointer<PeerListBox>>();
auto callback = [
ids = std::move(items),
callback = std::move(successCallback),
weak
](not_null<PeerData*> peer) mutable {
if (peer->isSelf()) {
auto items = Auth().data().idsToItems(ids);
if (!items.empty()) {
auto options = ApiWrap::SendOptions(peer->owner().history(peer));
options.generateLocal = false;
Auth().api().forwardMessages(std::move(items), options, [] {
Ui::Toast::Show(lang(lng_share_done));
});
}
} else {
App::main()->setForwardDraft(peer->id, std::move(ids));
}
if (const auto strong = *weak) {
strong->closeBox();
}
if (callback) {
callback();
}
};
auto initBox = [](not_null<PeerListBox*> box) {
box->addButton(langFactory(lng_cancel), [box] {
box->closeBox();
});
};
*weak = Ui::show(Box<PeerListBox>(
std::make_unique<ChooseRecipientBoxController>(std::move(callback)),
std::move(initBox)), LayerOption::KeepOther);
return weak->data();
}
void PeerMenuAddChannelMembers(not_null<ChannelData*> channel) {
if (!channel->isMegagroup()
&& channel->membersCount() >= Global::ChatSizeMax()) {
Ui::show(
Box<MaxInviteBox>(channel),
LayerOption::KeepOther);
return;
}
auto callback = [=](const MTPchannels_ChannelParticipants &result) {
Auth().api().parseChannelParticipants(channel, result, [&](
int availableCount,
const QVector<MTPChannelParticipant> &list) {
auto already = (
list
) | ranges::view::transform([](const MTPChannelParticipant &p) {
return p.match([](const auto &data) {
return data.vuser_id.v;
});
}) | ranges::view::transform([](UserId userId) {
return Auth().data().userLoaded(userId);
}) | ranges::view::filter([](UserData *user) {
return (user != nullptr);
}) | ranges::to_vector;
AddParticipantsBoxController::Start(
channel,
{ already.begin(), already.end() });
});
};
Auth().api().requestChannelMembersForAdd(channel, callback);
}
void PeerMenuAddMuteAction(
not_null<PeerData*> peer,
const PeerMenuCallback &addAction) {
Auth().data().requestNotifySettings(peer);
const auto muteText = [](bool isMuted) {
return lang(isMuted
? lng_enable_notifications_from_tray
: lng_disable_notifications_from_tray);
};
const auto muteAction = addAction(QString("-"), [=] {
if (!Auth().data().notifyIsMuted(peer)) {
Ui::show(Box<MuteSettingsBox>(peer));
} else {
Auth().data().updateNotifySettings(peer, 0);
}
});
const auto lifetime = Ui::CreateChild<rpl::lifetime>(muteAction);
Info::Profile::NotificationsEnabledValue(
peer
) | rpl::start_with_next([=](bool enabled) {
muteAction->setText(muteText(!enabled));
}, *lifetime);
}
// #feed
//void PeerMenuUngroupFeed(not_null<Data::Feed*> feed) {
// Ui::show(Box<ConfirmBox>(
// lang(lng_feed_sure_ungroup_all),
// lang(lng_feed_ungroup_sure),
// [=] { Ui::hideLayer(); Auth().api().ungroupAllFromFeed(feed); }));
//}
//
void ToggleHistoryArchived(not_null<History*> history, bool archived) {
const auto callback = [=] {
Ui::Toast::Config toast;
toast.text = lang(archived
? lng_archived_added
: lng_archived_removed);
toast.maxWidth = st::boxWideWidth;
if (archived) {
toast.durationMs = kArchivedToastDuration;
}
Ui::Toast::Show(toast);
};
history->session().api().toggleHistoryArchived(
history,
archived,
callback);
}
Fn<void()> ClearHistoryHandler(not_null<PeerData*> peer) {
return [=] {
Ui::show(Box<DeleteMessagesBox>(peer, true), LayerOption::KeepOther);
};
}
Fn<void()> DeleteAndLeaveHandler(not_null<PeerData*> peer) {
return [=] {
Ui::show(Box<DeleteMessagesBox>(peer, false), LayerOption::KeepOther);
};
}
void FillPeerMenu(
not_null<Controller*> controller,
not_null<PeerData*> peer,
const PeerMenuCallback &callback,
PeerMenuSource source) {
Filler filler(controller, peer, callback, source);
filler.fill();
}
void FillFolderMenu(
not_null<Controller*> controller,
not_null<Data::Folder*> folder,
const PeerMenuCallback &callback,
PeerMenuSource source) {
FolderFiller filler(controller, folder, callback, source);
filler.fill();
}
} // namespace Window