diff --git a/Telegram/Resources/icons/filters_type_archived.png b/Telegram/Resources/icons/filters_type_archived.png new file mode 100644 index 000000000..790d50335 Binary files /dev/null and b/Telegram/Resources/icons/filters_type_archived.png differ diff --git a/Telegram/Resources/icons/filters_type_archived@2x.png b/Telegram/Resources/icons/filters_type_archived@2x.png new file mode 100644 index 000000000..342c6cb55 Binary files /dev/null and b/Telegram/Resources/icons/filters_type_archived@2x.png differ diff --git a/Telegram/Resources/icons/filters_type_archived@3x.png b/Telegram/Resources/icons/filters_type_archived@3x.png new file mode 100644 index 000000000..f20b8b610 Binary files /dev/null and b/Telegram/Resources/icons/filters_type_archived@3x.png differ diff --git a/Telegram/Resources/icons/filters_type_bots.png b/Telegram/Resources/icons/filters_type_bots.png new file mode 100644 index 000000000..34d2ed00a Binary files /dev/null and b/Telegram/Resources/icons/filters_type_bots.png differ diff --git a/Telegram/Resources/icons/filters_type_bots@2x.png b/Telegram/Resources/icons/filters_type_bots@2x.png new file mode 100644 index 000000000..1f37ff794 Binary files /dev/null and b/Telegram/Resources/icons/filters_type_bots@2x.png differ diff --git a/Telegram/Resources/icons/filters_type_bots@3x.png b/Telegram/Resources/icons/filters_type_bots@3x.png new file mode 100644 index 000000000..afc773a3c Binary files /dev/null and b/Telegram/Resources/icons/filters_type_bots@3x.png differ diff --git a/Telegram/Resources/icons/filters_type_channels.png b/Telegram/Resources/icons/filters_type_channels.png new file mode 100644 index 000000000..e43f40012 Binary files /dev/null and b/Telegram/Resources/icons/filters_type_channels.png differ diff --git a/Telegram/Resources/icons/filters_type_channels@2x.png b/Telegram/Resources/icons/filters_type_channels@2x.png new file mode 100644 index 000000000..cf3804750 Binary files /dev/null and b/Telegram/Resources/icons/filters_type_channels@2x.png differ diff --git a/Telegram/Resources/icons/filters_type_channels@3x.png b/Telegram/Resources/icons/filters_type_channels@3x.png new file mode 100644 index 000000000..3e8d01368 Binary files /dev/null and b/Telegram/Resources/icons/filters_type_channels@3x.png differ diff --git a/Telegram/Resources/icons/filters_type_contacts.png b/Telegram/Resources/icons/filters_type_contacts.png new file mode 100644 index 000000000..1cfb2a575 Binary files /dev/null and b/Telegram/Resources/icons/filters_type_contacts.png differ diff --git a/Telegram/Resources/icons/filters_type_contacts@2x.png b/Telegram/Resources/icons/filters_type_contacts@2x.png new file mode 100644 index 000000000..6c81c14ea Binary files /dev/null and b/Telegram/Resources/icons/filters_type_contacts@2x.png differ diff --git a/Telegram/Resources/icons/filters_type_contacts@3x.png b/Telegram/Resources/icons/filters_type_contacts@3x.png new file mode 100644 index 000000000..f3ac2f12a Binary files /dev/null and b/Telegram/Resources/icons/filters_type_contacts@3x.png differ diff --git a/Telegram/Resources/icons/filters_type_groups.png b/Telegram/Resources/icons/filters_type_groups.png new file mode 100644 index 000000000..c66addebf Binary files /dev/null and b/Telegram/Resources/icons/filters_type_groups.png differ diff --git a/Telegram/Resources/icons/filters_type_groups@2x.png b/Telegram/Resources/icons/filters_type_groups@2x.png new file mode 100644 index 000000000..114156e54 Binary files /dev/null and b/Telegram/Resources/icons/filters_type_groups@2x.png differ diff --git a/Telegram/Resources/icons/filters_type_groups@3x.png b/Telegram/Resources/icons/filters_type_groups@3x.png new file mode 100644 index 000000000..fcaffe853 Binary files /dev/null and b/Telegram/Resources/icons/filters_type_groups@3x.png differ diff --git a/Telegram/Resources/icons/filters_type_muted.png b/Telegram/Resources/icons/filters_type_muted.png new file mode 100644 index 000000000..92e3b1adc Binary files /dev/null and b/Telegram/Resources/icons/filters_type_muted.png differ diff --git a/Telegram/Resources/icons/filters_type_muted@2x.png b/Telegram/Resources/icons/filters_type_muted@2x.png new file mode 100644 index 000000000..1dd51e26c Binary files /dev/null and b/Telegram/Resources/icons/filters_type_muted@2x.png differ diff --git a/Telegram/Resources/icons/filters_type_muted@3x.png b/Telegram/Resources/icons/filters_type_muted@3x.png new file mode 100644 index 000000000..0f0f81dbb Binary files /dev/null and b/Telegram/Resources/icons/filters_type_muted@3x.png differ diff --git a/Telegram/Resources/icons/filters_type_noncontacts.png b/Telegram/Resources/icons/filters_type_noncontacts.png new file mode 100644 index 000000000..213633ac0 Binary files /dev/null and b/Telegram/Resources/icons/filters_type_noncontacts.png differ diff --git a/Telegram/Resources/icons/filters_type_noncontacts@2x.png b/Telegram/Resources/icons/filters_type_noncontacts@2x.png new file mode 100644 index 000000000..f1f7695b6 Binary files /dev/null and b/Telegram/Resources/icons/filters_type_noncontacts@2x.png differ diff --git a/Telegram/Resources/icons/filters_type_noncontacts@3x.png b/Telegram/Resources/icons/filters_type_noncontacts@3x.png new file mode 100644 index 000000000..f5a58c269 Binary files /dev/null and b/Telegram/Resources/icons/filters_type_noncontacts@3x.png differ diff --git a/Telegram/Resources/icons/filters_type_read.png b/Telegram/Resources/icons/filters_type_read.png new file mode 100644 index 000000000..689da4ca7 Binary files /dev/null and b/Telegram/Resources/icons/filters_type_read.png differ diff --git a/Telegram/Resources/icons/filters_type_read@2x.png b/Telegram/Resources/icons/filters_type_read@2x.png new file mode 100644 index 000000000..f874a3c28 Binary files /dev/null and b/Telegram/Resources/icons/filters_type_read@2x.png differ diff --git a/Telegram/Resources/icons/filters_type_read@3x.png b/Telegram/Resources/icons/filters_type_read@3x.png new file mode 100644 index 000000000..b2a8feea1 Binary files /dev/null and b/Telegram/Resources/icons/filters_type_read@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index f3f815b62..7984173cc 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2240,14 +2240,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_filters_recommended" = "Recommended"; "lng_filters_recommended_add" = "Add"; "lng_filters_restore" = "Undo"; -"lng_filters_new" = "New folder"; +"lng_filters_new" = "New Folder"; +"lng_filters_edit" = "Edit Folder"; "lng_filters_new_name" = "Folder name"; "lng_filters_add_chats" = "Add chats"; -"lng_filters_include" = "Include"; +"lng_filters_include" = "Included Chats"; "lng_filters_include_about" = "Choose chats and types of chats that will appear in this folder."; -"lng_filters_exclude" = "Exclude"; +"lng_filters_exclude" = "Excluded Chats"; "lng_filters_exclude_about" = "Choose chats and types of chats that will never appear in this folder."; +"lng_filters_create_button" = "Create"; "lng_filters_add_title" = "Add Chats"; +"lng_filters_include_title" = "Include Chats"; +"lng_filters_exclude_title" = "Exclude Chats"; "lng_filters_edit_types" = "Chat types"; "lng_filters_edit_chats" = "Chats"; "lng_filters_include_contacts" = "Contacts"; @@ -2260,6 +2264,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_filters_exclude_archived" = "Archived"; "lng_filters_add" = "Done"; "lng_filters_limit" = "Sorry, you have reached folders limit."; +"lng_filters_empty" = "Please choose at least one chat for this folder."; +"lng_filters_default" = "Please change at least one rule for this folder."; +"lng_filters_type_contacts" = "Contacts"; +"lng_filters_type_non_contacts" = "Non-Contacts"; +"lng_filters_type_groups" = "Groups"; +"lng_filters_type_channels" = "Channels"; +"lng_filters_type_bots" = "Bots"; +"lng_filters_type_no_archived" = "No Archived"; +"lng_filters_type_no_muted" = "No Muted"; +"lng_filters_type_no_read" = "No Read"; // Wnd specific diff --git a/Telegram/SourceFiles/boxes/manage_filters_box.cpp b/Telegram/SourceFiles/boxes/manage_filters_box.cpp index b0726e85e..3f194157d 100644 --- a/Telegram/SourceFiles/boxes/manage_filters_box.cpp +++ b/Telegram/SourceFiles/boxes/manage_filters_box.cpp @@ -10,14 +10,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_chat_filters.h" #include "data/data_folder.h" +#include "data/data_peer.h" +#include "history/history.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/widgets/input_fields.h" #include "ui/text/text_utilities.h" #include "ui/wrap/slide_wrap.h" +#include "ui/painter.h" #include "settings/settings_common.h" #include "lang/lang_keys.h" #include "apiwrap.h" @@ -25,11 +29,72 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" +#include "styles/style_window.h" namespace { constexpr auto kRefreshSuggestedTimeout = 7200 * crl::time(1000); constexpr auto kFiltersLimit = 10; +constexpr auto kMaxFilterTitleLength = 20; + +using namespace Settings; + +using Flag = Data::ChatFilter::Flag; +using Flags = Data::ChatFilter::Flags; +using ExceptionPeersRef = const base::flat_set> &; +using ExceptionPeersGetter = ExceptionPeersRef(Data::ChatFilter::*)() const; + +constexpr auto kAllTypes = { + Flag::Contacts, + Flag::NonContacts, + Flag::Groups, + Flag::Channels, + Flag::Bots, + Flag::NoMuted, + Flag::NoArchived, + Flag::NoRead +}; + +class FilterChatsPreview final : public Ui::RpWidget { +public: + FilterChatsPreview( + not_null parent, + Flags flags, + const base::flat_set> &peers); + + [[nodiscard]] rpl::producer flagRemoved() const; + [[nodiscard]] rpl::producer> peerRemoved() const; + + int resizeGetHeight(int newWidth) override; + +private: + using Button = base::unique_qptr; + struct FlagButton { + Flag flag = Flag(); + Button button; + }; + struct PeerButton { + not_null history; + Button button; + }; + + void paintEvent(QPaintEvent *e) override; + + void setup( + Flags flags, + const base::flat_set> &peers); + void refresh(); + void removeFlag(Flag flag); + void removePeer(not_null history); + void paintFlagIcon(QPainter &p, int left, int top, Flag flag) const; + + std::vector _removeFlag; + std::vector _removePeer; + + rpl::event_stream _flagRemoved; + rpl::event_stream> _peerRemoved; + +}; class FilterRowButton final : public Ui::RippleButton { public: @@ -43,6 +108,7 @@ public: const QString &description); void setRemoved(bool removed); + void updateData(const Data::ChatFilter &filter); [[nodiscard]] rpl::producer<> removeRequests() const; [[nodiscard]] rpl::producer<> restoreRequests() const; @@ -57,6 +123,7 @@ private: FilterRowButton( not_null parent, + Main::Session *session, const Data::ChatFilter &filter, const QString &description, State state); @@ -67,6 +134,8 @@ private: void setState(State state, bool force = false); void updateButtonsVisibility(); + Main::Session *_session = nullptr; + Ui::IconButton _remove; Ui::RoundButton _restore; Ui::RoundButton _add; @@ -101,10 +170,16 @@ private: [[nodiscard]] int ComputeCount( not_null session, - const Data::ChatFilter &filter) { + const Data::ChatFilter &filter, + bool check = false) { const auto &list = session->data().chatsFilters().list(); const auto id = filter.id(); - if (ranges::contains(list, id, &Data::ChatFilter::id)) { + const auto i = ranges::find(list, id, &Data::ChatFilter::id); + if (i != end(list) + && (!check + || (i->flags() == filter.flags() + && i->always() == filter.always() + && i->never() == filter.never()))) { const auto chats = session->data().chatsFilters().chatsList(id); return chats->indexed()->size(); } @@ -113,19 +188,188 @@ private: [[nodiscard]] QString ComputeCountString( not_null session, - const Data::ChatFilter &filter) { - const auto count = ComputeCount(session, filter); + const Data::ChatFilter &filter, + bool check = false) { + const auto count = ComputeCount(session, filter, check); return count ? tr::lng_filters_chats_count(tr::now, lt_count_short, count) : tr::lng_filters_no_chats(tr::now); } +FilterChatsPreview::FilterChatsPreview( + not_null parent, + Flags flags, + const base::flat_set> &peers) +: RpWidget(parent) { + setup(flags, peers); +} + +void FilterChatsPreview::setup( + Flags flags, + const base::flat_set> &peers) { + const auto makeButton = [&](Fn handler) { + auto result = base::make_unique_q( + this, + st::windowFilterSmallRemove); + result->setClickedCallback(std::move(handler)); + return result; + }; + for (const auto flag : kAllTypes) { + if (flags & flag) { + _removeFlag.push_back({ + flag, + makeButton([=] { removeFlag(flag); }) }); + } + } + for (const auto history : peers) { + _removePeer.push_back({ + history, + makeButton([=] { removePeer(history); }) }); + } + refresh(); +} + +void FilterChatsPreview::refresh() { + resizeToWidth(width()); +} + +int FilterChatsPreview::resizeGetHeight(int newWidth) { + const auto right = st::windowFilterSmallRemoveRight; + const auto add = (st::windowFilterSmallItem.height + - st::windowFilterSmallRemove.height) / 2; + auto top = 0; + const auto moveNextButton = [&](not_null button) { + button->moveToRight(right, top + add, newWidth); + top += st::windowFilterSmallItem.height; + }; + for (const auto &[flag, button] : _removeFlag) { + moveNextButton(button.get()); + } + for (const auto &[history, button] : _removePeer) { + moveNextButton(button.get()); + } + return top; +} + +[[nodiscard]] QString TypeName(Flag flag) { + switch (flag) { + case Flag::Contacts: return tr::lng_filters_type_contacts(tr::now); + case Flag::NonContacts: + return tr::lng_filters_type_non_contacts(tr::now); + case Flag::Groups: return tr::lng_filters_type_groups(tr::now); + case Flag::Channels: return tr::lng_filters_type_channels(tr::now); + case Flag::Bots: return tr::lng_filters_type_bots(tr::now); + case Flag::NoMuted: return tr::lng_filters_type_no_muted(tr::now); + case Flag::NoArchived: return tr::lng_filters_type_no_archived(tr::now); + case Flag::NoRead: return tr::lng_filters_type_no_read(tr::now); + } + Unexpected("Flag in TypeName."); +} + +void FilterChatsPreview::paintEvent(QPaintEvent *e) { + auto p = Painter(this); + auto top = 0; + const auto &st = st::windowFilterSmallItem; + const auto iconLeft = st.photoPosition.x(); + const auto iconTop = st.photoPosition.y(); + const auto nameLeft = st.namePosition.x(); + p.setFont(st::windowFilterSmallItem.nameStyle.font); + const auto nameTop = st.namePosition.y(); + for (const auto &[flag, button] : _removeFlag) { + paintFlagIcon(p, iconLeft, top + iconTop, flag); + + p.setPen(st::contactsNameFg); + p.drawTextLeft(nameLeft, top + nameTop, width(), TypeName(flag)); + top += st.height; + } + for (const auto &[history, button] : _removePeer) { + history->peer->paintUserpicLeft( + p, + iconLeft, + top + iconTop, + width(), + st.photoSize); + history->peer->nameText().drawLeftElided( + p, + nameLeft, + top + nameTop, + button->x() - nameLeft, + width()); + top += st.height; + } +} + +void FilterChatsPreview::paintFlagIcon( + QPainter &p, + int left, + int top, + Flag flag) const { + const auto &color = [&]() -> const style::color& { + switch (flag) { + case Flag::Contacts: return st::historyPeer4UserpicBg; + case Flag::NonContacts: return st::historyPeer7UserpicBg; + case Flag::Groups: return st::historyPeer2UserpicBg; + case Flag::Channels: return st::historyPeer1UserpicBg; + case Flag::Bots: return st::historyPeer6UserpicBg; + case Flag::NoMuted: return st::historyPeer6UserpicBg; + case Flag::NoArchived: return st::historyPeer4UserpicBg; + case Flag::NoRead: return st::historyPeer7UserpicBg; + } + Unexpected("Flag in color paintFlagIcon."); + }(); + const auto &icon = [&]() -> const style::icon& { + switch (flag) { + case Flag::Contacts: return st::windowFilterTypeContacts; + case Flag::NonContacts: return st::windowFilterTypeNonContacts; + case Flag::Groups: return st::windowFilterTypeGroups; + case Flag::Channels: return st::windowFilterTypeChannels; + case Flag::Bots: return st::windowFilterTypeBots; + case Flag::NoMuted: return st::windowFilterTypeNoMuted; + case Flag::NoArchived: return st::windowFilterTypeNoArchived; + case Flag::NoRead: return st::windowFilterTypeNoRead; + } + Unexpected("Flag in icon paintFlagIcon."); + }(); + const auto size = st::windowFilterSmallItem.photoSize; + const auto rect = QRect(left, top, size, size); + auto hq = PainterHighQualityEnabler(p); + p.setBrush(color->b); + p.setPen(Qt::NoPen); + p.drawEllipse(rect); + icon.paintInCenter(p, rect); +} + +void FilterChatsPreview::removeFlag(Flag flag) { + const auto i = ranges::find(_removeFlag, flag, &FlagButton::flag); + Assert(i != end(_removeFlag)); + _removeFlag.erase(i); + refresh(); + _flagRemoved.fire_copy(flag); +} + +void FilterChatsPreview::removePeer(not_null history) { + const auto i = ranges::find(_removePeer, history, &PeerButton::history); + Assert(i != end(_removePeer)); + _removePeer.erase(i); + refresh(); + _peerRemoved.fire_copy(history); +} + +rpl::producer FilterChatsPreview::flagRemoved() const { + return _flagRemoved.events(); +} + +rpl::producer> FilterChatsPreview::peerRemoved() const { + return _peerRemoved.events(); +} + FilterRowButton::FilterRowButton( not_null parent, not_null session, const Data::ChatFilter &filter) : FilterRowButton( parent, + session, filter, ComputeCountString(session, filter), State::Normal) { @@ -135,15 +379,17 @@ FilterRowButton::FilterRowButton( not_null parent, const Data::ChatFilter &filter, const QString &description) -: FilterRowButton(parent, filter, description, State::Suggested) { +: FilterRowButton(parent, nullptr, filter, description, State::Suggested) { } FilterRowButton::FilterRowButton( not_null parent, + Main::Session *session, const Data::ChatFilter &filter, const QString &status, State state) : RippleButton(parent, st::defaultRippleAnimation) +, _session(session) , _remove(this, st::filtersRemove) , _restore(this, tr::lng_filters_restore(), st::stickersUndoRemove) , _add(this, tr::lng_filters_recommended_add(), st::stickersTrendingAdd) @@ -155,6 +401,14 @@ void FilterRowButton::setRemoved(bool removed) { setState(removed ? State::Removed : State::Normal); } +void FilterRowButton::updateData(const Data::ChatFilter &filter) { + Expects(_session != nullptr); + + _title.setText(st::contactsNameStyle, filter.title()); + _status = ComputeCountString(_session, filter, true); + update(); +} + void FilterRowButton::setState(State state, bool force) { if (!force && _state == state) { return; @@ -292,13 +546,15 @@ void ManageFiltersPrepare::showBox() { } void ManageFiltersPrepare::showBoxWithSuggested() { - _window->window().show(Box(CreateBox, _window, _suggested)); + _window->window().show(Box(SetupBox, _window, _suggested)); } -void ManageFiltersPrepare::CreateBox( +void ManageFiltersPrepare::SetupBox( not_null box, not_null window, const std::vector &suggestions) { + box->setTitle(tr::lng_filters_title()); + struct FilterRow { not_null button; Data::ChatFilter filter; @@ -306,11 +562,9 @@ void ManageFiltersPrepare::CreateBox( 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()); + AddSubsectionTitle(content, tr::lng_filters_subtitle()); const auto rows = box->lifetime().make_state>(); const auto find = [=](not_null button) { @@ -319,8 +573,14 @@ void ManageFiltersPrepare::CreateBox( return &*i; }; const auto countNonRemoved = [=] { + }; + const auto showLimitReached = [=] { const auto removed = ranges::count_if(*rows, &FilterRow::removed); - return rows->size() - removed; + if (rows->size() < kFiltersLimit + removed) { + return false; + } + window->window().showToast(tr::lng_filters_limit(tr::now)); + return true; }; const auto wrap = content->add(object_ptr(content)); const auto addFilter = [=](const Data::ChatFilter &filter) { @@ -333,11 +593,27 @@ void ManageFiltersPrepare::CreateBox( }, button->lifetime()); button->restoreRequests( ) | rpl::start_with_next([=] { - if (countNonRemoved() < kFiltersLimit) { - button->setRemoved(false); - find(button)->removed = false; + if (showLimitReached()) { + return; } + button->setRemoved(false); + find(button)->removed = false; }, button->lifetime()); + button->setClickedCallback([=] { + const auto found = find(button); + if (found->removed) { + return; + } + const auto doneCallback = [=](const Data::ChatFilter &result) { + find(button)->filter = result; + button->updateData(result); + }; + window->window().show(Box( + EditBox, + window, + found->filter, + crl::guard(button, doneCallback))); + }); rows->push_back({ button, filter }); }; const auto &list = session->data().chatsFilters().list(); @@ -345,11 +621,24 @@ void ManageFiltersPrepare::CreateBox( addFilter(filter); } - Settings::AddButton( + AddButton( content, tr::lng_filters_create() | Ui::Text::ToUpper(), - st::settingsUpdate); - Settings::AddSkip(content); + st::settingsUpdate + )->setClickedCallback([=] { + if (showLimitReached()) { + return; + } + const auto doneCallback = [=](const Data::ChatFilter &result) { + addFilter(result); + }; + window->window().show(Box( + EditBox, + window, + Data::ChatFilter(), + crl::guard(box, doneCallback))); + }); + AddSkip(content); const auto emptyAbout = content->add( object_ptr>( content, @@ -365,24 +654,15 @@ void ManageFiltersPrepare::CreateBox( object_ptr(content)) )->setDuration(0); const auto aboutRows = nonEmptyAbout->entity(); - Settings::AddDividerText(aboutRows, tr::lng_filters_about()); - Settings::AddSkip(aboutRows); - Settings::AddSubsectionTitle(aboutRows, tr::lng_filters_recommended()); + AddDividerText(aboutRows, tr::lng_filters_about()); + AddSkip(aboutRows); + AddSubsectionTitle(aboutRows, tr::lng_filters_recommended()); + const auto changed = box->lifetime().make_state(); const auto suggested = box->lifetime().make_state>(); for (const auto &suggestion : suggestions) { - 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) { + const auto &filter = suggestion.filter; + if (ranges::contains(list, filter)) { continue; } *suggested = suggested->current() + 1; @@ -392,6 +672,9 @@ void ManageFiltersPrepare::CreateBox( suggestion.description)); button->addRequests( ) | rpl::start_with_next([=] { + if (showLimitReached()) { + return; + } addFilter(filter); *suggested = suggested->current() - 1; delete button; @@ -427,15 +710,19 @@ void ManageFiltersPrepare::CreateBox( const auto save = [=] { auto ids = prepareGoodIdsForNewFilters(); - auto requests = std::deque(); + using Requests = std::vector; + auto addRequests = Requests(), removeRequests = Requests(); auto &realFilters = session->data().chatsFilters(); const auto &list = realFilters.list(); auto order = QVector(); 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)) { + const auto i = ranges::find(list, id, &Data::ChatFilter::id); + if (removed && i == end(list)) { + continue; + } else if (!removed && i != end(list) && *i == row.filter) { + order.push_back(MTP_int(id)); continue; } const auto newId = ids.take(id).value_or(id); @@ -447,9 +734,9 @@ void ManageFiltersPrepare::CreateBox( MTP_int(newId), tl); if (removed) { - requests.push_front(request); + removeRequests.push_back(request); } else { - requests.push_back(request); + addRequests.push_back(request); order.push_back(MTP_int(newId)); } realFilters.apply(MTP_updateDialogFilter( @@ -460,12 +747,13 @@ void ManageFiltersPrepare::CreateBox( tl)); } auto previousId = mtpRequestId(0); + auto &&requests = ranges::view::concat(removeRequests, addRequests); for (auto &request : requests) { previousId = session->api().request( std::move(request) ).afterRequest(previousId).send(); } - if (!order.isEmpty()) { + if (!order.isEmpty() && !addRequests.empty()) { realFilters.apply( MTP_updateDialogFilterOrder(MTP_vector(order))); session->api().request(MTPmessages_UpdateDialogFiltersOrder( @@ -474,6 +762,148 @@ void ManageFiltersPrepare::CreateBox( } box->closeBox(); }; - box->addButton(tr::lng_settings_save(), save); - box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + box->boxClosing() | rpl::start_with_next(save, box->lifetime()); + box->addButton(tr::lng_about_done(), [=] { box->closeBox(); }); } + +void SetupChatsPreview( + not_null content, + not_null data, + Flags flags, + ExceptionPeersGetter peers) { + const auto preview = content->add(object_ptr( + content, + data->flags() & flags, + (data->*peers)())); + + preview->flagRemoved( + ) | rpl::start_with_next([=](Flag flag) { + *data = Data::ChatFilter( + data->id(), + data->title(), + (data->flags() & ~flag), + data->always(), + data->never()); + }, preview->lifetime()); + + preview->peerRemoved( + ) | rpl::start_with_next([=](not_null history) { + auto always = data->always(); + auto never = data->never(); + always.remove(history); + never.remove(history); + *data = Data::ChatFilter( + data->id(), + data->title(), + data->flags(), + std::move(always), + std::move(never)); + }, preview->lifetime()); +} + +void ManageFiltersPrepare::EditBox( + not_null box, + not_null window, + const Data::ChatFilter &filter, + Fn doneCallback) { + const auto creating = filter.title().isEmpty(); + box->setTitle(creating ? tr::lng_filters_new() : tr::lng_filters_edit()); + + const auto content = box->verticalLayout(); + const auto name = content->add( + object_ptr( + box, + st::defaultInputField, + tr::lng_filters_new_name(), + filter.title()), + st::markdownLinkFieldPadding); + name->setMaxLength(kMaxFilterTitleLength); + + const auto data = box->lifetime().make_state(filter); + + constexpr auto kTypes = Flag::Contacts + | Flag::NonContacts + | Flag::Groups + | Flag::Channels + | Flag::Bots; + constexpr auto kExcludeTypes = Flag::NoMuted + | Flag::NoArchived + | Flag::NoRead; + + box->setFocusCallback([=] { + name->setFocusFast(); + }); + + AddSkip(content); + AddDivider(content); + AddSkip(content); + AddSubsectionTitle(content, tr::lng_filters_include()); + + SetupChatsPreview( + content, + data, + kTypes, + &Data::ChatFilter::always); + + AddButton( + content, + tr::lng_filters_add_chats() | Ui::Text::ToUpper(), + st::settingsUpdate + )->setClickedCallback([=] { + }); + + AddSkip(content); + AddDividerText(content, tr::lng_filters_include_about()); + AddSkip(content); + + AddSubsectionTitle(content, tr::lng_filters_exclude()); + + SetupChatsPreview( + content, + data, + kExcludeTypes, + &Data::ChatFilter::never); + + AddButton( + content, + tr::lng_filters_add_chats() | Ui::Text::ToUpper(), + st::settingsUpdate + )->setClickedCallback([=] { + }); + AddSkip(content); + content->add( + object_ptr( + content, + tr::lng_filters_exclude_about(), + st::boxDividerLabel), + st::settingsDividerLabelPadding); + + const auto save = [=] { + const auto title = name->getLastText().trimmed(); + if (title.isEmpty()) { + name->showError(); + return; + } else if (!(data->flags() & kTypes) && data->always().empty()) { + window->window().showToast(tr::lng_filters_empty(tr::now)); + return; + } else if ((data->flags() == (kTypes | Flag::NoArchived)) + && data->always().empty() + && data->never().empty()) { + window->window().showToast(tr::lng_filters_default(tr::now)); + return; + } + const auto result = Data::ChatFilter( + data->id(), + title, + data->flags(), + data->always(), + data->never()); + box->closeBox(); + + doneCallback(result); + }; + box->addButton( + creating ? tr::lng_filters_create_button() : tr::lng_settings_save(), + save); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); +} \ No newline at end of file diff --git a/Telegram/SourceFiles/boxes/manage_filters_box.h b/Telegram/SourceFiles/boxes/manage_filters_box.h index 1b4bd94e5..9900927e5 100644 --- a/Telegram/SourceFiles/boxes/manage_filters_box.h +++ b/Telegram/SourceFiles/boxes/manage_filters_box.h @@ -34,10 +34,15 @@ private: }; void showBoxWithSuggested(); - static void CreateBox( + static void SetupBox( not_null box, not_null window, const std::vector &suggested); + static void EditBox( + not_null box, + not_null window, + const Data::ChatFilter &filter, + Fn doneCallback); const not_null _window; const not_null _api; diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index 7dfe88416..c015de468 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -40,11 +40,11 @@ ChatFilter ChatFilter::FromTL( const auto flags = (data.is_contacts() ? Flag::Contacts : Flag(0)) | (data.is_non_contacts() ? Flag::NonContacts : Flag(0)) | (data.is_groups() ? Flag::Groups : Flag(0)) - | (data.is_broadcasts() ? Flag::Broadcasts : Flag(0)) + | (data.is_broadcasts() ? Flag::Channels : Flag(0)) | (data.is_bots() ? Flag::Bots : Flag(0)) | (data.is_exclude_muted() ? Flag::NoMuted : Flag(0)) | (data.is_exclude_read() ? Flag::NoRead : Flag(0)) - | (data.is_exclude_archived() ? Flag::NoArchive : Flag(0)); + | (data.is_exclude_archived() ? Flag::NoArchived : Flag(0)); auto &&to_histories = ranges::view::transform([&]( const MTPInputPeer &data) { const auto peer = data.match([&](const MTPDinputPeerUser &data) { @@ -87,11 +87,11 @@ MTPDialogFilter ChatFilter::tl() const { | ((_flags & Flag::Contacts) ? TLFlag::f_contacts : TLFlag(0)) | ((_flags & Flag::NonContacts) ? TLFlag::f_non_contacts : TLFlag(0)) | ((_flags & Flag::Groups) ? TLFlag::f_groups : TLFlag(0)) - | ((_flags & Flag::Broadcasts) ? TLFlag::f_broadcasts : TLFlag(0)) + | ((_flags & Flag::Channels) ? TLFlag::f_broadcasts : TLFlag(0)) | ((_flags & Flag::Bots) ? TLFlag::f_bots : TLFlag(0)) | ((_flags & Flag::NoMuted) ? TLFlag::f_exclude_muted : TLFlag(0)) | ((_flags & Flag::NoRead) ? TLFlag::f_exclude_read : TLFlag(0)) - | ((_flags & Flag::NoArchive) + | ((_flags & Flag::NoArchived) ? TLFlag::f_exclude_archived : TLFlag(0)); auto always = QVector(); @@ -147,7 +147,7 @@ bool ChatFilter::contains(not_null history) const { return Flag::Groups; } else if (const auto channel = peer->asChannel()) { if (channel->isBroadcast()) { - return Flag::Broadcasts; + return Flag::Channels; } else { return Flag::Groups; } @@ -162,7 +162,7 @@ bool ChatFilter::contains(not_null history) const { || ((_flags & flag) && (!(_flags & Flag::NoMuted) || !history->mute()) && (!(_flags & Flag::NoRead) || history->unreadCountForBadge()) - && (!(_flags & Flag::NoArchive) + && (!(_flags & Flag::NoArchived) || (history->folderKnown() && !history->folder()))) || _always.contains(history); } @@ -172,9 +172,9 @@ ChatFilters::ChatFilters(not_null owner) : _owner(owner) { //const auto all = Flag::Contacts // | Flag::NonContacts // | Flag::Groups - // | Flag::Broadcasts + // | Flag::Channels // | Flag::Bots - // | Flag::NoArchive; + // | Flag::NoArchived; //_list.push_back( // ChatFilter(1, "Unmuted", all | Flag::NoMuted, {}, {})); //_list.push_back( diff --git a/Telegram/SourceFiles/data/data_chat_filters.h b/Telegram/SourceFiles/data/data_chat_filters.h index ca9735200..7e6b2775d 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.h +++ b/Telegram/SourceFiles/data/data_chat_filters.h @@ -25,11 +25,11 @@ public: Contacts = 0x01, NonContacts = 0x02, Groups = 0x04, - Broadcasts = 0x08, + Channels = 0x08, Bots = 0x10, NoMuted = 0x20, NoRead = 0x40, - NoArchive = 0x80, + NoArchived = 0x80, }; friend constexpr inline bool is_flag_type(Flag) { return true; }; using Flags = base::flags; @@ -64,6 +64,17 @@ private: }; +inline bool operator==(const ChatFilter &a, const ChatFilter &b) { + return (a.title() == b.title()) + && (a.flags() == b.flags()) + && (a.always() == b.always()) + && (a.never() == b.never()); +} + +inline bool operator!=(const ChatFilter &a, const ChatFilter &b) { + return !(a == b); +} + class ChatFilters final { public: explicit ChatFilters(not_null owner); diff --git a/Telegram/SourceFiles/storage/localstorage.cpp b/Telegram/SourceFiles/storage/localstorage.cpp index 26ef1bf9f..7fb7c92e1 100644 --- a/Telegram/SourceFiles/storage/localstorage.cpp +++ b/Telegram/SourceFiles/storage/localstorage.cpp @@ -1173,14 +1173,6 @@ bool _readSetting(quint32 blockId, QDataStream &stream, int version, ReadSetting if (!_checkStreamStatus(stream)) return false; Global::SetDialogsFiltersEnabled(enabled == 1); - auto mode = FilterId(0); - if (enabled) { - mode = FilterId(modeInt); - if (mode == 1) { // #TODO filters - - } - } - Global::SetDialogsFilterId(mode); } break; case dbiModerateMode: { diff --git a/Telegram/SourceFiles/window/window.style b/Telegram/SourceFiles/window/window.style index bbbd6c1fd..aaabe5800 100644 --- a/Telegram/SourceFiles/window/window.style +++ b/Telegram/SourceFiles/window/window.style @@ -299,6 +299,23 @@ windowFiltersCustom: SideBarButton(windowFiltersButton) { windowFiltersSetup: SideBarButton(windowFiltersButton) { icon: icon {{ "filters_setup", sideBarIconFg }}; } +windowFilterSmallItem: PeerListItem(defaultPeerListItem) { + height: 44px; + photoPosition: point(15px, 5px); + namePosition: point(62px, 14px); + photoSize: 34px; +} +windowFilterSmallRemove: IconButton(notifyClose) { +} +windowFilterSmallRemoveRight: 10px; +windowFilterTypeContacts: icon {{ "filters_type_contacts", historyPeerUserpicFg }}; +windowFilterTypeNonContacts: icon {{ "filters_type_noncontacts", historyPeerUserpicFg }}; +windowFilterTypeGroups: icon {{ "filters_type_groups", historyPeerUserpicFg }}; +windowFilterTypeChannels: icon {{ "filters_type_channels", historyPeerUserpicFg }}; +windowFilterTypeBots: icon {{ "filters_type_bots", historyPeerUserpicFg }}; +windowFilterTypeNoMuted: icon {{ "filters_type_muted", historyPeerUserpicFg }}; +windowFilterTypeNoArchived: icon {{ "filters_type_archived", historyPeerUserpicFg }}; +windowFilterTypeNoRead: icon {{ "filters_type_read", historyPeerUserpicFg }}; // Mac specific diff --git a/Telegram/SourceFiles/window/window_controller.cpp b/Telegram/SourceFiles/window/window_controller.cpp index 766d25a2f..b754c9a2f 100644 --- a/Telegram/SourceFiles/window/window_controller.cpp +++ b/Telegram/SourceFiles/window/window_controller.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_account.h" #include "ui/layers/box_content.h" #include "ui/layers/layer_widget.h" +#include "ui/toast/toast.h" #include "window/window_session_controller.h" #include "window/themes/window_theme.h" #include "window/themes/window_theme_editor.h" @@ -80,6 +81,10 @@ void Controller::showSettings() { _widget.showSettings(); } +void Controller::showToast(const QString &text) { + Ui::Toast::Show(_widget.bodyWidget(), text); +} + void Controller::showBox( object_ptr content, Ui::LayerOptions options, diff --git a/Telegram/SourceFiles/window/window_controller.h b/Telegram/SourceFiles/window/window_controller.h index 289d6d390..75953a07c 100644 --- a/Telegram/SourceFiles/window/window_controller.h +++ b/Telegram/SourceFiles/window/window_controller.h @@ -52,6 +52,7 @@ public: showBox(std::move(content), options, animated); return result; } + void showToast(const QString &text); void showRightColumn(object_ptr widget); void sideBarChanged(); diff --git a/Telegram/SourceFiles/window/window_filters_menu.cpp b/Telegram/SourceFiles/window/window_filters_menu.cpp index 844bb2604..7e9bd6141 100644 --- a/Telegram/SourceFiles/window/window_filters_menu.cpp +++ b/Telegram/SourceFiles/window/window_filters_menu.cpp @@ -37,11 +37,11 @@ enum class Type { const auto all = Flag::Contacts | Flag::NonContacts | Flag::Groups - | Flag::Broadcasts + | Flag::Channels | Flag::Bots; const auto removed = Flag::NoRead | Flag::NoMuted; const auto people = Flag::Contacts | Flag::NonContacts; - const auto allNoArchive = all | Flag::NoArchive; + const auto allNoArchive = all | Flag::NoArchived; if (!filter.always().empty() || !filter.never().empty() || !(filter.flags() & all)) { @@ -52,7 +52,7 @@ enum class Type { return Type::People; } else if ((filter.flags() & all) == Flag::Groups) { return Type::Groups; - } else if ((filter.flags() & all) == Flag::Broadcasts) { + } else if ((filter.flags() & all) == Flag::Channels) { return Type::Channels; } else if ((filter.flags() & all) == Flag::Bots) { return Type::Bots;