Manage filters: delete, add suggested.

This commit is contained in:
John Preston 2020-03-10 16:41:12 +04:00
parent ca3419ef24
commit b8c11f3d8c
8 changed files with 580 additions and 18 deletions

View File

@ -204,6 +204,8 @@ PRIVATE
boxes/language_box.h
boxes/local_storage_box.cpp
boxes/local_storage_box.h
boxes/manage_filters_box.cpp
boxes/manage_filters_box.h
boxes/mute_settings_box.cpp
boxes/mute_settings_box.h
boxes/peer_list_box.cpp

View File

@ -2228,6 +2228,39 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_outdated_soon" = "Otherwise, Telegram Desktop will stop updating on {date}.";
"lng_outdated_now" = "So that Telegram Desktop can update to newer versions.";
"lng_filters_all" = "All chats";
"lng_filters_setup" = "Setup";
"lng_filters_title" = "Folders";
"lng_filters_subtitle" = "My folders";
"lng_filters_no_chats" = "No chats";
"lng_filters_chats_count#one" = "{count} chat";
"lng_filters_chats_count#other" = "{count} chats";
"lng_filters_create" = "Add a custom folder";
"lng_filters_about" = "Create folders for different groups of chats and quickly switch between them.";
"lng_filters_recommended" = "Recommended";
"lng_filters_recommended_add" = "Add";
"lng_filters_restore" = "Undo";
"lng_filters_new" = "New folder";
"lng_filters_new_name" = "Folder name";
"lng_filters_add_chats" = "Add chats";
"lng_filters_include" = "Include";
"lng_filters_include_about" = "Choose chats and types of chats that will appear in this folder.";
"lng_filters_exclude" = "Exclude";
"lng_filters_exclude_about" = "Choose chats and types of chats that will never appear in this folder.";
"lng_filters_add_title" = "Add Chats";
"lng_filters_edit_types" = "Chat types";
"lng_filters_edit_chats" = "Chats";
"lng_filters_include_contacts" = "Contacts";
"lng_filters_include_non_contacts" = "Non-Contacts";
"lng_filters_include_groups" = "Groups";
"lng_filters_include_channels" = "Channels";
"lng_filters_include_bots" = "Bots";
"lng_filters_exclude_muted" = "Muted";
"lng_filters_exclude_read" = "Read";
"lng_filters_exclude_archived" = "Archived";
"lng_filters_add" = "Done";
"lng_filters_limit" = "Sorry, you have reached folders limit.";
// Wnd specific
"lng_wnd_choose_program_menu" = "Choose Default Program...";

View File

@ -0,0 +1,465 @@
/*
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 "boxes/manage_filters_box.h"
#include "data/data_session.h"
#include "data/data_chat_filters.h"
#include "data/data_folder.h"
#include "main/main_session.h"
#include "window/window_session_controller.h"
#include "window/window_controller.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h"
#include "ui/text/text_utilities.h"
#include "settings/settings_common.h"
#include "lang/lang_keys.h"
#include "apiwrap.h"
#include "styles/style_settings.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
namespace {
constexpr auto kRefreshSuggestedTimeout = 7200 * crl::time(1000);
constexpr auto kFiltersLimit = 10;
class FilterRowButton final : public Ui::RippleButton {
public:
FilterRowButton(
not_null<QWidget*> parent,
not_null<Main::Session*> session,
const Data::ChatFilter &filter);
FilterRowButton(
not_null<QWidget*> parent,
const Data::ChatFilter &filter,
const QString &description);
void setRemoved(bool removed);
[[nodiscard]] rpl::producer<> removeRequests() const;
[[nodiscard]] rpl::producer<> restoreRequests() const;
[[nodiscard]] rpl::producer<> addRequests() const;
private:
enum class State {
Suggested,
Removed,
Normal,
};
FilterRowButton(
not_null<QWidget*> parent,
const Data::ChatFilter &filter,
const QString &description,
State state);
void paintEvent(QPaintEvent *e) override;
void setup(const Data::ChatFilter &filter, const QString &status);
void setState(State state, bool force = false);
void updateButtonsVisibility();
Ui::IconButton _remove;
Ui::RoundButton _restore;
Ui::RoundButton _add;
Ui::Text::String _title;
QString _status;
State _state = State::Normal;
};
[[nodiscard]] int CountFilterChats(
not_null<Main::Session*> session,
const Data::ChatFilter &filter) {
auto result = 0;
const auto addList = [&](not_null<Dialogs::MainList*> list) {
for (const auto &entry : list->indexed()->all()) {
if (const auto history = entry->history()) {
if (filter.contains(history)) {
++result;
}
}
}
};
addList(session->data().chatsList());
const auto folderId = Data::Folder::kId;
if (const auto folder = session->data().folderLoaded(folderId)) {
addList(folder->chatsList());
}
return result;
}
[[nodiscard]] int ComputeCount(
not_null<Main::Session*> session,
const Data::ChatFilter &filter) {
const auto &list = session->data().chatsFilters().list();
const auto id = filter.id();
if (ranges::contains(list, id, &Data::ChatFilter::id)) {
const auto chats = session->data().chatsFilters().chatsList(id);
return chats->indexed()->size();
}
return CountFilterChats(session, filter);
}
[[nodiscard]] QString ComputeCountString(
not_null<Main::Session*> session,
const Data::ChatFilter &filter) {
const auto count = ComputeCount(session, filter);
return count
? tr::lng_filters_chats_count(tr::now, lt_count_short, count)
: tr::lng_filters_no_chats(tr::now);
}
FilterRowButton::FilterRowButton(
not_null<QWidget*> parent,
not_null<Main::Session*> session,
const Data::ChatFilter &filter)
: FilterRowButton(
parent,
filter,
ComputeCountString(session, filter),
State::Normal) {
}
FilterRowButton::FilterRowButton(
not_null<QWidget*> parent,
const Data::ChatFilter &filter,
const QString &description)
: FilterRowButton(parent, filter, description, State::Suggested) {
}
FilterRowButton::FilterRowButton(
not_null<QWidget*> parent,
const Data::ChatFilter &filter,
const QString &status,
State state)
: RippleButton(parent, st::defaultRippleAnimation)
, _remove(this, st::filtersRemove)
, _restore(this, tr::lng_filters_restore(), st::stickersUndoRemove)
, _add(this, tr::lng_filters_recommended_add(), st::stickersTrendingAdd)
, _state(state) {
setup(filter, status);
}
void FilterRowButton::setRemoved(bool removed) {
setState(removed ? State::Removed : State::Normal);
}
void FilterRowButton::setState(State state, bool force) {
if (!force && _state == state) {
return;
}
_state = state;
setPointerCursor(_state == State::Normal);
setDisabled(_state != State::Normal);
updateButtonsVisibility();
update();
}
void FilterRowButton::setup(
const Data::ChatFilter &filter,
const QString &status) {
resize(width(), st::defaultPeerListItem.height);
_title.setText(st::contactsNameStyle, filter.title());
_status = status;
setState(_state, true);
sizeValue(
) | rpl::start_with_next([=](QSize size) {
const auto right = st::contactsPadding.right()
+ st::contactsCheckPosition.x();
const auto width = size.width();
const auto height = size.height();
_restore.moveToRight(right, (height - _restore.height()) / 2, width);
_add.moveToRight(right, (height - _add.height()) / 2, width);
const auto skipped = right - st::stickersRemoveSkip;
_remove.moveToRight(skipped, (height - _remove.height()) / 2, width);
}, lifetime());
}
void FilterRowButton::updateButtonsVisibility() {
_remove.setVisible(_state == State::Normal);
_restore.setVisible(_state == State::Removed);
_add.setVisible(_state == State::Suggested);
}
rpl::producer<> FilterRowButton::removeRequests() const {
return _remove.clicks() | rpl::map([] { return rpl::empty_value(); });
}
rpl::producer<> FilterRowButton::restoreRequests() const {
return _restore.clicks() | rpl::map([] { return rpl::empty_value(); });
}
rpl::producer<> FilterRowButton::addRequests() const {
return _add.clicks() | rpl::map([] { return rpl::empty_value(); });
}
void FilterRowButton::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
if (_state == State::Normal) {
if (isOver() || isDown()) {
p.fillRect(e->rect(), st::windowBgOver);
}
RippleButton::paintRipple(p, 0, 0);
} else if (_state == State::Removed) {
p.setOpacity(st::stickersRowDisabledOpacity);
}
const auto left = st::settingsSubsectionTitlePadding.left();
const auto buttonsLeft = std::min(
_add.x(),
std::min(_remove.x(), _restore.x()));
const auto availableWidth = buttonsLeft - left;
p.setPen(st::contactsNameFg);
_title.drawLeftElided(
p,
left,
st::contactsPadding.top() + st::contactsNameTop,
availableWidth,
width());
p.setFont(st::contactsStatusFont);
p.setPen(st::contactsStatusFg);
p.drawTextLeft(
left,
st::contactsPadding.top() + st::contactsStatusTop,
width(),
_status);
}
} // namespace
ManageFiltersPrepare::ManageFiltersPrepare(
not_null<Window::SessionController*> window)
: _window(window)
, _api(&_window->session().api()) {
}
ManageFiltersPrepare::~ManageFiltersPrepare() {
if (_requestId) {
_api->request(_requestId).cancel();
}
}
void ManageFiltersPrepare::showBox() {
if (_requestId) {
return;
}
if (_suggestedLastReceived > 0
&& crl::now() - _suggestedLastReceived < kRefreshSuggestedTimeout) {
showBoxWithSuggested();
return;
}
_requestId = _api->request(MTPmessages_GetSuggestedDialogFilters(
)).done([=](const MTPVector<MTPDialogFilterSuggested> &data) {
_requestId = 0;
_suggestedLastReceived = crl::now();
const auto owner = &_api->session().data();
_suggested = ranges::view::all(
data.v
) | ranges::view::transform([&](const MTPDialogFilterSuggested &f) {
return f.match([&](const MTPDdialogFilterSuggested &data) {
return Suggested{
Data::ChatFilter::FromTL(data.vfilter(), owner),
qs(data.vdescription())
};
});
}) | ranges::to_vector;
showBoxWithSuggested();
}).fail([=](const RPCError &error) {
_requestId = 0;
_suggestedLastReceived = crl::now() + kRefreshSuggestedTimeout / 2;
showBoxWithSuggested();
}).send();
}
void ManageFiltersPrepare::showBoxWithSuggested() {
_window->window().show(Box(CreateBox, _window, _suggested));
}
void ManageFiltersPrepare::CreateBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> window,
const std::vector<Suggested> &suggested) {
struct FilterRow {
not_null<FilterRowButton*> button;
Data::ChatFilter filter;
bool removed = false;
bool added = false;
};
box->setTitle(tr::lng_filters_title());
const auto session = &window->session();
const auto content = box->verticalLayout();
Settings::AddSubsectionTitle(content, tr::lng_filters_subtitle());
const auto rows = box->lifetime().make_state<std::vector<FilterRow>>();
const auto find = [=](not_null<FilterRowButton*> button) {
const auto i = ranges::find(*rows, button, &FilterRow::button);
Assert(i != end(*rows));
return &*i;
};
const auto countNonRemoved = [=] {
const auto removed = ranges::count_if(*rows, &FilterRow::removed);
return rows->size() - removed;
};
const auto wrap = content->add(object_ptr<Ui::VerticalLayout>(content));
const auto addFilter = [=](const Data::ChatFilter &filter) {
const auto button = wrap->add(
object_ptr<FilterRowButton>(wrap, session, filter));
button->removeRequests(
) | rpl::start_with_next([=] {
button->setRemoved(true);
find(button)->removed = true;
}, button->lifetime());
button->restoreRequests(
) | rpl::start_with_next([=] {
if (countNonRemoved() < kFiltersLimit) {
button->setRemoved(false);
find(button)->removed = false;
}
}, button->lifetime());
rows->push_back({ button, filter });
};
const auto &list = session->data().chatsFilters().list();
for (const auto &filter : list) {
addFilter(filter);
}
Settings::AddButton(
content,
tr::lng_filters_create() | Ui::Text::ToUpper(),
st::settingsUpdate);
Settings::AddSkip(content);
if (suggested.empty()) {
content->add(
object_ptr<Ui::FlatLabel>(
content,
tr::lng_filters_about(),
st::boxDividerLabel),
st::settingsDividerLabelPadding);
} else {
Settings::AddDividerText(content, tr::lng_filters_about());
Settings::AddSkip(content);
Settings::AddSubsectionTitle(content, tr::lng_filters_recommended());
for (const auto &suggestion : suggested) {
const auto filter = suggestion.filter;
const auto already = [&] {
for (const auto &entry : list) {
if (entry.flags() == filter.flags()
&& entry.always() == filter.always()
&& entry.never() == filter.never()) {
return true;
}
}
return false;
}();
if (already) {
continue;
}
const auto button = content->add(object_ptr<FilterRowButton>(
content,
filter,
suggestion.description));
button->addRequests(
) | rpl::start_with_next([=] {
addFilter(filter);
delete button;
}, button->lifetime());
}
}
const auto prepareGoodIdsForNewFilters = [=] {
const auto &list = session->data().chatsFilters().list();
auto localId = 2;
const auto chooseNextId = [&] {
while (ranges::contains(list, localId, &Data::ChatFilter::id)) {
++localId;
}
return localId;
};
auto result = base::flat_map<FilterId, FilterId>();
for (auto &row : *rows) {
const auto id = row.filter.id();
if (row.removed) {
continue;
} else if (!ranges::contains(list, id, &Data::ChatFilter::id)) {
result.emplace(row.filter.id(), chooseNextId());
}
}
return result;
};
const auto save = [=] {
auto ids = prepareGoodIdsForNewFilters();
auto requests = std::deque<MTPmessages_UpdateDialogFilter>();
auto &realFilters = session->data().chatsFilters();
const auto &list = realFilters.list();
auto order = QVector<MTPint>();
for (const auto &row : *rows) {
const auto id = row.filter.id();
const auto removed = row.removed;
if (removed
&& !ranges::contains(list, id, &Data::ChatFilter::id)) {
continue;
}
const auto newId = ids.take(id).value_or(id);
const auto tl = removed ? MTPDialogFilter() : row.filter.tl();
const auto request = MTPmessages_UpdateDialogFilter(
MTP_flags(removed
? MTPmessages_UpdateDialogFilter::Flag(0)
: MTPmessages_UpdateDialogFilter::Flag::f_filter),
MTP_int(newId),
tl);
if (removed) {
requests.push_front(request);
} else {
requests.push_back(request);
order.push_back(MTP_int(newId));
}
realFilters.apply(MTP_updateDialogFilter(
MTP_flags(removed
? MTPDupdateDialogFilter::Flag(0)
: MTPDupdateDialogFilter::Flag::f_filter),
MTP_int(newId),
tl));
}
auto previousId = mtpRequestId(0);
for (auto &request : requests) {
previousId = session->api().request(
std::move(request)
).afterRequest(previousId).send();
}
if (!order.isEmpty()) {
realFilters.apply(
MTP_updateDialogFilterOrder(MTP_vector(order)));
session->api().request(MTPmessages_UpdateDialogFiltersOrder(
MTP_vector(order)
)).afterRequest(previousId).send();
}
box->closeBox();
};
box->addButton(tr::lng_settings_save(), save);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}

View File

@ -0,0 +1,49 @@
/*
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 "data/data_chat_filters.h"
class ApiWrap;
namespace Ui {
class GenericBox;
} // namespace Ui
namespace Window {
class SessionController;
} // namespace Window
class ManageFiltersPrepare final {
public:
explicit ManageFiltersPrepare(
not_null<Window::SessionController*> window);
~ManageFiltersPrepare();
void showBox();
private:
struct Suggested {
Data::ChatFilter filter;
QString description;
};
void showBoxWithSuggested();
static void CreateBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> window,
const std::vector<Suggested> &suggested);
const not_null<Window::SessionController*> _window;
const not_null<ApiWrap*> _api;
mtpRequestId _requestId = 0;
std::vector<Suggested> _suggested;
crl::time _suggestedLastReceived = 0;
};

View File

@ -95,6 +95,10 @@ stickersSearch: icon {{ "stickers_search", emojiIconFg, point(0px, 1px) }};
stickersSettingsUnreadSize: 17px;
stickersSettingsUnreadPosition: point(4px, 5px);
filtersRemove: IconButton(stickersRemove) {
ripple: defaultRippleAnimation;
}
emojiPanMargins: margins(10px, 10px, 10px, 10px);
emojiTabs: SettingsSlider(defaultTabsSlider) {

View File

@ -168,17 +168,18 @@ bool ChatFilter::contains(not_null<History*> history) const {
}
ChatFilters::ChatFilters(not_null<Session*> owner) : _owner(owner) {
using Flag = ChatFilter::Flag;
const auto all = Flag::Contacts
| Flag::NonContacts
| Flag::Groups
| Flag::Broadcasts
| Flag::Bots
| Flag::NoArchive;
_list.push_back(
ChatFilter(1, "Unmuted", all | Flag::NoMuted, {}, {}));
_list.push_back(
ChatFilter(2, "Unread", all | Flag::NoRead, {}, {}));
//using Flag = ChatFilter::Flag;
//const auto all = Flag::Contacts
// | Flag::NonContacts
// | Flag::Groups
// | Flag::Broadcasts
// | Flag::Bots
// | Flag::NoArchive;
//_list.push_back(
// ChatFilter(1, "Unmuted", all | Flag::NoMuted, {}, {}));
//_list.push_back(
// ChatFilter(2, "Unread", all | Flag::NoRead, {}, {}));
load();
}
ChatFilters::~ChatFilters() = default;

View File

@ -9,9 +9,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mainwindow.h"
#include "window/window_session_controller.h"
#include "window/window_controller.h"
#include "main/main_session.h"
#include "data/data_session.h"
#include "data/data_chat_filters.h"
#include "boxes/manage_filters_box.h"
#include "lang/lang_keys.h"
#include "styles/style_widgets.h"
#include "styles/style_window.h"
@ -31,9 +34,9 @@ enum class Type {
| Flag::NonContacts
| Flag::Groups
| Flag::Broadcasts
| Flag::Bots
| Flag::NoArchive;
if (!filter.always().empty()) {
| Flag::Bots;
const auto allNoArchive = all | Flag::NoArchive;
if (!filter.always().empty() || !filter.never().empty()) {
return Type::Custom;
} else if (filter.flags() == (all | Flag::NoRead)) {
return Type::Unread;
@ -132,6 +135,11 @@ void FiltersMenu::setup() {
void FiltersMenu::refresh() {
const auto filters = &_session->session().data().chatsFilters();
if (filters->list().empty()) {
return;
}
const auto manage = _outer.lifetime().make_state<ManageFiltersPrepare>(
_session);
auto now = base::flat_map<int, base::unique_qptr<Ui::SideBarButton>>();
const auto prepare = [&](
FilterId id,
@ -149,12 +157,12 @@ void FiltersMenu::refresh() {
if (id >= 0) {
_session->setActiveChatsFilter(id);
} else {
// #TODO filters
manage->showBox();
}
});
now.emplace(id, std::move(button));
};
prepare(0, "All chats", st::windowFiltersAll, QString());
prepare(0, tr::lng_filters_all(tr::now), st::windowFiltersAll, {});
for (const auto filter : filters->list()) {
prepare(
filter.id(),
@ -162,7 +170,7 @@ void FiltersMenu::refresh() {
ComputeStyle(ComputeType(filter)),
QString());
}
prepare(-1, "Setup", st::windowFiltersSetup, QString());
prepare(-1, tr::lng_filters_setup(tr::now), st::windowFiltersSetup, {});
_filters = std::move(now);
}

@ -1 +1 @@
Subproject commit 1b673b7e406af46e931fd37feabaf9e277ada93b
Subproject commit c96119dcd18bff5974348524b1e15b3d396426dc