Initial add filter / chats / edit filter link.

This commit is contained in:
John Preston 2023-03-28 18:14:53 +04:00
parent b7d9d549ff
commit 8a9d13c6e4
9 changed files with 520 additions and 14 deletions

View File

@ -7,12 +7,331 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "api/api_chat_filters.h"
#include "data/data_session.h"
#include "data/data_chat_filters.h"
#include "main/main_session.h"
#include "apiwrap.h"
#include "boxes/peer_list_box.h"
#include "core/application.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/toasts/common_toasts.h"
#include "ui/widgets/buttons.h"
#include "window/window_session_controller.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
namespace Api {
namespace {
enum class ToggleAction {
Adding,
Removing,
};
enum class HeaderType {
AddingFilter,
AddingChats,
AllAdded,
Removing,
};
struct HeaderDescriptor {
base::required<HeaderType> type;
base::required<QString> title;
int badge = 0;
};
class ToggleChatsController final
: public PeerListController
, public base::has_weak_ptr {
public:
ToggleChatsController(
not_null<Window::SessionController*> window,
ToggleAction action,
const QString &slug,
FilterId filterId,
const QString &title,
std::vector<not_null<PeerData*>> chats);
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
Main::Session &session() const override;
[[nodiscard]] auto selectedValue() const
-> rpl::producer<base::flat_set<not_null<PeerData*>>>;
private:
void setupAboveWidget();
const not_null<Window::SessionController*> _window;
ToggleAction _action = ToggleAction::Adding;
QString _slug;
FilterId _filterId = 0;
QString _filterTitle;
std::vector<not_null<PeerData*>> _chats;
rpl::variable<base::flat_set<not_null<PeerData*>>> _selected;
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::lifetime _lifetime;
};
[[nodiscard]] rpl::producer<QString> TitleText(HeaderType type) {
// langs
switch (type) {
case HeaderType::AddingFilter:
return rpl::single(u"Add Folder"_q);
case HeaderType::AddingChats:
return rpl::single(u"Add Chats to Folder"_q);
case HeaderType::AllAdded:
return rpl::single(u"Folder Already Added"_q);
case HeaderType::Removing:
return rpl::single(u"Remove Folder"_q);
}
Unexpected("HeaderType in TitleText.");
}
void FillHeader(
not_null<Ui::VerticalLayout*> container,
HeaderDescriptor descriptor) {
// langs
const auto description = (descriptor.type == HeaderType::AddingFilter)
? (u"Do you want to add a new chat folder "_q
+ descriptor.title
+ u" and join its groups and channels?"_q)
: (descriptor.type == HeaderType::AddingChats)
? (u"Do you want to join "_q
+ QString::number(descriptor.badge)
+ u" chats and add them to your folder "_q
+ descriptor.title + '?')
: (descriptor.type == HeaderType::AllAdded)
? (u"You have already added the folder "_q
+ descriptor.title
+ u" and all its chats."_q)
: (u"Do you want to quit the chats you joined "
"when adding the folder "_q
+ descriptor.title + '?');
container->add(
object_ptr<Ui::FlatLabel>(
container,
description,
st::boxDividerLabel),
st::boxRowPadding);
}
void ImportInvite(
base::weak_ptr<Window::SessionController> weak,
const QString &slug,
const base::flat_set<not_null<PeerData*>> &peers) {
Expects(!peers.empty());
const auto peer = peers.front();
const auto api = &peer->session().api();
const auto callback = [=](const MTPUpdates &result) {
api->applyUpdates(result);
};
const auto error = [=](const MTP::Error &error) {
if (const auto strong = weak.get()) {
Ui::ShowMultilineToast({
.parentOverride = Window::Show(strong).toastParent(),
.text = { error.description() },
});
}
};
auto inputs = peers | ranges::views::transform([](auto peer) {
return MTPInputPeer(peer->input);
}) | ranges::to<QVector>();
api->request(MTPcommunities_JoinCommunityInvite(
MTP_string(slug),
MTP_vector<MTPInputPeer>(std::move(inputs))
)).done(callback).fail(error).send();
}
ToggleChatsController::ToggleChatsController(
not_null<Window::SessionController*> window,
ToggleAction action,
const QString &slug,
FilterId filterId,
const QString &title,
std::vector<not_null<PeerData*>> chats)
: _window(window)
, _action(action)
, _slug(slug)
, _filterId(filterId)
, _filterTitle(title)
, _chats(std::move(chats)) {
}
void ToggleChatsController::prepare() {
setupAboveWidget();
auto selected = base::flat_set<not_null<PeerData*>>();
for (const auto &peer : _chats) {
auto row = std::make_unique<PeerListRow>(peer);
const auto raw = row.get();
delegate()->peerListAppendRow(std::move(row));
delegate()->peerListSetRowChecked(raw, true);
selected.emplace(peer);
}
delegate()->peerListRefreshRows();
_selected = std::move(selected);
}
void ToggleChatsController::rowClicked(not_null<PeerListRow*> row) {
const auto peer = row->peer();
const auto checked = row->checked();
auto selected = _selected.current();
delegate()->peerListSetRowChecked(row, !checked);
if (checked) {
selected.remove(peer);
} else {
selected.emplace(peer);
}
_selected = std::move(selected);
}
void ToggleChatsController::setupAboveWidget() {
using namespace Settings;
auto wrap = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
const auto container = wrap.data();
const auto type = !_filterId
? HeaderType::AddingFilter
: (_action == ToggleAction::Adding)
? HeaderType::AddingChats
: HeaderType::Removing;
delegate()->peerListSetTitle(TitleText(type));
FillHeader(container, {
.type = type,
.title = _filterTitle,
.badge = (type == HeaderType::AddingChats) ? int(_chats.size()) : 0,
});
delegate()->peerListSetAboveWidget(std::move(wrap));
}
Main::Session &ToggleChatsController::session() const {
return _window->session();
}
auto ToggleChatsController::selectedValue() const
-> rpl::producer<base::flat_set<not_null<PeerData*>>> {
return _selected.value();
}
[[nodiscard]] void AlreadyFilterBox(
not_null<Ui::GenericBox*> box,
const QString &title) {
// langs
box->setTitle(TitleText(HeaderType::AllAdded));
FillHeader(box->verticalLayout(), {
.type = HeaderType::AllAdded,
.title = title,
});
box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });
}
void ProcessFilterInvite(
base::weak_ptr<Window::SessionController> weak,
const QString &slug,
FilterId filterId,
const QString &title,
std::vector<not_null<PeerData*>> peers) {
const auto strong = weak.get();
if (!strong) {
return;
}
Core::App().hideMediaView();
if (peers.empty()) {
if (filterId) {
strong->show(Box(AlreadyFilterBox, title));
} else {
Ui::ShowMultilineToast({
.parentOverride = Window::Show(strong).toastParent(),
.text = { tr::lng_group_invite_bad_link(tr::now) },
});
}
return;
}
auto controller = std::make_unique<ToggleChatsController>(
strong,
ToggleAction::Adding,
slug,
filterId,
title,
std::move(peers));
const auto raw = controller.get();
auto initBox = [=](not_null<Ui::BoxContent*> box) {
box->setStyle(st::filterInviteBox);
raw->selectedValue(
) | rpl::start_with_next([=](
base::flat_set<not_null<PeerData*>> &&peers) {
const auto count = int(peers.size());
box->clearButtons();
auto button = object_ptr<Ui::RoundButton>(
box,
rpl::single(count
? u"Add %1 Chats"_q.arg(count)
: u"Don't add chats"_q),
st::defaultActiveButton);
const auto raw = button.data();
box->widthValue(
) | rpl::start_with_next([=](int width) {
const auto &padding = st::filterInviteBox.buttonPadding;
raw->resizeToWidth(width
- padding.left()
- padding.right());
raw->moveToLeft(padding.left(), padding.top());
}, raw->lifetime());
raw->setClickedCallback([=] {
if (!count) {
box->closeBox();
//} else if (count + alreadyInFilter() >= ...) {
// #TODO filters
} else {
ImportInvite(weak, slug, peers);
}
});
box->addButton(std::move(button));
}, box->lifetime());
};
strong->show(
Box<PeerListBox>(std::move(controller), std::move(initBox)));
}
void ProcessFilterInvite(
base::weak_ptr<Window::SessionController> weak,
const QString &slug,
FilterId filterId,
std::vector<not_null<PeerData*>> peers) {
const auto strong = weak.get();
if (!strong) {
return;
}
Core::App().hideMediaView();
const auto &list = strong->session().data().chatsFilters().list();
const auto it = ranges::find(list, filterId, &Data::ChatFilter::id);
if (it == end(list)) {
Ui::ShowMultilineToast({
.parentOverride = Window::Show(strong).toastParent(),
.text = { u"Filter not found :shrug:"_q },
});
return;
}
ProcessFilterInvite(weak, slug, filterId, it->title(), std::move(peers));
}
} // namespace
void SaveNewFilterPinned(
not_null<Main::Session*> session,
@ -25,7 +344,70 @@ void SaveNewFilterPinned(
MTP_int(filterId),
filter.tl()
)).send();
}
void CheckFilterInvite(
not_null<Window::SessionController*> controller,
const QString &slug) {
const auto session = &controller->session();
const auto weak = base::make_weak(controller);
session->api().checkFilterInvite(slug, [=](
const MTPcommunities_CommunityInvite &result) {
const auto strong = weak.get();
if (!strong) {
return;
}
auto title = QString();
auto filterId = FilterId();
auto peers = std::vector<not_null<PeerData*>>();
auto already = std::vector<not_null<PeerData*>>();
auto &owner = strong->session().data();
result.match([&](const auto &data) {
owner.processUsers(data.vusers());
owner.processChats(data.vchats());
});
const auto parseList = [&](const MTPVector<MTPPeer> &list) {
auto result = std::vector<not_null<PeerData*>>();
result.reserve(list.v.size());
for (const auto &peer : list.v) {
result.push_back(owner.peer(peerFromMTP(peer)));
}
return result;
};
result.match([&](const MTPDcommunities_communityInvite &data) {
title = qs(data.vtitle());
peers = parseList(data.vpeers());
}, [&](const MTPDcommunities_communityInviteAlready &data) {
filterId = data.vfilter_id().v;
peers = parseList(data.vmissing_peers());
already = parseList(data.valready_peers());
});
const auto &filters = owner.chatsFilters();
const auto notLoaded = filterId
&& !ranges::contains(
owner.chatsFilters().list(),
filterId,
&Data::ChatFilter::id);
if (notLoaded) {
const auto lifetime = std::make_shared<rpl::lifetime>();
owner.chatsFilters().changed(
) | rpl::start_with_next([=] {
lifetime->destroy();
ProcessFilterInvite(weak, slug, filterId, std::move(peers));
}, *lifetime);
owner.chatsFilters().reload();
} else if (filterId) {
ProcessFilterInvite(weak, slug, filterId, std::move(peers));
} else {
ProcessFilterInvite(weak, slug, filterId, title, std::move(peers));
}
}, [=](const MTP::Error &error) {
if (error.code() != 400) {
return;
}
ProcessFilterInvite(weak, slug, FilterId(), QString(), {});
});
}
} // namespace Api

View File

@ -11,10 +11,18 @@ namespace Main {
class Session;
} // namespace Main
namespace Window {
class SessionController;
} // namespace Window
namespace Api {
void SaveNewFilterPinned(
not_null<Main::Session*> session,
FilterId filterId);
void CheckFilterInvite(
not_null<Window::SessionController*> controller,
const QString &slug);
} // namespace Api

View File

@ -380,6 +380,16 @@ void ApiWrap::checkChatInvite(
)).done(std::move(done)).fail(std::move(fail)).send();
}
void ApiWrap::checkFilterInvite(
const QString &slug,
FnMut<void(const MTPcommunities_CommunityInvite &)> done,
Fn<void(const MTP::Error &)> fail) {
request(base::take(_checkFilterInviteRequestId)).cancel();
_checkFilterInviteRequestId = request(
MTPcommunities_CheckCommunityInvite(MTP_string(slug))
).done(std::move(done)).fail(std::move(fail)).send();
}
void ApiWrap::savePinnedOrder(Data::Folder *folder) {
const auto &order = _session->data().pinnedChatsOrder(folder);
const auto input = [](Dialogs::Key key) {

View File

@ -202,6 +202,10 @@ public:
const QString &hash,
FnMut<void(const MTPChatInvite &)> done,
Fn<void(const MTP::Error &)> fail);
void checkFilterInvite(
const QString &slug,
FnMut<void(const MTPcommunities_CommunityInvite &)> done,
Fn<void(const MTP::Error &)> fail);
void processFullPeer(
not_null<PeerData*> peer,
@ -653,6 +657,7 @@ private:
mtpRequestId _termsUpdateRequestId = 0;
mtpRequestId _checkInviteRequestId = 0;
mtpRequestId _checkFilterInviteRequestId = 0;
struct MigrateCallbacks {
FnMut<void(not_null<ChannelData*>)> done;

View File

@ -77,6 +77,14 @@ struct InviteLinkAction {
}
}
void ShowEmptyLinkError(not_null<Window::SessionController*> window) {
// langs
Ui::ShowMultilineToast({
.parentOverride = Window::Show(window).toastParent(),
.text = { u"Link should have at least one chat shared."_q },
});
}
void ChatFilterLinkBox(
not_null<Ui::GenericBox*> box,
Data::ChatFilterLink data) {
@ -336,6 +344,7 @@ public:
void showFinished() override;
[[nodiscard]] rpl::producer<bool> hasChangesValue() const;
[[nodiscard]] base::flat_set<not_null<PeerData*>> selected() const;
private:
void setupAboveWidget();
@ -347,14 +356,13 @@ private:
base::flat_set<not_null<History*>> _filterChats;
base::flat_set<not_null<PeerData*>> _allowed;
rpl::variable<int> _selected = 0;
rpl::variable<base::flat_set<not_null<PeerData*>>> _selected;
base::flat_set<not_null<PeerData*>> _initial;
base::unique_qptr<Ui::PopupMenu> _menu;
QString _link;
Ui::RpWidget *_headerWidget = nullptr;
rpl::variable<int> _addedHeight;
rpl::variable<bool> _hasChanges = false;
rpl::event_stream<> _showFinished;
@ -495,7 +503,6 @@ void LinkController::addLinkBlock(not_null<Ui::VerticalLayout*> container) {
void LinkController::prepare() {
setupAboveWidget();
auto selected = 0;
for (const auto &history : _data.chats) {
const auto peer = history->peer;
_allowed.emplace(peer);
@ -503,7 +510,7 @@ void LinkController::prepare() {
const auto raw = row.get();
delegate()->peerListAppendRow(std::move(row));
delegate()->peerListSetRowChecked(raw, true);
++selected;
_initial.emplace(peer);
}
for (const auto &history : _filterChats) {
if (delegate()->peerListFindRow(history->peer->id.value)) {
@ -520,14 +527,23 @@ void LinkController::prepare() {
}
}
delegate()->peerListRefreshRows();
_selected = selected;
_selected = _initial;
}
void LinkController::rowClicked(not_null<PeerListRow*> row) {
if (_allowed.contains(row->peer())) {
const auto peer = row->peer();
const auto checked = row->checked();
auto selected = _selected.current();
delegate()->peerListSetRowChecked(row, !checked);
_selected = _selected.current() + (checked ? -1 : 1);
if (checked) {
selected.remove(peer);
} else {
selected.emplace(peer);
}
const auto has = (_initial != selected);
_selected = std::move(selected);
_hasChanges = has;
}
}
@ -546,9 +562,16 @@ void LinkController::setupAboveWidget() {
addLinkBlock(container);
}
// langs
auto subtitle = _selected.value(
) | rpl::map([](const base::flat_set<not_null<PeerData*>> &selected) {
return selected.empty()
? u"No chats selected"_q
: (QString::number(selected.size()) + u" chats selected"_q);
});
Settings::AddSubsectionTitle(
container,
rpl::single(u"3 chats selected"_q));
std::move(subtitle));
delegate()->peerListSetAboveWidget(std::move(wrap));
}
@ -561,6 +584,10 @@ rpl::producer<bool> LinkController::hasChangesValue() const {
return _hasChanges.value();
}
base::flat_set<not_null<PeerData*>> LinkController::selected() const {
return _selected.current();
}
LinksController::LinksController(
not_null<Window::SessionController*> window,
rpl::producer<std::vector<InviteLinkData>> content,
@ -804,7 +831,7 @@ void ExportFilterLink(
) | ranges::to<QVector>();
session->api().request(MTPcommunities_ExportCommunityInvite(
MTP_inputCommunityDialogFilter(MTP_int(id)),
MTP_string(),
MTP_string(), // title
MTP_vector<MTPInputPeer>(std::move(mtpPeers))
)).done([=](const MTPcommunities_ExportedCommunityInvite &result) {
const auto &data = result.data();
@ -821,6 +848,34 @@ void ExportFilterLink(
}).send();
}
void EditLinkChats(
const Data::ChatFilterLink &link,
base::flat_set<not_null<PeerData*>> peers) {
Expects(!peers.empty());
Expects(link.id != 0);
Expects(!link.url.isEmpty());
const auto id = link.id;
const auto front = peers.front();
const auto session = &front->session();
auto mtpPeers = peers | ranges::views::transform(
[](not_null<PeerData*> peer) { return MTPInputPeer(peer->input); }
) | ranges::to<QVector>();
session->api().request(MTPcommunities_EditExportedInvite(
MTP_flags(MTPcommunities_EditExportedInvite::Flag::f_peers),
MTP_inputCommunityDialogFilter(MTP_int(link.id)),
MTP_string(link.url),
MTPstring(), // title
MTP_vector<MTPInputPeer>(std::move(mtpPeers))
)).done([=](const MTPExportedCommunityInvite &result) {
const auto &data = result.data();
const auto link = session->data().chatsFilters().add(id, result);
//done(link);
}).fail([=](const MTP::Error &error) {
//done({ .id = id });
}).send();
}
object_ptr<Ui::BoxContent> ShowLinkBox(
not_null<Window::SessionController*> window,
const Data::ChatFilter &filter,
@ -837,7 +892,12 @@ object_ptr<Ui::BoxContent> ShowLinkBox(
box->clearButtons();
if (has) {
box->addButton(tr::lng_settings_save(), [=] {
const auto chosen = raw->selected();
if (chosen.empty()) {
ShowEmptyLinkError(window);
} else {
EditLinkChats(link, chosen);
}
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
} else {

View File

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_authorizations.h"
#include "api/api_confirm_phone.h"
#include "api/api_text_entities.h"
#include "api/api_chat_filters.h"
#include "api/api_chat_invite.h"
#include "base/qthelp_regex.h"
#include "base/qthelp_url.h"
@ -78,6 +79,18 @@ bool JoinGroupByHash(
return true;
}
bool JoinFilterBySlug(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
if (!controller) {
return false;
}
Api::CheckFilterInvite(controller, match->captured(1));
controller->window().activate();
return true;
}
bool ShowStickerSet(
Window::SessionController *controller,
const Match &match,
@ -829,6 +842,10 @@ const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
u"^join/?\\?invite=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q,
JoinGroupByHash
},
{
u"^list/?\\?slug=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q,
JoinFilterBySlug
},
{
u"^(addstickers|addemoji)/?\\?set=([a-zA-Z0-9\\.\\_]+)(&|$)"_q,
ShowStickerSet
@ -953,6 +970,8 @@ QString TryConvertUrlToLocal(QString url) {
return u"tg://resolve?phone="_q + phoneMatch->captured(1) + (params.isEmpty() ? QString() : '&' + params);
} else if (const auto joinChatMatch = regex_match(u"^(joinchat/|\\+|\\%20)([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) {
return u"tg://join?invite="_q + url_encode(joinChatMatch->captured(2));
} else if (const auto joinFilterMatch = regex_match(u"^(list/)([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) {
return u"tg://list?slug="_q + url_encode(joinFilterMatch->captured(2));
} else if (const auto stickerSetMatch = regex_match(u"^(addstickers|addemoji)/([a-zA-Z0-9\\.\\_]+)(\\?|$)"_q, query, matchOptions)) {
return u"tg://"_q + stickerSetMatch->captured(1) + "?set=" + url_encode(stickerSetMatch->captured(2));
} else if (const auto themeMatch = regex_match(u"^addtheme/([a-zA-Z0-9\\.\\_]+)(\\?|$)"_q, query, matchOptions)) {

View File

@ -329,6 +329,11 @@ void ChatFilters::load() {
load(false);
}
void ChatFilters::reload() {
_reloading = true;
load();
}
void ChatFilters::load(bool force) {
if (_loadRequestId && !force) {
return;
@ -341,6 +346,10 @@ void ChatFilters::load(bool force) {
_loadRequestId = 0;
}).fail([=] {
_loadRequestId = 0;
if (_reloading) {
_reloading = false;
_listChanged.fire({});
}
}).send();
}
@ -372,8 +381,9 @@ void ChatFilters::received(const QVector<MTPDialogFilter> &list) {
if (!ranges::contains(begin(_list), end(_list), 0, &ChatFilter::id)) {
_list.insert(begin(_list), ChatFilter());
}
if (changed || !_loaded) {
if (changed || !_loaded || _reloading) {
_loaded = true;
_reloading = false;
_listChanged.fire({});
}
}

View File

@ -112,6 +112,7 @@ public:
void setPreloaded(const QVector<MTPDialogFilter> &result);
void load();
void reload();
void apply(const MTPUpdate &update);
void set(ChatFilter filter);
void remove(FilterId id);
@ -175,6 +176,7 @@ private:
mtpRequestId _saveOrderRequestId = 0;
mtpRequestId _saveOrderAfterId = 0;
bool _loaded = false;
bool _reloading = false;
mtpRequestId _suggestedRequestId = 0;
std::vector<SuggestedFilter> _suggested;

View File

@ -538,3 +538,13 @@ powerSavingButtonNoIcon: SettingsButton(powerSavingButton) {
padding: margins(22px, 8px, 22px, 8px);
}
powerSavingSubtitlePadding: margins(0px, 4px, 0px, -2px);
filterInviteBox: Box(defaultBox) {
buttonPadding: margins(12px, 12px, 12px, 12px);
buttonHeight: 44px;
button: RoundButton(defaultActiveButton) {
height: 44px;
textTop: 12px;
font: font(13px semibold);
}
}