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 c26b5dfbb..be0a03be2 100644 --- a/Telegram/Resources/langs/rewrites/en.json +++ b/Telegram/Resources/langs/rewrites/en.json @@ -62,7 +62,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", @@ -91,6 +94,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 7287b56e3..cf136f832 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 "apiwrap.h" #include "boxes/peer_list_box.h" #include "boxes/premium_limits_box.h" @@ -671,11 +672,16 @@ void SaveNewFilterPinned( const auto &order = session->data().pinnedChatsOrder(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(); + } } void CheckFilterInvite( diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp index ce23d235f..03eedb753 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp @@ -7,6 +7,8 @@ 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 "boxes/filters/edit_filter_chats_preview.h" #include "boxes/filters/edit_filter_links.h" @@ -15,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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/fields/input_field.h" #include "ui/wrap/slide_wrap.h" @@ -37,6 +40,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" @@ -86,7 +90,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()); @@ -107,7 +113,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()); @@ -140,7 +148,8 @@ void EditExceptions( rules.flags() & options, include ? rules.always() : rules.never(), limit, - showLimitReached); + showLimitReached, + rules.isLocal()); const auto rawController = controller.get(); auto initBox = [=](not_null box) { box->setCloseByOutsideClick(false); @@ -174,7 +183,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(); @@ -225,7 +236,8 @@ void CreateIconSelector( }, toggle->lifetime()); const auto panel = toggle->lifetime().make_state( - outer); + outer, + rules.isLocal()); toggle->installEventFilter(panel); toggle->addClickHandler([=] { panel->toggleAnimated(); @@ -243,7 +255,9 @@ void CreateIconSelector( rules.flags(), rules.always(), rules.pinned(), - rules.never()); + rules.never(), + rules.isDefault(), + rules.isLocal()); }, panel->lifetime()); const auto updatePanelGeometry = [=] { @@ -375,10 +389,14 @@ void EditFilterBox( }, box->lifetime()); box->setWidth(st::boxWideWidth); - box->setTitle(rpl::conditional( - state->creating.value(), - tr::lng_filters_new(), - tr::lng_filters_edit())); + const auto isLocal = filter.isLocal(); + box->setTitle(rpl::single(state->creating.current() + ? (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( @@ -395,10 +413,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, @@ -422,7 +442,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); @@ -446,11 +468,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"), + (state->creating.current() ? false : isCurrent), + st::defaultBoxCheckbox), + style::margins( + st::boxPadding.left(), + st::boxPadding.bottom(), + st::boxPadding.right(), + st::boxPadding.bottom())); + Ui::AddSkip(content); Ui::AddDivider(content); Ui::AddSkip(content); @@ -493,7 +536,7 @@ void EditFilterBox( excludeInner, data, updateDefaultTitle, - kExcludeTypes, + (isLocal ? kExcludeTypesLocal : kExcludeTypes), &Data::ChatFilter::never); Ui::AddSkip(excludeInner); @@ -613,7 +656,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([=] { @@ -629,7 +672,7 @@ void EditFilterBox( EditExceptions( window, box, - kExcludeTypes, + (isLocal ? kExcludeTypesLocal : kExcludeTypes), data, updateDefaultTitle, refreshPreviews); @@ -656,24 +699,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(); + } }; const auto saveAnd = [=]( const Data::ChatFilter &data, diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp index dd694585c..030d35f8f 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 "data/data_premium_limits.h" #include "history/history.h" #include "window/window_session_controller.h" @@ -38,6 +39,12 @@ constexpr auto kAllTypes = { Flag::NoMuted, Flag::NoRead, Flag::NoArchived, + Flag::Owned, + Flag::Admin, + Flag::NotOwned, + Flag::NotAdmin, + Flag::Recent, + Flag::NoFilter, }; struct RowSelectionChange { @@ -232,6 +239,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."); } @@ -255,6 +268,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::historyPeer6UserpicBg2; case Flag::NoArchived: return st::historyPeer4UserpicBg2; case Flag::NoRead: return st::historyPeer7UserpicBg2; + case Flag::Owned: return st::historyPeer2UserpicBg2; + case Flag::Admin: return st::historyPeer3UserpicBg2; + case Flag::NotOwned: return st::historyPeer2UserpicBg2; + case Flag::NotAdmin: return st::historyPeer3UserpicBg2; + case Flag::Recent: return st::historyPeer6UserpicBg2; + case Flag::NoFilter: return st::historyPeer7UserpicBg2; } Unexpected("Flag in color paintFlagIcon."); }(); @@ -285,6 +310,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."); }(); @@ -334,7 +365,8 @@ EditFilterChatsListController::EditFilterChatsListController( Flags selected, const base::flat_set> &peers, int limit, - Fn showLimitReached) + Fn showLimitReached, + bool isLocal) : ChatsListBoxController(session) , _session(session) , _showLimitReached(std::move(showLimitReached)) @@ -343,7 +375,8 @@ EditFilterChatsListController::EditFilterChatsListController( , _options(options & ~Flag::Chatlist) , _selected(selected) , _limit(limit) -, _chatlist(options & Flag::Chatlist) { +, _chatlist(options & Flag::Chatlist) +, _isLocal(isLocal) { } Main::Session &EditFilterChatsListController::session() const { @@ -368,7 +401,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 if (const auto copy = _showLimitReached) { @@ -492,6 +525,8 @@ auto EditFilterChatsListController::createRow(not_null history) void EditFilterChatsListController::updateTitle() { const auto count = delegate()->peerListSelectedRowsCount() - selectedTypesCount(); - const auto additional = u"%1 / %2"_q.arg(count).arg(_limit); + const auto additional = _isLocal + ? tr::lng_filters_chats_count(tr::now, lt_count_short, count) + : u"%1 / %2"_q.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 26e0529c3..b68a1d18e 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h @@ -51,7 +51,8 @@ public: Flags selected, const base::flat_set> &peers, int limit, - Fn showLimitReached); + Fn showLimitReached, + bool isLocal = false); [[nodiscard]] Main::Session &session() const override; [[nodiscard]] Flags chosenOptions() const { @@ -79,6 +80,7 @@ private: Flags _selected; int _limit = 0; bool _chatlist = false; + bool _isLocal; Fn _deselectOption; diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index efac543af..610b795a4 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,13 +17,16 @@ 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" #include "ui/ui_utility.h" #include "ui/chat/more_chats_bar.h" #include "main/main_session.h" +#include "main/main_account.h" #include "main/main_app_config.h" +#include "mainwidget.h" #include "apiwrap.h" namespace Data { @@ -37,8 +42,199 @@ constexpr auto kLoadExceptionsPerRequest = 100; * crl::time(1000); } +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, @@ -46,19 +242,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::views::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::views::filter([](History *history) { + return history != nullptr; + }) | ranges::views::transform([](History *history) { + return not_null(history); + }); + auto &&always = ranges::views::concat( + data.always + ) | to_histories; + auto pinned = ranges::views::all( + data.pinned + ) | to_histories | ranges::to_vector; + auto &&never = ranges::views::all( + data.never + ) | to_histories; + auto &&all = ranges::views::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)) @@ -91,6 +346,7 @@ ChatFilter ChatFilter::FromTL( all.begin(), all.end() }; + const auto defaultFilterId = owner->session().account().defaultFilterId(); return ChatFilter( data.vid().v, qs(data.vtitle()), @@ -98,7 +354,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(); }, [&](const MTPDdialogFilterChatlist &data) { @@ -227,6 +485,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; } @@ -235,6 +526,10 @@ QString ChatFilter::title() const { return _title; } +bool ChatFilter::isDefault() const { + return _isDefault; +} + QString ChatFilter::iconEmoji() const { return _iconEmoji; } @@ -264,6 +559,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()) { @@ -284,14 +582,77 @@ 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; + }; const auto state = (_flags & (Flag::NoMuted | Flag::NoRead)) ? history->chatListBadgesState() : Dialogs::BadgesState(); return false || ((_flags & flag) + && filterAdmin() + && (!(_flags & Flag::Recent) + || history->owner().session().account().isRecent(history->peer->id)) && (!(_flags & Flag::NoMuted) || !history->muted() || (state.mention @@ -302,10 +663,15 @@ bool ChatFilter::contains(not_null history) const { || state.mention || 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) , _moreChatsTimer([=] { checkLoadMoreChatsLists(); }) { @@ -375,10 +741,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) { @@ -394,7 +766,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; @@ -555,7 +982,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)); } @@ -701,12 +1128,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(), @@ -714,7 +1142,9 @@ const ChatFilter &ChatFilters::applyUpdatedPinned( i->flags(), std::move(always), std::move(pinned), - i->never())); + i->never(), + (id == defaultFilterId), + i->isLocal())); return *i; } @@ -726,18 +1156,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 { @@ -1004,4 +1450,20 @@ void ChatFilters::checkLoadMoreChatsLists() { } } +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 7b5a96476..2c2570131 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.h +++ b/Telegram/SourceFiles/data/data_chat_filters.h @@ -24,10 +24,11 @@ struct MoreChatsBarContent; namespace Data { class Session; +struct LocalFolder; class ChatFilter final { public: - enum class Flag : ushort { + enum class Flag : uint32 { Contacts = (1 << 0), NonContacts = (1 << 1), Groups = (1 << 2), @@ -43,11 +44,20 @@ public: NewChats = (1 << 10), // Telegram Business exceptions. ExistingChats = (1 << 11), + + // Local flags + Owned = (1 << 12), + Admin = (1 << 13), + NotOwned = (1 << 14), + NotAdmin = (1 << 15), + Recent = (1 << 16), + NoFilter = (1 << 17), }; 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, @@ -55,7 +65,14 @@ 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]] ChatFilter withId(FilterId id) const; [[nodiscard]] ChatFilter withTitle(const QString &title) const; @@ -65,11 +82,14 @@ public: [[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]] bool chatlist() const; @@ -80,6 +100,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; @@ -88,6 +114,9 @@ private: std::vector> _pinned; base::flat_set> _never; Flags _flags; + bool _isDefault = false; + bool _isLocal = false; + int _cloudLocalOrder = 0; }; @@ -180,6 +209,7 @@ public: [[nodiscard]] const std::vector> &moreChats( FilterId id) const; void moreChatsHide(FilterId id, bool localOnly = false); + void saveLocal(); private: struct MoreChatsData { @@ -229,4 +259,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 892830ede..e9bc20f0b 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -2173,7 +2173,9 @@ bool Session::pinnedCanPin( const auto i = ranges::find(list, filterId, &Data::ChatFilter::id); return (i == end(list)) || (i->always().contains(history)) - || (i->always().size() < pinnedChatsLimit(filterId)); + || (i->always().size() < (i->isLocal() + ? std::numeric_limits::max() + : pinnedChatsLimit(filterId))); } int Session::pinnedChatsLimit(Data::Folder *folder) const { diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 5689b2066..cf2a9c3d8 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -1806,6 +1806,7 @@ void Widget::slideFinished() { void Widget::escape() { if (!cancelSearch()) { + const auto defaultFilterId = session().account().defaultFilterId(); if (controller()->shownForum().current()) { controller()->closeForum(); } else if (controller()->openedFolder().current()) { @@ -1815,7 +1816,11 @@ void Widget::escape() { } else { const auto filters = &session().data().chatsFilters(); const auto &list = filters->list(); - const auto first = list.empty() ? FilterId() : list.front().id(); + const auto first = list.empty() + ? FilterId() + : defaultFilterId != 0 + ? defaultFilterId + : list.front().id(); if (controller()->activeChatsFilterCurrent() != first) { controller()->setActiveChatsFilter(first); } diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 6042d3132..3d45ffa3d 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -148,6 +148,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/item_text_options.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" @@ -2547,6 +2548,8 @@ void HistoryWidget::showHistory( session().sponsoredMessages().request(_history, checkState); checkState(); } + _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 5658540bf..2d5619a46 100644 --- a/Telegram/SourceFiles/kotato/kotato_settings.cpp +++ b/Telegram/SourceFiles/kotato/kotato_settings.cpp @@ -306,6 +306,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, }}, @@ -318,6 +323,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 9e366ca54..9a5d284f8 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 "storage/storage_account.h" @@ -193,6 +194,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) { @@ -602,6 +608,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::setHandleLoginCode(Fn callback) { _handleLoginCode = std::move(callback); } diff --git a/Telegram/SourceFiles/main/main_account.h b/Telegram/SourceFiles/main/main_account.h index ba0915945..30d6b9350 100644 --- a/Telegram/SourceFiles/main/main_account.h +++ b/Telegram/SourceFiles/main/main_account.h @@ -117,6 +117,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 { @@ -164,6 +174,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 a5023ed90..411828ce3 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -80,6 +80,7 @@ settingsIconStickers: icon {{ "settings/stickers", settingsIconFg }}; settingsIconEmoji: icon {{ "settings/emoji", settingsIconFg }}; settingsIconKotato: icon {{ "settings/kotato", settingsIconFg }}; settingsIconKotatoOld: icon {{ "settings/kotato_old", settingsIconFg }}; +settingsIconCloud: icon {{ "settings/cloud", settingsIconFg }}; settingsPremiumIconWallpapers: icon {{ "settings/photo", settingsIconFg }}; settingsPremiumIconStories: icon {{ "settings/stories", settingsIconFg }}; diff --git a/Telegram/SourceFiles/settings/settings_folders.cpp b/Telegram/SourceFiles/settings/settings_folders.cpp index 34a8b9ec2..24901ce4a 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 "api/api_chat_filters.h" // ProcessFilterRemove. #include "boxes/premium_limits_box.h" @@ -24,6 +26,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "ui/boxes/confirm_box.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/vertical_list.h" @@ -37,12 +41,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "styles/style_settings.h" #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" +#include "styles/style_window.h" namespace Settings { namespace { +auto currentDefaultRemoved = false; + using Flag = Data::ChatFilter::Flag; using Flags = Data::ChatFilter::Flags; @@ -161,7 +169,11 @@ struct FilterRow { ? (result + QString::fromUtf8(" \xE2\x80\xA2 ") + tr::lng_filters_shareable_status(tr::now)) - : result; + : (result + + QString::fromUtf8(" \xE2\x80\xA2 ") + + (filter.isLocal() + ? ktr("ktg_filters_local") + : ktr("ktg_filters_cloud"))); } FilterRowButton::FilterRowButton( @@ -341,6 +353,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; + Ui::AddSkip(container, st::defaultVerticalListSkip); Ui::AddSubsectionTitle(container, tr::lng_filters_subtitle()); @@ -357,10 +384,15 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { Assert(i != end(state->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( - state->rows, - &FilterRow::removed); + const auto removed = ranges::count_if(state->rows, [](FilterRow row) { + return row.removed || row.filter.isLocal(); + }); const auto count = int(state->rows.size() - removed); if (count < limit()) { return false; @@ -368,6 +400,18 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { controller->show(Box(FiltersLimitBox, session, count)); return true; }; + const auto newCloudButton = AddButtonWithIcon( + container, + rktr("ktg_filters_create_cloud"), + st::settingsButton, + { &st::settingsIconCloud } + ); + const auto newLocalButton = AddButtonWithIcon( + container, + rktr("ktg_filters_create_local"), + st::settingsButton, + { &st::menuIconShowInFolder } + ); const auto markForRemovalSure = [=](not_null button) { const auto row = find(button); auto suggestRemoving = Api::ExtractSuggestRemoving(row->filter); @@ -442,17 +486,28 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { const auto wrap = container->add(object_ptr( container)); const auto addFilter = [=](const Data::ChatFilter &filter) { + if (state->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([=] { remove(button); + if (find(button)->filter.id() == account->defaultFilterId()) { + currentDefaultRemoved = true; + } }, button->lifetime()); button->restoreRequests( ) | rpl::start_with_next([=] { if (showLimitReached()) { return; } + if (find(button)->filter.id() == account->defaultFilterId()) { + currentDefaultRemoved = false; + } button->setRemoved(false); find(button)->removed = false; }, button->lifetime()); @@ -463,6 +518,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); }; const auto saveAnd = [=]( @@ -539,17 +599,15 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { j->button->updateCount(j->filter); }, container->lifetime()); - AddButtonWithIcon( - container, - tr::lng_filters_create(), - st::settingsButtonActive, - { &st::settingsIconAdd, IconType::Round, &st::windowBgActive } - )->setClickedCallback([=] { + newCloudButton->setClickedCallback([=] { if (showLimitReached()) { return; } const auto created = std::make_shared(nullptr); const auto doneCallback = [=](const Data::ChatFilter &result) { + if (result.isDefault()) { + account->setDefaultFilterId(result.id()); + } if (const auto button = *created) { find(button)->filter = result; button->updateData(result); @@ -566,7 +624,33 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { controller->window().show(Box( EditFilterBox, controller, - Data::ChatFilter(), + Data::ChatFilter(generateNewId()), + crl::guard(container, doneCallback), + crl::guard(container, saveAnd))); + }); + newLocalButton->setClickedCallback([=] { + const auto created = std::make_shared(nullptr); + const auto doneCallback = [=](const Data::ChatFilter &result) { + if (result.isDefault()) { + account->setDefaultFilterId(result.id()); + } + if (const auto button = *created) { + find(button)->filter = result; + button->updateData(result); + } else { + *created = addFilter(result); + } + }; + const auto saveAnd = [=]( + const Data::ChatFilter &data, + Fn next) { + doneCallback(data); + state->save(*created, next); + }; + controller->window().show(Box( + EditFilterBox, + controller, + Data::ChatFilter(generateNewId(), true), crl::guard(container, doneCallback), crl::guard(container, saveAnd))); }); @@ -577,7 +661,7 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { object_ptr(container)) )->setDuration(0); const auto aboutRows = nonEmptyAbout->entity(); - Ui::AddDivider(aboutRows); + Ui::AddDividerText(aboutRows, rktr("ktg_filters_description")); Ui::AddSkip(aboutRows); Ui::AddSubsectionTitle(aboutRows, tr::lng_filters_recommended()); @@ -638,11 +722,28 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { auto result = base::flat_map, FilterId>(); for (auto &row : state->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 : state->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; @@ -652,6 +753,7 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { const FilterRowButton *single, Fn next) { auto ids = prepareGoodIdsForNewFilters(); + bool needSave = false; auto updated = Data::ChatFilter(); @@ -664,6 +766,7 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { auto &realFilters = session->data().chatsFilters(); const auto &list = realFilters.list(); order.reserve(state->rows.size()); + auto localFoldersChanged = false; for (auto &row : state->rows) { if (row.button.get() == single) { updated = row.filter; @@ -691,7 +794,16 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { const auto removeChatlistWithChats = removed && row.filter.chatlist() && !row.removePeers.empty(); - if (removeChatlistWithChats) { + if (row.filter.isLocal()) { + if (removed) { + realFilters.remove(id); + } else { + realFilters.set(row.filter); + order.push_back(id); + } + localFoldersChanged = true; + needSave = true; + } else if (removeChatlistWithChats) { auto inputs = ranges::views::all( row.removePeers ) | ranges::views::transform([](not_null peer) { @@ -714,6 +826,12 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { 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)); } updates.push_back(MTP_updateDialogFilter( MTP_flags(removed @@ -751,6 +869,11 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { session, next, updated, + account, + controller, + localFoldersChanged, + currentDefaultId, + &needSave, order = std::move(order), updates = std::move(updates), addRequests = std::move(addRequests), @@ -758,7 +881,7 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { removeChatlistRequests = std::move(removeChatlistRequests) ] { const auto api = &session->api(); - const auto filters = &session->data().chatsFilters(); + auto &filters = session->data().chatsFilters(); const auto ids = std::make_shared< base::flat_set >(); @@ -769,7 +892,7 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { } }; for (const auto &update : updates) { - filters->apply(update); + filters.apply(update); } auto previousId = mtpRequestId(0); const auto sendRequests = [&](const auto &requests) { @@ -792,9 +915,22 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { sendRequests(removeChatlistRequests); sendRequests(addRequests); if (!order.empty() && !addRequests.empty()) { - filters->saveOrder(order, previousId); + filters.saveOrder(order, previousId); } checkFinished(); + if (currentDefaultRemoved) { + account->setDefaultFilterId(0); + controller->setActiveChatsFilter(0); + } + if (localFoldersChanged) { + filters.saveLocal(); + } + if (currentDefaultId != account->defaultFilterId()) { + needSave = true; + } + if (needSave) { + Kotato::JsonSettings::Write(); + } }); }; return [copy = state->save] { diff --git a/Telegram/SourceFiles/ui/filter_icon_panel.cpp b/Telegram/SourceFiles/ui/filter_icon_panel.cpp index 34299d15c..382c32483 100644 --- a/Telegram/SourceFiles/ui/filter_icon_panel.cpp +++ b/Telegram/SourceFiles/ui/filter_icon_panel.cpp @@ -66,12 +66,28 @@ 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)) -, _innerBg(ImageRoundRadius::Small, st::dialogsBg) { +, _innerBg(ImageRoundRadius::Small, st::dialogsBg) +, _isLocal(isLocal) { setup(); } @@ -104,7 +120,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; @@ -149,6 +167,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); @@ -221,7 +258,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); } } @@ -239,8 +280,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 99caea7a6..4e3627fb4 100644 --- a/Telegram/SourceFiles/ui/filter_icon_panel.h +++ b/Telegram/SourceFiles/ui/filter_icon_panel.h @@ -19,7 +19,7 @@ class PanelAnimation; class FilterIconPanel final : public Ui::RpWidget { public: - FilterIconPanel(QWidget *parent); + FilterIconPanel(QWidget *parent, bool isLocal); ~FilterIconPanel(); void hideFast(); @@ -82,6 +82,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 b52b74299..0aaa5dd8b 100644 --- a/Telegram/SourceFiles/window/window.style +++ b/Telegram/SourceFiles/window/window.style @@ -306,6 +306,12 @@ windowFilterTypeNoArchived: icon {{ "folders/folders_type_archived", historyPeer windowFilterTypeNoRead: icon {{ "folders/folders_type_read", historyPeerUserpicFg }}; windowFilterTypeNewChats: icon {{ "folders/folder_new_chats", historyPeerUserpicFg }}; windowFilterTypeExistingChats: icon {{ "folders/folder_existing_chats", 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 }}; windowFilterChatsSectionSubtitleHeight: 28px; windowFilterChatsSectionSubtitle: FlatLabel(defaultFlatLabel) { style: TextStyle(defaultTextStyle) { diff --git a/Telegram/SourceFiles/window/window_controller.cpp b/Telegram/SourceFiles/window/window_controller.cpp index 19c12c30e..d368fb7c4 100644 --- a/Telegram/SourceFiles/window/window_controller.cpp +++ b/Telegram/SourceFiles/window/window_controller.cpp @@ -26,6 +26,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_editor.h" #include "ui/boxes/confirm_box.h" #include "data/data_peer.h" @@ -221,6 +222,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 68f79fe58..307444587 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" @@ -75,8 +76,14 @@ namespace { }); } +bool FiltersFirstLoad = true; + } // namespace +void ResetFiltersFirstLoad() { + FiltersFirstLoad = true; +} + FiltersMenu::FiltersMenu( not_null parent, not_null session) @@ -128,6 +135,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) @@ -135,7 +143,6 @@ void FiltersMenu::setup() { refresh(); }, _outer.lifetime()); - _activeFilterId = _session->activeChatsFilterCurrent(); _session->activeChatsFilter( ) | rpl::filter([=](FilterId id) { return (id != _activeFilterId); @@ -246,6 +253,11 @@ void FiltersMenu::refresh() { // After the filters are refreshed, the scroll is reset, // so we have to restore it. _scroll.scrollToY(oldTop); + + if (FiltersFirstLoad) { + _session->setActiveChatsFilter(_session->session().account().defaultFilterId()); + FiltersFirstLoad = false; + } } void FiltersMenu::setupList() { @@ -406,6 +418,7 @@ void FiltersMenu::showMenu(QPoint position, FilterId id) { if ((i == end(_filters)) && id) { return; } + const auto defaultFilterId = _session->session().account().defaultFilterId(); _popupMenu = base::make_unique_q( i->second.get(), st::popupMenuWithIcons); @@ -430,7 +443,17 @@ void FiltersMenu::showMenu(QPoint position, FilterId id) { _session, std::move(filteredChats), addAction); - + if (defaultFilterId != id) { + _popupMenu->addAction( + ktr("ktg_filters_context_make_default"), + crl::guard(&_outer, [=] { setDefaultFilter(id); }), + &st::menuIconFave); + } else { + _popupMenu->addAction( + ktr("ktg_filters_context_reset_default"), + crl::guard(&_outer, [=] { setDefaultFilter(0); }), + &st::menuIconUnfave); + } addAction( tr::lng_filters_context_remove(tr::now), [=] { showRemoveBox(id); }, @@ -447,7 +470,12 @@ void FiltersMenu::showMenu(QPoint position, FilterId id) { [=] { return _session->session().data().chatsList(); }, addAction, std::move(customUnreadState)); - + if (defaultFilterId != id) { + _popupMenu->addAction( + ktr("ktg_filters_context_make_default"), + crl::guard(&_outer, [=] { setDefaultFilter(0); }), + &st::menuIconFave); + } addAction( tr::lng_filters_setup_menu(tr::now), [=] { openFiltersSettings(); }, @@ -484,6 +512,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); } @@ -615,6 +651,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 fc00b63df..9737dd48e 100644 --- a/Telegram/SourceFiles/window/window_filters_menu.h +++ b/Telegram/SourceFiles/window/window_filters_menu.h @@ -23,6 +23,8 @@ namespace Window { class SessionController; +void ResetFiltersFirstLoad(); + class FiltersMenu final { public: FiltersMenu( @@ -49,6 +51,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, std::vector> leave = {}); diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index e77bc9f81..0bb809f5c 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -72,6 +72,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_app_config.h" #include "main/main_domain.h" #include "main/main_session.h" +#include "main/main_account.h" #include "main/main_session_settings.h" #include "lang/lang_keys.h" #include "apiwrap.h" @@ -1499,6 +1500,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()); } @@ -1513,8 +1541,10 @@ void SessionController::checkOpenedFilter() { const auto &list = session().data().chatsFilters().list(); const auto i = ranges::find(list, filterId, &Data::ChatFilter::id); if (i == end(list)) { + const auto defaultFilterId = session().account().defaultFilterId(); + const auto j = ranges::find(list, FilterId(defaultFilterId), &Data::ChatFilter::id); setActiveChatsFilter( - 0, + j == end(list) ? 0 : defaultFilterId, { anim::type::normal, anim::activation::background }); } } @@ -1547,8 +1577,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::showForum( @@ -2494,7 +2530,7 @@ void SessionController::setActiveChatsFilter( _activeChatsFilter.force_assign(id); if (id || !changed) { closeForum(); - closeFolder(); + closeFolder(true); } if (adaptive().isOneColumn()) { clearSectionStack(params); diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 9888dc1c4..5c3c5a388 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -365,7 +365,7 @@ public: bool uniqueChatsInSearchResults() const; void openFolder(not_null folder); - void closeFolder(); + void closeFolder(bool force = false); const rpl::variable &openedFolder() const; void showForum( @@ -549,6 +549,7 @@ public: const SectionShow ¶ms = SectionShow::Way::ClearStack); void toggleFiltersMenu(bool enabled); + void reloadFiltersMenu(); [[nodiscard]] rpl::producer<> filtersMenuChanged() const; [[nodiscard]] auto defaultChatTheme() const