diff --git a/Telegram/Resources/icons/folders/folders_local_book.png b/Telegram/Resources/icons/folders/folders_local_book.png new file mode 100644 index 000000000..7a0fddc99 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_book.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_book@2x.png b/Telegram/Resources/icons/folders/folders_local_book@2x.png new file mode 100644 index 000000000..2a03b1365 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_book@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_book@3x.png b/Telegram/Resources/icons/folders/folders_local_book@3x.png new file mode 100644 index 000000000..08888a0c6 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_book@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_book_active.png b/Telegram/Resources/icons/folders/folders_local_book_active.png new file mode 100644 index 000000000..a29a4ec7c Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_book_active.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_book_active@2x.png b/Telegram/Resources/icons/folders/folders_local_book_active@2x.png new file mode 100644 index 000000000..b5d858f62 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_book_active@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_book_active@3x.png b/Telegram/Resources/icons/folders/folders_local_book_active@3x.png new file mode 100644 index 000000000..b374cfbdd Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_book_active@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_brackets.png b/Telegram/Resources/icons/folders/folders_local_brackets.png new file mode 100644 index 000000000..43196adfa Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_brackets.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_brackets@2x.png b/Telegram/Resources/icons/folders/folders_local_brackets@2x.png new file mode 100644 index 000000000..2692489e8 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_brackets@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_brackets@3x.png b/Telegram/Resources/icons/folders/folders_local_brackets@3x.png new file mode 100644 index 000000000..aaba28e9d Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_brackets@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_candle.png b/Telegram/Resources/icons/folders/folders_local_candle.png new file mode 100644 index 000000000..dd022cdc6 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_candle.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_candle@2x.png b/Telegram/Resources/icons/folders/folders_local_candle@2x.png new file mode 100644 index 000000000..bb4b4deb6 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_candle@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_candle@3x.png b/Telegram/Resources/icons/folders/folders_local_candle@3x.png new file mode 100644 index 000000000..a5e43ef13 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_candle@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_candle_active.png b/Telegram/Resources/icons/folders/folders_local_candle_active.png new file mode 100644 index 000000000..95f265f14 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_candle_active.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_candle_active@2x.png b/Telegram/Resources/icons/folders/folders_local_candle_active@2x.png new file mode 100644 index 000000000..b30a02514 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_candle_active@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_candle_active@3x.png b/Telegram/Resources/icons/folders/folders_local_candle_active@3x.png new file mode 100644 index 000000000..2b682272a Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_candle_active@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_city.png b/Telegram/Resources/icons/folders/folders_local_city.png new file mode 100644 index 000000000..2353e21c0 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_city.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_city@2x.png b/Telegram/Resources/icons/folders/folders_local_city@2x.png new file mode 100644 index 000000000..3c535a676 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_city@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_city@3x.png b/Telegram/Resources/icons/folders/folders_local_city@3x.png new file mode 100644 index 000000000..d04a586a0 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_city@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_city_active.png b/Telegram/Resources/icons/folders/folders_local_city_active.png new file mode 100644 index 000000000..10dd74501 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_city_active.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_city_active@2x.png b/Telegram/Resources/icons/folders/folders_local_city_active@2x.png new file mode 100644 index 000000000..ad7a92f2a Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_city_active@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_city_active@3x.png b/Telegram/Resources/icons/folders/folders_local_city_active@3x.png new file mode 100644 index 000000000..a1cf0cb52 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_city_active@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_desktop.png b/Telegram/Resources/icons/folders/folders_local_desktop.png new file mode 100644 index 000000000..1363663a6 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_desktop.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_desktop@2x.png b/Telegram/Resources/icons/folders/folders_local_desktop@2x.png new file mode 100644 index 000000000..d0bc8e817 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_desktop@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_desktop@3x.png b/Telegram/Resources/icons/folders/folders_local_desktop@3x.png new file mode 100644 index 000000000..fba4b26af Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_desktop@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_desktop_active.png b/Telegram/Resources/icons/folders/folders_local_desktop_active.png new file mode 100644 index 000000000..2d950f204 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_desktop_active.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_desktop_active@2x.png b/Telegram/Resources/icons/folders/folders_local_desktop_active@2x.png new file mode 100644 index 000000000..cef1aa33d Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_desktop_active@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_desktop_active@3x.png b/Telegram/Resources/icons/folders/folders_local_desktop_active@3x.png new file mode 100644 index 000000000..5e557a80e Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_desktop_active@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_earth.png b/Telegram/Resources/icons/folders/folders_local_earth.png new file mode 100644 index 000000000..086a78f5c Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_earth.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_earth@2x.png b/Telegram/Resources/icons/folders/folders_local_earth@2x.png new file mode 100644 index 000000000..5751b69b9 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_earth@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_earth@3x.png b/Telegram/Resources/icons/folders/folders_local_earth@3x.png new file mode 100644 index 000000000..e7850d6c8 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_earth@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_music.png b/Telegram/Resources/icons/folders/folders_local_music.png new file mode 100644 index 000000000..a83360a24 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_music.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_music@2x.png b/Telegram/Resources/icons/folders/folders_local_music@2x.png new file mode 100644 index 000000000..4abd97472 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_music@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_music@3x.png b/Telegram/Resources/icons/folders/folders_local_music@3x.png new file mode 100644 index 000000000..2cf80be45 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_music@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_music_active.png b/Telegram/Resources/icons/folders/folders_local_music_active.png new file mode 100644 index 000000000..341741cd8 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_music_active.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_music_active@2x.png b/Telegram/Resources/icons/folders/folders_local_music_active@2x.png new file mode 100644 index 000000000..da5334ee9 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_music_active@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_music_active@3x.png b/Telegram/Resources/icons/folders/folders_local_music_active@3x.png new file mode 100644 index 000000000..a5e39118e Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_music_active@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_news.png b/Telegram/Resources/icons/folders/folders_local_news.png new file mode 100644 index 000000000..94a07708c Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_news.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_news@2x.png b/Telegram/Resources/icons/folders/folders_local_news@2x.png new file mode 100644 index 000000000..5582529ba Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_news@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_news@3x.png b/Telegram/Resources/icons/folders/folders_local_news@3x.png new file mode 100644 index 000000000..de901af6d Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_news@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_news_active.png b/Telegram/Resources/icons/folders/folders_local_news_active.png new file mode 100644 index 000000000..1b102cee0 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_news_active.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_news_active@2x.png b/Telegram/Resources/icons/folders/folders_local_news_active@2x.png new file mode 100644 index 000000000..5da39880d Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_news_active@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_news_active@3x.png b/Telegram/Resources/icons/folders/folders_local_news_active@3x.png new file mode 100644 index 000000000..24eb071ca Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_news_active@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_phone.png b/Telegram/Resources/icons/folders/folders_local_phone.png new file mode 100644 index 000000000..f620c7874 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_phone.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_phone@2x.png b/Telegram/Resources/icons/folders/folders_local_phone@2x.png new file mode 100644 index 000000000..1d324e103 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_phone@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_phone@3x.png b/Telegram/Resources/icons/folders/folders_local_phone@3x.png new file mode 100644 index 000000000..9cdde59e1 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_phone@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_phone_active.png b/Telegram/Resources/icons/folders/folders_local_phone_active.png new file mode 100644 index 000000000..5163642a3 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_phone_active.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_phone_active@2x.png b/Telegram/Resources/icons/folders/folders_local_phone_active@2x.png new file mode 100644 index 000000000..cd1dc6460 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_phone_active@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_phone_active@3x.png b/Telegram/Resources/icons/folders/folders_local_phone_active@3x.png new file mode 100644 index 000000000..e0cc8e541 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_phone_active@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_smile.png b/Telegram/Resources/icons/folders/folders_local_smile.png new file mode 100644 index 000000000..88de7ca3b Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_smile.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_smile@2x.png b/Telegram/Resources/icons/folders/folders_local_smile@2x.png new file mode 100644 index 000000000..9685e77f6 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_smile@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_smile@3x.png b/Telegram/Resources/icons/folders/folders_local_smile@3x.png new file mode 100644 index 000000000..21107845c Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_smile@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_smile_active.png b/Telegram/Resources/icons/folders/folders_local_smile_active.png new file mode 100644 index 000000000..b50df0b0c Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_smile_active.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_smile_active@2x.png b/Telegram/Resources/icons/folders/folders_local_smile_active@2x.png new file mode 100644 index 000000000..473bd6a2d Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_smile_active@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_smile_active@3x.png b/Telegram/Resources/icons/folders/folders_local_smile_active@3x.png new file mode 100644 index 000000000..c2fc71a5d Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_smile_active@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_sun.png b/Telegram/Resources/icons/folders/folders_local_sun.png new file mode 100644 index 000000000..a0383a249 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_sun.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_sun@2x.png b/Telegram/Resources/icons/folders/folders_local_sun@2x.png new file mode 100644 index 000000000..5b74e3cf7 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_sun@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_sun@3x.png b/Telegram/Resources/icons/folders/folders_local_sun@3x.png new file mode 100644 index 000000000..2eee38f2d Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_sun@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_sun_active.png b/Telegram/Resources/icons/folders/folders_local_sun_active.png new file mode 100644 index 000000000..664c01d55 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_sun_active.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_sun_active@2x.png b/Telegram/Resources/icons/folders/folders_local_sun_active@2x.png new file mode 100644 index 000000000..909114250 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_sun_active@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_sun_active@3x.png b/Telegram/Resources/icons/folders/folders_local_sun_active@3x.png new file mode 100644 index 000000000..adc2f3057 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_sun_active@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_video.png b/Telegram/Resources/icons/folders/folders_local_video.png new file mode 100644 index 000000000..abe54e5be Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_video.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_video@2x.png b/Telegram/Resources/icons/folders/folders_local_video@2x.png new file mode 100644 index 000000000..1d0abbdfe Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_video@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_video@3x.png b/Telegram/Resources/icons/folders/folders_local_video@3x.png new file mode 100644 index 000000000..b4591851f Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_video@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_video_active.png b/Telegram/Resources/icons/folders/folders_local_video_active.png new file mode 100644 index 000000000..c3cf65deb Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_video_active.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_video_active@2x.png b/Telegram/Resources/icons/folders/folders_local_video_active@2x.png new file mode 100644 index 000000000..f79cde899 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_video_active@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_local_video_active@3x.png b/Telegram/Resources/icons/folders/folders_local_video_active@3x.png new file mode 100644 index 000000000..caaa2b49c Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_local_video_active@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_type_admin.png b/Telegram/Resources/icons/folders/folders_type_admin.png new file mode 100644 index 000000000..e09c3ff5f Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_type_admin.png differ diff --git a/Telegram/Resources/icons/folders/folders_type_admin@2x.png b/Telegram/Resources/icons/folders/folders_type_admin@2x.png new file mode 100644 index 000000000..f8e11bf9a Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_type_admin@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_type_admin@3x.png b/Telegram/Resources/icons/folders/folders_type_admin@3x.png new file mode 100644 index 000000000..e7582d031 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_type_admin@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_type_filtered.png b/Telegram/Resources/icons/folders/folders_type_filtered.png new file mode 100644 index 000000000..83adbe76b Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_type_filtered.png differ diff --git a/Telegram/Resources/icons/folders/folders_type_filtered@2x.png b/Telegram/Resources/icons/folders/folders_type_filtered@2x.png new file mode 100644 index 000000000..805a19dc7 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_type_filtered@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_type_filtered@3x.png b/Telegram/Resources/icons/folders/folders_type_filtered@3x.png new file mode 100644 index 000000000..374222fbd Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_type_filtered@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_type_not_admin.png b/Telegram/Resources/icons/folders/folders_type_not_admin.png new file mode 100644 index 000000000..3716685be Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_type_not_admin.png differ diff --git a/Telegram/Resources/icons/folders/folders_type_not_admin@2x.png b/Telegram/Resources/icons/folders/folders_type_not_admin@2x.png new file mode 100644 index 000000000..3ae1cab7e Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_type_not_admin@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_type_not_admin@3x.png b/Telegram/Resources/icons/folders/folders_type_not_admin@3x.png new file mode 100644 index 000000000..67846f330 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_type_not_admin@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_type_not_owner.png b/Telegram/Resources/icons/folders/folders_type_not_owner.png new file mode 100644 index 000000000..b79afc726 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_type_not_owner.png differ diff --git a/Telegram/Resources/icons/folders/folders_type_not_owner@2x.png b/Telegram/Resources/icons/folders/folders_type_not_owner@2x.png new file mode 100644 index 000000000..7589433b8 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_type_not_owner@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_type_not_owner@3x.png b/Telegram/Resources/icons/folders/folders_type_not_owner@3x.png new file mode 100644 index 000000000..af528028c Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_type_not_owner@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_type_owner.png b/Telegram/Resources/icons/folders/folders_type_owner.png new file mode 100644 index 000000000..8450e7d76 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_type_owner.png differ diff --git a/Telegram/Resources/icons/folders/folders_type_owner@2x.png b/Telegram/Resources/icons/folders/folders_type_owner@2x.png new file mode 100644 index 000000000..03335ba2f Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_type_owner@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_type_owner@3x.png b/Telegram/Resources/icons/folders/folders_type_owner@3x.png new file mode 100644 index 000000000..461122328 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_type_owner@3x.png differ diff --git a/Telegram/Resources/icons/folders/folders_type_recent.png b/Telegram/Resources/icons/folders/folders_type_recent.png new file mode 100644 index 000000000..d54155751 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_type_recent.png differ diff --git a/Telegram/Resources/icons/folders/folders_type_recent@2x.png b/Telegram/Resources/icons/folders/folders_type_recent@2x.png new file mode 100644 index 000000000..e30e54f45 Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_type_recent@2x.png differ diff --git a/Telegram/Resources/icons/folders/folders_type_recent@3x.png b/Telegram/Resources/icons/folders/folders_type_recent@3x.png new file mode 100644 index 000000000..88d8ec74f Binary files /dev/null and b/Telegram/Resources/icons/folders/folders_type_recent@3x.png differ diff --git a/Telegram/Resources/icons/settings/cloud.png b/Telegram/Resources/icons/settings/cloud.png new file mode 100644 index 000000000..a671b324c Binary files /dev/null and b/Telegram/Resources/icons/settings/cloud.png differ diff --git a/Telegram/Resources/icons/settings/cloud@2x.png b/Telegram/Resources/icons/settings/cloud@2x.png new file mode 100644 index 000000000..a2fe0079b Binary files /dev/null and b/Telegram/Resources/icons/settings/cloud@2x.png differ diff --git a/Telegram/Resources/icons/settings/cloud@3x.png b/Telegram/Resources/icons/settings/cloud@3x.png new file mode 100644 index 000000000..3cbc43cf7 Binary files /dev/null and b/Telegram/Resources/icons/settings/cloud@3x.png differ diff --git a/Telegram/Resources/langs/rewrites/en.json b/Telegram/Resources/langs/rewrites/en.json index 0db096ca3..e148b0099 100644 --- a/Telegram/Resources/langs/rewrites/en.json +++ b/Telegram/Resources/langs/rewrites/en.json @@ -79,7 +79,10 @@ "other": "Recent stickers: show {count} stickers" }, "ktg_settings_recent_stickers_limit_none": "Recent stickers: hide all", + "ktg_filters_default": "Default folder", "ktg_filters_context_edit_all": "Edit folders", + "ktg_filters_context_make_default": "Make folder default", + "ktg_filters_context_reset_default": "Reset default folder", "ktg_settings_filters": "Folders", "ktg_settings_filters_only_unmuted_counter": "Do not count muted chats", "ktg_settings_filters_hide_edit": "Hide Edit button", @@ -114,6 +117,22 @@ "ktg_supergroup_id_copied": "Supergroup ID copied to clipboard.", "ktg_channel_id_copied": "Channel ID copied to clipboard.", "ktg_settings_forward": "Forward", + "ktg_filters_exclude_not_owned": "Not owned", + "ktg_filters_exclude_not_admin": "Not administrated", + "ktg_filters_exclude_owned": "Owned", + "ktg_filters_exclude_admin": "Administrated", + "ktg_filters_exclude_not_recent": "Not opened in this session", + "ktg_filters_exclude_filtered": "From other folders", + "ktg_filters_create_cloud": "Create cloud folder", + "ktg_filters_create_local": "Create local folder", + "ktg_filters_description": "Cloud folders are synced between all your Telegram apps, but local folders have more features to offer.", + "ktg_filters_new_cloud": "New cloud folder", + "ktg_filters_new_local": "New local folder", + "ktg_filters_edit_cloud": "Edit cloud folder", + "ktg_filters_edit_local": "Edit local folder", + "ktg_filters_local": "local folder", + "ktg_filters_cloud": "cloud folder", + "ktg_filters_cloud_limit": "Sorry, you can't create more cloud folders. You can create local folder instead.", "ktg_filters_hide_folder": "Hide folder", "ktg_filters_hide_button": "Hide button", "ktg_filters_hide_all_chats_toast": "\"All Chats\" folder is hidden.\nYou can enable it back in Kotatogram Settings.", diff --git a/Telegram/SourceFiles/api/api_chat_filters.cpp b/Telegram/SourceFiles/api/api_chat_filters.cpp index 5cb157b5c..abcaae96b 100644 --- a/Telegram/SourceFiles/api/api_chat_filters.cpp +++ b/Telegram/SourceFiles/api/api_chat_filters.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "api/api_chat_filters.h" +#include "kotato/kotato_settings.h" #include "data/data_session.h" #include "data/data_chat_filters.h" #include "main/main_session.h" @@ -22,11 +23,16 @@ void SaveNewFilterPinned( filterId); auto &filters = session->data().chatsFilters(); const auto &filter = filters.applyUpdatedPinned(filterId, order); - session->api().request(MTPmessages_UpdateDialogFilter( - MTP_flags(MTPmessages_UpdateDialogFilter::Flag::f_filter), - MTP_int(filterId), - filter.tl() - )).send(); + if (filter.isLocal()) { + filters.saveLocal(); + Kotato::JsonSettings::Write(); + } else { + session->api().request(MTPmessages_UpdateDialogFilter( + MTP_flags(MTPmessages_UpdateDialogFilter::Flag::f_filter), + MTP_int(filterId), + filter.tl() + )).send(); + } } diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp index 175dbeb08..4ae08c8d5 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp @@ -7,11 +7,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/filters/edit_filter_box.h" +#include "kotato/kotato_lang.h" +#include "kotato/kotato_settings.h" #include "boxes/filters/edit_filter_chats_list.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/text/text_options.h" +#include "ui/widgets/checkbox.h" #include "ui/widgets/buttons.h" #include "ui/widgets/input_fields.h" #include "ui/effects/panel_animation.h" @@ -28,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "history/history.h" #include "main/main_session.h" +#include "main/main_account.h" #include "window/window_session_controller.h" #include "window/window_controller.h" #include "apiwrap.h" @@ -56,6 +60,12 @@ constexpr auto kAllTypes = { Flag::NoMuted, Flag::NoRead, Flag::NoArchived, + Flag::Owned, + Flag::Admin, + Flag::NotOwned, + Flag::NotAdmin, + Flag::Recent, + Flag::NoFilter, }; class FilterChatsPreview final : public Ui::RpWidget { @@ -129,7 +139,9 @@ not_null SetupChatsPreview( (rules.flags() & ~flag), rules.always(), rules.pinned(), - rules.never()); + rules.never(), + rules.isDefault(), + rules.isLocal()); updateDefaultTitle(computed); *data = std::move(computed); }, preview->lifetime()); @@ -150,7 +162,9 @@ not_null SetupChatsPreview( rules.flags(), std::move(always), std::move(pinned), - std::move(never)); + std::move(never), + rules.isDefault(), + rules.isLocal()); updateDefaultTitle(computed); *data = std::move(computed); }, preview->lifetime()); @@ -335,7 +349,8 @@ void EditExceptions( : tr::lng_filters_exclude_title()), options, rules.flags() & options, - include ? rules.always() : rules.never()); + include ? rules.always() : rules.never(), + rules.isLocal()); const auto rawController = controller.get(); auto initBox = [=](not_null box) { box->setCloseByOutsideClick(false); @@ -369,7 +384,9 @@ void EditExceptions( | rawController->chosenOptions()), include ? std::move(changed) : std::move(removeFrom), std::move(pinned), - include ? std::move(removeFrom) : std::move(changed)); + include ? std::move(removeFrom) : std::move(changed), + rules.isDefault(), + rules.isLocal()); updateDefaultTitle(computed); *data = computed; refresh(); @@ -423,7 +440,8 @@ void CreateIconSelector( }, toggle->lifetime()); const auto panel = toggle->lifetime().make_state( - outer); + outer, + rules.isLocal()); toggle->installEventFilter(panel); toggle->addClickHandler([=] { panel->toggleAnimated(); @@ -441,7 +459,9 @@ void CreateIconSelector( rules.flags(), rules.always(), rules.pinned(), - rules.never()); + rules.never(), + rules.isDefault(), + rules.isLocal()); }, panel->lifetime()); const auto updatePanelGeometry = [=] { @@ -510,7 +530,14 @@ void EditFilterBox( Fn doneCallback) { const auto creating = filter.title().isEmpty(); box->setWidth(st::boxWideWidth); - box->setTitle(creating ? tr::lng_filters_new() : tr::lng_filters_edit()); + const auto isLocal = filter.isLocal(); + box->setTitle(rpl::single(creating + ? (isLocal + ? ktr("ktg_filters_new_local") + : ktr("ktg_filters_new_cloud")) + : (isLocal + ? ktr("ktg_filters_edit_local") + : ktr("ktg_filters_edit_cloud")))); box->setCloseByOutsideClick(false); Data::AmPremiumValue( @@ -530,10 +557,12 @@ void EditFilterBox( tr::lng_filters_new_name(), filter.title()), st::markdownLinkFieldPadding); - name->setMaxLength(kMaxFilterTitleLength); name->setInstantReplaces(Ui::InstantReplaces::Default()); name->setInstantReplacesEnabled( Core::App().settings().replaceEmojiValue()); + if (!isLocal) { + name->setMaxLength(kMaxFilterTitleLength); + } Ui::Emoji::SuggestionsController::Init( box->getDelegate()->outerContainer(), name, @@ -551,7 +580,9 @@ void EditFilterBox( if (nameEditing->custom) { return; } - const auto title = TrimDefaultTitle(DefaultTitle(filter)); + const auto title = isLocal + ? DefaultTitle(filter) + : TrimDefaultTitle(DefaultTitle(filter)); if (nameEditing->field->getLastText() != title) { nameEditing->settingDefault = true; nameEditing->field->setText(title); @@ -575,11 +606,32 @@ void EditFilterBox( constexpr auto kExcludeTypes = Flag::NoMuted | Flag::NoArchived | Flag::NoRead; + constexpr auto kExcludeTypesLocal = kExcludeTypes + | Flag::Owned + | Flag::Admin + | Flag::NotOwned + | Flag::NotAdmin + | Flag::Recent + | Flag::NoFilter; box->setFocusCallback([=] { name->setFocusFast(); }); + const auto defaultFilterId = window->session().account().defaultFilterId(); + const auto isCurrent = filter.id() == defaultFilterId; + const auto checkboxDefault = content->add( + object_ptr( + box, + ktr("ktg_filters_default"), + (creating ? false : isCurrent), + st::defaultBoxCheckbox), + style::margins( + st::boxPadding.left(), + st::boxPadding.bottom(), + st::boxPadding.right(), + st::boxPadding.bottom())); + AddSkip(content); AddDivider(content); AddSkip(content); @@ -614,7 +666,7 @@ void EditFilterBox( content, data, updateDefaultTitle, - kExcludeTypes, + (isLocal ? kExcludeTypesLocal : kExcludeTypes), &Data::ChatFilter::never); AddSkip(content); @@ -625,7 +677,7 @@ void EditFilterBox( data->current().flags() & kTypes, data->current().always()); exclude->updateData( - data->current().flags() & kExcludeTypes, + data->current().flags() & (isLocal ? kExcludeTypesLocal : kExcludeTypes), data->current().never()); }; includeAdd->setClickedCallback([=] { @@ -641,7 +693,7 @@ void EditFilterBox( EditExceptions( window, box, - kExcludeTypes, + (isLocal ? kExcludeTypesLocal : kExcludeTypes), data, updateDefaultTitle, refreshPreviews); @@ -650,6 +702,7 @@ void EditFilterBox( const auto save = [=] { const auto title = name->getLastText().trimmed(); const auto rules = data->current(); + const auto checked = checkboxDefault && checkboxDefault->checked(); const auto result = Data::ChatFilter( rules.id(), title, @@ -657,7 +710,9 @@ void EditFilterBox( rules.flags(), rules.always(), rules.pinned(), - rules.never()); + rules.never(), + checked, + isLocal); if (title.isEmpty()) { name->showError(); return; @@ -686,24 +741,42 @@ void EditExistingFilter( Expects(id != 0); const auto session = &window->session(); - const auto &list = session->data().chatsFilters().list(); + const auto filters = &session->data().chatsFilters(); + const auto &list = filters->list(); const auto i = ranges::find(list, id, &Data::ChatFilter::id); if (i == end(list)) { return; } const auto doneCallback = [=](const Data::ChatFilter &result) { Expects(id == result.id()); + auto needSave = false; - const auto tl = result.tl(); - session->data().chatsFilters().apply(MTP_updateDialogFilter( - MTP_flags(MTPDupdateDialogFilter::Flag::f_filter), - MTP_int(id), - tl)); - session->api().request(MTPmessages_UpdateDialogFilter( - MTP_flags(MTPmessages_UpdateDialogFilter::Flag::f_filter), - MTP_int(id), - tl - )).send(); + if (result.isLocal()) { + filters->set(result); + filters->saveLocal(); + needSave = true; + } else { + const auto tl = result.tl(); + session->data().chatsFilters().apply(MTP_updateDialogFilter( + MTP_flags(MTPDupdateDialogFilter::Flag::f_filter), + MTP_int(id), + tl)); + session->api().request(MTPmessages_UpdateDialogFilter( + MTP_flags(MTPmessages_UpdateDialogFilter::Flag::f_filter), + MTP_int(id), + tl + )).send(); + } + const auto defaultFilterId = session->account().defaultFilterId(); + const auto isCurrentDefault = result.id() == defaultFilterId; + if ((isCurrentDefault && !result.isDefault()) + || (!isCurrentDefault && result.isDefault())) { + session->account().setDefaultFilterId(result.isDefault() ? result.id() : 0); + needSave = true; + } + if (needSave) { + Kotato::JsonSettings::Write(); + } }; window->window().show(Box( EditFilterBox, diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp index 32c3aa1cb..6b62d2c1a 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/filters/edit_filter_chats_list.h" +#include "kotato/kotato_lang.h" #include "history/history.h" #include "window/window_session_controller.h" #include "boxes/premium_limits_box.h" @@ -35,6 +36,12 @@ constexpr auto kAllTypes = { Flag::NoMuted, Flag::NoRead, Flag::NoArchived, + Flag::Owned, + Flag::Admin, + Flag::NotOwned, + Flag::NotAdmin, + Flag::Recent, + Flag::NoFilter, }; struct RowSelectionChange { @@ -131,7 +138,7 @@ PaintRoundImageCallback TypeRow::generatePaintUserpicCallback() { } Flag TypeRow::flag() const { - return static_cast(id() & 0xFF); + return static_cast(id() & 0xFFFF); } ExceptionRow::ExceptionRow(not_null history) : Row(history) { @@ -236,6 +243,12 @@ auto TypeController::rowSelectionChanges() const 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); + case Flag::Owned: return ktr("ktg_filters_exclude_not_owned"); + case Flag::Admin: return ktr("ktg_filters_exclude_not_admin"); + case Flag::NotOwned: return ktr("ktg_filters_exclude_owned"); + case Flag::NotAdmin: return ktr("ktg_filters_exclude_admin"); + case Flag::Recent: return ktr("ktg_filters_exclude_not_recent"); + case Flag::NoFilter: return ktr("ktg_filters_exclude_filtered"); } Unexpected("Flag in TypeName."); } @@ -257,6 +270,12 @@ void PaintFilterChatsTypeIcon( case Flag::NoMuted: return st::historyPeer6UserpicBg; case Flag::NoArchived: return st::historyPeer4UserpicBg; case Flag::NoRead: return st::historyPeer7UserpicBg; + case Flag::Owned: return st::historyPeer2UserpicBg; + case Flag::Admin: return st::historyPeer3UserpicBg; + case Flag::NotOwned: return st::historyPeer2UserpicBg; + case Flag::NotAdmin: return st::historyPeer3UserpicBg; + case Flag::Recent: return st::historyPeer6UserpicBg; + case Flag::NoFilter: return st::historyPeer7UserpicBg; } Unexpected("Flag in color paintFlagIcon."); }(); @@ -270,6 +289,12 @@ void PaintFilterChatsTypeIcon( case Flag::NoMuted: return st::windowFilterTypeNoMuted; case Flag::NoArchived: return st::windowFilterTypeNoArchived; case Flag::NoRead: return st::windowFilterTypeNoRead; + case Flag::Owned: return st::windowFilterTypeOwned; + case Flag::Admin: return st::windowFilterTypeAdmin; + case Flag::NotOwned: return st::windowFilterTypeNotOwned; + case Flag::NotAdmin: return st::windowFilterTypeNotAdmin; + case Flag::Recent: return st::windowFilterTypeRecent; + case Flag::NoFilter: return st::windowFilterTypeNoFilter; } Unexpected("Flag in icon paintFlagIcon."); }(); @@ -332,14 +357,16 @@ EditFilterChatsListController::EditFilterChatsListController( rpl::producer title, Flags options, Flags selected, - const base::flat_set> &peers) + const base::flat_set> &peers, + bool isLocal) : ChatsListBoxController(session) , _session(session) , _title(std::move(title)) , _peers(peers) , _options(options) , _selected(selected) -, _limit(Limit(session)) { +, _limit(Limit(session)) +, _isLocal(isLocal) { } Main::Session &EditFilterChatsListController::session() const { @@ -361,7 +388,7 @@ int EditFilterChatsListController::selectedTypesCount() const { void EditFilterChatsListController::rowClicked(not_null row) { const auto count = delegate()->peerListSelectedRowsCount() - selectedTypesCount(); - if (count < _limit || row->checked()) { + if (count < _limit || row->checked() || _isLocal) { delegate()->peerListSetRowChecked(row, !row->checked()); updateTitle(); } else { @@ -479,6 +506,8 @@ auto EditFilterChatsListController::createRow(not_null history) void EditFilterChatsListController::updateTitle() { const auto count = delegate()->peerListSelectedRowsCount() - selectedTypesCount(); - const auto additional = qsl("%1 / %2").arg(count).arg(_limit); + const auto additional = _isLocal + ? tr::lng_filters_chats_count(tr::now, lt_count_short, count) + : qsl("%1 / %2").arg(count).arg(_limit); delegate()->peerListSetAdditionalTitle(rpl::single(additional)); } diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h index 5302480c3..f37d037a5 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h @@ -49,7 +49,8 @@ public: rpl::producer title, Flags options, Flags selected, - const base::flat_set> &peers); + const base::flat_set> &peers, + bool isLocal); [[nodiscard]] Main::Session &session() const override; [[nodiscard]] Flags chosenOptions() const { @@ -75,6 +76,7 @@ private: Flags _options; Flags _selected; int _limit = 0; + bool _isLocal; Fn _deselectOption; diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index d77b47c02..aada9637f 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_chat_filters.h" +#include "kotato/kotato_settings.h" +#include "boxes/premium_limits_box.h" #include "history/history.h" #include "data/data_peer.h" #include "data/data_user.h" @@ -15,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_folder.h" #include "data/data_histories.h" +#include "data/data_premium_limits.h" #include "dialogs/dialogs_main_list.h" #include "history/history.h" #include "history/history_unread_things.h" @@ -22,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "main/main_account.h" #include "main/main_app_config.h" +#include "mainwidget.h" #include "apiwrap.h" namespace Data { @@ -31,8 +35,200 @@ constexpr auto kRefreshSuggestedTimeout = 7200 * crl::time(1000); constexpr auto kLoadExceptionsAfter = 100; constexpr auto kLoadExceptionsPerRequest = 100; +const std::map LocalFolderSettingsFlags { + { ChatFilter::Flag::Contacts, qsl("include_contacts") }, + { ChatFilter::Flag::NonContacts, qsl("include_non_contacts") }, + { ChatFilter::Flag::Groups, qsl("include_groups") }, + { ChatFilter::Flag::Channels, qsl("include_channels") }, + { ChatFilter::Flag::Bots, qsl("include_bots") }, + { ChatFilter::Flag::NoMuted, qsl("exclude_muted") }, + { ChatFilter::Flag::NoRead, qsl("exclude_read") }, + { ChatFilter::Flag::NoArchived, qsl("exclude_archived") }, + { ChatFilter::Flag::Owned, qsl("exclude_not_owned") }, + { ChatFilter::Flag::Admin, qsl("exclude_not_admin") }, + { ChatFilter::Flag::NotOwned, qsl("exclude_owned") }, + { ChatFilter::Flag::NotAdmin, qsl("exclude_admin") }, + { ChatFilter::Flag::Recent, qsl("exclude_non_recent") }, + { ChatFilter::Flag::NoFilter, qsl("exclude_filtered") }, +}; + +bool ReadOption(QJsonObject obj, QString key, std::function callback) { + const auto it = obj.constFind(key); + if (it == obj.constEnd()) { + return false; + } + callback(*it); + return true; +} + +bool ReadStringOption(QJsonObject obj, QString key, std::function callback) { + auto readResult = false; + auto readValueResult = ReadOption(obj, key, [&](QJsonValue v) { + if (v.isString()) { + callback(v.toString()); + readResult = true; + } + }); + return (readValueResult && readResult); +} + +bool ReadIntOption(QJsonObject obj, QString key, std::function callback) { + auto readResult = false; + auto readValueResult = ReadOption(obj, key, [&](QJsonValue v) { + if (v.isDouble()) { + callback(v.toInt()); + readResult = true; + } + }); + return (readValueResult && readResult); +} + +bool ReadArrayOption(QJsonObject obj, QString key, std::function callback) { + auto readResult = false; + auto readValueResult = ReadOption(obj, key, [&](QJsonValue v) { + if (v.isArray()) { + callback(v.toArray()); + readResult = true; + } + }); + return (readValueResult && readResult); +} + + } // namespace +QJsonObject LocalFolder::toJson() { + auto folderObject = QJsonObject(); + + folderObject.insert(qsl("id"), id); + folderObject.insert(qsl("order"), cloudOrder); + folderObject.insert(qsl("name"), name); + folderObject.insert(qsl("emoticon"), emoticon); + + for (const auto &[flag, option] : LocalFolderSettingsFlags) { + if (flags & flag) { + folderObject.insert(option, true); + } + } + + const auto peerToStr = [](uint64 peer) { + auto peerId = PeerId(peer); + return (peerIsChannel(peerId)) + ? qsl("channel") + : (peerIsChat(peerId)) + ? qsl("chat") + : qsl("user"); + }; + + const auto peerToLocalBare = [](uint64 peer) { + auto peerId = PeerId(peer); + return QString::number((peerIsChannel(peerId)) + ? peerToChannel(peerId).bare + : (peerIsChat(peerId)) + ? peerToChat(peerId).bare + : peerToUser(peerId).bare); + }; + + const auto fillChatsArray = [peerToStr, peerToLocalBare] (const std::vector &chats) -> QJsonArray { + auto result = QJsonArray(); + for (auto peer : chats) { + auto peerObj = QJsonObject(); + peerObj.insert(qsl("type"), peerToStr(peer)); + peerObj.insert(qsl("id"), peerToLocalBare(peer)); + result << peerObj; + } + return result; + }; + + folderObject.insert(qsl("never"), fillChatsArray(never)); + folderObject.insert(qsl("pinned"), fillChatsArray(pinned)); + folderObject.insert(qsl("always"), fillChatsArray(always)); + + return folderObject; +} + +LocalFolder MakeLocalFolder(const QJsonObject &obj) { + auto result = LocalFolder(); + + ReadIntOption(obj, "id", [&](auto v) { + result.id = v; + }); + + ReadIntOption(obj, "order", [&](auto v) { + result.cloudOrder = v; + }); + + ReadStringOption(obj, "name", [&](auto v) { + result.name = v; + }); + + ReadStringOption(obj, "emoticon", [&](auto v) { + result.emoticon = v; + }); + + for (const auto &[flag, option] : LocalFolderSettingsFlags) { + const auto it = obj.constFind(option); + if (it != obj.constEnd()) { + const auto v = *it; + if (v.isBool() && v.toBool()) { + result.flags |= flag; + } + } + } + + const auto readChatsArray = [obj] (const QString &key, std::vector &chats) { + ReadArrayOption(obj, key, [&](auto a) { + for (auto i = a.constBegin(), e = a.constEnd(); i != e; ++i) { + if (!(*i).isObject()) { + continue; + } + + auto peer = (*i).toObject(); + BareId peerId = 0; + + auto isPeerIdRead = ReadIntOption(peer, "id", [&](auto v) { + peerId = v; + }); + + if (!isPeerIdRead) { + isPeerIdRead = ReadStringOption(peer, "id", [&](auto v) { + peerId = static_cast(v.toLongLong()); + }); + } + + if (peerId == 0 || !isPeerIdRead) { + continue; + } + + auto isPeerTypeRead = ReadStringOption(peer, "type", [&](auto v) { + peerId = (QString::compare(v.toLower(), "channel") == 0) + ? peerFromChannel(ChannelId(peerId)).value + : (QString::compare(v.toLower(), "chat") == 0) + ? peerFromChat(ChatId(peerId)).value + : peerFromUser(UserId(peerId)).value; + }); + + if (!isPeerTypeRead) { + peerId = peerFromUser(UserId(peerId)).value; + } + + chats.push_back(peerId); + } + }); + }; + + readChatsArray(qsl("never"), result.never); + readChatsArray(qsl("pinned"), result.pinned); + readChatsArray(qsl("always"), result.always); + + return result; +} + +ChatFilter::ChatFilter(FilterId id, bool isLocal) +: _id(id) +, _isLocal(isLocal) { +} + ChatFilter::ChatFilter( FilterId id, const QString &title, @@ -40,19 +236,78 @@ ChatFilter::ChatFilter( Flags flags, base::flat_set> always, std::vector> pinned, - base::flat_set> never) + base::flat_set> never, + bool isDefault, + bool isLocal, + int cloudLocalOrder) : _id(id) , _title(title) , _iconEmoji(iconEmoji) , _always(std::move(always)) , _pinned(std::move(pinned)) , _never(std::move(never)) -, _flags(flags) { +, _flags(flags) +, _isDefault(isDefault) +, _isLocal(isLocal) +, _cloudLocalOrder(cloudLocalOrder) { +} + +ChatFilter ChatFilter::local( + const LocalFolder &data, + not_null owner) { + auto &&to_histories = ranges::view::transform([&]( + const uint64 &filterPeer) { + PeerData *peer = nullptr; + auto peerId = PeerId(filterPeer); + + if (peerIsUser(peerId)) { + const auto user = owner->user(peerToUser(peerId).bare); + peer = (PeerData *)user; + } else if (peerIsChat(peerId)) { + const auto chat = owner->chat(peerToChat(peerId).bare); + peer = (PeerData *)chat; + } else if (peerIsChannel(peerId)) { + const auto channel = owner->channel(peerToChannel(peerId).bare); + peer = (PeerData *)channel; + } + return peer ? owner->history(peer).get() : nullptr; + }) | ranges::view::filter([](History *history) { + return history != nullptr; + }) | ranges::view::transform([](History *history) { + return not_null(history); + }); + auto &&always = ranges::view::concat( + data.always + ) | to_histories; + auto pinned = ranges::view::all( + data.pinned + ) | to_histories | ranges::to_vector; + auto &&never = ranges::view::all( + data.never + ) | to_histories; + auto &&all = ranges::view::concat(always, pinned); + auto list = base::flat_set>{ + all.begin(), + all.end() + }; + const auto defaultFilterId = owner->session().account().defaultFilterId(); + return ChatFilter( + data.id, + data.name, + data.emoticon, + data.flags, + std::move(list), + std::move(pinned), + { never.begin(), never.end() }, + (data.id == defaultFilterId), + true, + data.cloudOrder); } ChatFilter ChatFilter::FromTL( const MTPDialogFilter &data, - not_null owner) { + not_null owner, + bool isLocal) { return data.match([&](const MTPDdialogFilter &data) { const auto flags = (data.is_contacts() ? Flag::Contacts : Flag(0)) | (data.is_non_contacts() ? Flag::NonContacts : Flag(0)) @@ -99,6 +354,7 @@ ChatFilter ChatFilter::FromTL( all.begin(), all.end() }; + const auto defaultFilterId = owner->session().account().defaultFilterId(); return ChatFilter( data.vid().v, qs(data.vtitle()), @@ -106,7 +362,9 @@ ChatFilter ChatFilter::FromTL( flags, std::move(list), std::move(pinned), - { never.begin(), never.end() }); + { never.begin(), never.end() }, + (data.vid().v == defaultFilterId), + isLocal); }, [](const MTPDdialogFilterDefault &d) { return ChatFilter(); }); @@ -152,6 +410,39 @@ MTPDialogFilter ChatFilter::tl(FilterId replaceId) const { MTP_vector(never)); } +LocalFolder ChatFilter::toLocal(FilterId replaceId) const { + auto always = _always; + auto pinned = std::vector(); + pinned.reserve(_pinned.size()); + for (const auto &history : _pinned) { + const auto &peer = history->peer; + pinned.push_back(peer->id.value); + always.remove(history); + } + auto include = std::vector(); + include.reserve(always.size()); + for (const auto &history : always) { + const auto &peer = history->peer; + include.push_back(peer->id.value); + } + auto never = std::vector(); + never.reserve(_never.size()); + for (const auto &history : _never) { + const auto &peer = history->peer; + never.push_back(peer->id.value); + } + return { + .id = replaceId ? replaceId : _id, + .cloudOrder = _cloudLocalOrder, + .name = _title, + .emoticon = _iconEmoji, + .always = include, + .never = never, + .pinned = pinned, + .flags = _flags + }; +} + FilterId ChatFilter::id() const { return _id; } @@ -160,6 +451,10 @@ QString ChatFilter::title() const { return _title; } +bool ChatFilter::isDefault() const { + return _isDefault; +} + QString ChatFilter::iconEmoji() const { return _iconEmoji; } @@ -181,6 +476,9 @@ const base::flat_set> &ChatFilter::never() const { } bool ChatFilter::contains(not_null history) const { + if (_never.contains(history)) { + return false; + } const auto flag = [&] { const auto peer = history->peer; if (const auto user = peer->asUser()) { @@ -201,11 +499,74 @@ bool ChatFilter::contains(not_null history) const { Unexpected("Peer type in ChatFilter::contains."); } }(); - if (_never.contains(history)) { + const auto filterAdmin = [&] { + if (!(_flags & Flag::Owned) + && !(_flags & Flag::NotOwned) + && !(_flags & Flag::Admin) + && !(_flags & Flag::NotAdmin)) { + return true; + } + + const auto peer = history->peer; + if (const auto chat = peer->asChat()) { + // if i created the chat: + // // if the filter excludes owned chats, don't add in list + // // if the filter excludes non-admin chats, add only if filter includes owned chats + // else if i am admin in chat: + // // if the filter excludes admin chats, don't add in list + // // if the filter excludes non-owned chats, add only if filter includes admin chats + // else: + // // add in list only if filter doesn't exclude non-owned or non-admin chats + if (chat->amCreator()) { + return !(_flags & Flag::NotOwned) && ((_flags & Flag::Admin) + ? (_flags & Flag::Owned) + : true); + } else if (chat->hasAdminRights()) { + return !(_flags & Flag::NotAdmin) && ((_flags & Flag::Owned) + ? (_flags & Flag::Admin) + : true); + } else { + return !(_flags & Flag::Owned) && !(_flags & Flag::Admin); + } + } else if (const auto channel = peer->asChannel()) { + if (channel->amCreator()) { + return !(_flags & Flag::NotOwned) && ((_flags & Flag::Admin) + ? (_flags & Flag::Owned) + : true); + } else if (channel->hasAdminRights()) { + return !(_flags & Flag::NotAdmin) && ((_flags & Flag::Owned) + ? (_flags & Flag::Admin) + : true); + } else { + return !(_flags & Flag::Owned) && !(_flags & Flag::Admin); + } + } + return false; - } + }; + const auto filterUnfiltered = [&] { + if (!(_flags & Flag::NoFilter)) { + return true; + } + + const auto &list = history->owner().chatsFilters().list(); + for (auto filter : list) { + if (filter.id() == _id) { + continue; + } + + if (filter.contains(history)) { + return false; + } + } + + return true; + }; return false || ((_flags & flag) + && filterAdmin() + && (!(_flags & Flag::Recent) + || history->owner().session().account().isRecent(history->peer->id)) && (!(_flags & Flag::NoMuted) || !history->mute() || (history->unreadMentions().has() @@ -217,10 +578,15 @@ bool ChatFilter::contains(not_null history) const { || history->unreadMentions().has() || history->fakeUnreadWhileOpened()) && (!(_flags & Flag::NoArchived) - || (history->folderKnown() && !history->folder()))) + || (history->folderKnown() && !history->folder())) + && filterUnfiltered()) || _always.contains(history); } +bool ChatFilter::isLocal() const { + return _isLocal; +} + ChatFilters::ChatFilters(not_null owner) : _owner(owner) { _list.emplace_back(); crl::on_main(&owner->session(), [=] { load(); }); @@ -274,10 +640,16 @@ void ChatFilters::load(bool force) { } void ChatFilters::received(const QVector &list) { + const auto account = &_owner->session().account(); + const auto accountId = account->session().userId().bare; + const auto isTestAccount = account->mtp().isTestMode(); + auto localFilters = ::Kotato::JsonSettings::GetJsonArray("folders/local", accountId, isTestAccount); + const auto limit = Data::PremiumLimits(&_owner->session()).dialogFiltersCurrent(); auto position = 0; + auto originalPosition = 0; auto changed = false; - for (const auto &filter : list) { - auto parsed = ChatFilter::FromTL(filter, _owner); + + auto addToList = [&] (ChatFilter parsed) { const auto b = begin(_list) + position, e = end(_list); const auto i = ranges::find(b, e, parsed.id(), &ChatFilter::id); if (i == e) { @@ -293,7 +665,62 @@ void ChatFilters::received(const QVector &list) { changed = true; } ++position; + }; + + // First we're ensuring that IDs are correct + auto leastLocalId = limit; + for (auto localFilter : localFilters) { + auto local = localFilter.toObject(); + if (leastLocalId > local.value("id").toInt()) { + leastLocalId = local.value("id").toInt(); + } } + if (leastLocalId < limit) { + const auto diff = limit - leastLocalId; + auto localFolders = QJsonArray(); + for (auto localFilter : localFilters) { + auto local = localFilter.toObject(); + local.insert("id", local.value("id").toInt() + diff); + localFolders << local; + } + ::Kotato::JsonSettings::Set("folders/local", localFolders, accountId, isTestAccount); + ::Kotato::JsonSettings::Write(); + } + + // Now we're adding cloud filters and corresponding local filters. + for (const auto &filter : list) { + addToList(ChatFilter::FromTL(filter, _owner)); + for (const auto &localFilter : localFilters) { + auto local = MakeLocalFolder(localFilter.toObject()); + if (local.cloudOrder != originalPosition) { + continue; + } + addToList(ChatFilter::local(local, _owner)); + } + ++originalPosition; + } + + // Then we adding local filters, retaining cloud order + while (originalPosition < limit) { + for (const auto &localFilter : localFilters) { + auto local = MakeLocalFolder(localFilter.toObject()); + if (local.cloudOrder != originalPosition) { + continue; + } + addToList(ChatFilter::local(local, _owner)); + } + ++originalPosition; + } + + // And finally we adding other filters + for (const auto &localFilter : localFilters) { + auto local = MakeLocalFolder(localFilter.toObject()); + if (local.cloudOrder < limit) { + continue; + } + addToList(ChatFilter::local(local, _owner)); + } + while (position < _list.size()) { applyRemove(position); changed = true; @@ -345,7 +772,7 @@ void ChatFilters::applyInsert(ChatFilter filter, int position) { _list.insert( begin(_list) + position, - ChatFilter(filter.id(), {}, {}, {}, {}, {}, {})); + ChatFilter(filter.id(), {}, {}, {}, {}, {}, {}, false, filter.isLocal())); applyChange(*(begin(_list) + position), std::move(filter)); } @@ -482,12 +909,13 @@ const ChatFilter &ChatFilters::applyUpdatedPinned( if (const auto history = row.history()) { if (always.contains(history)) { pinned.push_back(history); - } else if (always.size() < limit) { + } else if (always.size() < limit || i->isLocal()) { always.insert(history); pinned.push_back(history); } } } + const auto defaultFilterId = _owner->session().account().defaultFilterId(); set(ChatFilter( id, i->title(), @@ -495,7 +923,9 @@ const ChatFilter &ChatFilters::applyUpdatedPinned( i->flags(), std::move(always), std::move(pinned), - i->never())); + i->never(), + (id == defaultFilterId), + i->isLocal())); return *i; } @@ -507,18 +937,34 @@ void ChatFilters::saveOrder( } const auto api = &_owner->session().api(); api->request(_saveOrderRequestId).cancel(); + const auto limit = Data::PremiumLimits(&_owner->session()).dialogFiltersCurrent(); auto ids = QVector(); ids.reserve(order.size()); + auto cloudIds = QVector(); + cloudIds.reserve(limit); + for (const auto id : order) { ids.push_back(MTP_int(id)); + + auto i = ranges::find(_list, id, &ChatFilter::id); + Assert(i != end(_list)); + + if ((*i).isLocal()) { + i->setLocalCloudOrder(cloudIds.size()); + } else { + cloudIds.push_back(MTP_int(id)); + } } const auto wrapped = MTP_vector(ids); - apply(MTP_updateDialogFilterOrder(wrapped)); - _saveOrderRequestId = api->request(MTPmessages_UpdateDialogFiltersOrder( - wrapped - )).afterRequest(_saveOrderAfterId).send(); + + if (!cloudIds.isEmpty()) { + const auto cloudWrapped = MTP_vector(cloudIds); + _saveOrderRequestId = api->request(MTPmessages_UpdateDialogFiltersOrder( + cloudWrapped + )).afterRequest(_saveOrderAfterId).send(); + } } bool ChatFilters::archiveNeeded() const { @@ -666,4 +1112,20 @@ rpl::producer<> ChatFilters::suggestedUpdated() const { return _suggestedUpdated.events(); } +void ChatFilters::saveLocal() { + auto localFolders = QJsonArray(); + const auto account = &_owner->session().account(); + const auto accountId = account->session().userId().bare; + const auto isTestAccount = account->mtp().isTestMode(); + + for (const auto &folder : _list) { + if (folder.isLocal()) { + localFolders << folder.toLocal().toJson(); + } + } + + ::Kotato::JsonSettings::Set("folders/local", localFolders, accountId, isTestAccount); + ::Kotato::JsonSettings::Write(); +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_chat_filters.h b/Telegram/SourceFiles/data/data_chat_filters.h index b4637e0c2..b212ab9de 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.h +++ b/Telegram/SourceFiles/data/data_chat_filters.h @@ -19,10 +19,11 @@ class Key; namespace Data { class Session; +struct LocalFolder; class ChatFilter final { public: - enum class Flag : uchar { + enum class Flag : ushort { Contacts = 0x01, NonContacts = 0x02, Groups = 0x04, @@ -31,11 +32,20 @@ public: NoMuted = 0x20, NoRead = 0x40, NoArchived = 0x80, + + // Local flags + Owned = 0x0100, + Admin = 0x0200, + NotOwned = 0x0400, + NotAdmin = 0x0800, + Recent = 0x1000, + NoFilter = 0x2000, }; friend constexpr inline bool is_flag_type(Flag) { return true; }; using Flags = base::flags; ChatFilter() = default; + ChatFilter(FilterId id, bool isLocal = false); ChatFilter( FilterId id, const QString &title, @@ -43,15 +53,25 @@ public: Flags flags, base::flat_set> always, std::vector> pinned, - base::flat_set> never); + base::flat_set> never, + bool isDefault = false, + bool isLocal = false, + int localCloudOrder = 0); + + [[nodiscard]] static ChatFilter local( + const LocalFolder &data, + not_null owner); [[nodiscard]] static ChatFilter FromTL( const MTPDialogFilter &data, - not_null owner); + not_null owner, + bool isLocal = false); [[nodiscard]] MTPDialogFilter tl(FilterId replaceId = 0) const; + [[nodiscard]] LocalFolder toLocal(FilterId replaceId = 0) const; [[nodiscard]] FilterId id() const; [[nodiscard]] QString title() const; + [[nodiscard]] bool isDefault() const; [[nodiscard]] QString iconEmoji() const; [[nodiscard]] Flags flags() const; [[nodiscard]] const base::flat_set> &always() const; @@ -60,6 +80,12 @@ public: [[nodiscard]] bool contains(not_null history) const; + [[nodiscard]] bool isLocal() const; + + void setLocalCloudOrder(int order) { + _cloudLocalOrder = order; + } + private: FilterId _id = 0; QString _title; @@ -68,6 +94,9 @@ private: std::vector> _pinned; base::flat_set> _never; Flags _flags; + bool _isDefault = false; + bool _isLocal = false; + int _cloudLocalOrder = 0; }; @@ -129,6 +158,8 @@ public: -> const std::vector &; [[nodiscard]] rpl::producer<> suggestedUpdated() const; + void saveLocal(); + private: void load(bool force); void received(const QVector &list); @@ -157,4 +188,19 @@ private: }; +struct LocalFolder { + QJsonObject toJson(); + + int id = 0; + int cloudOrder = 0; + QString name; + QString emoticon; + std::vector always; + std::vector never; + std::vector pinned; + ChatFilter::Flags flags = Data::ChatFilter::Flags(0); +}; + +LocalFolder MakeLocalFolder(const QJsonObject &obj); + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index e74136f0b..ea531190c 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -1863,7 +1863,9 @@ int Session::pinnedCanPin( const auto i = ranges::find(list, filterId, &Data::ChatFilter::id); return (i == end(list)) || (i->always().contains(history)) - || (i->always().size() < pinnedChatsLimit(folder, filterId)); + || (i->always().size() < (i->isLocal() + ? std::numeric_limits::max() + : pinnedChatsLimit(folder, filterId))); } int Session::pinnedChatsLimit( diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 7b6a29c60..55edaa4e3 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -856,10 +856,11 @@ void Widget::escape() { if (controller()->openedFolder().current()) { controller()->closeFolder(); } else if (!cancelSearch()) { + const auto defaultFilterId = session().account().defaultFilterId(); if (controller()->activeChatEntryCurrent().key) { controller()->content()->dialogsCancelled(); - } else if (controller()->activeChatsFilterCurrent()) { - controller()->setActiveChatsFilter(FilterId(0)); + } else if (controller()->activeChatsFilterCurrent() != defaultFilterId) { + controller()->setActiveChatsFilter(defaultFilterId); } } else if (!_searchInChat && !controller()->selectingPeer()) { if (controller()->activeChatEntryCurrent().key) { diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 66f112501..57564557f 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -138,6 +138,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/unread_badge.h" #include "main/main_session.h" #include "main/main_session_settings.h" +#include "main/main_account.h" #include "main/session/send_as_peers.h" #include "window/notifications_manager.h" #include "window/window_adaptive.h" @@ -2406,6 +2407,8 @@ void HistoryWidget::showHistory( sponsored.request(_history); _scroll->setTrackingContent(sponsored.canHaveFor(_history)); } + _history->owner().session().account().addToRecent(_peer->id); + _history->owner().chatsFilters().refreshHistory(_history); } else { _chooseForReport = nullptr; refreshTopBarActiveChat(); diff --git a/Telegram/SourceFiles/kotato/kotato_settings.cpp b/Telegram/SourceFiles/kotato/kotato_settings.cpp index 3974285fc..0c8ee45a0 100644 --- a/Telegram/SourceFiles/kotato/kotato_settings.cpp +++ b/Telegram/SourceFiles/kotato/kotato_settings.cpp @@ -386,6 +386,11 @@ const std::map> DefinitionMap { .type = SettingType::IntSetting, .defaultValue = 0, .limitHandler = IntLimit(0, 5), }}, + { "folders/default", { + .scope = SettingScope::Account, + .type = SettingType::IntSetting, + .defaultValue = 0, + .limitHandler = IntLimitMin(0), }}, { "folders/count_unmuted_only", { .type = SettingType::BoolSetting, .defaultValue = false, }}, @@ -398,6 +403,9 @@ const std::map> DefinitionMap { { "folders/hide_all_chats", { .type = SettingType::BoolSetting, .defaultValue = false, }}, + { "folders/local", { + .scope = SettingScope::Account, + .type = SettingType::QJsonArraySetting, }}, }; using OldOptionKey = QString; diff --git a/Telegram/SourceFiles/main/main_account.cpp b/Telegram/SourceFiles/main/main_account.cpp index 2b3897b13..ebf62445b 100644 --- a/Telegram/SourceFiles/main/main_account.cpp +++ b/Telegram/SourceFiles/main/main_account.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "main/main_account.h" +#include "kotato/kotato_settings.h" #include "base/platform/base_platform_info.h" #include "core/application.h" #include "core/shortcuts.h" @@ -191,6 +192,11 @@ void Account::createSession( _sessionValue = _session.get(); Ensures(_session != nullptr); + + _defaultFilterId = ::Kotato::JsonSettings::GetInt( + "folders/default", + session().userId().bare, + _mtp->isTestMode()); } void Account::destroySession(DestroyReason reason) { @@ -600,6 +606,37 @@ void Account::destroyStaleAuthorizationKeys() { } } +void Account::setDefaultFilterId(uint64 id) { + Expects(_mtp != nullptr); + Expects(_session != nullptr); + + _defaultFilterId = id; + + ::Kotato::JsonSettings::Set( + "folders/default", + _defaultFilterId, + session().userId().bare, + _mtp->isTestMode()); +} + +bool Account::isCurrent(uint64 id, bool testMode) { + Expects(_mtp != nullptr); + Expects(_session != nullptr); + + return id == session().userId().bare + && _mtp->isTestMode() == testMode; +} + +void Account::addToRecent(PeerId id) { + if (!_recent.contains(id.value)) { + _recent << id.value; + } +} + +bool Account::isRecent(PeerId id) { + return _recent.contains(id.value); +} + void Account::resetAuthorizationKeys() { Expects(_mtp != nullptr); diff --git a/Telegram/SourceFiles/main/main_account.h b/Telegram/SourceFiles/main/main_account.h index f4978655d..d14b7ec2f 100644 --- a/Telegram/SourceFiles/main/main_account.h +++ b/Telegram/SourceFiles/main/main_account.h @@ -114,6 +114,16 @@ public: return _lifetime; } + [[nodiscard]] int defaultFilterId() { + return _defaultFilterId; + } + void setDefaultFilterId(uint64 id); + + [[nodiscard]] bool isCurrent(uint64 id, bool testMode); + + void addToRecent(PeerId id); + [[nodiscard]] bool isRecent(PeerId id); + private: static constexpr auto kDefaultSaveDelay = crl::time(1000); enum class DestroyReason { @@ -159,6 +169,9 @@ private: MTP::Instance::Fields _mtpFields; MTP::AuthKeysList _mtpKeysToDestroy; bool _loggingOut = false; + int _defaultFilterId = 0; + + QSet _recent; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 36a80dc7c..8d889dfb8 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -70,6 +70,7 @@ settingsIconEmoji: icon {{ "settings/emoji", settingsIconFg }}; settingsIconThemes: icon {{ "settings/palette", settingsIconFg }}; settingsIconKotato: icon {{ "settings/kotato", settingsIconFg }}; settingsIconKotatoOld: icon {{ "settings/kotato_old", settingsIconFg }}; +settingsIconCloud: icon {{ "settings/cloud", settingsIconFg }}; settingsIconGroup: icon {{ "settings/group", settingsIconFg }}; settingsIconChannel: icon {{ "settings/channel", settingsIconFg }}; settingsIconUser: icon {{ "settings/user", settingsIconFg }}; diff --git a/Telegram/SourceFiles/settings/settings_folders.cpp b/Telegram/SourceFiles/settings/settings_folders.cpp index aa62e5746..1251f7965 100644 --- a/Telegram/SourceFiles/settings/settings_folders.cpp +++ b/Telegram/SourceFiles/settings/settings_folders.cpp @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/settings_folders.h" +#include "kotato/kotato_lang.h" +#include "kotato/kotato_settings.h" #include "apiwrap.h" #include "boxes/premium_limits_box.h" #include "boxes/filters/edit_filter_box.h" @@ -23,6 +25,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "settings/settings_common.h" #include "ui/filter_icons.h" +#include "main/main_account.h" +#include "ui/toast/toast.h" #include "ui/layers/generic_box.h" #include "ui/painter.h" #include "ui/text/text_utilities.h" @@ -42,6 +46,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Settings { namespace { +auto currentDefaultRemoved = false; + using Flag = Data::ChatFilter::Flag; using Flags = Data::ChatFilter::Flags; @@ -150,9 +156,13 @@ struct FilterRow { 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); + return (count + ? tr::lng_filters_chats_count(tr::now, lt_count_short, count) + : tr::lng_filters_no_chats(tr::now)) + + ", " + + (filter.isLocal() + ? ktr("ktg_filters_local") + : ktr("ktg_filters_cloud")); } FilterRowButton::FilterRowButton( @@ -331,6 +341,21 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { const auto limit = [=] { return Data::PremiumLimits(session).dialogFiltersCurrent(); }; + const auto account = &session->account(); + const auto currentDefaultId = account->defaultFilterId(); + auto localNewFilterId = limit(); + const auto generateNewId = [=, &localNewFilterId] { + const auto filters = &controller->session().data().chatsFilters(); + + do { + localNewFilterId++; + } while (ranges::contains(filters->list(), localNewFilterId, &Data::ChatFilter::id)); + + return localNewFilterId; + }; + + currentDefaultRemoved = false; + AddSkip(container, st::settingsSectionSkip); AddSubsectionTitle(container, tr::lng_filters_subtitle()); @@ -341,22 +366,49 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { Assert(i != end(*rows)); return &*i; }; + const auto toast = Ui::Toast::Config{ + .text = { ktr("ktg_filters_cloud_limit") }, + .st = &st::windowArchiveToast, + .multiline = true, + }; const auto showLimitReached = [=] { - const auto removed = ranges::count_if(*rows, &FilterRow::removed); + const auto removed = ranges::count_if(*rows, [](FilterRow row) { + return row.removed || row.filter.isLocal(); + }); if (rows->size() < limit() + removed) { return false; } controller->show(Box(FiltersLimitBox, session)); return true; }; + const auto newCloudButton = AddButton( + container, + rktr("ktg_filters_create_cloud"), + st::settingsButton, + { &st::settingsIconCloud, kIconLightOrange } + ); + const auto newLocalButton = AddButton( + container, + rktr("ktg_filters_create_local"), + st::settingsButton, + { &st::settingsIconFolders, kIconLightBlue } + ); const auto wrap = container->add(object_ptr( container)); const auto addFilter = [=](const Data::ChatFilter &filter) { + if (rows->size() == 0) { + AddSkip(wrap); + AddDivider(wrap); + AddSkip(wrap); + } const auto button = wrap->add( object_ptr(wrap, session, filter)); button->removeRequests( ) | rpl::start_with_next([=] { button->setRemoved(true); + if (find(button)->filter.id() == account->defaultFilterId()) { + currentDefaultRemoved = true; + } find(button)->removed = true; }, button->lifetime()); button->restoreRequests( @@ -364,6 +416,9 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { if (showLimitReached()) { return; } + if (find(button)->filter.id() == account->defaultFilterId()) { + currentDefaultRemoved = false; + } button->setRemoved(false); find(button)->removed = false; }, button->lifetime()); @@ -374,6 +429,11 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { } const auto doneCallback = [=](const Data::ChatFilter &result) { find(button)->filter = result; + const auto isCurrentDefault = result.id() == account->defaultFilterId(); + if ((isCurrentDefault && !result.isDefault()) + || (!isCurrentDefault && result.isDefault())) { + account->setDefaultFilterId(result.isDefault() ? result.id() : 0); + } button->updateData(result); }; controller->window().show(Box( @@ -426,22 +486,33 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { } } - AddButton( - container, - tr::lng_filters_create(), - st::settingsButtonActive, - { &st::settingsIconAdd, 0, IconType::Round, &st::windowBgActive } - )->setClickedCallback([=] { + newCloudButton->setClickedCallback([=] { if (showLimitReached()) { return; } const auto doneCallback = [=](const Data::ChatFilter &result) { + if (result.isDefault()) { + account->setDefaultFilterId(result.id()); + } addFilter(result); }; controller->window().show(Box( EditFilterBox, controller, - Data::ChatFilter(), + Data::ChatFilter(generateNewId()), + crl::guard(container, doneCallback))); + }); + newLocalButton->setClickedCallback([=] { + const auto doneCallback = [=](const Data::ChatFilter &result) { + if (result.isDefault()) { + account->setDefaultFilterId(result.id()); + } + addFilter(result); + }; + controller->window().show(Box( + EditFilterBox, + controller, + Data::ChatFilter(generateNewId(), true), crl::guard(container, doneCallback))); }); AddSkip(container); @@ -451,7 +522,7 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { object_ptr(container)) )->setDuration(0); const auto aboutRows = nonEmptyAbout->entity(); - AddDivider(aboutRows); + AddDividerText(aboutRows, rktr("ktg_filters_description")); AddSkip(aboutRows); AddSubsectionTitle(aboutRows, tr::lng_filters_recommended()); @@ -513,11 +584,28 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { auto result = base::flat_map, FilterId>(); for (auto &row : *rows) { const auto id = row.filter.id(); - if (row.removed) { + if (row.removed || row.filter.isLocal()) { continue; } else if (!id || !ranges::contains(list, id, &Data::ChatFilter::id)) { result.emplace(row.button, chooseNextId()); + if (account->defaultFilterId() == id) { + account->setDefaultFilterId(localId); + } + } + } + + // We're prioritizing cloud IDs before local. + localId = limit(); + for (auto &row : *rows) { + const auto id = row.filter.id(); + if (row.removed || !row.filter.isLocal()) { + continue; + } else if (!ranges::contains(list, id, &Data::ChatFilter::id)) { + result.emplace(row.button, chooseNextId()); + if (account->defaultFilterId() == id) { + account->setDefaultFilterId(localId); + } } } return result; @@ -525,6 +613,7 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { return [=] { auto ids = prepareGoodIdsForNewFilters(); + bool needSave = false; using Requests = std::vector; auto addRequests = Requests(), removeRequests = Requests(); @@ -532,6 +621,7 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { const auto &list = realFilters.list(); auto order = std::vector(); order.reserve(rows->size()); + auto localFoldersChanged = false; for (const auto &row : *rows) { const auto id = row.filter.id(); const auto removed = row.removed; @@ -542,28 +632,39 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { order.push_back(id); continue; } - const auto newId = ids.take(row.button).value_or(id); - const auto tl = removed - ? MTPDialogFilter() - : row.filter.tl(newId); - const auto request = MTPmessages_UpdateDialogFilter( - MTP_flags(removed - ? MTPmessages_UpdateDialogFilter::Flag(0) - : MTPmessages_UpdateDialogFilter::Flag::f_filter), - MTP_int(newId), - tl); - if (removed) { - removeRequests.push_back(request); + if (row.filter.isLocal()) { + if (removed) { + realFilters.remove(id); + } else { + realFilters.set(row.filter); + order.push_back(id); + } + localFoldersChanged = true; + needSave = true; } else { - addRequests.push_back(request); - order.push_back(newId); + const auto newId = ids.take(row.button).value_or(id); + const auto tl = removed + ? MTPDialogFilter() + : row.filter.tl(newId); + const auto request = MTPmessages_UpdateDialogFilter( + MTP_flags(removed + ? MTPmessages_UpdateDialogFilter::Flag(0) + : MTPmessages_UpdateDialogFilter::Flag::f_filter), + MTP_int(newId), + tl); + if (removed) { + removeRequests.push_back(request); + } else { + addRequests.push_back(request); + order.push_back(newId); + } + realFilters.apply(MTP_updateDialogFilter( + MTP_flags(removed + ? MTPDupdateDialogFilter::Flag(0) + : MTPDupdateDialogFilter::Flag::f_filter), + MTP_int(newId), + tl)); } - realFilters.apply(MTP_updateDialogFilter( - MTP_flags(removed - ? MTPDupdateDialogFilter::Flag(0) - : MTPDupdateDialogFilter::Flag::f_filter), - MTP_int(newId), - tl)); } if (!ranges::contains(order, FilterId(0))) { auto position = 0; @@ -585,9 +686,22 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { std::move(request) ).afterRequest(previousId).send(); } - if (!order.empty() && !addRequests.empty()) { + if (!order.empty() && (!addRequests.empty() || localFoldersChanged)) { realFilters.saveOrder(order, previousId); } + if (currentDefaultRemoved) { + account->setDefaultFilterId(0); + controller->setActiveChatsFilter(0); + } + if (localFoldersChanged) { + realFilters.saveLocal(); + } + if (currentDefaultId != account->defaultFilterId()) { + needSave = true; + } + if (needSave) { + Kotato::JsonSettings::Write(); + } }; } diff --git a/Telegram/SourceFiles/ui/filter_icon_panel.cpp b/Telegram/SourceFiles/ui/filter_icon_panel.cpp index f50a11b21..2bd66c767 100644 --- a/Telegram/SourceFiles/ui/filter_icon_panel.cpp +++ b/Telegram/SourceFiles/ui/filter_icon_panel.cpp @@ -65,11 +65,27 @@ constexpr auto kIcons = std::array{ // FilterIcon::Poo, }; +constexpr auto kLocalIcons = std::array{ + FilterIcon::LocalBook, + FilterIcon::LocalBrackets, + FilterIcon::LocalCandle, + FilterIcon::LocalCity, + FilterIcon::LocalDesktop, + FilterIcon::LocalEarth, + FilterIcon::LocalMusic, + FilterIcon::LocalNews, + FilterIcon::LocalPhone, + FilterIcon::LocalSmile, + FilterIcon::LocalSun, + FilterIcon::LocalVideo, +}; + } // namespace -FilterIconPanel::FilterIconPanel(QWidget *parent) +FilterIconPanel::FilterIconPanel(QWidget *parent, bool isLocal) : RpWidget(parent) -, _inner(Ui::CreateChild(this)) { +, _inner(Ui::CreateChild(this)) +, _isLocal(isLocal) { setup(); } @@ -102,7 +118,9 @@ void FilterIconPanel::setup() { } void FilterIconPanel::setupInner() { - const auto count = kIcons.size(); + const auto count = kIcons.size() + (_isLocal + ? kLocalIcons.size() + : 0); const auto rows = (count / kIconsPerRow) + ((count % kIconsPerRow) ? 1 : 0); const auto single = st::windowFilterIconSingle; @@ -151,6 +169,25 @@ void FilterIconPanel::setupInner() { ? st::dialogsUnreadBgMutedOver : st::dialogsUnreadBgMuted)->c); } + if (_isLocal) { + const auto cloudSize = kIcons.size(); + const auto fullSize = kIcons.size() + kLocalIcons.size(); + for (auto i = cloudSize; i != fullSize; ++i) { + const auto rect = countRect(i); + if (!rect.intersects(clip)) { + continue; + } + if (i == selected) { + Ui::FillRoundRect( + p, + rect, + st::emojiPanHover, + StickerHoverCorners); + } + const auto icon = LookupFilterIcon(kLocalIcons[i-cloudSize]).normal; + icon->paintInCenter(p, rect, st::emojiIconFg->c); + } + } }, _inner->lifetime()); _inner->setMouseTracking(true); @@ -223,7 +260,11 @@ void FilterIconPanel::mouseMove(QPoint position) { const auto column = point.x() / st::windowFilterIconSingle.width(); const auto row = point.y() / st::windowFilterIconSingle.height(); const auto index = row * kIconsPerRow + column; - setSelected(index < kIcons.size() ? index : -1); + const auto size = kIcons.size() + + (_isLocal + ? kLocalIcons.size() + : 0); + setSelected(index < size ? index : -1); } } @@ -241,8 +282,12 @@ void FilterIconPanel::mouseRelease(Qt::MouseButton button) { const auto pressed = _pressed; setPressed(-1); if (pressed == _selected && pressed >= 0) { - Assert(pressed < kIcons.size()); - _chosen.fire_copy(kIcons[pressed]); + Assert(pressed < kIcons.size() + (_isLocal + ? kLocalIcons.size() + : 0)); + _chosen.fire_copy(pressed < kIcons.size() + ? kIcons[pressed] + : kLocalIcons[pressed-kIcons.size()]); } } diff --git a/Telegram/SourceFiles/ui/filter_icon_panel.h b/Telegram/SourceFiles/ui/filter_icon_panel.h index 04a02f936..28ff78373 100644 --- a/Telegram/SourceFiles/ui/filter_icon_panel.h +++ b/Telegram/SourceFiles/ui/filter_icon_panel.h @@ -18,7 +18,7 @@ class PanelAnimation; class FilterIconPanel final : public Ui::RpWidget { public: - FilterIconPanel(QWidget *parent); + FilterIconPanel(QWidget *parent, bool isLocal); ~FilterIconPanel(); void hideFast(); @@ -80,6 +80,7 @@ private: QPixmap _cache; Ui::Animations::Simple _a_opacity; base::Timer _hideTimer; + bool _isLocal = false; }; diff --git a/Telegram/SourceFiles/ui/filter_icons.cpp b/Telegram/SourceFiles/ui/filter_icons.cpp index 01a982dc8..dd4165c14 100644 --- a/Telegram/SourceFiles/ui/filter_icons.cpp +++ b/Telegram/SourceFiles/ui/filter_icons.cpp @@ -205,6 +205,66 @@ const auto kIcons = std::vector{ // &st::foldersPooActive, // "\xF0\x9F\x92\xA9"_cs.utf16() //}, + { + &st::foldersLocalBook, + &st::foldersLocalBookActive, + "\xF0\x9F\x94\x96"_cs.utf16() + }, + { + &st::foldersLocalBrackets, + &st::foldersLocalBracketsActive, + "\xE2\x8C\xA8"_cs.utf16() + }, + { + &st::foldersLocalCandle, + &st::foldersLocalCandleActive, + "\xF0\x9F\x95\xAF"_cs.utf16() + }, + { + &st::foldersLocalCity, + &st::foldersLocalCityActive, + "\xF0\x9F\x8F\x99"_cs.utf16() + }, + { + &st::foldersLocalDesktop, + &st::foldersLocalDesktopActive, + "\xF0\x9F\x96\xA5"_cs.utf16() + }, + { + &st::foldersLocalEarth, + &st::foldersLocalEarthActive, + "\xF0\x9F\x8C\x8E"_cs.utf16() + }, + { + &st::foldersLocalMusic, + &st::foldersLocalMusicActive, + "\xF0\x9F\x8E\xA7"_cs.utf16() + }, + { + &st::foldersLocalNews, + &st::foldersLocalNewsActive, + "\xF0\x9F\x93\xB0"_cs.utf16() + }, + { + &st::foldersLocalPhone, + &st::foldersLocalPhoneActive, + "\xF0\x9F\x93\xB1"_cs.utf16() + }, + { + &st::foldersLocalSmile, + &st::foldersLocalSmileActive, + "\xF0\x9F\x99\x82"_cs.utf16() + }, + { + &st::foldersLocalSun, + &st::foldersLocalSunActive, + "\xE2\x98\x80"_cs.utf16() + }, + { + &st::foldersLocalVideo, + &st::foldersLocalVideoActive, + "\xF0\x9F\x93\xB9"_cs.utf16() + }, { &st::filtersEdit, &st::filtersEdit, diff --git a/Telegram/SourceFiles/ui/filter_icons.h b/Telegram/SourceFiles/ui/filter_icons.h index 0af815006..059917ab5 100644 --- a/Telegram/SourceFiles/ui/filter_icons.h +++ b/Telegram/SourceFiles/ui/filter_icons.h @@ -59,6 +59,19 @@ enum class FilterIcon : uchar { Setup, // Poo, + LocalBook, + LocalBrackets, + LocalCandle, + LocalCity, + LocalDesktop, + LocalEarth, + LocalMusic, + LocalNews, + LocalPhone, + LocalSmile, + LocalSun, + LocalVideo, + Edit, }; diff --git a/Telegram/SourceFiles/ui/filter_icons.style b/Telegram/SourceFiles/ui/filter_icons.style index ca45d10fd..8863dc9a2 100644 --- a/Telegram/SourceFiles/ui/filter_icons.style +++ b/Telegram/SourceFiles/ui/filter_icons.style @@ -85,3 +85,28 @@ foldersPoo: icon {{ "folders/folders_poo", sideBarIconFg }}; foldersPooActive: icon {{ "folders/folders_poo", sideBarIconFgActive }}; filtersEdit: icon {{ "folders/folders_edit", sideBarIconFg }}; + +foldersLocalBook: icon {{ "folders/folders_local_book", sideBarIconFg }}; +foldersLocalBookActive: icon {{ "folders/folders_local_book_active", sideBarIconFgActive }}; +foldersLocalBrackets: icon {{ "folders/folders_local_brackets", sideBarIconFg }}; +foldersLocalBracketsActive: icon {{ "folders/folders_local_brackets", sideBarIconFgActive }}; +foldersLocalCandle: icon {{ "folders/folders_local_candle", sideBarIconFg }}; +foldersLocalCandleActive: icon {{ "folders/folders_local_candle_active", sideBarIconFgActive }}; +foldersLocalCity: icon {{ "folders/folders_local_city", sideBarIconFg }}; +foldersLocalCityActive: icon {{ "folders/folders_local_city_active", sideBarIconFgActive }}; +foldersLocalDesktop: icon {{ "folders/folders_local_desktop", sideBarIconFg }}; +foldersLocalDesktopActive: icon {{ "folders/folders_local_desktop_active", sideBarIconFgActive }}; +foldersLocalEarth: icon {{ "folders/folders_local_earth", sideBarIconFg }}; +foldersLocalEarthActive: icon {{ "folders/folders_local_earth", sideBarIconFgActive }}; +foldersLocalMusic: icon {{ "folders/folders_local_music", sideBarIconFg }}; +foldersLocalMusicActive: icon {{ "folders/folders_local_music_active", sideBarIconFgActive }}; +foldersLocalNews: icon {{ "folders/folders_local_news", sideBarIconFg }}; +foldersLocalNewsActive: icon {{ "folders/folders_local_news_active", sideBarIconFgActive }}; +foldersLocalPhone: icon {{ "folders/folders_local_phone", sideBarIconFg }}; +foldersLocalPhoneActive: icon {{ "folders/folders_local_phone_active", sideBarIconFgActive }}; +foldersLocalSmile: icon {{ "folders/folders_local_smile", sideBarIconFg }}; +foldersLocalSmileActive: icon {{ "folders/folders_local_smile_active", sideBarIconFgActive }}; +foldersLocalSun: icon {{ "folders/folders_local_sun", sideBarIconFg }}; +foldersLocalSunActive: icon {{ "folders/folders_local_sun_active", sideBarIconFgActive }}; +foldersLocalVideo: icon {{ "folders/folders_local_video", sideBarIconFg }}; +foldersLocalVideoActive: icon {{ "folders/folders_local_video_active", sideBarIconFgActive }}; diff --git a/Telegram/SourceFiles/window/window.style b/Telegram/SourceFiles/window/window.style index 79c291f64..b5d4fef3f 100644 --- a/Telegram/SourceFiles/window/window.style +++ b/Telegram/SourceFiles/window/window.style @@ -322,6 +322,12 @@ windowFilterTypeBots: icon {{ "folders/folders_type_bots", historyPeerUserpicFg windowFilterTypeNoMuted: icon {{ "folders/folders_type_muted", historyPeerUserpicFg }}; windowFilterTypeNoArchived: icon {{ "folders/folders_type_archived", historyPeerUserpicFg }}; windowFilterTypeNoRead: icon {{ "folders/folders_type_read", historyPeerUserpicFg }}; +windowFilterTypeOwned: icon {{ "folders/folders_type_not_owner", historyPeerUserpicFg }}; +windowFilterTypeAdmin: icon {{ "folders/folders_type_not_admin", historyPeerUserpicFg }}; +windowFilterTypeNotOwned: icon {{ "folders/folders_type_owner", historyPeerUserpicFg }}; +windowFilterTypeNotAdmin: icon {{ "folders/folders_type_admin", historyPeerUserpicFg }}; +windowFilterTypeRecent: icon {{ "folders/folders_type_recent", historyPeerUserpicFg }}; +windowFilterTypeNoFilter: icon {{ "folders/folders_type_filtered", historyPeerUserpicFg }}; windowFilterChatsSectionSubtitle: FlatLabel(defaultFlatLabel) { style: TextStyle(defaultTextStyle) { font: searchedBarFont; diff --git a/Telegram/SourceFiles/window/window_controller.cpp b/Telegram/SourceFiles/window/window_controller.cpp index 124ce16bc..60aaffa96 100644 --- a/Telegram/SourceFiles/window/window_controller.cpp +++ b/Telegram/SourceFiles/window/window_controller.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/emoji_config.h" #include "chat_helpers/emoji_sets_manager.h" #include "window/window_session_controller.h" +#include "window/window_filters_menu.h" #include "window/themes/window_theme.h" #include "window/themes/window_theme_editor.h" #include "ui/boxes/confirm_box.h" @@ -152,6 +153,7 @@ void Controller::setupSideBar() { }, _sessionController->lifetime()); if (_sessionController->session().settings().dialogsFiltersEnabled()) { + ResetFiltersFirstLoad(); _sessionController->toggleFiltersMenu(true); } else { sideBarChanged(); diff --git a/Telegram/SourceFiles/window/window_filters_menu.cpp b/Telegram/SourceFiles/window/window_filters_menu.cpp index 2faeac5de..ae45a097f 100644 --- a/Telegram/SourceFiles/window/window_filters_menu.cpp +++ b/Telegram/SourceFiles/window/window_filters_menu.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_main_menu.h" #include "window/window_peer_menu.h" #include "main/main_session.h" +#include "main/main_account.h" #include "data/data_session.h" #include "data/data_chat_filters.h" #include "data/data_folder.h" @@ -67,8 +68,14 @@ namespace { }); } +bool FiltersFirstLoad = true; + } // namespace +void ResetFiltersFirstLoad() { + FiltersFirstLoad = true; +} + FiltersMenu::FiltersMenu( not_null parent, not_null session) @@ -115,6 +122,7 @@ void FiltersMenu::setup() { auto premium = Data::AmPremiumValue(&_session->session()); const auto filters = &_session->session().data().chatsFilters(); + _activeFilterId = _session->activeChatsFilterCurrent(); rpl::combine( rpl::single(rpl::empty) | rpl::then(filters->changed()), std::move(premium) @@ -122,7 +130,6 @@ void FiltersMenu::setup() { refresh(); }, _outer.lifetime()); - _activeFilterId = _session->activeChatsFilterCurrent(); _session->activeChatsFilter( ) | rpl::filter([=](FilterId id) { return (id != _activeFilterId); @@ -238,6 +245,11 @@ void FiltersMenu::refresh() { if (button) { scrollToButton(button); } + + if (FiltersFirstLoad) { + _session->setActiveChatsFilter(_session->session().account().defaultFilterId()); + FiltersFirstLoad = false; + } } void FiltersMenu::setupList() { @@ -365,6 +377,7 @@ void FiltersMenu::showMenu(QPoint position, FilterId id) { if (i == end(_filters)) { return; } + const auto defaultFilterId = _session->session().account().defaultFilterId(); _popupMenu = base::make_unique_q( i->second.get(), st::popupMenuWithIcons); @@ -380,7 +393,6 @@ void FiltersMenu::showMenu(QPoint position, FilterId id) { tr::lng_filters_context_edit(tr::now), crl::guard(&_outer, [=] { showEditBox(id); }), &st::menuIconEdit); - auto filteredChats = [=] { return _session->session().data().chatsFilters().chatsList(id); }; @@ -388,8 +400,18 @@ void FiltersMenu::showMenu(QPoint position, FilterId id) { _session, std::move(filteredChats), addAction); - - addAction( + if (defaultFilterId != id) { + _popupMenu->addAction( + ktr("ktg_filters_context_make_default"), + crl::guard(&_outer, [=] { setDefaultFilter(id); }), + &st::menuIconFave); + } else if (id) { + _popupMenu->addAction( + ktr("ktg_filters_context_reset_default"), + crl::guard(&_outer, [=] { setDefaultFilter(0); }), + &st::menuIconUnfave); + } + _popupMenu->addAction( tr::lng_filters_context_remove(tr::now), [=] { showRemoveBox(id); }, &st::menuIconDelete); @@ -420,6 +442,14 @@ void FiltersMenu::showEditMenu(QPoint position) { _popupMenu->popup(position); } +void FiltersMenu::setDefaultFilter(FilterId id) { + const auto defaultFilterId = _session->session().account().defaultFilterId(); + if (defaultFilterId != id) { + _session->session().account().setDefaultFilterId(id); + Kotato::JsonSettings::Write(); + } +} + void FiltersMenu::showEditBox(FilterId id) { EditExistingFilter(_session, id); } @@ -433,15 +463,37 @@ void FiltersMenu::showRemoveBox(FilterId id) { } void FiltersMenu::remove(FilterId id) { - _session->session().data().chatsFilters().apply(MTP_updateDialogFilter( - MTP_flags(MTPDupdateDialogFilter::Flag(0)), - MTP_int(id), - MTPDialogFilter())); - _session->session().api().request(MTPmessages_UpdateDialogFilter( - MTP_flags(MTPmessages_UpdateDialogFilter::Flag(0)), - MTP_int(id), - MTPDialogFilter() - )).send(); + const auto defaultFilterId = _session->session().account().defaultFilterId(); + const auto filters = &_session->session().data().chatsFilters(); + const auto &list = filters->list(); + const auto i = ranges::find(list, id, &Data::ChatFilter::id); + Assert(i != end(list)); + bool needSave = false; + if (i->isLocal()) { + filters->remove(id); + filters->saveLocal(); + needSave = true; + } else { + _session->session().data().chatsFilters().apply(MTP_updateDialogFilter( + MTP_flags(MTPDupdateDialogFilter::Flag(0)), + MTP_int(id), + MTPDialogFilter())); + _session->session().api().request(MTPmessages_UpdateDialogFilter( + MTP_flags(MTPmessages_UpdateDialogFilter::Flag(0)), + MTP_int(id), + MTPDialogFilter() + )).send(); + } + if (id == defaultFilterId) { + needSave = true; + _session->session().account().setDefaultFilterId(0); + if (id == _session->activeChatsFilterCurrent()) { + _session->setActiveChatsFilter(0); + } + } + if (needSave) { + Kotato::JsonSettings::Write(); + } } void FiltersMenu::applyReorder( @@ -476,6 +528,8 @@ void FiltersMenu::applyReorder( _ignoreRefresh = true; filters->saveOrder(order); _ignoreRefresh = false; + filters->saveLocal(); + Kotato::JsonSettings::Write(); } } // namespace Window diff --git a/Telegram/SourceFiles/window/window_filters_menu.h b/Telegram/SourceFiles/window/window_filters_menu.h index 4d3544744..a530e08d2 100644 --- a/Telegram/SourceFiles/window/window_filters_menu.h +++ b/Telegram/SourceFiles/window/window_filters_menu.h @@ -26,6 +26,8 @@ namespace Window { class SessionController; +void ResetFiltersFirstLoad(); + class FiltersMenu final { public: FiltersMenu( @@ -52,6 +54,7 @@ private: void setupMainMenuIcon(); void showMenu(QPoint position, FilterId id); void showEditMenu(QPoint position); + void setDefaultFilter(FilterId id); void showEditBox(FilterId id); void showRemoveBox(FilterId id); void remove(FilterId id); diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index c6ba504f5..cacb4b1eb 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -70,6 +70,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwidget.h" #include "mainwindow.h" #include "main/main_session.h" +#include "main/main_account.h" #include "main/main_session_settings.h" #include "apiwrap.h" #include "api/api_chat_invite.h" @@ -804,6 +805,33 @@ void SessionController::toggleFiltersMenu(bool enabled) { _filtersMenuChanged.fire({}); } +void SessionController::reloadFiltersMenu() { + const auto enabled = !session().data().chatsFilters().list().empty(); + if (enabled) { + auto previousFilter = activeChatsFilterCurrent(); + rpl::single(rpl::empty) | rpl::then( + filtersMenuChanged() + ) | rpl::start_with_next([=] { + toggleFiltersMenu(true); + if (previousFilter) { + if (activeChatsFilterCurrent() != previousFilter) { + resetFakeUnreadWhileOpened(); + } + _activeChatsFilter.force_assign(previousFilter); + if (previousFilter) { + closeFolder(true); + } + } + }, lifetime()); + + if (activeChatsFilterCurrent() != 0) { + resetFakeUnreadWhileOpened(); + } + _activeChatsFilter.force_assign(0); + toggleFiltersMenu(false); + } +} + void SessionController::refreshFiltersMenu() { toggleFiltersMenu(session().data().chatsFilters().has()); } @@ -818,7 +846,9 @@ void SessionController::checkOpenedFilter() { const auto &list = session().data().chatsFilters().list(); const auto i = ranges::find(list, filterId, &Data::ChatFilter::id); if (i == end(list)) { - setActiveChatsFilter(0); + const auto defaultFilterId = session().account().defaultFilterId(); + const auto j = ranges::find(list, FilterId(defaultFilterId), &Data::ChatFilter::id); + setActiveChatsFilter(j == end(list) ? 0 : defaultFilterId); } } } @@ -845,8 +875,14 @@ void SessionController::openFolder(not_null folder) { _openedFolder = folder.get(); } -void SessionController::closeFolder() { - _openedFolder = nullptr; +void SessionController::closeFolder(bool force) { + const auto defaultFilterId = session().account().defaultFilterId(); + if (defaultFilterId == 0 || force) { + _openedFolder = nullptr; + } else { + setActiveChatsFilter(defaultFilterId); + checkOpenedFilter(); + } } void SessionController::setupPremiumToast() { @@ -1567,7 +1603,7 @@ void SessionController::setActiveChatsFilter(FilterId id) { } _activeChatsFilter.force_assign(id); if (id) { - closeFolder(); + closeFolder(true); } if (adaptive().isOneColumn()) { Ui::showChatsList(&session()); diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index e503f7d57..a1af4a131 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -336,7 +336,7 @@ public: rpl::variable searchInChat; bool uniqueChatsInSearchResults() const; void openFolder(not_null folder); - void closeFolder(); + void closeFolder(bool force = false); const rpl::variable &openedFolder() const; void setActiveChatEntry(Dialogs::RowDescriptor row); @@ -467,6 +467,7 @@ public: void setActiveChatsFilter(FilterId id); void toggleFiltersMenu(bool enabled); + void reloadFiltersMenu(); [[nodiscard]] rpl::producer<> filtersMenuChanged() const; [[nodiscard]] auto defaultChatTheme() const